#174 root-caused + fixed with a negative control: the binary's ONLY re-descend protection is the burst loop's zone-state guard wrapping the WHOLE per-burst application (@0x4a0446, part_012.c:14661 -- crit roll, TakeDamage, score tally, destroyed-flag; RecurseSegmentTable/SendSubsystemDamage have NO internal re-entry protection: no used-marking, no top gate). The port had narrowed the guard to the crit roll -- continued fire re-saturated dead zones (cascade re-fired: night-16 zone 18 x7, each pass re-pushing crit allotment = the ram 'crits worsened with each collision'), and hits on wreckage kept SCORING (binary awards zero -- a quiet farm-the-wreck exploit). Fix: guard restored to binary scope. Bench: BT_ZONE_HAMMER hook (4Hz explicit-zone self-damage) -- fixed build 1 cascade/10 hits/0 post-saturation applications; NEGATIVE CONTROL (fix stashed): 147 cascades. 147 -> 1. Gotcha 31: guard-scope narrowing -- diff the BRACE SCOPE when transcribing decomp ifs; and prove a bench detects the bug before trusting its pass
This commit is contained in:
@@ -1044,3 +1044,23 @@ initializes every reconstructed field -- an unwritten int in pooled memory is a
|
||||
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.
|
||||
|
||||
|
||||
## 31. GUARD-SCOPE NARROWING: a binary guard wrapping a whole block, transcribed onto its first statement (#174, 2026-08-13)
|
||||
The binary's burst loop guards the ENTIRE per-burst application on zone-not-destroyed
|
||||
(`if (piVar1[9] != 1) { crit-roll | zone TakeDamage; score tally; destroyed-flag }`,
|
||||
part_012.c:14661-14677 @0x4a0446) -- a burst landing on a dead zone is a structural no-op:
|
||||
no re-application, no cascade re-fire, NO SCORE. The port transcribed that guard onto the
|
||||
crit roll ONLY; the unguarded TakeDamage re-saturated the zone every burst, whose >=1.0 tail
|
||||
re-ran RecurseSegmentTable (night-16 field: zone 18 x7, zone 9 x4 per mission), each pass
|
||||
re-pushing crit allotment into surviving subsystems (the ram "crits worsened with each
|
||||
collision") -- and hits on wreckage kept SCORING (the binary awards zero). Neither
|
||||
RecurseSegmentTable nor SendSubsystemDamage has any internal re-entry protection in the
|
||||
binary (verified @0049cad4/@0049c9a8: no `used` write, no top gate) -- the delivery-layer
|
||||
guard is the ONLY protection, so its scope is load-bearing.
|
||||
**Rule:** when transcribing a decomp `if` whose body spans multiple statements, diff the
|
||||
BRACE SCOPE against the port -- a guard attached to the wrong statement compiles, runs, and
|
||||
passes every reader-side bench. **Bench rule (the #165 lesson, second application in one
|
||||
week):** prove the bench detects the bug BEFORE trusting its pass -- the negative control
|
||||
here (fix stashed) scored 147 cascades where the fixed build scores 1.
|
||||
Bench: BT_ZONE_HAMMER=<zone>[,<amt>] (mech4.cpp) + scratchpad/night16/cascade_bench.sh.
|
||||
|
||||
+36
-18
@@ -1233,29 +1233,47 @@ void
|
||||
{
|
||||
Mech__DamageZone *zone =
|
||||
(Mech__DamageZone *)damageZones[zoneIndex]; // this[0x120][idx]
|
||||
Subsystem *critted = 0;
|
||||
|
||||
if (zone->GetDamageZoneState() != DamageZone::BurningState // zone state != 1
|
||||
&& BTMechCriticalChance(this, zoneIndex)) // @0x4a0164 (roll below)
|
||||
//
|
||||
// #174 (2026-08-13): the binary's zone-state guard wraps the WHOLE
|
||||
// per-burst application -- `if (piVar1[9] != 1) { crit-roll |
|
||||
// TakeDamage; tally; destroyed-flag }` (part_012.c:14661-14677,
|
||||
// @0x4a0446) -- a burst landing on an already-destroyed zone is a
|
||||
// structural NO-OP: no re-application, no cascade re-fire, NO SCORE.
|
||||
// The port had narrowed the guard to the crit roll only, so
|
||||
// continued fire into a dead zone re-saturated it every burst ->
|
||||
// Mech__DamageZone::TakeDamage's >=1.0 tail re-ran
|
||||
// RecurseSegmentTable (night-16: zone 18 x7, zone 9 x4 in single
|
||||
// missions), each pass re-pushing the crit allotment into the
|
||||
// surviving subsystems (the ram "crits worsened with each
|
||||
// collision") -- and the shooter's damageTally kept scoring hits
|
||||
// on wreckage the binary scores as ZERO.
|
||||
//
|
||||
if (zone->GetDamageZoneState() != DamageZone::BurningState) // zone state != 1
|
||||
{
|
||||
critted = zone->CriticalHit(message->damageData); // @0049ccc4
|
||||
if (critted != 0)
|
||||
Subsystem *critted = 0;
|
||||
if (BTMechCriticalChance(this, zoneIndex)) // @0x4a0164 (roll below)
|
||||
{
|
||||
damageTally +=
|
||||
((MechSubsystem *)critted)->CriticalScoreBonus(); // +0x108
|
||||
if (BTEnvOn("BT_CRIT_LOG", 0))
|
||||
DEBUG_STREAM << "[critroll] zone=" << zoneIndex
|
||||
<< " -> " << (critted->GetName() ? critted->GetName() : "?")
|
||||
<< " subLvl=" << ((MechSubsystem *)critted)->GetSubsystemDamageLevel()
|
||||
<< "\n" << std::flush;
|
||||
critted = zone->CriticalHit(message->damageData); // @0049ccc4
|
||||
if (critted != 0)
|
||||
{
|
||||
damageTally +=
|
||||
((MechSubsystem *)critted)->CriticalScoreBonus(); // +0x108
|
||||
if (BTEnvOn("BT_CRIT_LOG", 0))
|
||||
DEBUG_STREAM << "[critroll] zone=" << zoneIndex
|
||||
<< " -> " << (critted->GetName() ? critted->GetName() : "?")
|
||||
<< " subLvl=" << ((MechSubsystem *)critted)->GetSubsystemDamageLevel()
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (critted == 0)
|
||||
zone->TakeDamage(message->damageData); // zone vtbl+0x18 @0x4a0488
|
||||
if (critted == 0)
|
||||
zone->TakeDamage(message->damageData); // zone vtbl+0x18 @0x4a0488
|
||||
|
||||
damageTally += message->damageData.damageAmount; // +0x30
|
||||
if (zone->GetDamageZoneState() == DamageZone::BurningState)
|
||||
zoneDestroyed = 1;
|
||||
damageTally += message->damageData.damageAmount; // +0x30
|
||||
if (zone->GetDamageZoneState() == DamageZone::BurningState)
|
||||
zoneDestroyed = 1; // set only by the burst that
|
||||
} // DESTROYS it (binary: inside
|
||||
// the guard, part_012.c:14674)
|
||||
|
||||
if (--burstsLeft == 0)
|
||||
break;
|
||||
|
||||
@@ -7554,6 +7554,44 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// #174 BENCH (BT_ZONE_HAMMER=<zoneIdx>[,<amt>]): 4x/sec, dispatch a
|
||||
// TakeDamage with an EXPLICIT damageZone at the viewpoint mech itself.
|
||||
// Pointed at a descend=1 non-vital zone (dz_larm=2 on most chassis),
|
||||
// the zone saturates, cascades ONCE, the mech survives, and the hammer
|
||||
// keeps landing on the DEAD zone -- the exact repro of the night-16
|
||||
// re-descend (zone 18 x7). With the burst-loop guard at binary scope:
|
||||
// ONE [cascade] for the life and the [dmghit] stream for that zone
|
||||
// STOPS at saturation (the guard skips application entirely).
|
||||
if ((Entity *)this == application->GetViewpointEntity()
|
||||
&& getenv("BT_ZONE_HAMMER"))
|
||||
{
|
||||
static int s_zhFrame = 0;
|
||||
++s_zhFrame;
|
||||
if (s_zhFrame >= 900 && (s_zhFrame % 15) == 0) // settle, then ~4 Hz
|
||||
{
|
||||
int zidx = 2;
|
||||
float amt = 8.0f;
|
||||
{
|
||||
char spec[64];
|
||||
strncpy(spec, getenv("BT_ZONE_HAMMER"), sizeof(spec) - 1);
|
||||
spec[sizeof(spec) - 1] = 0;
|
||||
char *comma = strchr(spec, ',');
|
||||
if (comma != 0) { *comma = 0; amt = (float)atof(comma + 1); }
|
||||
if (spec[0]) zidx = atoi(spec);
|
||||
}
|
||||
Damage dmg;
|
||||
dmg.damageType = Damage::ExplosiveDamageType;
|
||||
dmg.damageAmount = amt;
|
||||
dmg.burstCount = 1;
|
||||
dmg.impactPoint = localOrigin.linearPosition;
|
||||
Entity::TakeDamageMessage td(
|
||||
Entity::TakeDamageMessageID,
|
||||
sizeof(Entity::TakeDamageMessage),
|
||||
GetEntityID(), zidx, dmg);
|
||||
Dispatch(&td);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user