Gitea #46: ammo bay fire now DETONATES and KILLS (+ #47 fire icon) -- three stacked kill-switches removed, the fuse decoded, and a vptr-alias corruption caught by regression
Field report (night 3): "two ammo bay fires and no death" (Cyd + RajelAran; one
purged, one left burning). Root cause = THREE independent kill-switches stacked
on the same path, all in ammobin.cpp:
1. `GameClock::Now() { return 0; }` -- `cookOffTime < Now()` was `0 < 0`, so
an ARMED bay fire never detonated.
2. `InjectHeat(void*) {}` -- the detonation body was a no-op.
3. The bin's Damage record was never stamped -- 0 damage of type 0 (which the
mech TakeDamage handler drops) even if 1+2 had fired.
THE FUSE (raw disasm, scratchpad/disammo.py): the old "RandomDelay" was a Ghidra
carve artifact -- FUN_004dcd94 is __ftol and the export DROPPED the caller's x87
expression. The real bytes @004bd450: fld 10.0 / fmul [ticksPerSecond] /
fadd 0.5 / __ftol -- a FIXED 10.0-SECOND fuse in clock ticks. New gotcha #19
(reconstruction-gotchas.md) documents the __ftol export blind spot.
THE DAMAGE RECORD: bin+0x1F0..0x21C is a real engine `Damage` (FUN_0041db7c IS
Damage::Damage(), byte-matched to T0 DAMAGE.cpp). The linked ProjectileWeapon's
ctor @004bc3fc stamps it from weapon->damageData @0x3A8 via
owner->roster[0x128][res+0x1C0] -- projweap.cpp's old comment called this "the
bin's HUD display block ... wired in the AmmoBin family"; both halves were wrong
and it was wired nowhere. Now stamped (before MissileLauncher's ctor divides by
missileCount -- a missile bin authentically holds the per-SALVO amount).
THE DETONATION (@004ac274 = MechSubsystem::DistributeCriticalHit -- the old
"HeatableSubsystem::InjectHeat" label was wrong, and the old reconstruction
iterated a stand-in CriticalChain whose First()/Next() returned 0):
statusAlarm pulse Exploding(2)->Destroyed(1) (slot 13 = the printSimulationState
state PRINT @004ac8c0, not an "explosion notify"), own private zone pinned
destroyed, then collect the mech DamageZones whose crit entries plug the bin
(the binary filters plug classID 0x4E = DamageZoneClassID -- VDATA.h idx 78,
cross-checked via idx 28 = AudioStateTrigger), split the amount evenly, and send
the OWNER one full Entity::TakeDamageMessage per zone: inflictingEntity = SELF,
damageZone = the zone index, inflictingSubsystemID = the bin (the message-
manager explosion-bundling key, ENTITY3.h's own NOTE), printing the binary's
exact "ammo explosion damaging <zoneName>" @0050df61.
Port shape: Mech::AmmoExplosionFanOut (mechdmg.cpp) behind a databinding bridge;
guarded deviation: zoneCount==0 warns instead of the binary's unguarded divide.
CriticalChain/CriticalEntry stand-ins DELETED from mechrecon.hpp.
VERIFIED LIVE (BT_BAYTEST hook = message 1, the crit-induced arm channel):
scratchpad/baytest.py : arm -> 10s -> "20 rounds x 35 = 700 (type 2)" ->
"ammo explosion damaging dz_ltorso" -> zone cascade -> mech DESTROYED
(authentic death list).
scratchpad/baypurge.py: arm -> eject-hold purge -> "bay fire EXTINGUISHED
(bin empty)", no detonation.
scratchpad/sim3.py : the HEAT route arms organically in combat (overheated
AFC100), detonates "11 x 25 = 275 (type 1)" split across dz_larm + dz_lgun.
BAYBOOM matchlog record added for MP field forensics.
FIX-OF-THE-FIX (caught by the sim3 regression, would have shipped a crash):
MechSubsystem's ReconDamageZone proxy puts structureLevel at OFFSET 0 -- which
ALIASES THE REAL DamageZone's VTABLE POINTER (the private zone is `new
DamageZone`, mechsub.cpp:154; mechsub.hpp:260 documents the alias). My first
DistributeCriticalHit kept the old body's `damageZone->structureLevel = 1.0f`
and OVERWROTE THE ZONE'S VPTR with 0x3F800000; the respawn sweep's virtual
SetGraphicState (vtable+0xC) then called through it -> AV at 0x3f80000c in
RespawnRepair, one frame after a bay-fire death. ALL EIGHT proxy-view sites in
mechsub.cpp swept to the engine view (((DamageZone*)damageZone)->damageLevel
@0x158) -- including two silently-wrong LIVE readers: GetStatusFlags (vptr as
float -> always "intact") and ApplyDamageAndMeasure (the crit cascade's
measure). Ruled out first by evidence: the weapon->bin stamps were all clean
(six stamps, all classID 0xbcb, logged).
#47 (half 1 -- the FIRE ICON): BallisticWeaponCluster::Execute @004c9a38 reads
bin+0x18C = cookOffArmed into the btefire.pcc TwoState, and while armed computes
(Now - cookOffTime)/ticksPerSecond -- the COOK-OFF COUNTDOWN -- into the numeric
beside it. The old reconstruction misread 0x18C as "the reload state" and
bridged the icon to BTAmmoBinFeeding, so it blinked on every feed and never lit
on a bay fire (RajelAran: "it doesn't"). Now driven by the
BTAmmoBinCookOffArmed/CookOffTime complete-type bridges. [T2 -- the data path
is rig-verified; the pixels await the next live session.]
#47 (half 2 -- the ENG-BUTTON FLASH): fully mapped, deliberately NOT built this
session. The authored data SHIPS (BTL4.RES carries exactly one type-31
GaugeAlarmStream); the chain is alarm SetLevel -> gauge-watcher socket ->
Renderer msg 7 -> GaugeAlarmManager::Activate @00448d00 (T0) -> the BTL4
override @004cc148..@004cc2fc (btl4galm.cpp's provenance note claiming "no
override body exists" is WRONG -- corrected in-file) -> LampManager::FindLamp
@00444c80 -> Lamp::SetAlertState @00444e64 (flash counter) -> the L4 flush
@00474e94 emitting flashFast states 0x37/0x13 (== T0 L4LAMP.cpp:234-239).
Missing: the override bodies, the gauge-watcher sender, the aux-button lamps.
3-piece plan in context/open-questions.md.
Also logged: HandleMessage is vtable slot 8/9 in the binary but NON-virtual
across 10 reconstruction classes (bit the BT_BAYTEST hook; typed call used, gap
documented in open-questions).
KB: combat-damage.md (the full cook-off section), decomp-reference.md (the
cluster addresses + the GaugeAlarm/lamp map + BT_BAYTEST env), gauges-hud.md
(the fire-icon correction), reconstruction-gotchas.md #19 (__ftol),
open-questions.md (2 entries), btl4galm.cpp provenance correction.
checkctx CLEAN. 40 LNK2019 unchanged (the two pre-existing families).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d39227ef39
commit
5ae4410914
@@ -1160,3 +1160,105 @@ void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// Mech::AmmoExplosionFanOut -- the mech side of @0x4ac274 (Gitea #46)
|
||||
//
|
||||
// The binary iterates the plugs connected to the EXPLODING SUBSYSTEM
|
||||
// (FUN_004acfa9 walks its Node link chain), collecting every object whose
|
||||
// classID@+4 == 0x4E = DamageZoneClassID -- i.e. the mech damage zones whose
|
||||
// critical-subsystem entries plug this subsystem -- then splits the total
|
||||
// damage evenly and sends the OWNER one full Entity::TakeDamageMessage
|
||||
// (id 0x12, 100 bytes) per zone:
|
||||
//
|
||||
// +0x1C inflictingEntity = owner+0x184 (the mech ITSELF)
|
||||
// +0x24 damageZone = zone+0x13C (damageZoneIndex)
|
||||
// +0x2C damageData = the split Damage record
|
||||
// +0x5C inflictingSubsystemID = subsystem+0xD8 (so the message manager
|
||||
// can bundle the bin's explosion resource for the view --
|
||||
// ENTITY3.h's own NOTE)
|
||||
//
|
||||
// printing "ammo explosion damaging <zoneName>" (@0050df61, name from
|
||||
// zone+0x15C = DamageZone::damageZoneName) per zone. [T1 raw decomp]
|
||||
//
|
||||
// The port walks its own zone table with the SAME membership test (the
|
||||
// criticalSubsystems[] plug back-reference IS the plug link the binary
|
||||
// iterated from the subsystem's side). ONE guarded deviation: the binary
|
||||
// divides by the collected count UNGUARDED (every authored bin lives in at
|
||||
// least one zone); the port warns-and-returns on zero so a malformed model
|
||||
// cannot div-by-zero.
|
||||
//
|
||||
void
|
||||
Mech::AmmoExplosionFanOut(
|
||||
::Subsystem *inflicting_subsystem,
|
||||
int inflicting_subsystem_id,
|
||||
Damage &total_damage
|
||||
)
|
||||
{
|
||||
enum { kMaxZones = 32 };
|
||||
int zones[kMaxZones];
|
||||
int zoneCount = 0;
|
||||
|
||||
for (int z = 0; z < damageZoneCount && zoneCount < kMaxZones; ++z)
|
||||
{
|
||||
Mech__DamageZone *dz = Zone(z);
|
||||
if (dz == 0)
|
||||
continue;
|
||||
for (int c = 0; c < dz->criticalSubsystemCount; ++c)
|
||||
{
|
||||
MechCriticalSubsystem *cs = dz->criticalSubsystems[c];
|
||||
if (cs != 0 && (::Subsystem *)cs->subsystemPlug.GetCurrent() == inflicting_subsystem)
|
||||
{
|
||||
zones[zoneCount++] = z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneCount < 1)
|
||||
{
|
||||
// Guarded deviation (see the header comment).
|
||||
DEBUG_STREAM << "[ammo] explosion fan-out: subsystem "
|
||||
<< inflicting_subsystem_id << " is in NO damage zone -- dropped\n" << std::flush;
|
||||
return;
|
||||
}
|
||||
|
||||
total_damage.damageAmount /= (Scalar)zoneCount; // even split (@004ac2xx)
|
||||
|
||||
for (int i = 0; i < zoneCount; ++i)
|
||||
{
|
||||
Mech__DamageZone *dz = Zone(zones[i]);
|
||||
|
||||
// The binary's exact print (@0050df61) -- warning channel.
|
||||
DEBUG_STREAM << "ammo explosion damaging "
|
||||
<< (const char *)dz->damageZoneName << "\n" << std::flush;
|
||||
|
||||
Entity::TakeDamageMessage td(
|
||||
Entity::TakeDamageMessageID, // 0x12
|
||||
sizeof(Entity::TakeDamageMessage),
|
||||
GetEntityID(), // inflictingEntity = SELF (owner+0x184)
|
||||
dz->damageZoneIndex, // zone+0x13C
|
||||
total_damage,
|
||||
inflicting_subsystem_id); // the bin (explosion bundling)
|
||||
Dispatch(&td); // (**owner_vtbl)(owner,&msg) -- the
|
||||
// proven master-side damage path
|
||||
}
|
||||
|
||||
// MP MATCH FORENSICS: the bay-fire detonation landed (field verify for #46).
|
||||
if (BTMatchLogActive())
|
||||
BTMatchLog("BAYBOOM", "mech=%d:%d sub=%d zones=%d dmg=%.3f",
|
||||
BTMatchHostOf(GetEntityID()), (int)GetEntityID(),
|
||||
inflicting_subsystem_id, zoneCount, total_damage.damageAmount);
|
||||
}
|
||||
|
||||
//
|
||||
// The cross-family bridge (called from MechSubsystem::DistributeCriticalHit in
|
||||
// mechsub.cpp, where Mech is incomplete -- the databinding rule).
|
||||
//
|
||||
void BTAmmoExplosionFanOut(
|
||||
void *owner_mech, void *inflicting_subsystem,
|
||||
int inflicting_subsystem_id, Damage &total_damage)
|
||||
{
|
||||
((Mech *)owner_mech)->AmmoExplosionFanOut(
|
||||
(::Subsystem *)inflicting_subsystem, inflicting_subsystem_id, total_damage);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user