#165 ROOT-CAUSED after three failed fixes -- the binary keeps ONE destroyed cell per subsystem (+0x40: fire gate, dial X, wire header, every writer) and the port split it into FOUR stores; the everyday CRIT-LOTTERY kill (MechWeapon::TakeDamage) bypassed the reconstructed base and wrote only the X store, so X'd weapons kept firing while every prior fix aimed at a gate that was never broken (the 4,213-refusal bench drove BT_KILL_SUBSYS, which writes all cells -- it validated readers, not writers). FIX: MechWeapon::TakeDamage chains to MechSubsystem::TakeDamage (@0x4ac0bc semantics); the port-authored FSM-from-damage tail is DELETED (it parked partial-crit weapons at level 1 -- no FSM case, the silent freeze, and with beamFlag armed the 883 hanging beam: #164's upstream writer, caught red-handed by berserker's 20x STUCK-ON receipts); the phantom uninitialized simulationState int is ctor-initialized and kept in lockstep at every destroyed write; always-on [crit] DESTROYED receipt makes crit kills field-visible. BT_CRIT_SUBSYS bench hook drives the REAL writer path; weaponx_bench PASS: kill phase 0 FIRED post / 5,664 refusals / X+gate agree, partial phase keeps firing. Gotcha 30; #174 filed (cascade re-descend)
This commit is contained in:
@@ -1328,3 +1328,19 @@ record shipped penalty-off for entire steam nights, #162); (3) the bench for ANY
|
||||
here is scratchpad/night15/kd_bench.sh -- five HARD invariants (deaths==increments,
|
||||
cost-per-death, credit-per-kill, zero swallowed, arithmetic witness); its predecessor's
|
||||
assertions were greps that matched nothing, which is how both halves shipped broken.
|
||||
|
||||
|
||||
## Weapon destroyed-state: the ONE-cell contract (#165 root cause, night-16) [T1]
|
||||
Binary: subsystem `+0x40` (statusAlarm level) is THE destroyed cell -- all writers (base
|
||||
TakeDamage @0x4ac0bc zone-saturation tail; cascade descent @0049c9a8; slot-8 pump watchdog),
|
||||
all readers (Emitter gate @004baa88 `==1` strict; small-dial X @004c7844; techstat; the wire
|
||||
header). Four alarm cells exist per weapon and are NOT interchangeable: +0x40 status/health,
|
||||
+0x184 temperature (gate clause `==2`), +0x278 power feed (eng-page X only!), +0x364 weaponAlarm
|
||||
FSM (level 7 "unavailable" = AMMO weapons only, @004bb9b8/@004bbd04 -- the #30 answer). The
|
||||
eng-page energy X (@004c8990) draws on `+0x40==1 OR power!=4`: an X over a LOADED emitter that
|
||||
legally fires its one loaded shot is AUTHENTIC binary behavior (power-fault case). Destroyed
|
||||
emitters park at weaponAlarm=3 (Loading) via the gate's per-frame refusal -- "recharging under
|
||||
the X" is authentic. Replication: weapon statusAlarm NEVER crosses the wire; destroyed-ness
|
||||
reaches peers via ZONE records only; replicant emitters run a gateless Simulate (@004ba8d0) --
|
||||
fire legality is master-only. Port fix + the four-store split it repaired: [[reconstruction-gotchas]] SS30;
|
||||
bench: `scratchpad/night16/weaponx_bench.sh` (crit-path kill + partial-crit freeze regression).
|
||||
|
||||
@@ -1226,5 +1226,14 @@ consistent with internals-only, see #103). Logs: `scratchpad/night16/` + Oracle
|
||||
no flash on any MFD, died ~9:38PM CT -- the match's only death, receipt findable in night16).
|
||||
Distinct from the closed #135: this is the SET-edge / first-flash arming path.
|
||||
- **#173 -- chicken-walker seek-4 myomer overheat** (see [[subsystems]] night-16 note).
|
||||
- **#165 ROOT-CAUSED + FIXED + BENCHED (2026-08-13, the 4-agent hunt):** gotcha SS30 -- the
|
||||
binary's ONE destroyed cell was split into four port stores and the crit-lottery kill path
|
||||
(MechWeapon::TakeDamage) wrote none the gate reads. Fixed (chain to MechSubsystem::TakeDamage,
|
||||
FSM tail deleted, phantom simulationState initialized + bridged); weaponx_bench PASS both
|
||||
phases. RESIDUAL UNKNOWNS from the hunt: (i) Elengil's 20:17 ET no-X facet needs ALIA's
|
||||
PRE-ROTATION steam_20260812.log (ask Elengil -- staged file starts 22:20 ET); (ii) cascade
|
||||
re-descend re-entrancy (zone 18 x7 re-rolls, own ticket); (iii) incident-(b) "no damage on
|
||||
target": shooter log computed FULL damage (11.76) -- wire/victim-side loss, needs paired
|
||||
matchlogs (fleet-wide BT_DMG_LOG + matchlog next night would make this attributable).
|
||||
- Nanook lost the coolant MFD glass window, recovered only by config edit -> #76 comment
|
||||
(in-game layout-reset recovery path).
|
||||
|
||||
@@ -1014,3 +1014,33 @@ sampled a cell whose write timing differs from the binary's". Fix shape: run the
|
||||
transition inline at the destruction edge (mech.cpp death_tail → `UpdateDeathState(0)`), keep the
|
||||
per-frame caller for maintenance. Verified: kd4_bench 9/9/9/9 + kd8_stress 26/26/26/26, both
|
||||
SWALLOWED=0 (the receiver dedup tripwire now never fires — no duplicate is ever GENERATED).
|
||||
|
||||
|
||||
## 30. ONE binary cell, FOUR port stores: the destroyed-state split that survived three "fixes" (#165, night-16 2026-08-13)
|
||||
The binary keeps ONE destroyed cell per subsystem: `+0x40` = the statusAlarm level -- written by
|
||||
EVERY destruction path (base TakeDamage @0x4ac0bc at private-zone saturation; zone-cascade
|
||||
descent @0049c9a8; the per-frame watchdog in the slot-8 message pump), read by EVERY consumer
|
||||
(the Emitter fire gate @004baa88, the small weapon-dial X @004c7844, the techstat scan, the wire
|
||||
via the Simulation record header). The port split that cell into FOUR stores that never talked:
|
||||
the private DamageZone.damageLevel (drives the X), the ReconAlarm statusAlarm (gate clause 2 --
|
||||
written ONLY by the cascade + bench hook), a phantom `int simulationState` (gate clause 1 --
|
||||
ZERO writers, NEVER ctor-initialized: uninitialized pool memory, MEMBLOCK recycles unzeroed),
|
||||
and the weaponAlarm FSM cell. `MechWeapon::TakeDamage` then bypassed the reconstructed
|
||||
`MechSubsystem::TakeDamage` (which HAS the destroyed tail) for the raw engine base -- so the
|
||||
everyday CRIT-LOTTERY kill saturated the zone (X lit) without ever writing a gate cell: X'd
|
||||
weapons kept firing. Three prior "fixes" all aimed at the GATE, and the bench (BT_KILL_SUBSYS)
|
||||
wrote both cells directly -- 4,213 refusals proving a gate that was never broken.
|
||||
**Sibling defect in the same function:** the port-authored tail drove the weapon FSM from zone
|
||||
state -- saturation -> level 0 (=Firing!), partial damage -> level 1 (NO case in either FSM:
|
||||
silent freeze; with beamFlag armed = the 883 hanging beam, #164's upstream writer).
|
||||
**Rules:** (1) when the binary reads/writes ONE offset, the port must have ONE authority --
|
||||
alias, don't duplicate; if layout forces a twin field, every writer writes BOTH (grep-audit the
|
||||
writer census, not the reader census). (2) Never let an override "simplify away" the
|
||||
reconstructed base -- `MechWeapon::TakeDamage` calling `Subsystem::TakeDamage` instead of
|
||||
`MechSubsystem::TakeDamage` dropped the invariant silently. (3) A bench hook that writes state
|
||||
directly validates READERS only; destruction/state benches must drive the REAL writer path
|
||||
(BT_CRIT_SUBSYS: Damage -> ApplyDamageAndMeasure -> virtual TakeDamage). (4) Every ctor
|
||||
initializes every reconstructed field -- an unwritten int in pooled memory is a time bomb.
|
||||
Verified: weaponx_bench P1 (crit kill: 0 FIRED after, 5,664 refusals, X+gate agree) + P2
|
||||
(partial crit keeps firing -- the freeze is gone). Full mechanism: the night-16 workflow dossier
|
||||
(4 agents, cross-examined) + docs/GEN_WARNING_AUDIT-style lanes in the #165 ticket thread.
|
||||
|
||||
@@ -226,3 +226,11 @@ wrote); affinity masks 2-cores-each up to 4 nodes, 1-each at 8.
|
||||
- Uses: [[build-and-run]] (parity, env gates, BT_SHOT capture) · [[experience-levels]] (expert vs novice gating)
|
||||
- Informs: [[reconstruction-method]] (step 4 "verify honestly" — this file is the how)
|
||||
- Incident sources: [[reconstruction-gotchas]] §23 (pixels), §25 (same-pass latch timing — caught by the 4-node bench), §26 (silent stubs); [[multiplayer]] (replication layers)
|
||||
|
||||
## weaponx_bench (night16) -- #165 crit-path destruction
|
||||
`scratchpad/night16/weaponx_bench.sh`: single node, vulture, BT_AUTOFIRE; P1 kills ERSLaser_1
|
||||
via BT_CRIT_SUBSYS (the REAL writer path: Damage -> ApplyDamageAndMeasure -> virtual TakeDamage)
|
||||
and asserts [crit] DESTROYED + [techstat] X + zero named FIRED after + REFUSED>0 + survivor
|
||||
fires; P2 partial-crits to 0.5 and asserts the weapon KEEPS firing (freeze regression, the #164
|
||||
upstream writer). LESSON (gotcha SS30): BT_KILL_SUBSYS writes the cells directly and can only
|
||||
validate READERS -- state benches must drive the real writer path.
|
||||
|
||||
@@ -7494,6 +7494,66 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// #165 BENCH (BT_CRIT_SUBSYS=<name>[,<name>...]): kill named subsystems
|
||||
// through the REAL crit path -- Damage record -> ApplyDamageAndMeasure ->
|
||||
// virtual TakeDamage -- NOT the direct cell writes BT_KILL_SUBSYS does.
|
||||
// This is the path the field takes (the crit lottery), and the path the
|
||||
// old 4,213-refusal bench never exercised: BT_KILL_SUBSYS wrote
|
||||
// statusAlarm AND the zone directly, so it validated the fire gate while
|
||||
// the field's writer (MechWeapon::TakeDamage) silently dropped the
|
||||
// destroyed-side write. Applies one energy-damage chunk per frame from
|
||||
// frame 900 until the subsystem's own zone saturates ([crit] receipt
|
||||
// fires from MechSubsystem::TakeDamage on the edge), then stops.
|
||||
// Env-gated, viewpoint mech only.
|
||||
if ((Entity *)this == application->GetViewpointEntity()
|
||||
&& getenv("BT_CRIT_SUBSYS"))
|
||||
{
|
||||
static int s_csFrame = 0;
|
||||
if (++s_csFrame >= 900) // same settle window as killsub
|
||||
{
|
||||
char want_list[256];
|
||||
strncpy(want_list, getenv("BT_CRIT_SUBSYS"), sizeof(want_list) - 1);
|
||||
want_list[sizeof(want_list) - 1] = 0;
|
||||
for (char *want = strtok(want_list, ","); want != 0;
|
||||
want = strtok(0, ","))
|
||||
{
|
||||
// PARTIAL form ("Name=0.5"): stop at that zone level -- the
|
||||
// freeze-regression phase (a partially-critted weapon must
|
||||
// keep recycling; the old port tail froze it at alarm 1).
|
||||
float target = 1.0f;
|
||||
char *eq = strchr(want, '=');
|
||||
if (eq != 0)
|
||||
{
|
||||
*eq = 0;
|
||||
target = (float)atof(eq + 1);
|
||||
}
|
||||
for (int i = 2; i < GetSubsystemCount(); ++i)
|
||||
{
|
||||
Subsystem *s = GetSubsystem(i);
|
||||
if (s == 0 || s->GetName() == 0) continue;
|
||||
if (stricmp(s->GetName(), want) != 0) continue;
|
||||
if (!s->IsDerivedFrom(MechSubsystem::ClassDerivations)) continue;
|
||||
MechSubsystem *ms = (MechSubsystem *)s;
|
||||
if (ms->GetSubsystemDamageLevel() >= target) break; // done
|
||||
Damage d;
|
||||
d.damageType = Damage::EnergyDamageType;
|
||||
d.damageAmount = 0.75f; // FINE-GRAINED: a small-laser
|
||||
// private zone saturates on a
|
||||
// single 20.0 chunk (first run);
|
||||
// ~0.04 level/frame ramps let
|
||||
// the PARTIAL stop actually stop
|
||||
d.burstCount = 1;
|
||||
ms->ApplyDamageAndMeasure(d); // FUN_004ac07c -> virtual TakeDamage
|
||||
if ((s_csFrame % 30) == 0)
|
||||
DEBUG_STREAM << "[critsub] '" << s->GetName()
|
||||
<< "' zone=" << (float)ms->GetSubsystemDamageLevel()
|
||||
<< std::endl << std::flush;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// task #13 scripted verify (BT_VALVE_TEST=1): one MoveValve press at
|
||||
// frame ~600 -- Condenser1's valveState cycles 1 -> 5, so the flow
|
||||
// redistribution gives it 5/10 of the total coolant flow ([valve] log
|
||||
|
||||
@@ -142,6 +142,11 @@ MechSubsystem::MechSubsystem(
|
||||
{
|
||||
refCount = NewRefCount(0x10); // this[0x3d]
|
||||
this->owner = owner; // this[0x34] -- the owning Mech (canonical)
|
||||
simulationState = 0; // #165: was NEVER initialized anywhere --
|
||||
// MemoryBlock pools recycle unzeroed
|
||||
// (MEMBLOCK.cpp), so stale garbage ==1
|
||||
// could brick a weapon from spawn and
|
||||
// draw the seek-graph destroyed bitmap
|
||||
controlDestination = 0; // this[0x3a] = param_7 (ctor pass-through;
|
||||
controlMessageID = 0; // this[0x3b] = param_8 weapons override -- task #6)
|
||||
vitalSubsystem = 0; // this[0x39]
|
||||
@@ -187,6 +192,8 @@ MechSubsystem::MechSubsystem(
|
||||
// ctor only set hostEntity; subclasses read
|
||||
// `owner` widely -> must be set or it is garbage.)
|
||||
statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0xb, 0)
|
||||
simulationState = 0; // #165: uninitialized-landmine fix (see
|
||||
// the sibling ctor)
|
||||
controlDestination = 0; // this[0x3a]=param_6 (ctor pass-through;
|
||||
controlMessageID = 0; // this[0x3b]=param_7 weapons override -- task #6)
|
||||
resource = subsystem_resource; // this[0x3c]
|
||||
@@ -279,6 +286,7 @@ Scalar MechSubsystem::GetSubsystemDamageLevel() const
|
||||
void MechSubsystem::ForceCriticalFailure()
|
||||
{
|
||||
statusAlarm.SetLevel(1); // +0x2C: 1 = Destroyed
|
||||
simulationState = 1; // #165 twin (see TakeDamage)
|
||||
if (printSimulationState) // +0x104 gate
|
||||
{
|
||||
PrintState(); // vtable slot 0x34 @4ac8c0
|
||||
@@ -303,6 +311,7 @@ void MechSubsystem::ForceCriticalFailure()
|
||||
void MechSubsystem::RespawnRepair()
|
||||
{
|
||||
statusAlarm.SetLevel(0); // operational
|
||||
simulationState = 0; // #165 twin (see TakeDamage)
|
||||
if (damageZone != 0)
|
||||
{
|
||||
((DamageZone *)damageZone)->damageLevel = 0.0f;
|
||||
@@ -358,6 +367,7 @@ Logical
|
||||
if (StatusThreshold <= ((DamageZone *)damageZone)->damageLevel) // _DAT_004ac140 (engine view)
|
||||
{
|
||||
statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb, 1)
|
||||
simulationState = 1; // #165 twin (see TakeDamage)
|
||||
if (printSimulationState != 0) // this[0x41]
|
||||
{
|
||||
OnAlarmChanged(); // (*this.vtable+0x34)(this)
|
||||
@@ -386,6 +396,7 @@ void
|
||||
((DamageZone *)damageZone)->damageLevel = 1.0f; // dz+0x158 = 1.0 (engine view)
|
||||
}
|
||||
statusAlarm.SetLevel(1); // FUN_0041bbd8(this+0xb, 1)
|
||||
simulationState = 1; // #165 twin (see TakeDamage)
|
||||
if (printSimulationState != 0)
|
||||
{
|
||||
OnAlarmChanged(); // (*this.vtable+0x34)(this)
|
||||
@@ -407,6 +418,7 @@ void
|
||||
((DamageZone *)damageZone)->damageLevel = 0.0f; // dz+0x158 = 0 (engine view)
|
||||
damageZone->alarm.SetLevel(0); // FUN_0041bbd8(dz+0x10, 0)
|
||||
statusAlarm.SetLevel(0); // FUN_0041bbd8(this+0xb, 0)
|
||||
simulationState = 0; // #165 twin (see TakeDamage)
|
||||
if (printSimulationState != 0)
|
||||
{
|
||||
OnAlarmChanged();
|
||||
@@ -520,7 +532,18 @@ void MechSubsystem::TakeDamage(Damage &damage)
|
||||
|
||||
if (dz->damageLevel >= 1.0f) // _DAT_004ac140
|
||||
{
|
||||
// #165: named destruction receipt on the EDGE only (damage keeps
|
||||
// landing on dead subsystems; the write below is idempotent) -- the
|
||||
// crit-path kill had NO field receipt, which is how an X'd-but-firing
|
||||
// weapon hid for a month. Always-on: once per subsystem per life.
|
||||
if (statusAlarm.GetLevel() != 1)
|
||||
DEBUG_STREAM << "[crit] '" << (GetName() ? GetName() : "?")
|
||||
<< "' DESTROYED (zone saturated)\n" << std::flush;
|
||||
statusAlarm.SetLevel(1); // Destroyed
|
||||
simulationState = 1; // #165: the binary's +0x40 is ONE
|
||||
// cell; the port split it -- keep
|
||||
// the twin in lockstep (gate clause
|
||||
// 1, seek-graph destroyed bitmap)
|
||||
if (printSimulationState) // +0x104
|
||||
{
|
||||
PrintState(); // vtable +0x34 @4ac8c0
|
||||
|
||||
@@ -411,34 +411,39 @@ Logical
|
||||
// @004b96d4 -- slot 9 (TASK #51 RENAME: this body was mislabeled slot-6
|
||||
// ReadUpdateRecord; @004b96d4 chains @004b0efc = PoweredSubsystem::TakeDamage,
|
||||
// which dispatches on *param==4 == Damage::damageType -- damage semantics).
|
||||
// The PoweredSubsystem::TakeDamage body (electrical-short handling) is not yet
|
||||
// reconstructed; chain to the engine Subsystem base, and keep the damage-zone
|
||||
// alarm tracking this port has shipped with (best-effort; the binary's alarm
|
||||
// display on weapon damage flows through the unreconstructed @004b0efc chain).
|
||||
//
|
||||
// #165 ROOT CAUSE (night-16 hunt, 2026-08-13). The old body here called the
|
||||
// raw ENGINE Subsystem::TakeDamage (zone accumulation ONLY) and then drove
|
||||
// weaponAlarm from the zone state -- two defects in one:
|
||||
// (1) it DROPPED the destroyed-side invariant: the binary keeps ONE
|
||||
// destroyed cell per subsystem (+0x40 = statusAlarm level) that every
|
||||
// destruction path writes and the fire gate reads. A weapon killed by
|
||||
// the CRIT LOTTERY (the everyday field path) saturated its private
|
||||
// zone (-> the MFD X, [techstat] condition 0) but never wrote the gate
|
||||
// cell -- the X'd weapon kept firing (Oracle 20260812: ER PPC firing
|
||||
// indefinitely; the PPC+SL pair firing until the arm CASCADE finally
|
||||
// wrote the cell). The 4,213-refusal bench passed because
|
||||
// BT_KILL_SUBSYS writes both cells -- it validated the gate, not the
|
||||
// writers.
|
||||
// (2) the weaponAlarm tail wrote the FSM cell BACKWARDS: saturation ->
|
||||
// level 0 (Firing!), partial damage -> level 1 -- a state with NO case
|
||||
// in either weapon FSM: the silent freeze (berserker's 20x [beam]
|
||||
// STUCK-ON, and the state-1+beamFlag combination that drew Conn Man's
|
||||
// 883 hanging beam -- #164's upstream cause). The binary never drives
|
||||
// the FSM from damage: a destroyed weapon parks at Loading(3) via the
|
||||
// fire gate's per-frame refusal (ResetFiringState, emitter.cpp).
|
||||
// FIX: chain to MechSubsystem::TakeDamage -- the correctly-reconstructed
|
||||
// binary base (@0x4ac0bc: zone TakeDamage, then damageLevel>=1.0 ->
|
||||
// statusAlarm=1 + pin + vital-kill) -- and do NOT touch the FSM. The
|
||||
// binary's electrical-short middle layer (@004b0efc) remains unreconstructed
|
||||
// (a marked gap, unchanged); what is restored is the destruction invariant.
|
||||
// MechSubsystem::TakeDamage guards damageZone==0 itself (the 2026-07-29
|
||||
// Owens NULL-zone field crash stays guarded).
|
||||
//
|
||||
void
|
||||
MechWeapon::TakeDamage(Damage &damage)
|
||||
{
|
||||
// FIELD CRASH GUARD (2026-07-29, build 641): the engine base derefs
|
||||
// damageZone unguarded; an Owens weapon reached here with zone == NULL
|
||||
// the first night crits could land (#80). See ApplyDamageAndMeasure.
|
||||
if (this->Subsystem::damageZone != 0)
|
||||
Subsystem::TakeDamage(damage); // engine base (real chain: @004b0efc)
|
||||
|
||||
// Port behavior (kept): a destroyed weapon clears its display alarm;
|
||||
// otherwise the alarm tracks the resolved damage-zone state.
|
||||
::DamageZone *dz = this->Subsystem::damageZone;
|
||||
if (dz != 0
|
||||
&& dz->GetGraphicState() == DamageZone::DestroyedGraphicState)
|
||||
{
|
||||
weaponAlarm.SetLevel(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
weaponAlarm.SetLevel(
|
||||
(dz != 0 && dz->damageLevel > 0.0f) ? 1 : 0
|
||||
);
|
||||
}
|
||||
MechSubsystem::TakeDamage(damage); // @0x4ac0bc semantics
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user