Fix two Jul-16 regressions: BT_DEV_GAUGES crash + -net dead controls mapper
Both surfaced today (dormant ~1 day) the moment the pod launch flags
(BT_DEV_GAUGES=1 BT_START_INSIDE=1, tools/mp_launch.sh) were used to drive in
multiplayer -- which made the same-day paint change look guilty. Confirmed on
the pre-paint build too; the trigger was the Jul-16 audio attribute work.
1) BT_DEV_GAUGES crash (cdb: CoolingLoopConnection::Update, ~15s in as the gauge
builds lazily). The dev-gauge cooling-loop lamp reached the cooling master by
RAW attribute index -- GetAttributePointer(3) + *(master+0x1d4). The audio
commits (cc2b109 ReportLeak etc.) inserted rows into the chained attribute
tables, so index 3 shifted onto a scalar, the resolve walked garbage, and the
background pass AV'd. Fix: route through a complete-type bridge reading NAMED
members (heat.cpp BTCoolingLoopFrame: linkedSinks.Resolve() +
Condenser::condenserNumber) -- the databinding rule; never a raw numeric
attribute index. Removed the now-orphaned GetAttributePointer-by-index
helper. Name-keyed samplers (InputVoltage) were unaffected.
2) -net dead controls mapper (mech wouldn't walk; turning/weapons still worked).
The RIO mapper is built from a stack SubsystemResource in btl4app.cpp that set
only name/classID/modelSize -- leaving subsystemFlags as stack GARBAGE, which
Subsystem::Subsystem copies into simulationFlags. A stray DontExecuteFlag
(0x2) froze the mapper (speedDemand stuck at 0). Config-dependent: clean in
SP, dirty in -net; the audio commits shifted the stack and flipped the bit.
Fix: memset each hand-built control-mapper resource to 0 (flags 0 =
AlwaysExecute, the faithful value -- a mapper must tick every frame).
Verified with the pod launch flags: SP alive 32s no crash; MP mech walks
(speedDemand 61.5 at full throttle, ~430u traveled), both nodes alive, no crash.
KB: reconstruction-gotchas.md gains gotcha 18 (uninitialized stack-resource
flags + the raw-attribute-index-into-a-growing-table variant) and a
verify-under-the-user's-launch-flags note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e0474ff92a
commit
63250aee41
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user