From c01e57ab22ffdae47a5372aa9fe0a297be722372 Mon Sep 17 00:00:00 2001 From: Cyd Date: Fri, 24 Jul 2026 08:43:01 -0500 Subject: [PATCH] BT410 Phase 5.3.13: cockpit-button message layer -- valves, cooling, flush LIVE The subsystem-family cockpit buttons now work through the authentic Receiver dispatch -> per-class handler-table path. The id space decodes cleanly: Receiver::NextMessageID == 3, so ToggleCooling = 3 on the HeatSink chain and the per-class id 4 is MoveValve on a Condenser / InjectCoolant on the Reservoir -- same number, different class, the binary's per-receiver-class convention. - HeatSink::ToggleCoolingMessageHandler (@004ad6f8, id 3): novice-locked, press-only; toggles coolantAvailable + coolantFlowScale together. - Condenser::MoveValveMessageHandler (@4ae464, id 4): novice-locked; cycles the valve 1->5->50->0->1 and calls Condenser::RecomputeValves (@0049f788): every condenser's coolantFlowScale = valve / sum-of-valves. The ctor now streams the AUTHENTIC flowScale=0; the Mech ctor seeds equal shares once at spawn -- the flowScale=1 interim is retired. - Reservoir::InjectCoolantMessageHandler (@4aee70, id 4): novice-locked; press arms the flush when the tank holds charge, release drops it. - MechSubsystem::NoviceLockout() (@4ac9c8): owner -> playerLink -> experience == novice; unlinked mechs read unlocked. - DEV harness BT_PRESS_VALVE / BT_PRESS_FLUSH: dispatch the REAL messages ~6s into the mission from Mech::Simulate. VERIFIED: spawn shares 6 x 0.166667; one MoveValve press -> Condenser1 valve=5 flow=0.5, others 0.1 (valve/sum exact); flush arms via the real button message; NOVICE locks both presses (valve lines stay spawn-only, zero flush). Zero Fail throughout. Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/HEAT.CPP | 172 +++++++++++++++++++++++-- restoration/source410/BT/HEAT.HPP | 44 +++++++ restoration/source410/BT/HEAT.NOTES.md | 32 +++++ restoration/source410/BT/MECH.CPP | 59 +++++++++ restoration/source410/BT/MECHSUB.CPP | 34 +++++ restoration/source410/BT/MECHSUB.HPP | 9 ++ restoration/source410/BT/RESERVR.CPP | 58 ++++++++- restoration/source410/BT/RESERVR.HPP | 17 +++ 8 files changed, 416 insertions(+), 9 deletions(-) diff --git a/restoration/source410/BT/HEAT.CPP b/restoration/source410/BT/HEAT.CPP index 2ab3d865..2a361ff7 100644 --- a/restoration/source410/BT/HEAT.CPP +++ b/restoration/source410/BT/HEAT.CPP @@ -154,10 +154,26 @@ Derivation "HeatSink" ); +// +// The cockpit cooling toggle (id 3 -- the first subsystem-family message). +// +const HeatSink::HandlerEntry + HeatSink::MessageHandlerEntries[]= +{ + MESSAGE_ENTRY(HeatSink, ToggleCooling) +}; + +HeatSink::MessageHandlerSet + HeatSink::MessageHandlers( + ELEMENTS(HeatSink::MessageHandlerEntries), + HeatSink::MessageHandlerEntries, + Subsystem::MessageHandlers + ); + HeatSink::SharedData HeatSink::DefaultData( HeatSink::ClassDerivations, - Subsystem::MessageHandlers, + HeatSink::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); @@ -603,6 +619,47 @@ Scalar return 0.0f; } +// +//############################################################################# +// ToggleCoolingMessageHandler -- the cockpit cooling on/off switch (binary +// @004ad6f8, id 3): novice-locked, press-only; toggles the coolant supply and +// the conduction flow together (OFF = this sink stops conducting into the +// bank -- its heat strands until cooling is switched back on). +//############################################################################# +// +void + HeatSink::ToggleCoolingMessageHandler(ReceiverDataMessageOf *message) +{ + Check(this); + Check(message); + + if (NoviceLockout()) + { + return; + } + if (message->dataContents <= 0) // press only + { + return; + } + + if (coolantAvailable != 0) + { + coolantAvailable = 0; + coolantFlowScale = 0.0f; + } + else + { + coolantAvailable = 1; + coolantFlowScale = 1.0f; + } + + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[cool] '" << GetName() + << "' cooling toggled -> " << coolantAvailable << endl << flush; + } +} + //########################################################################### //############################# HeatWatcher ############################# //########################################################################### @@ -693,10 +750,28 @@ Derivation "Condenser" ); +// +// The cockpit condenser-valve button (the binary's Condenser handler table +// has exactly ONE entry: id 4, "MoveValve"). ToggleCooling (id 3) is +// inherited from the HeatSink chain. +// +const Condenser::HandlerEntry + Condenser::MessageHandlerEntries[]= +{ + MESSAGE_ENTRY(Condenser, MoveValve) +}; + +Condenser::MessageHandlerSet + Condenser::MessageHandlers( + ELEMENTS(Condenser::MessageHandlerEntries), + Condenser::MessageHandlerEntries, + HeatSink::MessageHandlers + ); + Condenser::SharedData Condenser::DefaultData( Condenser::ClassDerivations, - Subsystem::MessageHandlers, + Condenser::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); @@ -715,14 +790,12 @@ Condenser::Condenser( // // The authentic ctor (binary @4ae568): the valve opens at 1, the // refrigeration output rides the inherited massScale slot, and the - // condenser's own coolantFlowScale streams 0 (the valve recompute -- - // BTRecomputeCondenserValves, the cockpit MoveValve wave -- assigns each - // condenser its share of the total valve opening). INTERIM: flow scale is - // left at the inherited 1.0 until the valve-recompute wave lands -- a zero - // flow scale would zero the condenser->bank conduction exponent and strand - // the heat in the condensers with no way to reach the central sink. + // condenser's own coolantFlowScale starts 0 -- the valve recompute + // (RecomputeValves: MoveValve presses, and once at mech spawn) assigns + // each condenser its share of the total valve opening. // valveState = 1; + coolantFlowScale = 0.0f; refrigerationFactor = subsystem_resource->refrigerationFactor; massScale = refrigerationFactor; @@ -790,3 +863,86 @@ void HeatSink::HeatSinkSimulation(time_slice); } + +// +//############################################################################# +// MoveValveMessageHandler -- the cockpit condenser-valve button (binary +// @4ae464, id 4): novice-locked, press-only; cycles THIS condenser's valve +// 1 -> 5 -> 50 -> 0 -> 1, then recomputes every condenser's flow share. +//############################################################################# +// +void + Condenser::MoveValveMessageHandler(ReceiverDataMessageOf *message) +{ + Check(this); + Check(message); + + if (NoviceLockout()) + { + return; + } + if (message->dataContents <= 0) // press only + { + return; + } + + switch (valveState) + { + case 0: valveState = 1; break; + case 1: valveState = 5; break; + case 5: valveState = 50; break; + case 50: valveState = 0; break; + default: return; // unknown -> no change, no recompute + } + + RecomputeValves((Entity *)owner); +} + +// +//############################################################################# +// RecomputeValves -- assign every condenser its coolant flow share +// (valve / sum-of-valves) after a valve move (binary @0049f788). All valves +// at the spawn default (1) yield equal shares. (The condenser-alarm change +// pulse joins with the gauge wave.) +//############################################################################# +// +void + Condenser::RecomputeValves(Entity *owner_mech) +{ + Check(owner_mech); + + int count = owner_mech->GetSubsystemCount(); + int total = 0; + int i; + + for (i = 2; i < count; ++i) + { + Subsystem *s = owner_mech->GetSubsystem(i); + if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) + { + total += ((Condenser *)s)->valveState; + } + } + + for (i = 2; i < count; ++i) + { + Subsystem *s = owner_mech->GetSubsystem(i); + if (s != NULL && s->IsDerivedFrom(Condenser::ClassDerivations)) + { + Condenser *condenser = (Condenser *)s; + condenser->coolantFlowScale = (total > 0) + ? ((Scalar)condenser->valveState / (Scalar)total) + : 0.0f; + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[valve] '" << condenser->GetName() + << "' valve=" << condenser->valveState + << " flow=" << condenser->coolantFlowScale << endl; + } + } + } + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << flush; + } +} diff --git a/restoration/source410/BT/HEAT.HPP b/restoration/source410/BT/HEAT.HPP index e920da88..fb61bcb5 100644 --- a/restoration/source410/BT/HEAT.HPP +++ b/restoration/source410/BT/HEAT.HPP @@ -219,6 +219,24 @@ void ResetToInitialState(Logical powered); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Messaging Support. The cockpit cooling toggle rides the first + // subsystem-family message slot (id 3 = Receiver::NextMessageID); the + // per-class id-4 slot is claimed by each subclass (Condenser MoveValve, + // Reservoir InjectCoolant, ...). + // + public: + enum { + ToggleCoolingMessageID = Receiver::NextMessageID, // 3 + NextMessageID // 4 + }; + + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + void + ToggleCoolingMessageHandler(ReceiverDataMessageOf *message); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Heat state (carried in heatAlarm, 3 levels). The thresholds are the // authored degradation/failure temperatures. @@ -342,6 +360,32 @@ Logical TestInstance() const; + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Messaging Support: the cockpit condenser-valve button (binary handler + // table @0x50E52C, exactly one entry -- id 4, "MoveValve"). Each press + // cycles this condenser's valve 1 -> 5 -> 50 -> 0 -> 1 and recomputes every + // condenser's coolant flow share (valve / sum-of-valves). + // + public: + enum { + MoveValveMessageID = HeatSink::NextMessageID, // 4 + NextMessageID + }; + + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + void + MoveValveMessageHandler(ReceiverDataMessageOf *message); + + // + // Recompute every condenser's coolantFlowScale as its share of the + // total valve opening (called by MoveValve, and once at mech spawn so + // the initial all-1 valves yield equal shares). + // + static void + RecomputeValves(Entity *owner_mech); + public: typedef Condenser__SubsystemResource SubsystemResource; diff --git a/restoration/source410/BT/HEAT.NOTES.md b/restoration/source410/BT/HEAT.NOTES.md index 72508d54..820c49ee 100644 --- a/restoration/source410/BT/HEAT.NOTES.md +++ b/restoration/source410/BT/HEAT.NOTES.md @@ -168,3 +168,35 @@ family (the central bank's count/radiator loop + reservoir attach + capacity rescale + its forward-linked drain), the MoveValve / ToggleCooling / InjectCoolant cockpit-button message handlers + BTRecomputeCondenserValves, Myomers movement heat, jam recovery via ResetToInitialState (mission reset). + +## The cockpit-button message layer (Phase 5.3.13, 2026-07-24) + +The subsystem-family button messages are LIVE through the authentic Receiver +dispatch -> per-class handler-table path (the id space decodes cleanly: +Receiver::NextMessageID == 3, so ToggleCooling = 3 on the HeatSink chain and +the per-class id 4 is MoveValve on a Condenser / InjectCoolant on the +Reservoir -- the same number, different class, exactly the binary's +per-receiver-class convention): + +- **HeatSink::ToggleCoolingMessageHandler** (@004ad6f8, id 3): novice-locked, + press-only; toggles coolantAvailable + coolantFlowScale together (OFF + strands this sink's heat until switched back). +- **Condenser::MoveValveMessageHandler** (@4ae464, id 4): novice-locked, + press-only; cycles the valve 1 -> 5 -> 50 -> 0 -> 1 and calls + **Condenser::RecomputeValves** (@0049f788): every condenser's + coolantFlowScale = valve / sum-of-valves. The Condenser ctor now streams + the authentic flowScale = 0; the Mech ctor seeds the shares once at spawn + (all valves 1 -> equal shares) -- retiring the flowScale=1 interim. (The + condenser-alarm change pulse joins the gauge wave.) +- **Reservoir::InjectCoolantMessageHandler** (@4aee70, id 4): novice-locked; + press raises the inject alarm when the tank holds charge (flush-cloud psfx + deferred), release drops it. +- **MechSubsystem::NoviceLockout()** (@4ac9c8): owner -> playerLink -> + experience == novice; unlinked mechs read unlocked. +- DEV harness: BT_PRESS_VALVE / BT_PRESS_FLUSH dispatch the REAL messages + ~6 s in (Mech::Simulate). + +VERIFIED: spawn shares 6 x 0.166667; one MoveValve press -> Condenser1 +valve=5 flow=0.5, the others 0.1 (valve/sum exactly); the flush arms via the +real button message; NOVICE: both presses swallowed by the lockout (valve +lines stay spawn-only, zero flush). Zero Fail throughout. diff --git a/restoration/source410/BT/MECH.CPP b/restoration/source410/BT/MECH.CPP index c7e3de09..f4c3efcb 100644 --- a/restoration/source410/BT/MECH.CPP +++ b/restoration/source410/BT/MECH.CPP @@ -454,6 +454,13 @@ Mech::Mech( } } + // + // Seed the condenser flow shares: every valve spawns at 1, so the initial + // recompute yields equal shares across the bank (a MoveValve press + // re-shares them). + // + Condenser::RecomputeValves(this); + // // Install the per-frame body Performance. Until now the mech ran the base // DoNothingOnce; from here the engine dispatches Mech::Simulate every frame @@ -775,6 +782,58 @@ void worldLinearVelocity.y = -zAxis.y * currentBodySpeed; worldLinearVelocity.z = -zAxis.z * currentBodySpeed; + // + // DEV cockpit-button harness: BT_PRESS_VALVE / BT_PRESS_FLUSH dispatch the + // REAL cockpit messages (MoveValve to Condenser1, InjectCoolant to the + // Reservoir) once, ~6 s in -- exercising the authentic Receiver dispatch -> + // handler-table path headlessly. + // + { + static Scalar pressClock = 0.0f; + static int pressed = 0; + pressClock += time_slice; + if (!pressed && pressClock >= 6.0f) + { + pressed = 1; + if (getenv("BT_PRESS_VALVE")) + { + for (int s = 2; s < subsystemCount; ++s) + { + Subsystem *sub = subsystemArray[s]; + if (sub != NULL + && sub->IsDerivedFrom(Condenser::ClassDerivations)) + { + ReceiverDataMessageOf press( + Condenser::MoveValveMessageID, + sizeof(ReceiverDataMessageOf), + 1 + ); + sub->Dispatch(&press); + break; // one press, the first condenser + } + } + } + if (getenv("BT_PRESS_FLUSH")) + { + for (int s = 2; s < subsystemCount; ++s) + { + Subsystem *sub = subsystemArray[s]; + if (sub != NULL + && sub->IsDerivedFrom(Reservoir::ClassDerivations)) + { + ReceiverDataMessageOf press( + Reservoir::InjectCoolantMessageID, + sizeof(ReceiverDataMessageOf), + 1 + ); + sub->Dispatch(&press); + break; + } + } + } + } + } + // // DEV override: BT_DRIVE forces a raw world velocity (bypasses the demands, // for the pure integrate/transform test). diff --git a/restoration/source410/BT/MECHSUB.CPP b/restoration/source410/BT/MECHSUB.CPP index be832246..ebba69dc 100644 --- a/restoration/source410/BT/MECHSUB.CPP +++ b/restoration/source410/BT/MECHSUB.CPP @@ -23,6 +23,14 @@ # include #endif +#if !defined(BTPLAYER_HPP) +# include +#endif + +#if !defined(BTMSSN_HPP) +# include +#endif + // //############################################################################# // Shared data support -- MechSubsystem adds no message handlers or attributes @@ -216,3 +224,29 @@ int Fail("MechSubsystem::CreateStreamedSubsystem -- mechsub.cpp not yet reconstructed"); return 0; } + +// +//############################################################################# +// NoviceLockout -- the novice-experience lockout predicate (binary @4ac9c8): +// owner mech -> Entity::playerLink -> the BTPlayer's experience level == 0. +// Locks the advanced cockpit controls (condenser valves, coolant flush, +// cooling toggles). An unlinked mech reads UNLOCKED (dev-permissive). +//############################################################################# +// +Logical + MechSubsystem::NoviceLockout() +{ + Check(this); + + if (owner == NULL) + { + return False; + } + BTPlayer *player = Cast_Object(BTPlayer *, owner->GetPlayerLink()); + if (player == NULL) + { + return False; + } + return (player->GetExperienceLevel() == BTMission::NoviceMode) + ? True : False; +} diff --git a/restoration/source410/BT/MECHSUB.HPP b/restoration/source410/BT/MECHSUB.HPP index facbe90d..5e4db625 100644 --- a/restoration/source410/BT/MECHSUB.HPP +++ b/restoration/source410/BT/MECHSUB.HPP @@ -132,6 +132,15 @@ void ForceCriticalFailure(); + // + // The NOVICE-experience lockout predicate (binary @4ac9c8): a novice + // pilot's advanced cockpit controls (condenser valves, coolant flush, + // cooling toggles) are locked out. Owner mech -> playerLink -> + // experience level == novice. Unlinked mechs read UNLOCKED. + // + Logical + NoviceLockout(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Local Data // diff --git a/restoration/source410/BT/RESERVR.CPP b/restoration/source410/BT/RESERVR.CPP index c200e1bc..c78fa6c3 100644 --- a/restoration/source410/BT/RESERVR.CPP +++ b/restoration/source410/BT/RESERVR.CPP @@ -31,10 +31,27 @@ Derivation "Reservoir" ); +// +// The cockpit coolant-flush button (id 4, "InjectCoolant"); ToggleCooling +// (id 3) is inherited from the HeatSink chain. +// +const Reservoir::HandlerEntry + Reservoir::MessageHandlerEntries[]= +{ + MESSAGE_ENTRY(Reservoir, InjectCoolant) +}; + +Reservoir::MessageHandlerSet + Reservoir::MessageHandlers( + ELEMENTS(Reservoir::MessageHandlerEntries), + Reservoir::MessageHandlerEntries, + HeatSink::MessageHandlers + ); + Reservoir::SharedData Reservoir::DefaultData( Reservoir::ClassDerivations, - Subsystem::MessageHandlers, + Reservoir::MessageHandlers, Subsystem::AttributeIndex, Subsystem::StateCount ); @@ -129,6 +146,45 @@ Scalar return supplied; } +// +//############################################################################# +// InjectCoolantMessageHandler -- the cockpit coolant-flush button (binary +// @4aee70, id 4): novice-locked. Release drops the inject alarm (flush OFF); +// press raises it when the tank holds charge (the flush-cloud effect joins +// the psfx wave) and zeroes the elapsed accumulator. +//############################################################################# +// +void + Reservoir::InjectCoolantMessageHandler(ReceiverDataMessageOf *message) +{ + Check(this); + Check(message); + + if (NoviceLockout()) + { + return; + } + + if (message->dataContents < 1) + { + reservoirAlarm.SetLevel(0); // release -> flush OFF + } + else + { + if (reservoirAlarm.GetLevel() != 1 && coolantLevel > 0.0f) + { + reservoirAlarm.SetLevel(1); // flush ON + if (getenv("BT_MECH_LOG")) + { + DEBUG_STREAM << "[resv] FLUSH ON via button (charge=" + << coolantLevel << ")" << endl << flush; + } + } + injectAccumulator = 0.0f; + } + ForceUpdate(); +} + // //############################################################################# // CoolantSimulation -- the registered Performance (binary @4aef78): while diff --git a/restoration/source410/BT/RESERVR.HPP b/restoration/source410/BT/RESERVR.HPP index 2154cca8..1be0bf9b 100644 --- a/restoration/source410/BT/RESERVR.HPP +++ b/restoration/source410/BT/RESERVR.HPP @@ -66,6 +66,23 @@ ); ~Reservoir(); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Messaging Support: the cockpit coolant-flush button (id 4, + // "InjectCoolant"). Press starts the flush (raises the inject alarm); + // release stops it. Novice-locked. + // + public: + enum { + InjectCoolantMessageID = HeatSink::NextMessageID, // 4 + NextMessageID + }; + + static const HandlerEntry MessageHandlerEntries[]; + static MessageHandlerSet MessageHandlers; + + void + InjectCoolantMessageHandler(ReceiverDataMessageOf *message); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // The coolant-store interface. //