From bfff7fe8741c82010b55cd519d2b6e392da0c5b6 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 24 Jul 2026 14:36:32 -0500 Subject: [PATCH] BT410 Phase 5.3.22: the cockpit weapon-button column -- generator panel LIVE The full per-receiver message chain, contiguous ids 3..0xb: PoweredSubsystem 4-8 (SelectGeneratorA-D + ToggleGeneratorMode, binary table @0x50F4EC) chained on HeatSink's ToggleCooling(3); MechWeapon 9/10 (config buttons, staged latch bodies); Emitter 0xb ToggleSeekVoltage (@0x511DB8). SelectGenerator = release the old tap -> roster walk by generatorNumber -> AttachToVoltageSource -> Connected; mode toggle cycles Manual->Auto-> (detach)->Manual; the seek dial steps the authored voltage ladder with wrap (a step up re-enters Loading, charge persists). All novice-locked. Generator::UntapVoltageSource + PoweredSubsystem::DetachFromVoltageSource added. Dev harness: BT_PRESS_GEN=1..4 / BT_PRESS_SEEK. Verified: PPC_1 retaps GeneratorB (tapped); seek index 2->3, target 9900V (authored fraction x rated); novice presses INERT; missile fight 14/14 and smoke green, zero faults. Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/EMITTER.CPP | 1164 +++++++++++--------- restoration/source410/BT/EMITTER.HPP | 15 + restoration/source410/BT/MECH.CPP | 45 + restoration/source410/BT/MECHWEAP.CPP | 60 +- restoration/source410/BT/MECHWEAP.HPP | 16 + restoration/source410/BT/MECHWEAP.NOTES.md | 19 + restoration/source410/BT/POWERSUB.CPP | 192 +++- restoration/source410/BT/POWERSUB.HPP | 49 + 8 files changed, 1000 insertions(+), 560 deletions(-) diff --git a/restoration/source410/BT/EMITTER.CPP b/restoration/source410/BT/EMITTER.CPP index 05c199bc..dd0a942e 100644 --- a/restoration/source410/BT/EMITTER.CPP +++ b/restoration/source410/BT/EMITTER.CPP @@ -1,550 +1,614 @@ -//===========================================================================// -// File: emitter.cpp // -// Project: BattleTech Brick: Mech weapons // -// Contents: Emitter -- the energy-weapon (beam) base // -//---------------------------------------------------------------------------// -// Copyright (C) 1995, Virtual World Entertainment, Inc. // -// All Rights reserved worldwide // -// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // -//===========================================================================// - -#include -#pragma hdrstop - -#if !defined(EMITTER_HPP) -# include -#endif - -#if !defined(MECH_HPP) -# include -#endif - -#include - -Derivation - Emitter::ClassDerivations( - MechWeapon::ClassDerivations, - "Emitter" - ); - -// -//############################################################################# -// Attribute Support. Emitter publishes the beam charge level (ChargeLevel, at -// the authentic 0x1D past the MechWeapon table end): the HUD weapon-charge -// gauge binds it, and -- load-bearing -- GaussRifle's AttributeIndex chains -// THIS index (GAUSS.CPP), and PPC::DefaultData binds it via the inherited -// PPC::AttributeIndex. It MUST be a real defined index (a declared-but- -// undefined Emitter::AttributeIndex leaves activeAttributeIndex NULL and -// faults the first GetAttributePointer on any PPC/Gauss). Chained to the -// MechWeapon index so every energy weapon exposes the full weapon table -// (TriggerState / PercentDone / ...). -//############################################################################# -// -const Emitter::IndexEntry - Emitter::AttributePointers[]= -{ - ATTRIBUTE_ENTRY(Emitter, ChargeLevel, chargeLevel) -}; - -Emitter::AttributeIndexSet - Emitter::AttributeIndex( - ELEMENTS(Emitter::AttributePointers), - Emitter::AttributePointers, - MechWeapon::AttributeIndex - ); - -Emitter::SharedData - Emitter::DefaultData( - Emitter::ClassDerivations, - MechWeapon::MessageHandlers, - Emitter::AttributeIndex, - Subsystem::StateCount - ); - -Emitter::Emitter( - Mech *owner, - int subsystem_ID, - SubsystemResource *subsystem_resource, - SharedData &shared_data -): - MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data) -{ - Check(owner); - Check_Pointer(subsystem_resource); - - chargeLevel = 0.0f; - seekRate = 0.0f; - damagePortion = 0.0f; - heatPortion = 0.0f; - firingActive = 0; - - graphicLength = subsystem_resource->graphicLength; - dischargeTime = subsystem_resource->dischargeTime; - dischargeTimer = dischargeTime; - - // - // Bring-up-safe pre-init: the electrical block below overwrites these with - // the calibrated values when the voltage source is live. - // - energyCoefficient = 1.0f; - seekVoltageIndex = 0; - seekVoltageRecommendedIndex = 0; - minSeekVoltageIndex = 0; - maxSeekVoltageIndex = 0; - { - for (int sv = 0; sv < 5; ++sv) - { - seekVoltage[sv] = 1.0f; - } - } - - // - // energyTotal = (damage + heat) x 1e7 -- the discharge energy in the - // 1e7-native heat units. - // - energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; - - // - // The SeekVoltage calibration (binary ctor @004bb120): the authored curve - // values are FRACTIONS of the powering generator's rated voltage (-1 - // sentinel ends the list); the energy coefficient pins E = 0.5*V^2*EC to - // energyTotal exactly at the recommended gear; and voltageScale calibrates - // the exponential charge level(t) = Vrated*(1 - exp(-t/(EC*voltageScale))) - // to reach the recommended seek voltage in the authored RechargeRate - // seconds on a COLD generator (0.0001 == 1/Vrated). Generator heat then - // stretches the scale through ChargeTimeScale -- the heat/firepower - // feedback. - // - { - Generator *src = (Generator *)ResolveVoltageSource(); - if (src != NULL) - { - int i; - for (i = 0; i < 5; ++i) - { - seekVoltage[i] = 0.0f; - } - for (i = 0; i < 5; ++i) - { - if (subsystem_resource->seekVoltage[i] == -1.0f) - { - maxSeekVoltageIndex = i - 1; - break; - } - seekVoltage[i] = subsystem_resource->seekVoltage[i] - * src->RatedVoltageOf(); - } - seekVoltageRecommendedIndex = - subsystem_resource->seekVoltageRecommendedIndex; - seekVoltageIndex = seekVoltageRecommendedIndex; - minSeekVoltageIndex = 0; - - Scalar v = seekVoltage[seekVoltageRecommendedIndex]; - energyCoefficient = energyTotal / (v * v * 0.5f); - voltageScale = (rechargeRate - / -(Scalar)log((double)(1.0f - 0.0001f * v))) - / energyCoefficient; - - if (getenv("BT_POWER_LOG")) - { - DEBUG_STREAM << "[charge] '" << GetName() - << "' seekV={" << seekVoltage[0] << "," << seekVoltage[1] - << "," << seekVoltage[2] << "," << seekVoltage[3] - << "," << seekVoltage[4] << "} rec=" << seekVoltageRecommendedIndex - << " max=" << maxSeekVoltageIndex - << " EC=" << energyCoefficient - << " vScale=" << voltageScale - << " discharge=" << dischargeTime - << " E=" << energyTotal << endl << flush; - } - } - } - - // - // damageFraction = the damage share of the discharge energy. - // - damageFraction = damageData.damageAmount / - (damageData.damageAmount + heatCostToFire); - - // - // Install the beam-weapon fire state machine; spawn LOADING (the charge - // integrates up from zero). (A replicant copy is driven by console - // updates / ServiceDischarge instead -- that path joins the network wave.) - // - weaponAlarm.SetLevel(LoadingState); - if (owner->GetInstance() != Entity::ReplicantInstance) - { - SetPerformance(&Emitter::EmitterSimulation); - } - - Check_Fpu(); -} - -// -//############################################################################# -// TrackSeekVoltage -- integrate the charge from the powering generator -// (binary @004ba838): derive the seek rate from the voltage gap over the -// (heat-stretched) charge time-scale, step the level, and feed the charging -// I^2R loss back into the GENERATOR's heat -- recharging weapons is what -// heats generators, which conduct to their condensers and slow further -// charging through ChargeTimeScale. The heat/firepower feedback loop. -//############################################################################# -// -void - Emitter::TrackSeekVoltage(Scalar time_slice) -{ - Check(this); - - Generator *src = (Generator *)ResolveVoltageSource(); - if (src == NULL) - { - return; - } - - Scalar dtScale = ChargeTimeScale(); - seekRate = (src->MeasuredVoltage() - chargeLevel) / dtScale; - chargeLevel = (seekRate / energyCoefficient) * time_slice + chargeLevel; - - if (HeatModelActive()) - { - src->AddPendingHeat(seekRate * seekRate * dtScale * time_slice); - } -} - -// -//############################################################################# -// ComputeOutputVoltage -- the Emitter override (binary @004ba738): the -// recharge dial = the charge level as a fraction of the selected seek -// voltage, snapped to 1.0 within 0.01 and clamped to [0,1] (the byte-verified -// over-1 clamp zeroes the dial). -//############################################################################# -// -void - Emitter::ComputeOutputVoltage() -{ - Check(this); - - Scalar magnitude = (chargeLevel < 0.0f) ? -chargeLevel : chargeLevel; - if (magnitude > 1.0e-4f) - { - rechargeLevel = chargeLevel / seekVoltage[seekVoltageIndex]; - } - else - { - rechargeLevel = 0.0f; - } - - Scalar snap = rechargeLevel - 1.0f; - if (snap < 0.0f) - { - snap = -snap; - } - if (snap <= 0.01f) - { - rechargeLevel = 1.0f; - } - - if (rechargeLevel < 0.0f) - { - rechargeLevel = 0.0f; - } - else if (rechargeLevel > 1.0f) - { - rechargeLevel = 0.0f; - } -} - -Emitter::~Emitter() -{ -} - -Logical - Emitter::TestClass(Mech &) -{ - return True; -} - -Logical - Emitter::TestInstance() const -{ - return IsDerivedFrom(ClassDerivations); -} - -// -//############################################################################# -// FireWeapon The energy-beam discharge (PARTIAL -- binary @004bace8). The -// authentic body: re-arm the beam-on countdown, compute the per-shot damage / -// heat from the charge energy (0.5*V^2*EC closed forms), dump the heat into -// the inherited thermal accumulator, spend the charge, build the beam -// (muzzle -> target) and submit the Damage record at the owner's target. -// The energy/heat algebra needs the electrical charge model (TrackSeekVoltage -// / seekVoltage / generator -- the powersub wave), and the beam/damage need -// the targeting slot + renderer. This partial performs the DISCHARGE -// bookkeeping -- countdown re-arm, charge spent, recoil loaded so the recharge -// dial animates -- so the fire state machine runs end-to-end. -//############################################################################# -// -void - Emitter::FireWeapon() -{ - Check(this); - - // - // Re-arm the beam-on countdown, then THE AUTHENTIC ENERGY ALGEBRA (binary - // @004bace8): the shot's damage and heat are the two shares of the stored - // charge energy, scaled by the SQUARE of the charge ratio (the level as a - // fraction of the recommended seek voltage) -- an under-charged or - // down-geared shot delivers proportionally less of both. At full charge on - // the recommended gear: damage = the authored DamageAmount verbatim, heat = - // heatCostToFire x 1e7 (the 1e7-native chain). - // - dischargeTimer = dischargeTime; - - Scalar vRec = seekVoltage[seekVoltageRecommendedIndex]; - Scalar chargeRatio = (vRec > 0.0f) ? (chargeLevel / vRec) : 1.0f; - - damagePortion = (damageFraction * energyTotal * 1.0e-7f) - * chargeRatio * chargeRatio; - heatPortion = (1.0f - damageFraction) * energyTotal - * chargeRatio * chargeRatio; - - // - // The damage submission (binary @004bace8 tail): the shot's portion rides - // the inherited Damage record to the owner's target when it is inside the - // weapon's effective range. (The beam build / impact point join the - // render wave.) - // - damageData.damageAmount = damagePortion; - damageData.burstCount = 1; - if (targetWithinRange && owner != NULL - && owner->GetTargetEntity() != NULL) - { - SendDamage(owner->GetTargetEntity(), damageData); - } - - if (HeatModelActive()) - { - AddPendingHeat(heatPortion); - } - - // - // Spend the charge. - // - ComputeOutputVoltage(); - chargeLevel = 0.0f; - firingActive = 1; - - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' FIRED (dmg=" << damagePortion - << " heat=" << heatPortion - << " ratio=" << chargeRatio - << " T=" << CurrentTemperatureOf() << ")" << endl << flush; - } -} - -// -//############################################################################# -// ResetFiringState (binary @004ba9a8) -- the beam has finished: drop back to -// Loading so the recharge cycle begins. -//############################################################################# -// -void - Emitter::ResetFiringState() -{ - Check(this); - - firingActive = 0; - weaponAlarm.SetLevel(LoadingState); -} - -// -//############################################################################# -// EmitterSimulation The beam-weapon per-frame fire state machine (binary -// @004baa88). The weapon state is carried in the weapon alarm level: -// 0 = Firing, 2 = Loaded (ready), 3 = Loading, 4 = the trigger-during-load -// blip. PARTIAL: the leading PoweredSubsystem electrical step and the -// destroyed / heat-failure / dead-mech hard gates are deferred with the -// power / heat / damage waves; the Loading charge uses the authored -// RechargeRate seconds directly (recoil decay -> ComputeOutputVoltage dial) -// instead of the TrackSeekVoltage generator integration; the Loaded->Firing -// gate honors viewFireEnable (the look-view arm) -- the HasActiveTarget gate -// joins it with the targeting wave. -// -// DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded -// (press while Loaded, release otherwise), so every armed weapon auto-fires at -// its authored recharge cadence -- the headless fire-cycle verification. -//############################################################################# -// -void - Emitter::EmitterSimulation(Scalar time_slice) -{ - Check(this); - - // - // The PoweredSubsystem step first (binary @004baa88 head): the HeatSink - // thermal absorb/conduct + the electrical state machine. - // - PoweredSubsystem::PoweredSubsystemSimulation(time_slice); - - // - // Hard failure (@4baab9): weapon DESTROYED or its own sink at FailureHeat - // -> drop the beam state and the charge, and hold there. Unlike the - // ballistic roach-motel this recovers by itself: once conduction cools the - // sink below the failure threshold the gate stops firing and the weapon - // resumes from Loading. (The owning-mech-disabled half joins with the - // damage wave.) - // - if (GetSimulationState() == 1 || GetHeatState() == FailureHeat) - { - if (getenv("BT_MECH_LOG") && GetWeaponState() != LoadingState) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' THERMAL SHUTDOWN (T=" << CurrentTemperatureOf() - << ")" << endl << flush; - } - ResetFiringState(); - chargeLevel = 0.0f; - ComputeOutputVoltage(); - Check_Fpu(); - return; - } - - { - static int forceFire = -1; - if (forceFire < 0) - { - forceFire = (getenv("BT_FORCE_FIRE") != NULL) ? 1 : 0; - } - if (forceFire) - { - fireImpulse = (GetWeaponState() == LoadedState) ? 1.0f : 0.0f; - } - } - - Logical fireEdge = CheckFireEdge(); - - targetWithinRange = UpdateTargeting(); - - switch (GetWeaponState()) - { - case FiringState: - // - // Count the beam-on timer down; when it expires, drop to Loading. - // - dischargeTimer -= time_slice; - if (dischargeTimer <= 0.0f) - { - ResetFiringState(); - } - break; - - case LoadedState: - if (fireEdge) - { - // - // The authentic Loaded->Firing gate: armed in the current look - // view AND a target under the reticle -- a denied shot is the - // one-frame blip, no discharge. - // - if (viewFireEnable && HasActiveTarget()) - { - weaponAlarm.SetLevel(FiringState); - FireWeapon(); - } - else - { - weaponAlarm.SetLevel(TriggerDuringLoadState); - weaponAlarm.SetLevel(LoadedState); - } - } - break; - - case LoadingState: - if (fireEdge) - { - weaponAlarm.SetLevel(TriggerDuringLoadState); - weaponAlarm.SetLevel(LoadingState); - } - // - // THE CHARGE INTEGRATION, sub-stepped at the pod's locked 60 fps: the - // binary's Loading tick assumes fixed frames -- the Loaded transition - // fires while the dial crosses the +-0.01 snap window around the - // selected seek voltage, a window a variable-dt spike can jump clean - // over (the level then overshoots, the over-1 clamp zeroes the dial, - // and the weapon bricks in Loading -- the BT411 weapon-brick fix). - // Charging gates on the electrical Ready state. - // - { - const Scalar kPodFrame = 1.0f / 60.0f; - Scalar remaining = time_slice; - int guard = 600; - while (remaining > 0.0f && guard-- > 0 - && GetWeaponState() == LoadingState) - { - Scalar slice = (remaining < kPodFrame) ? remaining : kPodFrame; - remaining -= slice; - if (GetVoltageState() == Ready) - { - TrackSeekVoltage(slice); - } - ComputeOutputVoltage(); - if (rechargeLevel == 1.0f) - { - weaponAlarm.SetLevel(LoadedState); - if (getenv("BT_MECH_LOG")) - { - DEBUG_STREAM << "[fire] '" << GetName() - << "' LOADED (level=" << chargeLevel - << " T=" << CurrentTemperatureOf() << ")" - << endl << flush; - } - break; - } - // - // Overcharge rescue (a DOCUMENTED DIVERGENCE, BT411 issue #21: - // the binary deadlocks here -- a mid-charge gear change can - // land the level above the new gear's snap window, the over-1 - // clamp zeroes the dial forever, and the weapon bricks. The - // arcade's own math says full == the gear's seek voltage, so - // an overcharged weapon IS loaded). - // - if (chargeLevel > seekVoltage[seekVoltageIndex] - && seekVoltage[seekVoltageIndex] > 0.0f) - { - rechargeLevel = 1.0f; - weaponAlarm.SetLevel(LoadedState); - break; - } - } - } - break; - - default: - break; - } - - Check_Fpu(); -} - -// -//############################################################################# -// CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed. -//############################################################################# -// -int - Emitter::CreateStreamedSubsystem( - ResourceFile *, - NotationFile *, - const char *, - const char *, - SubsystemResource *, - NotationFile *, - const ResourceDirectories *, - int - ) -{ - Fail("Emitter::CreateStreamedSubsystem -- emitter.cpp not yet reconstructed"); - return 0; -} +//===========================================================================// +// File: emitter.cpp // +// Project: BattleTech Brick: Mech weapons // +// Contents: Emitter -- the energy-weapon (beam) base // +//---------------------------------------------------------------------------// +// Copyright (C) 1995, Virtual World Entertainment, Inc. // +// All Rights reserved worldwide // +// This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // +//===========================================================================// + +#include +#pragma hdrstop + +#if !defined(EMITTER_HPP) +# include +#endif + +#if !defined(MECH_HPP) +# include +#endif + +#include + +Derivation + Emitter::ClassDerivations( + MechWeapon::ClassDerivations, + "Emitter" + ); + +// +//############################################################################# +// Attribute Support. Emitter publishes the beam charge level (ChargeLevel, at +// the authentic 0x1D past the MechWeapon table end): the HUD weapon-charge +// gauge binds it, and -- load-bearing -- GaussRifle's AttributeIndex chains +// THIS index (GAUSS.CPP), and PPC::DefaultData binds it via the inherited +// PPC::AttributeIndex. It MUST be a real defined index (a declared-but- +// undefined Emitter::AttributeIndex leaves activeAttributeIndex NULL and +// faults the first GetAttributePointer on any PPC/Gauss). Chained to the +// MechWeapon index so every energy weapon exposes the full weapon table +// (TriggerState / PercentDone / ...). +//############################################################################# +// +const Emitter::IndexEntry + Emitter::AttributePointers[]= +{ + ATTRIBUTE_ENTRY(Emitter, ChargeLevel, chargeLevel) +}; + +Emitter::AttributeIndexSet + Emitter::AttributeIndex( + ELEMENTS(Emitter::AttributePointers), + Emitter::AttributePointers, + MechWeapon::AttributeIndex + ); + +// +//############################################################################# +// The energy-weapon handler table (binary @0x511DB8): ToggleSeekVoltage +// (id 0xb) on top of MechWeapon's config buttons (9/10) -- PPC / GaussRifle +// copy-inherit this set through their DefaultData. +//############################################################################# +// +const Emitter::HandlerEntry + Emitter::MessageHandlerEntries[] = +{ + MESSAGE_ENTRY(Emitter, ToggleSeekVoltage) // id 0xb @004ba478 +}; + +Emitter::MessageHandlerSet + Emitter::MessageHandlers( + ELEMENTS(Emitter::MessageHandlerEntries), + Emitter::MessageHandlerEntries, + MechWeapon::MessageHandlers + ); + +Emitter::SharedData + Emitter::DefaultData( + Emitter::ClassDerivations, + Emitter::MessageHandlers, + Emitter::AttributeIndex, + Subsystem::StateCount + ); + +Emitter::Emitter( + Mech *owner, + int subsystem_ID, + SubsystemResource *subsystem_resource, + SharedData &shared_data +): + MechWeapon(owner, subsystem_ID, subsystem_resource, shared_data) +{ + Check(owner); + Check_Pointer(subsystem_resource); + + chargeLevel = 0.0f; + seekRate = 0.0f; + damagePortion = 0.0f; + heatPortion = 0.0f; + firingActive = 0; + + graphicLength = subsystem_resource->graphicLength; + dischargeTime = subsystem_resource->dischargeTime; + dischargeTimer = dischargeTime; + + // + // Bring-up-safe pre-init: the electrical block below overwrites these with + // the calibrated values when the voltage source is live. + // + energyCoefficient = 1.0f; + seekVoltageIndex = 0; + seekVoltageRecommendedIndex = 0; + minSeekVoltageIndex = 0; + maxSeekVoltageIndex = 0; + { + for (int sv = 0; sv < 5; ++sv) + { + seekVoltage[sv] = 1.0f; + } + } + + // + // energyTotal = (damage + heat) x 1e7 -- the discharge energy in the + // 1e7-native heat units. + // + energyTotal = (damageData.damageAmount + heatCostToFire) * 1.0e7f; + + // + // The SeekVoltage calibration (binary ctor @004bb120): the authored curve + // values are FRACTIONS of the powering generator's rated voltage (-1 + // sentinel ends the list); the energy coefficient pins E = 0.5*V^2*EC to + // energyTotal exactly at the recommended gear; and voltageScale calibrates + // the exponential charge level(t) = Vrated*(1 - exp(-t/(EC*voltageScale))) + // to reach the recommended seek voltage in the authored RechargeRate + // seconds on a COLD generator (0.0001 == 1/Vrated). Generator heat then + // stretches the scale through ChargeTimeScale -- the heat/firepower + // feedback. + // + { + Generator *src = (Generator *)ResolveVoltageSource(); + if (src != NULL) + { + int i; + for (i = 0; i < 5; ++i) + { + seekVoltage[i] = 0.0f; + } + for (i = 0; i < 5; ++i) + { + if (subsystem_resource->seekVoltage[i] == -1.0f) + { + maxSeekVoltageIndex = i - 1; + break; + } + seekVoltage[i] = subsystem_resource->seekVoltage[i] + * src->RatedVoltageOf(); + } + seekVoltageRecommendedIndex = + subsystem_resource->seekVoltageRecommendedIndex; + seekVoltageIndex = seekVoltageRecommendedIndex; + minSeekVoltageIndex = 0; + + Scalar v = seekVoltage[seekVoltageRecommendedIndex]; + energyCoefficient = energyTotal / (v * v * 0.5f); + voltageScale = (rechargeRate + / -(Scalar)log((double)(1.0f - 0.0001f * v))) + / energyCoefficient; + + if (getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[charge] '" << GetName() + << "' seekV={" << seekVoltage[0] << "," << seekVoltage[1] + << "," << seekVoltage[2] << "," << seekVoltage[3] + << "," << seekVoltage[4] << "} rec=" << seekVoltageRecommendedIndex + << " max=" << maxSeekVoltageIndex + << " EC=" << energyCoefficient + << " vScale=" << voltageScale + << " discharge=" << dischargeTime + << " E=" << energyTotal << endl << flush; + } + } + } + + // + // damageFraction = the damage share of the discharge energy. + // + damageFraction = damageData.damageAmount / + (damageData.damageAmount + heatCostToFire); + + // + // Install the beam-weapon fire state machine; spawn LOADING (the charge + // integrates up from zero). (A replicant copy is driven by console + // updates / ServiceDischarge instead -- that path joins the network wave.) + // + weaponAlarm.SetLevel(LoadingState); + if (owner->GetInstance() != Entity::ReplicantInstance) + { + SetPerformance(&Emitter::EmitterSimulation); + } + + Check_Fpu(); +} + +// +//############################################################################# +// TrackSeekVoltage -- integrate the charge from the powering generator +// (binary @004ba838): derive the seek rate from the voltage gap over the +// (heat-stretched) charge time-scale, step the level, and feed the charging +// I^2R loss back into the GENERATOR's heat -- recharging weapons is what +// heats generators, which conduct to their condensers and slow further +// charging through ChargeTimeScale. The heat/firepower feedback loop. +//############################################################################# +// +void + Emitter::TrackSeekVoltage(Scalar time_slice) +{ + Check(this); + + Generator *src = (Generator *)ResolveVoltageSource(); + if (src == NULL) + { + return; + } + + Scalar dtScale = ChargeTimeScale(); + seekRate = (src->MeasuredVoltage() - chargeLevel) / dtScale; + chargeLevel = (seekRate / energyCoefficient) * time_slice + chargeLevel; + + if (HeatModelActive()) + { + src->AddPendingHeat(seekRate * seekRate * dtScale * time_slice); + } +} + +// +//############################################################################# +// ComputeOutputVoltage -- the Emitter override (binary @004ba738): the +// recharge dial = the charge level as a fraction of the selected seek +// voltage, snapped to 1.0 within 0.01 and clamped to [0,1] (the byte-verified +// over-1 clamp zeroes the dial). +//############################################################################# +// +void + Emitter::ComputeOutputVoltage() +{ + Check(this); + + Scalar magnitude = (chargeLevel < 0.0f) ? -chargeLevel : chargeLevel; + if (magnitude > 1.0e-4f) + { + rechargeLevel = chargeLevel / seekVoltage[seekVoltageIndex]; + } + else + { + rechargeLevel = 0.0f; + } + + Scalar snap = rechargeLevel - 1.0f; + if (snap < 0.0f) + { + snap = -snap; + } + if (snap <= 0.01f) + { + rechargeLevel = 1.0f; + } + + if (rechargeLevel < 0.0f) + { + rechargeLevel = 0.0f; + } + else if (rechargeLevel > 1.0f) + { + rechargeLevel = 0.0f; + } +} + +Emitter::~Emitter() +{ +} + +Logical + Emitter::TestClass(Mech &) +{ + return True; +} + +Logical + Emitter::TestInstance() const +{ + return IsDerivedFrom(ClassDerivations); +} + +// +//############################################################################# +// FireWeapon The energy-beam discharge (PARTIAL -- binary @004bace8). The +// authentic body: re-arm the beam-on countdown, compute the per-shot damage / +// heat from the charge energy (0.5*V^2*EC closed forms), dump the heat into +// the inherited thermal accumulator, spend the charge, build the beam +// (muzzle -> target) and submit the Damage record at the owner's target. +// The energy/heat algebra needs the electrical charge model (TrackSeekVoltage +// / seekVoltage / generator -- the powersub wave), and the beam/damage need +// the targeting slot + renderer. This partial performs the DISCHARGE +// bookkeeping -- countdown re-arm, charge spent, recoil loaded so the recharge +// dial animates -- so the fire state machine runs end-to-end. +//############################################################################# +// +void + Emitter::FireWeapon() +{ + Check(this); + + // + // Re-arm the beam-on countdown, then THE AUTHENTIC ENERGY ALGEBRA (binary + // @004bace8): the shot's damage and heat are the two shares of the stored + // charge energy, scaled by the SQUARE of the charge ratio (the level as a + // fraction of the recommended seek voltage) -- an under-charged or + // down-geared shot delivers proportionally less of both. At full charge on + // the recommended gear: damage = the authored DamageAmount verbatim, heat = + // heatCostToFire x 1e7 (the 1e7-native chain). + // + dischargeTimer = dischargeTime; + + Scalar vRec = seekVoltage[seekVoltageRecommendedIndex]; + Scalar chargeRatio = (vRec > 0.0f) ? (chargeLevel / vRec) : 1.0f; + + damagePortion = (damageFraction * energyTotal * 1.0e-7f) + * chargeRatio * chargeRatio; + heatPortion = (1.0f - damageFraction) * energyTotal + * chargeRatio * chargeRatio; + + // + // The damage submission (binary @004bace8 tail): the shot's portion rides + // the inherited Damage record to the owner's target when it is inside the + // weapon's effective range. (The beam build / impact point join the + // render wave.) + // + damageData.damageAmount = damagePortion; + damageData.burstCount = 1; + if (targetWithinRange && owner != NULL + && owner->GetTargetEntity() != NULL) + { + SendDamage(owner->GetTargetEntity(), damageData); + } + + if (HeatModelActive()) + { + AddPendingHeat(heatPortion); + } + + // + // Spend the charge. + // + ComputeOutputVoltage(); + chargeLevel = 0.0f; + firingActive = 1; + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' FIRED (dmg=" << damagePortion + << " heat=" << heatPortion + << " ratio=" << chargeRatio + << " T=" << CurrentTemperatureOf() << ")" << endl << flush; + } +} + +// +//############################################################################# +// ResetFiringState (binary @004ba9a8) -- the beam has finished: drop back to +// Loading so the recharge cycle begins. +//############################################################################# +// +void + Emitter::ResetFiringState() +{ + Check(this); + + firingActive = 0; + weaponAlarm.SetLevel(LoadingState); +} + +// +//############################################################################# +// ToggleSeekVoltage (id 0xb, binary @004ba478): step the charge target up +// through the authored seekVoltage ladder, wrapping to the bottom. Novice- +// locked and heat-model gated (standard cockpits have no seek dial); a step +// UP restarts the charge (the level must climb to the new target), the wrap +// DOWN keeps the weapon Loaded (already past the lower target). +//############################################################################# +// +void + Emitter::ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf *message) +{ + Check(this); + + if (NoviceLockout()) + { + return; + } + if (!HeatModelActive() || message->dataContents <= 0) + { + return; + } + + int previous = seekVoltageIndex; + int modulus = maxSeekVoltageIndex + 1; + seekVoltageIndex = (previous + 1) % modulus; + if (previous < seekVoltageIndex) + { + // + // A step UP re-enters Loading (the stored charge persists and + // climbs to the new, higher target); the wrap DOWN keeps Loaded. + // + ResetFiringState(); + } + + if (getenv("BT_MECH_LOG") || getenv("BT_POWER_LOG")) + { + DEBUG_STREAM << "[seek] '" << GetName() + << "' seek index " << previous << " -> " << seekVoltageIndex + << " target=" << seekVoltage[seekVoltageIndex] + << endl << flush; + } +} + +// +//############################################################################# +// EmitterSimulation The beam-weapon per-frame fire state machine (binary +// @004baa88). The weapon state is carried in the weapon alarm level: +// 0 = Firing, 2 = Loaded (ready), 3 = Loading, 4 = the trigger-during-load +// blip. PARTIAL: the leading PoweredSubsystem electrical step and the +// destroyed / heat-failure / dead-mech hard gates are deferred with the +// power / heat / damage waves; the Loading charge uses the authored +// RechargeRate seconds directly (recoil decay -> ComputeOutputVoltage dial) +// instead of the TrackSeekVoltage generator integration; the Loaded->Firing +// gate honors viewFireEnable (the look-view arm) -- the HasActiveTarget gate +// joins it with the targeting wave. +// +// DEV hook BT_FORCE_FIRE=1: pulses the trigger whenever the weapon is Loaded +// (press while Loaded, release otherwise), so every armed weapon auto-fires at +// its authored recharge cadence -- the headless fire-cycle verification. +//############################################################################# +// +void + Emitter::EmitterSimulation(Scalar time_slice) +{ + Check(this); + + // + // The PoweredSubsystem step first (binary @004baa88 head): the HeatSink + // thermal absorb/conduct + the electrical state machine. + // + PoweredSubsystem::PoweredSubsystemSimulation(time_slice); + + // + // Hard failure (@4baab9): weapon DESTROYED or its own sink at FailureHeat + // -> drop the beam state and the charge, and hold there. Unlike the + // ballistic roach-motel this recovers by itself: once conduction cools the + // sink below the failure threshold the gate stops firing and the weapon + // resumes from Loading. (The owning-mech-disabled half joins with the + // damage wave.) + // + if (GetSimulationState() == 1 || GetHeatState() == FailureHeat) + { + if (getenv("BT_MECH_LOG") && GetWeaponState() != LoadingState) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' THERMAL SHUTDOWN (T=" << CurrentTemperatureOf() + << ")" << endl << flush; + } + ResetFiringState(); + chargeLevel = 0.0f; + ComputeOutputVoltage(); + Check_Fpu(); + return; + } + + { + static int forceFire = -1; + if (forceFire < 0) + { + forceFire = (getenv("BT_FORCE_FIRE") != NULL) ? 1 : 0; + } + if (forceFire) + { + fireImpulse = (GetWeaponState() == LoadedState) ? 1.0f : 0.0f; + } + } + + Logical fireEdge = CheckFireEdge(); + + targetWithinRange = UpdateTargeting(); + + switch (GetWeaponState()) + { + case FiringState: + // + // Count the beam-on timer down; when it expires, drop to Loading. + // + dischargeTimer -= time_slice; + if (dischargeTimer <= 0.0f) + { + ResetFiringState(); + } + break; + + case LoadedState: + if (fireEdge) + { + // + // The authentic Loaded->Firing gate: armed in the current look + // view AND a target under the reticle -- a denied shot is the + // one-frame blip, no discharge. + // + if (viewFireEnable && HasActiveTarget()) + { + weaponAlarm.SetLevel(FiringState); + FireWeapon(); + } + else + { + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadedState); + } + } + break; + + case LoadingState: + if (fireEdge) + { + weaponAlarm.SetLevel(TriggerDuringLoadState); + weaponAlarm.SetLevel(LoadingState); + } + // + // THE CHARGE INTEGRATION, sub-stepped at the pod's locked 60 fps: the + // binary's Loading tick assumes fixed frames -- the Loaded transition + // fires while the dial crosses the +-0.01 snap window around the + // selected seek voltage, a window a variable-dt spike can jump clean + // over (the level then overshoots, the over-1 clamp zeroes the dial, + // and the weapon bricks in Loading -- the BT411 weapon-brick fix). + // Charging gates on the electrical Ready state. + // + { + const Scalar kPodFrame = 1.0f / 60.0f; + Scalar remaining = time_slice; + int guard = 600; + while (remaining > 0.0f && guard-- > 0 + && GetWeaponState() == LoadingState) + { + Scalar slice = (remaining < kPodFrame) ? remaining : kPodFrame; + remaining -= slice; + if (GetVoltageState() == Ready) + { + TrackSeekVoltage(slice); + } + ComputeOutputVoltage(); + if (rechargeLevel == 1.0f) + { + weaponAlarm.SetLevel(LoadedState); + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[fire] '" << GetName() + << "' LOADED (level=" << chargeLevel + << " T=" << CurrentTemperatureOf() << ")" + << endl << flush; + } + break; + } + // + // Overcharge rescue (a DOCUMENTED DIVERGENCE, BT411 issue #21: + // the binary deadlocks here -- a mid-charge gear change can + // land the level above the new gear's snap window, the over-1 + // clamp zeroes the dial forever, and the weapon bricks. The + // arcade's own math says full == the gear's seek voltage, so + // an overcharged weapon IS loaded). + // + if (chargeLevel > seekVoltage[seekVoltageIndex] + && seekVoltage[seekVoltageIndex] > 0.0f) + { + rechargeLevel = 1.0f; + weaponAlarm.SetLevel(LoadedState); + break; + } + } + } + break; + + default: + break; + } + + Check_Fpu(); +} + +// +//############################################################################# +// CreateStreamedSubsystem Model-load-time construction. Not yet reconstructed. +//############################################################################# +// +int + Emitter::CreateStreamedSubsystem( + ResourceFile *, + NotationFile *, + const char *, + const char *, + SubsystemResource *, + NotationFile *, + const ResourceDirectories *, + int + ) +{ + Fail("Emitter::CreateStreamedSubsystem -- emitter.cpp not yet reconstructed"); + return 0; +} diff --git a/restoration/source410/BT/EMITTER.HPP b/restoration/source410/BT/EMITTER.HPP index 6d4b46ff..da59447c 100644 --- a/restoration/source410/BT/EMITTER.HPP +++ b/restoration/source410/BT/EMITTER.HPP @@ -54,6 +54,21 @@ public: static Derivation ClassDerivations; static SharedData DefaultData; + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + // + // The energy-weapon seek-voltage button (binary table @0x511DB8, + // id 0xb, chained after MechWeapon's 9/10; the ammo-weapon branch + // binds 0xb to EjectAmmo in ITS OWN table instead). + // + enum { + ToggleSeekVoltageMessageID = MechWeapon::NextMessageID, // 0xb + NextMessageID + }; + + void + ToggleSeekVoltageMessageHandler(ReceiverDataMessageOf *message); //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute Support. The energy-weapon family publishes past the MechWeapon diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index c23aff1a..e661575d 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -951,6 +951,51 @@ void } } } + // + // BT_PRESS_GEN=1..4: SelectGenerator at the first Emitter + // (the generator panel re-tap); BT_PRESS_SEEK: ToggleSeekVoltage + // at the first Emitter (the seek dial). + // + { + const char *press_gen = getenv("BT_PRESS_GEN"); + const char *press_seek = getenv("BT_PRESS_SEEK"); + if (press_gen != NULL || press_seek != NULL) + { + for (int s = 2; s < subsystemCount; ++s) + { + Subsystem *sub = subsystemArray[s]; + if (sub != NULL + && sub->IsDerivedFrom(Emitter::ClassDerivations)) + { + if (press_gen != NULL) + { + int n = atoi(press_gen); + if (n >= 1 && n <= 4) + { + ReceiverDataMessageOf press( + PoweredSubsystem:: + SelectGeneratorAMessageID + + (n - 1), + sizeof(ReceiverDataMessageOf), + 1 + ); + sub->Dispatch(&press); + } + } + if (press_seek != NULL) + { + ReceiverDataMessageOf press( + Emitter::ToggleSeekVoltageMessageID, + sizeof(ReceiverDataMessageOf), + 1 + ); + sub->Dispatch(&press); + } + break; + } + } + } + } } } diff --git a/restoration/source410/BT/MECHWEAP.CPP b/restoration/source410/BT/MECHWEAP.CPP index 58da218a..2c9dd623 100644 --- a/restoration/source410/BT/MECHWEAP.CPP +++ b/restoration/source410/BT/MECHWEAP.CPP @@ -38,19 +38,26 @@ Derivation // //############################################################################# -// Message handlers. MechWeapon publishes no handlers of its own yet (the -// weapon fire/damage handler wave), but the set MUST be a real defined object: -// the surviving CODE PPC.CPP binds the inherited name (`PPC::MessageHandlers`) -// into PPC::DefaultData, and a declared-but-undefined static links as a -// zero-filled common block -- the same silent-NULL trap as the -// Emitter::AttributeIndex crash (see MECHWEAP.NOTES.md). +// Message handlers (binary ids 9/10 @004b9550/@004b95b8) -- the weapon-group +// config buttons, chained onto the PoweredSubsystem generator panel (4-8), +// which chains HeatSink (3): a weapon's dispatch reaches the whole cockpit +// column. (The set MUST be a real defined object: the surviving CODE +// PPC.CPP binds the inherited name into PPC::DefaultData, and a +// declared-but-undefined static links as a zero-filled common block.) //############################################################################# // +const MechWeapon::HandlerEntry + MechWeapon::MessageHandlerEntries[] = +{ + MESSAGE_ENTRY(MechWeapon, ConfigureMappables), // id 9 @004b9550 + MESSAGE_ENTRY(MechWeapon, ChooseButton) // id 10 @004b95b8 +}; + MechWeapon::MessageHandlerSet MechWeapon::MessageHandlers( - 0, - NULL, - Subsystem::MessageHandlers + ELEMENTS(MechWeapon::MessageHandlerEntries), + MechWeapon::MessageHandlerEntries, + PoweredSubsystem::MessageHandlers ); // @@ -196,6 +203,41 @@ MechWeapon::MechWeapon( Check_Fpu(); } +// +//############################################################################# +// The weapon-group config buttons (ids 9/10). STAGED: the binary bodies +// (@004b9550/@004b95b8) drive the group-assign flow with the HUD config +// page; until that page exists the press is latched (configureActivePress) +// and logged -- the dispatch path itself is the live, authentic part. +//############################################################################# +// +void + MechWeapon::ConfigureMappablesMessageHandler( + ReceiverDataMessageOf *message) +{ + Check(this); + configureActivePress = message->dataContents; + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[btn] '" << GetName() + << "' ConfigureMappables press=" << message->dataContents + << " (staged latch)" << endl << flush; + } +} + +void + MechWeapon::ChooseButtonMessageHandler( + ReceiverDataMessageOf *message) +{ + Check(this); + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[btn] '" << GetName() + << "' ChooseButton press=" << message->dataContents + << " (staged)" << endl << flush; + } +} + MechWeapon::~MechWeapon() { } diff --git a/restoration/source410/BT/MECHWEAP.HPP b/restoration/source410/BT/MECHWEAP.HPP index cb3fc2a8..9cdd2d65 100644 --- a/restoration/source410/BT/MECHWEAP.HPP +++ b/restoration/source410/BT/MECHWEAP.HPP @@ -68,8 +68,24 @@ public: static Derivation ClassDerivations; static SharedData DefaultData; + static const HandlerEntry MessageHandlerEntries[]; static MessageHandlerSet MessageHandlers; + // + // The weapon-group config buttons (binary ids 9/10, chained after + // the PoweredSubsystem generator panel 4-8). + // + enum { + ConfigureMappablesMessageID = PoweredSubsystem::NextMessageID, // 9 + ChooseButtonMessageID, // 10 + NextMessageID // 11 + }; + + void + ConfigureMappablesMessageHandler(ReceiverDataMessageOf *message); + void + ChooseButtonMessageHandler(ReceiverDataMessageOf *message); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Attribute Support. The real IDs are PINNED to the 1995 binary numbering // (table @0x511890, ids 0x12..0x1C): the streamed per-mech control mappings diff --git a/restoration/source410/BT/MECHWEAP.NOTES.md b/restoration/source410/BT/MECHWEAP.NOTES.md index cf6b3018..7a8f67fc 100644 --- a/restoration/source410/BT/MECHWEAP.NOTES.md +++ b/restoration/source410/BT/MECHWEAP.NOTES.md @@ -160,3 +160,22 @@ with burstCount=missileCount (burstCount feeds only gyro bounce + splash falloff — never the zone formula). MISLANCH now dispatches a single SendDamage per salvo (verified 1:1 FIRED:[dmg]). The cluster SPLASH component arrives with the Missile entity wave (proximity-fuse detonation). + +## 5.3.22 — the cockpit weapon-button column + +The full per-receiver message chain is registered (contiguous, no pads): +HeatSink 3 ToggleCooling → PoweredSubsystem 4-8 (SelectGeneratorA-D @004b099c+, +ToggleGeneratorMode @004b0abc — binary table @0x50F4EC) → MechWeapon 9/10 +(ConfigureMappables/ChooseButton @004b9550/@004b95b8 — STAGED press-latch +bodies, the group-config flow needs the HUD page) → Emitter 0xb +ToggleSeekVoltage (@004ba478, table @0x511DB8; the ammo branch's 0xb +EjectAmmo is deferred — its absence is safe, no gap slot below a registered +max). SelectGenerator = detach old tap → FindGeneratorByNumber roster walk → +AttachToVoltageSource → modeAlarm Connected; ToggleGeneratorMode cycles +Manual→AutoConnect→(detach)→Manual (the auto-hunt itself = a +PoweredSubsystemSimulation brick). ToggleSeekVoltage steps the seek ladder +with wrap; a step UP re-enters Loading (charge persists and climbs). +All novice-locked. Dev: BT_PRESS_GEN=1..4 / BT_PRESS_SEEK (~6s dispatch at +the first Emitter). Verified: PPC_1 → GeneratorB (tapped); seek 2→3 target +9900V; novice presses inert; fight/smoke green. Static-init order is safe +under the authentic .MAK lib order (heat→powersub→mechweap→emitter). diff --git a/restoration/source410/BT/POWERSUB.CPP b/restoration/source410/BT/POWERSUB.CPP index fab25026..3975f7cb 100644 --- a/restoration/source410/BT/POWERSUB.CPP +++ b/restoration/source410/BT/POWERSUB.CPP @@ -30,10 +30,35 @@ Derivation "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 + ); + PoweredSubsystem::SharedData PoweredSubsystem::DefaultData( PoweredSubsystem::ClassDerivations, - Subsystem::MessageHandlers, + PoweredSubsystem::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); @@ -179,6 +204,171 @@ int 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): diff --git a/restoration/source410/BT/POWERSUB.HPP b/restoration/source410/BT/POWERSUB.HPP index e5fe25a4..b652a86f 100644 --- a/restoration/source410/BT/POWERSUB.HPP +++ b/restoration/source410/BT/POWERSUB.HPP @@ -86,6 +86,46 @@ // Power support // public: + // + // The cockpit generator-select buttons (binary handler table + // @0x50F4EC, ids 4-8, chained onto the HeatSink set so id 3 + // ToggleCooling stays reachable through every powered subsystem). + // + enum { + SelectGeneratorAMessageID = HeatSink::NextMessageID, // 4 + SelectGeneratorBMessageID, // 5 + SelectGeneratorCMessageID, // 6 + SelectGeneratorDMessageID, // 7 + ToggleGeneratorModeMessageID, // 8 + NextMessageID // 9 + }; + + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + void + SelectGeneratorAMessageHandler(ReceiverDataMessageOf *message); + void + SelectGeneratorBMessageHandler(ReceiverDataMessageOf *message); + void + SelectGeneratorCMessageHandler(ReceiverDataMessageOf *message); + void + SelectGeneratorDMessageHandler(ReceiverDataMessageOf *message); + void + ToggleGeneratorModeMessageHandler(ReceiverDataMessageOf *message); + + // + // The manual generator re-tap (binary @004b0b18/@004b0dd8 family): + // find generator N in the owner's roster, release the current tap, + // tap the new source, drop the connect mode to Connected. + // + void + SelectGenerator(int generator_number); + Subsystem* + FindGeneratorByNumber(int generator_number); + void + DetachFromVoltageSource(); + Subsystem* ResolveVoltageSource() { return voltageSource.Resolve(); } int @@ -309,6 +349,15 @@ ++currentTapCount; return 0; } + void + UntapVoltageSource() + { + Check(this); + if (currentTapCount > 0) + { + --currentTapCount; + } + } typedef void (Generator::*Performance)(Scalar time_slice);