From 0d97d8c38b05bb24892634ce91a9c638a5a4df65 Mon Sep 17 00:00:00 2001 From: Cyd Date: Tue, 28 Jul 2026 02:42:43 -0500 Subject: [PATCH] BT410 5.3.45: six more watcher names published; the Reservoir one CRASHES (bisected) Published and verified green on the pod rig: Condenser/CondenserState, Generator/GeneratorState, Emitter/LaserOn, ControlsMapper/TargetRangeExponent, plus the Torso and Myomers tables from the previous pass. Reservoir/ReservoirState CRASHES the pod on the DOS extender, and a bisect pins it to exactly that change: revert it and the run returns to a clean Fail, re-apply it alone and the crash returns. It is reverted; the tree is green and the ladder is blocked there. The important lesson is general: AttributeWatcherOf does currentValue = *(T*)attributePointer AT CONSTRUCTION, so a published name is read the moment its watcher is built. My earlier staging note claimed the provisional types could not matter because nothing drives the values yet -- that is wrong, and this is the counterexample. Ruled out by measurement and recorded so they are not retried: the AlarmIndicator-vs-int type (a plain int got further, 232 -> 749 bytes of log, but still crashed), static-init order (reservr.obj sorts last, after heat), an id gap (contiguous at HeatSink::NextAttributeID), and the gauge rig (same binary runs clean there -- only the pod/arena context faults). Crash signature for whoever picks it up: 0044B4AD, mov eax,[edx+0x18] then call [eax+4], EAX=0x15, fault at 0x19 -- a small integer called through as an object, i.e. the AttributeIndexSet::Build uninitialised-slot pattern. Condenser is the control: same base class, plain int, no crash. Also fixed on the way: staged members must be appended at the END of a class (offset-sensitive readers exist -- condenserNumber is reached as master+0x1d4) and never added to a resource struct, which is a wire format. Co-Authored-By: Claude Fable 5 --- restoration/source410/BT/EMITTER.CPP | 4 +- restoration/source410/BT/EMITTER.HPP | 2 + restoration/source410/BT/HEAT.CPP | 4 +- restoration/source410/BT/HEAT.HPP | 8 ++++ restoration/source410/BT/MECHMPPR.CPP | 5 ++- restoration/source410/BT/MECHMPPR.HPP | 8 +++- restoration/source410/BT/POWERSUB.CPP | 4 +- restoration/source410/BT/POWERSUB.HPP | 2 + restoration/source410/BT/TORSO.HPP | 2 +- restoration/source410/RENDER-ROADMAP.NOTES.md | 41 +++++++++++++++++++ 10 files changed, 74 insertions(+), 6 deletions(-) diff --git a/restoration/source410/BT/EMITTER.CPP b/restoration/source410/BT/EMITTER.CPP index a3918b56..7f68f579 100644 --- a/restoration/source410/BT/EMITTER.CPP +++ b/restoration/source410/BT/EMITTER.CPP @@ -43,7 +43,8 @@ Derivation const Emitter::IndexEntry Emitter::AttributePointers[]= { - ATTRIBUTE_ENTRY(Emitter, ChargeLevel, chargeLevel) + ATTRIBUTE_ENTRY(Emitter, ChargeLevel, chargeLevel), + ATTRIBUTE_ENTRY(Emitter, LaserOn, laserOn) }; Emitter::AttributeIndexSet @@ -93,6 +94,7 @@ Emitter::Emitter( Check_Pointer(subsystem_resource); chargeLevel = 0.0f; + laserOn = 0; // staged: bound by an authored watcher seekRate = 0.0f; damagePortion = 0.0f; heatPortion = 0.0f; diff --git a/restoration/source410/BT/EMITTER.HPP b/restoration/source410/BT/EMITTER.HPP index da59447c..6ad65ba1 100644 --- a/restoration/source410/BT/EMITTER.HPP +++ b/restoration/source410/BT/EMITTER.HPP @@ -77,6 +77,7 @@ public: enum { ChargeLevelAttributeID = MechWeapon::NextAttributeID, // 0x1D + LaserOnAttributeID, // authored watcher: /LaserOn NextAttributeID }; @@ -164,6 +165,7 @@ Scalar energyCoefficient; Scalar energyTotal; Scalar damageFraction; + int laserOn; // staged: authored watcher /LaserOn Scalar damagePortion; Scalar heatPortion; int firingActive; diff --git a/restoration/source410/BT/HEAT.CPP b/restoration/source410/BT/HEAT.CPP index 694ee037..663a76cb 100644 --- a/restoration/source410/BT/HEAT.CPP +++ b/restoration/source410/BT/HEAT.CPP @@ -842,7 +842,8 @@ Condenser::MessageHandlerSet const Condenser::IndexEntry Condenser::AttributePointers[]= { - ATTRIBUTE_ENTRY(Condenser, ValveSetting, coolantFlowScale) + ATTRIBUTE_ENTRY(Condenser, ValveSetting, coolantFlowScale), + ATTRIBUTE_ENTRY(Condenser, CondenserState, condenserState) }; Condenser::AttributeIndexSet @@ -879,6 +880,7 @@ Condenser::Condenser( // each condenser its share of the total valve opening. // valveState = 1; + condenserState = 0; // staged: bound by an authored watcher coolantFlowScale = 0.0f; refrigerationFactor = subsystem_resource->refrigerationFactor; massScale = refrigerationFactor; diff --git a/restoration/source410/BT/HEAT.HPP b/restoration/source410/BT/HEAT.HPP index 6a5c4819..f99a1415 100644 --- a/restoration/source410/BT/HEAT.HPP +++ b/restoration/source410/BT/HEAT.HPP @@ -423,6 +423,7 @@ public: enum { ValveSettingAttributeID = HeatSink::NextAttributeID, // 0x0A + CondenserStateAttributeID, // authored watcher: CondenserN/CondenserState NextAttributeID }; @@ -504,6 +505,13 @@ int valveState; int condenserNumber; Scalar refrigerationFactor; + // + // APPENDED LAST ON PURPOSE. This class has offset-sensitive + // readers -- condenserNumber is reached as master+0x1d4 -- so a new + // member goes at the END of the CLASS (never into the resource + // struct above, which is a wire format). + // + int condenserState; // authored watcher: CondenserN/CondenserState }; //########################################################################### diff --git a/restoration/source410/BT/MECHMPPR.CPP b/restoration/source410/BT/MECHMPPR.CPP index c20147d5..76ab86fd 100644 --- a/restoration/source410/BT/MECHMPPR.CPP +++ b/restoration/source410/BT/MECHMPPR.CPP @@ -60,7 +60,9 @@ const MechControlsMapper::IndexEntry ATTRIBUTE_ENTRY(MechControlsMapper, ControlMode, controlMode), ATTRIBUTE_ENTRY(MechControlsMapper, DisplayMode, displayMode), ATTRIBUTE_ENTRY(MechControlsMapper, PilotArrayPage, pilotArrayPage), - ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray, pilotArray) + ATTRIBUTE_ENTRY(MechControlsMapper, PilotArray, pilotArray), + ATTRIBUTE_ENTRY(MechControlsMapper, TargetRangeExponent, + targetRangeExponent) }; MechControlsMapper::AttributeIndexSet @@ -119,6 +121,7 @@ MechControlsMapper::MechControlsMapper( displayMode = 0; pilotArrayPage = 0; pilotArray = 0; + targetRangeExponent = 0.0f; // staged: bound by an authored watcher lookState = LookNone; previousLookState = LookNone; { diff --git a/restoration/source410/BT/MECHMPPR.HPP b/restoration/source410/BT/MECHMPPR.HPP index e38b8cc9..a3f16891 100644 --- a/restoration/source410/BT/MECHMPPR.HPP +++ b/restoration/source410/BT/MECHMPPR.HPP @@ -78,6 +78,7 @@ DisplayModeAttributeID, // 14 PilotArrayPageAttributeID, // 15 PilotArrayAttributeID, // 16 + TargetRangeExponentAttributeID, // authored watcher NextAttributeID }; @@ -188,7 +189,12 @@ int pilotArray; int lookState; int previousLookState; - int reserved[22]; + // + // Takes one reserved word (Scalar and int are both 4 bytes here), so + // the class size and every existing offset stay exactly as they were. + // + Scalar targetRangeExponent; // authored watcher + int reserved[21]; }; #endif diff --git a/restoration/source410/BT/POWERSUB.CPP b/restoration/source410/BT/POWERSUB.CPP index 0f660557..6589d003 100644 --- a/restoration/source410/BT/POWERSUB.CPP +++ b/restoration/source410/BT/POWERSUB.CPP @@ -595,7 +595,8 @@ const Generator::IndexEntry { ATTRIBUTE_ENTRY(Generator, OutputVoltage, outputVoltage), ATTRIBUTE_ENTRY(Generator, RatedVoltage, ratedVoltage), - ATTRIBUTE_ENTRY(Generator, GeneratorNumber, generatorNumber) + ATTRIBUTE_ENTRY(Generator, GeneratorNumber, generatorNumber), + ATTRIBUTE_ENTRY(Generator, GeneratorState, generatorState) }; Generator::AttributeIndexSet @@ -639,6 +640,7 @@ Generator::Generator( startTimer = startTime; stateAlarm.SetLevel(GeneratorReady); generatorOn = 1; + generatorState = 0; // staged shortRecoveryTime = subsystem_resource->shortRecoveryTime; shortTimer = shortRecoveryTime; diff --git a/restoration/source410/BT/POWERSUB.HPP b/restoration/source410/BT/POWERSUB.HPP index 4200ad0c..3800db9f 100644 --- a/restoration/source410/BT/POWERSUB.HPP +++ b/restoration/source410/BT/POWERSUB.HPP @@ -354,6 +354,7 @@ OutputVoltageAttributeID = HeatSink::NextAttributeID, // 0x0A RatedVoltageAttributeID, // 0x0B GeneratorNumberAttributeID, // 0x0C + GeneratorStateAttributeID, // authored watcher: GeneratorX/GeneratorState NextAttributeID }; @@ -478,6 +479,7 @@ Scalar shortRecoveryTime; Scalar shortTimer; AlarmIndicator stateAlarm; + int generatorState; // authored watcher: plain int, see RESERVR.HPP }; #endif diff --git a/restoration/source410/BT/TORSO.HPP b/restoration/source410/BT/TORSO.HPP index 6a847199..4cd15a96 100644 --- a/restoration/source410/BT/TORSO.HPP +++ b/restoration/source410/BT/TORSO.HPP @@ -136,7 +136,6 @@ static AttributeIndexSet AttributeIndex; protected: - int motionState; // staged: no gait state feed yet int isDamagedCopy; int statusFlags; Logical horizontalEnabled; @@ -160,6 +159,7 @@ Scalar currentElevation; Scalar analogElevationAxis; Scalar analogTwistAxis; + int motionState; // staged: authored watcher Torso/MotionState // // Twist/elevation dynamics accumulators advanced by TorsoSimulation. // Reserved until the per-frame sim is reconstructed (phase 5). diff --git a/restoration/source410/RENDER-ROADMAP.NOTES.md b/restoration/source410/RENDER-ROADMAP.NOTES.md index bf6310c0..199e400e 100644 --- a/restoration/source410/RENDER-ROADMAP.NOTES.md +++ b/restoration/source410/RENDER-ROADMAP.NOTES.md @@ -316,3 +316,44 @@ deliberate session with the A/B rig open, not a bulk edit at the end of a long one. Neither HeatLoad nor CoolantAvailable is bound by any gauge (0 hits in L4GAUGE.CFG), so the drop side looks cheap; the risk is the id shift, not the names. + +-------------------------------------------------------------------------------- +A TRAP IN THE WATCHER WORK: publishing a name can CRASH the pod +-------------------------------------------------------------------------------- +Publishing Reservoir/ReservoirState crashes the pod on the DOS extender +("Reference to a page you don't own" -> PF), and BISECTING proved it is that +one change: revert it and the run returns to a clean Fail; re-apply it alone +and the crash returns. Everything else in the same batch (Condenser, +Generator, Emitter, ControlsMapper, Torso, Myomers) is innocent and is IN. + +WHY IT MATTERS BEYOND ONE ATTRIBUTE: AttributeWatcherOf does + + currentValue = *(T*)attributePointer; (WATCHER.HPP:306) + +AT CONSTRUCTION. So a published name is READ the moment its watcher is +built -- "nothing drives the value yet, so the type cannot matter" (which is +what the earlier staging note claimed) is WRONG. A wrong-typed target is a +live crash, not a deferred cosmetic issue. + +Things ruled out by measurement, so nobody repeats them: + * NOT the AlarmIndicator-vs-int type. Publishing a plain int + reservoirState instead got FURTHER (log 232 -> 749 bytes) but still + crashed, so the object copy is not the whole story. + * NOT static-init order. reservr.obj sorts LAST in the bt lib (build410.sh + puts TUs missing from the 1995 order list at the end), so + HeatSink::AttributeIndex is constructed before Reservoir's. + * NOT an id gap. ReservoirState sits at HeatSink::NextAttributeID with the + parent chain contiguous behind it. + * NOT visible on the gauge rig. The same binary runs clean under + vis_rec.conf -- only the pod/arena context faults, so reproduce it there. + +The crash itself: 0044B4AD, `mov eax,[edx+0x18]` then `call [eax+4]`, with +EAX = 0x15 and the fault at 0x19. A small integer is being called through as +an object -- an index slot holding an ID where a pointer belongs. That is +the AttributeIndexSet::Build uninitialised-slot signature, so the next step +is to dump Reservoir's built index set and compare it against a class whose +publication works (Condenser is the control: same base, plain int, no crash). + +STATE: Reservoir is REVERTED, the tree is green (clean Fail at ReservoirState, +no crash), and the ladder is blocked there until this is understood. Do not +"fix" it by publishing the name some other way without re-running the bisect.