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<T> 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 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-28 02:42:43 -05:00
co-authored by Claude Fable 5
parent 3ecd88aaef
commit 0d97d8c38b
10 changed files with 74 additions and 6 deletions
+3 -1
View File
@@ -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;
+2
View File
@@ -77,6 +77,7 @@
public:
enum {
ChargeLevelAttributeID = MechWeapon::NextAttributeID, // 0x1D
LaserOnAttributeID, // authored watcher: <weapon>/LaserOn
NextAttributeID
};
@@ -164,6 +165,7 @@
Scalar energyCoefficient;
Scalar energyTotal;
Scalar damageFraction;
int laserOn; // staged: authored watcher <weapon>/LaserOn
Scalar damagePortion;
Scalar heatPortion;
int firingActive;
+3 -1
View File
@@ -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;
+8
View File
@@ -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
};
//###########################################################################
+4 -1
View File
@@ -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;
{
+7 -1
View File
@@ -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
+3 -1
View File
@@ -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;
+2
View File
@@ -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
+1 -1
View File
@@ -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).
@@ -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<T> 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.