//===========================================================================// // File: powersub.cpp // // Project: BattleTech Brick: Mech subsystems // // Contents: PoweredSubsystem -- a HeatSink drawing electrical power // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(POWERSUB_HPP) # include #endif #if !defined(MECH_HPP) # include #endif // //############################################################################# // Shared data support //############################################################################# // Derivation PoweredSubsystem::ClassDerivations( HeatSink::ClassDerivations, "PoweredSubsystem" ); // //############################################################################# // The cockpit generator buttons (binary handler table @0x50F4EC, ids 4-8), // chained onto the HeatSink set (id 3 ToggleCooling) so every powered // subsystem's dispatch reaches the Eng-page Coolant button too. Static-init // order is safe under the authentic .MAK lib order (heat precedes powersub). //############################################################################# // const PoweredSubsystem::HandlerEntry PoweredSubsystem::MessageHandlerEntries[] = { MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorA), // id 4 @004b099c MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorB), // id 5 @004b09e4 MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorC), // id 6 @004b0a2c MESSAGE_ENTRY(PoweredSubsystem, SelectGeneratorD), // id 7 @004b0a74 MESSAGE_ENTRY(PoweredSubsystem, ToggleGeneratorMode) // id 8 @004b0abc }; PoweredSubsystem::MessageHandlerSet PoweredSubsystem::MessageHandlers( ELEMENTS(PoweredSubsystem::MessageHandlerEntries), PoweredSubsystem::MessageHandlerEntries, HeatSink::MessageHandlers ); // //############################################################################# // Attribute tables (cockpit binding by name). //############################################################################# // const PoweredSubsystem::IndexEntry PoweredSubsystem::AttributePointers[]= { ATTRIBUTE_ENTRY(PoweredSubsystem, InputVoltage, inputVoltage), ATTRIBUTE_ENTRY(PoweredSubsystem, OutputVoltage, outputVoltage), ATTRIBUTE_ENTRY(PoweredSubsystem, RatedVoltage, ratedVoltage), ATTRIBUTE_ENTRY(PoweredSubsystem, VoltageState, electricalStateAlarm), ATTRIBUTE_ENTRY(PoweredSubsystem, ConnectMode, modeAlarm) }; PoweredSubsystem::AttributeIndexSet PoweredSubsystem::AttributeIndex( ELEMENTS(PoweredSubsystem::AttributePointers), PoweredSubsystem::AttributePointers, HeatSink::AttributeIndex ); PoweredSubsystem::SharedData PoweredSubsystem::DefaultData( PoweredSubsystem::ClassDerivations, PoweredSubsystem::MessageHandlers, PoweredSubsystem::AttributeIndex, Subsystem::StateCount ); // //############################################################################# // A HeatSink that draws electrical power from a generator (binary ctor // @004b0f74). Resolves the "VoltageSource" roster index to the powering // generator, attaches the tap, and primes the electrical state machine. The // voltageSourceIndex indexes the owner mech's SUBSYSTEM ROSTER (the same // index space the AmmoBin link uses) -- the roster slots ahead of this // subsystem are already constructed by the segment walk, and the shipped // stream orders the generators first. //############################################################################# // PoweredSubsystem::PoweredSubsystem( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), voltageSource(), electricalStateAlarm(5), modeAlarm(3) { Check(owner); Check_Pointer(subsystem_resource); inputVoltage = 0.0f; outputVoltage = 0.0f; ratedVoltage = 0.0f; thermalResistivityCoefficient = subsystem_resource->thermalResistivityCoefficient; startTime = subsystem_resource->startTime; // // Cache the authored aux-screen block NOW -- the resource memory dies // with the Mech ctor's stream buffer. // auxScreenNumber = subsystem_resource->auxScreenNumber; auxScreenPlacement = subsystem_resource->auxScreenPlacement; Str_Copy(auxScreenLabel, subsystem_resource->auxScreenLabel, sizeof(auxScreenLabel)); Str_Copy(engScreenLabel, subsystem_resource->engScreenLabel, sizeof(engScreenLabel)); startTimer = startTime; voltageScale = 1.0f; // // Resolve the voltage source from the roster and attach the tap. // Subsystem *source = NULL; if (subsystem_resource->voltageSourceIndex >= 0 && subsystem_resource->voltageSourceIndex < owner->GetSubsystemCount()) { source = owner->GetSubsystem(subsystem_resource->voltageSourceIndex); } if (source != NULL) { AttachToVoltageSource(source); } if (getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[power] '" << GetName() << "' srcIdx=" << subsystem_resource->voltageSourceIndex << " -> "; if (source != NULL) { DEBUG_STREAM << source->GetName(); } else { DEBUG_STREAM << ""; } DEBUG_STREAM << " startTime=" << startTime << endl << flush; } electricalStateAlarm.SetLevel(Ready); modeAlarm.SetLevel(Connected); // // A master (non-replicant) instance runs the per-frame electrical // simulation. Derived subsystems (the weapons, Sensor, ...) override with // their own Performance in their ctors, each of which chains this step. // if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&PoweredSubsystem::PoweredSubsystemSimulation); } Check_Fpu(); } // //############################################################################# //############################################################################# // PoweredSubsystem::~PoweredSubsystem() { } // //############################################################################# //############################################################################# // void PoweredSubsystem::ResetToInitialState(Logical powered) { Check(this); HeatSink::ResetToInitialState(powered); inputVoltage = 0.0f; outputVoltage = 0.0f; electricalStateAlarm.SetLevel(0); modeAlarm.SetLevel(0); } // //############################################################################# //############################################################################# // Logical PoweredSubsystem::TestClass(Mech &) { return True; } Logical PoweredSubsystem::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# // AttachToVoltageSource Link this subsystem to its powering generator // (binary @004b0dd8): take a tap on the generator (-1 when every tap is // taken) and hold the live connection. //############################################################################# // int PoweredSubsystem::AttachToVoltageSource(Subsystem *source) { Check(this); Check(source); Generator *generator = (Generator *)source; if (generator->TapVoltageSource() != 0) { return -1; } voltageSource.Add(source); inputVoltage = generator->MeasuredVoltage(); return 0; } // //############################################################################# // DetachFromVoltageSource (binary @004b0e30): release the tap on the current // source and clear the connection. //############################################################################# // void PoweredSubsystem::DetachFromVoltageSource() { Check(this); Generator *source = (Generator *)voltageSource.Resolve(); if (source != NULL) { source->UntapVoltageSource(); voltageSource.Clear(); } } // //############################################################################# // FindGeneratorByNumber (binary @004b0b18): walk the owner's roster for the // Generator whose authored generatorNumber matches (1=A .. 4=D). //############################################################################# // Subsystem* PoweredSubsystem::FindGeneratorByNumber(int generator_number) { Check(this); for (int slot = 2; slot < owner->GetSubsystemCount(); ++slot) { Subsystem *sub = owner->GetSubsystem(slot); if (sub != NULL && sub->IsDerivedFrom(Generator::ClassDerivations) && ((Generator *)sub)->GetGeneratorNumber() == generator_number) { return sub; } } return NULL; } // //############################################################################# // SelectGenerator -- the manual re-tap: release the current source, tap // generator N, drop the connect mode back to Connected (a manual selection // ends any auto-hunt). The binary dereferences the find unguarded (authored // mechs always carry A-D); we skip loud instead. //############################################################################# // void PoweredSubsystem::SelectGenerator(int generator_number) { Check(this); Subsystem *generator = FindGeneratorByNumber(generator_number); if (generator == NULL) { if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[gensel] '" << GetName() << "' -> generator " << generator_number << " NOT FOUND" << endl << flush; } return; } DetachFromVoltageSource(); int tap = AttachToVoltageSource(generator); modeAlarm.SetLevel(Connected); if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[gensel] '" << GetName() << "' -> '" << generator->GetName() << "'" << ((tap >= 0) ? " (tapped)" : " (REFUSED: no spare tap)") << endl << flush; } } // //############################################################################# // The cockpit button handlers (ids 4-8). Press-only (dataContents > 0), // novice-locked -- a novice cockpit's generator panel is inert. //############################################################################# // void PoweredSubsystem::SelectGeneratorAMessageHandler( ReceiverDataMessageOf *message) { Check(this); if (!NoviceLockout() && message->dataContents > 0) { SelectGenerator(1); } } void PoweredSubsystem::SelectGeneratorBMessageHandler( ReceiverDataMessageOf *message) { Check(this); if (!NoviceLockout() && message->dataContents > 0) { SelectGenerator(2); } } void PoweredSubsystem::SelectGeneratorCMessageHandler( ReceiverDataMessageOf *message) { Check(this); if (!NoviceLockout() && message->dataContents > 0) { SelectGenerator(3); } } void PoweredSubsystem::SelectGeneratorDMessageHandler( ReceiverDataMessageOf *message) { Check(this); if (!NoviceLockout() && message->dataContents > 0) { SelectGenerator(4); } } // //############################################################################# // ToggleGeneratorMode (id 8, binary @004b0abc): cycle Manual -> AutoConnect // -> (detach +) Manual. The auto-hunt itself (a shorted/dead source makes // the subsystem walk for a live generator) lives in // PoweredSubsystemSimulation -- a later brick. //############################################################################# // void PoweredSubsystem::ToggleGeneratorModeMessageHandler( ReceiverDataMessageOf *message) { Check(this); if (NoviceLockout() || message->dataContents <= 0) { return; } if ((unsigned)modeAlarm.GetLevel() < (unsigned)AutoConnect) { modeAlarm.SetLevel(AutoConnect); } else if (modeAlarm.GetLevel() == AutoConnect) { DetachFromVoltageSource(); modeAlarm.SetLevel(ManualConnect); } if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) { DEBUG_STREAM << "[gensel] '" << GetName() << "' mode -> " << modeAlarm.GetLevel() << endl << flush; } } // //############################################################################# // ChargeTimeScale -- the heat/firepower feedback (binary @004b0d50): // rise = max(0, sourceTemperature - sourceStartingTemperature) // scale = max(voltageScale, // (thermalResistivityCoefficient * rise + 1) * voltageScale) // A hot generator stretches the exponential charge constant, so recharging // slows exactly when the electrical plant is cooking. //############################################################################# // Scalar PoweredSubsystem::ChargeTimeScale() { Check(this); Generator *source = (Generator *)voltageSource.Resolve(); if (source == NULL) { return voltageScale; } Scalar rise = source->CurrentTemperatureOf() - source->StartingTemperatureOf(); if (rise < 0.0f) { rise = 0.0f; } Scalar stretched = (thermalResistivityCoefficient * rise + 1.0f) * voltageScale; return (stretched > voltageScale) ? stretched : voltageScale; } // //############################################################################# // DeathReset -- respawn restore for the powered chain and the generators. //############################################################################# // void PoweredSubsystem::DeathReset(Logical full_reset) { Check(this); MechSubsystem::DeathReset(full_reset); ResetToInitialState(True); // // ResetToInitialState drops the connect-mode indicator to Manual; the // respawned subsystem keeps its live tap, so the mode is Connected // (the ctor's spawn state). // modeAlarm.SetLevel(Connected); } void Generator::DeathReset(Logical full_reset) { Check(this); MechSubsystem::DeathReset(full_reset); // // PRESERVE the tap accounting across the reset: consumers keep their // voltageSource links through respawn (nothing detaches), so zeroing // currentTapCount here would desync the maxTapCount invariant and let // post-respawn SelectGenerator presses oversubscribe the generator // (caught by the 5.3.24 adversarial review). // int live_taps = currentTapCount; ResetToInitialState(); currentTapCount = live_taps; } // //############################################################################# // ForceShortRecovery (binary @004b11bc): on a short event, drive the powering // generator to Shorted and clear its output; GeneratorSimulation then runs // the short-recovery timer back to Ready. Both @4ac9c8 calls in the binary // are the not-novice experience predicate (this part and its source share // the same mech, hence the same player) -- novice cockpits never see // electrical shorts. //############################################################################# // void PoweredSubsystem::ForceShortRecovery() { Check(this); if (NoviceLockout()) { return; } Generator *source = (Generator *)voltageSource.Resolve(); if (source != NULL) { source->ForceShort(); if (getenv("BT_POWER_LOG") || getenv("BT_MECH_LOG")) { DEBUG_STREAM << "[short] '" << source->GetName() << "' SHORTED by special damage" << endl << flush; } } } // //############################################################################# // PoweredSubsystemSimulation -- the per-frame electrical step (binary // @004b0bd0). Runs the HeatSink thermal step, then advances the electrical // state machine from the state of the powering generator. // // PARTIAL: the AutoConnect replacement-generator hunt (modeAlarm AutoConnect + // the status-flag gate) joins with the damage wave. //############################################################################# // void PoweredSubsystem::PoweredSubsystemSimulation(Scalar time_slice) { Check(this); HeatSink::HeatSinkSimulation(time_slice); Generator *source = (Generator *)voltageSource.Resolve(); if (source == NULL) { electricalStateAlarm.SetLevel(NoVoltage); } else { if (source->GeneratorStateOf() == Generator::GeneratorShorted) { electricalStateAlarm.SetLevel(Shorted); } if (source->GeneratorStateOf() == Generator::GeneratorStarting || source->GeneratorStateOf() == Generator::GeneratorFailed) { electricalStateAlarm.SetLevel(GeneratorOff); } } switch (electricalStateAlarm.GetLevel()) { case Starting: startTimer += time_slice; if (startTime <= startTimer) { electricalStateAlarm.SetLevel(Ready); } break; case NoVoltage: if (source != NULL) { electricalStateAlarm.SetLevel(Starting); startTimer = 0.0f; } break; case Shorted: case GeneratorOff: if (source != NULL && source->GeneratorStateOf() == Generator::GeneratorReady) { electricalStateAlarm.SetLevel(Starting); startTimer = 0.0f; } break; } if (source != NULL) { inputVoltage = source->MeasuredVoltage(); } Check_Fpu(); } //########################################################################### //############################## Generator ############################# //########################################################################### // //############################################################################# // Shared data support //############################################################################# // Derivation Generator::ClassDerivations( HeatSink::ClassDerivations, "Generator" ); const Generator::IndexEntry Generator::AttributePointers[]= { ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage), ATTRIBUTE_ENTRY(Generator, RatedVoltage, ratedVoltage), ATTRIBUTE_ENTRY(Generator, GeneratorNumber, generatorNumber), ATTRIBUTE_ENTRY(Generator, GeneratorState, stateAlarm), { (int)Generator::GeneratorOnAttributeID2, "GeneratorOn", (Simulation::AttributePointer)&Generator::generatorOn } }; Generator::AttributeIndexSet Generator::AttributeIndex( ELEMENTS(Generator::AttributePointers), Generator::AttributePointers, HeatSink::AttributeIndex ); Generator::SharedData Generator::DefaultData( Generator::ClassDerivations, Subsystem::MessageHandlers, Generator::AttributeIndex, Subsystem::StateCount ); // //############################################################################# // The generator -- the voltage source loads tap. //############################################################################# // Generator::Generator( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): HeatSink(owner, subsystem_ID, subsystem_resource, shared_data), stateAlarm(5) { Check(owner); Check_Pointer(subsystem_resource); ratedVoltage = subsystem_resource->ratedVoltage; outputVoltage = ratedVoltage; maxTapCount = subsystem_resource->maxTapCount; currentTapCount = 0; percentVoltageAvailable = 1.0f; startTime = subsystem_resource->startTime; startTimer = startTime; stateAlarm.SetLevel(GeneratorReady); generatorOn = 1; shortRecoveryTime = subsystem_resource->shortRecoveryTime; shortTimer = shortRecoveryTime; // // Generator number from the last character of the segment name // ('A' -> 1, 'B' -> 2, ...). // const char *name = GetName(); generatorNumber = name[strlen(name) - 1] - 0x40; // // Install the generator's per-frame electrical Performance. // if (owner->GetInstance() != Entity::ReplicantInstance) { SetPerformance(&Generator::GeneratorSimulation); } Check_Fpu(); } // //############################################################################# //############################################################################# // Generator::~Generator() { } // //############################################################################# //############################################################################# // Logical Generator::TestClass(Mech &) { return True; } Logical Generator::TestInstance() const { return IsDerivedFrom(ClassDerivations); } // //############################################################################# //############################################################################# // void Generator::ResetToInitialState() { Check(this); HeatSink::ResetToInitialState(True); outputVoltage = ratedVoltage; currentTapCount = 0; percentVoltageAvailable = 1.0f; startTimer = startTime; shortTimer = shortRecoveryTime; generatorOn = 1; stateAlarm.SetLevel(GeneratorReady); } // //############################################################################# // GeneratorSimulation -- the generator's per-frame step (PARTIAL). Runs the // HeatSink thermal step and the start/short-recovery timers. The authentic // load model (output voltage sag under tap load / I^2R self-heat feeding the // charge integration) joins with the electrical-charge wave // (TrackSeekVoltage). A healthy generator holds GeneratorReady at its rated // voltage. //############################################################################# // void Generator::GeneratorSimulation(Scalar time_slice) { Check(this); HeatSink::HeatSinkSimulation(time_slice); switch (stateAlarm.GetLevel()) { case GeneratorStarting: startTimer += time_slice; if (startTime <= startTimer) { stateAlarm.SetLevel(GeneratorReady); outputVoltage = ratedVoltage; } break; case GeneratorShorted: shortTimer -= time_slice; if (shortTimer <= 0.0f) { shortTimer = shortRecoveryTime; stateAlarm.SetLevel(GeneratorStarting); startTimer = 0.0f; } break; default: break; } Check_Fpu(); } //########################################################################### //############################ PowerWatcher ############################ //########################################################################### Derivation PowerWatcher::ClassDerivations( HeatWatcher::ClassDerivations, "PowerWatcher" ); PowerWatcher::SharedData PowerWatcher::DefaultData( PowerWatcher::ClassDerivations, Subsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); PowerWatcher::PowerWatcher( Mech *owner, int subsystem_ID, SubsystemResource *subsystem_resource, SharedData &shared_data ): HeatWatcher(owner, subsystem_ID, subsystem_resource, shared_data), watchdogAlarm(5) { Check(owner); Check_Pointer(subsystem_resource); // // minVoltage is a scaled fraction of the watched supply; the exact scale // constant is a tuning value (stored 1:1 here until located). // minVoltage = subsystem_resource->minVoltagePercent; Check_Fpu(); } PowerWatcher::~PowerWatcher() { } Logical PowerWatcher::TestClass(Mech &) { return True; } Logical PowerWatcher::TestInstance() const { return IsDerivedFrom(ClassDerivations); } void PowerWatcher::ResetToInitialState(Logical powered) { Check(this); HeatWatcher::ResetToInitialState(powered); watchdogAlarm.SetLevel(0); } void PowerWatcher::DeathReset(Logical full_reset) { Check(this); MechSubsystem::DeathReset(full_reset); ResetToInitialState(True); } // // Per-frame supply-voltage watchdog. Not yet reconstructed. // void PowerWatcher::Simulation(Scalar) { Fail("PowerWatcher::Simulation -- powersub.cpp not yet reconstructed"); }