diff --git a/context/reconstruction-gotchas.md b/context/reconstruction-gotchas.md index 100345c..e48ce4c 100644 --- a/context/reconstruction-gotchas.md +++ b/context/reconstruction-gotchas.md @@ -250,6 +250,11 @@ state transitions: charge/seek loops, snap comparisons, timers compared with `== ucrtbased **abort() dialog** ("Debug Error!"), not an AV — `sxe av` won't break there; the box blocks the event loop (a headless node just "stops logging"). cdb: run with a config that does `g` then `kb 40` — the int3 lands ON the aborting thread. [T2] +- **Verify under the USER'S launch flags, not a bare run:** a "30 s regression: stable" check that + omits `BT_DEV_GAUGES=1` / `BT_START_INSIDE=1` (the `tools/mp_launch.sh` set) misses any bug on the + gauge/cockpit paths those flags gate. The task-#7 gauge crash was DORMANT for a day because every + post-commit check ran flagless; it surfaced the instant the pod launch config was used. Reproduce + with the real launcher's env, and drive (`-net` keyboard) — not just spawn-and-look. [T2] --- @@ -345,6 +350,36 @@ hundreds of pixels off-scope (the radar looked simply "empty"; nothing crashed, decompile) before assigning it an engine method — a wrong-but-plausible identity survives every compile and every "it runs" test. +## 18. Uninitialized stack-built resource → garbage `simulationFlags` (the "worked for weeks then broke" trap) + +**Symptom:** a subsystem installed from a HAND-BUILT (stack) `*__SubsystemResource` silently stops +ticking — `IsNonReplicantExecutable()` returns false because a stray `DontExecuteFlag` (0x2) bit is +set. Config-dependent and nondeterministic: it "works for weeks" then an UNRELATED commit that shifts +the stack layout before the install flips the garbage bit. Archetype (task #7, 2026-07-17): the RIO +controls mapper built on the stack in `btl4app.cpp MakeViewpointEntity` — `Subsystem::Subsystem` +copies `model->subsystemFlags` into `simulationFlags`, but the stack struct only set +`subsystemName`/`classID`/`subsystemModelSize`, leaving `subsystemFlags`+`segmentIndex` = stack +garbage. When the garbage carried 0x2 the mapper froze → `speedDemand` stuck at 0 → no forward motion +(MASKED in single-player when the garbage happened to be clean; surfaced in `-net` where the stack +differed; the July-16 audio commits shifted the stack and flipped it). **Cause:** partial init of a +struct the engine reads in full. **Fix:** `memset(&res, 0, sizeof(res))` before filling ANY +stack-built resource — flags 0 = `AlwaysExecute`, the faithful value for a mapper (it MUST tick every +frame). **Tell:** a subsystem present in the roster but absent from the executed set; instrument the +tick's `!IsNonReplicantExecutable()` branch to name the slot + dump `simulationFlags` (bit 0x2 = +DontExecute). **Rule:** zero every hand-built resource struct; never rely on a partially-filled one. +[T2] + +**Related — raw numeric attribute index into a GROWING table (same root as gotcha 11):** the dev +gauges' `CoolingLoopConnection` reached the cooling master with a RAW `GetAttributePointer(3)` + +`*(master+0x1d4)` walk. The July-16 audio work inserted attribute rows (`ReportLeak` etc.), shifting +the chained ids — index 3 landed on a scalar, the resolve walked garbage, and the gauge background +pass AV'd (only with `BT_DEV_GAUGES=1`, the pod launch flag, ~15 s in when the gauge builds lazily — +the cdb stack pinned it to `CoolingLoopConnection::Update`). **Fix:** route through a complete-type +bridge reading NAMED members (`heat.cpp BTCoolingLoopFrame`: `linkedSinks.Resolve()` + +`Condenser::condenserNumber`) — the databinding rule (gotcha 8), never a raw numeric attribute index. +The name-keyed samplers (`PowerSourceConnection`/`GeneratorVoltageConnection`, which look up +`"InputVoltage"` by NAME) were unaffected — name lookup survives table growth. [T2] + --- ## Diagnostic recipe (the standard loop) diff --git a/game/reconstructed/btl4app.cpp b/game/reconstructed/btl4app.cpp index 2f4a5c2..5254d9b 100644 --- a/game/reconstructed/btl4app.cpp +++ b/game/reconstructed/btl4app.cpp @@ -445,6 +445,8 @@ BTL4Application::SharedData //------------------------------------------------------------ CameraControlsMapper::SubsystemResource control_subsystem_resource; + // task #7: zero the stack resource (see the mech mapper note below). + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, @@ -530,6 +532,14 @@ BTL4Application::SharedData // MechControlsMapper::SubsystemResource control_subsystem_resource; + // task #7 ROOT-CAUSE FIX: Subsystem::Subsystem copies + // model->subsystemFlags into simulationFlags -- an + // uninitialized stack resource left GARBAGE there, and a + // stray DontExecuteFlag bit froze the installed controls + // mapper for the whole session (the -net "can't drive" + // freeze; config-dependent because stack contents are). + // Zero the whole struct before filling it. + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, @@ -555,6 +565,14 @@ BTL4Application::SharedData { MechControlsMapper::SubsystemResource control_subsystem_resource; + // task #7 ROOT-CAUSE FIX: Subsystem::Subsystem copies + // model->subsystemFlags into simulationFlags -- an + // uninitialized stack resource left GARBAGE there, and a + // stray DontExecuteFlag bit froze the installed controls + // mapper for the whole session (the -net "can't drive" + // freeze; config-dependent because stack contents are). + // Zero the whole struct before filling it. + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, @@ -589,6 +607,14 @@ BTL4Application::SharedData { MechControlsMapper::SubsystemResource control_subsystem_resource; + // task #7 ROOT-CAUSE FIX: Subsystem::Subsystem copies + // model->subsystemFlags into simulationFlags -- an + // uninitialized stack resource left GARBAGE there, and a + // stray DontExecuteFlag bit froze the installed controls + // mapper for the whole session (the -net "can't drive" + // freeze; config-dependent because stack contents are). + // Zero the whole struct before filling it. + memset(&control_subsystem_resource, 0, sizeof(control_subsystem_resource)); Str_Copy( control_subsystem_resource.subsystemName, diff --git a/game/reconstructed/btl4gau2.cpp b/game/reconstructed/btl4gau2.cpp index ace13f7..d7b8292 100644 --- a/game/reconstructed/btl4gau2.cpp +++ b/game/reconstructed/btl4gau2.cpp @@ -154,22 +154,11 @@ static void *ResolveLink(void *plug) // extern bool BTGetSubsystemAuxScreen(Subsystem *sub, int *screen, int *placement, char *label64); -// -// FUN_0041bf44 -- GetAttributePointer(index): a pointer to the subsystem's Nth -// attribute member (the cooling-loop lamp reaches its cooling-master link at -// attribute slot 3), or 0 if the index is out of range. The engine exposes this -// via Simulation::GetAttributePointer(AttributeID); a miss returns &NullAttribute, -// normalised to 0. -// -static void *FindLinkedSubsystem(void *subsystem, int slot) -{ - if (subsystem == 0) - return 0; - void *p = ((Simulation *)subsystem)->GetAttributePointer((Simulation::AttributeID)slot); - if (p == (void *)&Simulation::NullAttribute) - return 0; - return p; -} +// (FUN_0041bf44 GetAttributePointer-by-index helper removed with the task #7 +// fix: its only caller was CoolingLoopConnection, which now resolves the +// cooling master through the named-member bridge BTCoolingLoopFrame -- reaching +// an attribute by RAW numeric index is exactly what broke when the audio +// attribute rows grew the chained tables.) // // FUN_0041a1a4 IsDerivedFrom(0x50f4bc == PoweredSubsystem's ClassDerivations). @@ -191,10 +180,16 @@ static Logical IsGeneratorDerived(Subsystem *sub) { return BTIsPoweredSubsystem( // // CoolingLoopConnection -- @004c3134 ctor / @004c31a0 sampler (vt 0x518ea8). -// When the source subsystem's coolant loop is active (source+0x134 == 1) it -// follows the linked cooling master (FindLinkedSubsystem(source,3) then Resolve) -// and copies its loop-lamp state (+0x1d4); otherwise 0. +// When the source subsystem's coolant loop is available (coolantAvailable == 1, +// the binary's source+0x134 gate) it follows the linked cooling master (binary +// attr id 3 == "HeatSink" == linkedSinks) and shows its Condenser loop number +// (+0x1d4 == condenserNumber, the image-strip frame); otherwise 0. +// task #7 REGRESSION FIX (2026-07-17): the old raw GetAttributePointer(3) + +// *(+0x1d4) walk AV'd after the audio attribute-table rows shifted the chained +// ids (BT_DEV_GAUGES crash). The sampler now routes through a complete-type +// bridge (heat.cpp) reading NAMED members -- the databinding rule (gotcha 8). // +extern int BTCoolingLoopFrame(void *subsystem); class CoolingLoopConnection : public GaugeConnection { public: @@ -203,14 +198,7 @@ public: source(source), destination(destination) {} void Update() // @004c31a0 { - void *resolved = 0; - if (source != 0 && *(int *)((char *)source + 0x134) == 1) - { - void *linked = FindLinkedSubsystem(source, 3); - if (linked != 0) - resolved = ResolveLink(linked); - } - *destination = (resolved == 0) ? 0 : *(int *)((char *)resolved + 0x1d4); + *destination = BTCoolingLoopFrame(source); } protected: void *source; // @0x10 this[4] diff --git a/game/reconstructed/heat.cpp b/game/reconstructed/heat.cpp index 5bd5c62..dc7e549 100644 --- a/game/reconstructed/heat.cpp +++ b/game/reconstructed/heat.cpp @@ -1123,3 +1123,31 @@ Subsystem *CreateCondenserSubsystem(Mech *owner, int id, void *seg) // CreateHeatSinkBankSubsystem (0xBBE) now lives in heatfamily_reslice.cpp -- it // builds the real AggregateHeatSink (which needs that TU's class definition). + +//===========================================================================// +// Gauge cooling-loop bridge (task #7 regression fix, 2026-07-17). +// +// The dev-gauge CoolingLoopConnection (btl4gau2.cpp @004c31a0) reached the +// cooling master with a RAW attribute index (GetAttributePointer(3)) and then +// raw-read master+0x1d4. The binary heat attribute table (@0x50e438..0x50e4c8) +// shows id 3 == "HeatSink" == the linkedSinks plug, and +0x1d4 on the resolved +// master is Condenser::condenserNumber (the loop number that selects the +// image-strip frame). The numeric-id read crashed when the AUDIO_FIDELITY rows +// (ReportLeak et al., cc2b109) grew the chained tables: index 3 landed on a +// scalar, the resolve walked garbage, and the gauge background pass AV'd +// (BT_DEV_GAUGES=1, the pod launch flag). House rule (gotcha 8): named members +// via a complete-type TU -- never a raw numeric attribute index. +//===========================================================================// +int BTCoolingLoopFrame(void *subsystem_v) +{ + Subsystem *sub = (Subsystem *)subsystem_v; + if (sub == 0 || !sub->IsDerivedFrom(*HeatSink::GetClassDerivations())) + return 0; + HeatSink *sink = (HeatSink *)sub; + if (sink->coolantAvailable != 1) // the binary's src+0x134 gate + return 0; + Subsystem *master = sink->linkedSinks.Resolve(); // attr id 3 "HeatSink" + FUN_00417ab4 + if (master == 0 || !master->IsDerivedFrom(*Condenser::GetClassDerivations())) + return 0; + return ((Condenser *)master)->condenserNumber; // master+0x1d4 (loop/frame number) +}