From 98907f45afd6a9ed34137897e44130ce0707ce54 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 29 Jul 2026 21:41:02 -0500 Subject: [PATCH] hotfix: null-damageZone guard on the crit path (field crash, build 641, Conn Man's Owens) First field session with real crits (#80) found a weapon subsystem with a NULL damageZone: Mech__DamageZone::CriticalHit -> ApplyDamageAndMeasure -> MechWeapon::TakeDamage +0xf (the engine base derefs the zone unguarded -- every 1995 subsystem shipped with one). Stack symbolized from the field log; the path was unreachable before tonight because ApplyDamageAndMeasure was a stub until #80. Guards at both choke points; a one-shot [crit] log NAMES the zoneless subsystem when hit so the root cause (build that subsystem's zone) can be fixed from the next field log. Owens crit bench: no crash, guard inert on zoned subsystems. Co-Authored-By: Claude Fable 5 --- game/reconstructed/mechsub.cpp | 16 ++++++++++++++++ game/reconstructed/mechweap.cpp | 6 +++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/game/reconstructed/mechsub.cpp b/game/reconstructed/mechsub.cpp index 4a8bfa8..36d3d10 100644 --- a/game/reconstructed/mechsub.cpp +++ b/game/reconstructed/mechsub.cpp @@ -440,6 +440,22 @@ Logical Scalar MechSubsystem::ApplyDamageAndMeasure(Damage &damage) { + // FIELD CRASH GUARD (2026-07-29, build 641, Conn Man's Owens): a crit + // landed on a weapon subsystem whose damageZone is NULL -- the engine + // base derefs it unguarded (every 1995 subsystem shipped with a zone). + // This path was unreachable until #80 made crits land for real. No + // zone -> nothing to damage or measure; the crit fizzles here. The + // one-shot log names the zoneless subsystem so its zone can be BUILT + // (the root-cause follow-up). + if (damageZone == 0) + { + static int s_nz = 0; + if (s_nz++ < 8) + DEBUG_STREAM << "[crit] NULL damageZone on subsystem '" + << GetName() << "' -- crit fizzled (see the Owens field crash)\n" + << std::flush; + return 0.0f; + } Scalar before = ((DamageZone *)damageZone)->damageLevel; // dz+0x158 (engine view) TakeDamage(damage); // (*this.vtable+0x24)(this, &damage) return ((DamageZone *)damageZone)->damageLevel - before; diff --git a/game/reconstructed/mechweap.cpp b/game/reconstructed/mechweap.cpp index 35e6dbc..cb74ea8 100644 --- a/game/reconstructed/mechweap.cpp +++ b/game/reconstructed/mechweap.cpp @@ -405,7 +405,11 @@ Logical void MechWeapon::TakeDamage(Damage &damage) { - Subsystem::TakeDamage(damage); // engine base (real chain: @004b0efc) + // 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.