From c2f70f6348be849398c8454f81aa42b67ab47fe0 Mon Sep 17 00:00:00 2001 From: arcattack Date: Wed, 8 Jul 2026 18:46:08 -0500 Subject: [PATCH] Beams CUT on a destroyed target (the real FUN_0049fb54, was a mislabeled stub) Answers "can I keep targeting the dead mech?": the lock isn't cleared by anything we've found, but the binary REFUSES to sustain energy beams on a destroyed mech: ServiceDischarge (@004ba8d0) and ContinueDischarge (@004baa20) check the beam's target EVERY discharge frame -- IsDerivedFrom(Mech) then Mech::IsDestroyed (FUN_0049fb54 = movementMode 2||9) -- and kill the beam the moment it dies. EmitterSimulation (@004baa88) applies the same check to the OWNER (a dead mech's own weapons drop everything). Our recon had FUN_0049fb54 as a mislabeled "cockpit/HUD query" no-op stub called with NULL, and never set the beam's target (0x474) at fire -- all three authentic gates were dead. Now: FireWeapon stashes the owner's target entity; both discharge paths + the owner gate run the real check (BTMechDestroyed). Verified: 40 beam samples before the kill, 0 after -- lasers flash-fail on the wreck instead of cutting it like butter (heat is still spent per the binary's FireWeapon, which fires blind; only the DISCHARGE checks the corpse). Co-Authored-By: Claude Fable 5 --- game/reconstructed/emitter.cpp | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/game/reconstructed/emitter.cpp b/game/reconstructed/emitter.cpp index d02071a..bce823a 100644 --- a/game/reconstructed/emitter.cpp +++ b/game/reconstructed/emitter.cpp @@ -51,7 +51,7 @@ // FUN_004b0bd0 PoweredSubsystem simulation step FUN_004b0d50 PoweredSubsystem scalar query // FUN_004ad7d4 HeatableSubsystem "is online/active" FUN_004ac9c8 HeatableSubsystem heat-state query // FUN_00417ab4 follow a SharedData link (voltage source / ammo bin) -// FUN_0049fb54 cockpit/HUD query FUN_0041a1a4 IsDerivedFrom(classDerivations) +// FUN_0049fb54 Mech::IsDestroyed (movementMode 2||9) FUN_0041a1a4 IsDerivedFrom(classDerivations) // FUN_0041bbd8 AlarmIndicator::SetLevel(n) (weaponAlarm @0x350, state @0x364) // FUN_0041c350 Subsystem::SendMessage(template, a, b) // FUN_00408440 Vector copy FUN_00409968 Vector set FUN_0040a968 Vector negate/copy @@ -102,7 +102,20 @@ static void CopyColor(void *, const void *) {} // engine-internal artifacts the decomp called by address (stand-ins) static int FUN_004ad7d4(void *) { return 1; } // HeatableSubsystem active? -static int FUN_0049fb54(int) { return 0; } // cockpit/HUD query +// FUN_0049fb54 == Mech::IsDestroyed (movementMode 2||9; reconstructed as +// Mech::IsDisabled). Was a mislabeled "cockpit/HUD query" no-op stub -- so a +// live beam never CUT when its target mech died (@004ba8d0/@004baa20 check +// this every discharge frame), and a destroyed mech's own weapons kept firing +// (@004baa88). The binary's call sites first verify the object IS a Mech +// (IsDerivedFrom vs 0x50bdb4 = Mech::ClassDerivations), reproduced here. +static int BTMechDestroyed(Entity *e) +{ + if (e == 0) + return 0; + if (!e->IsDerivedFrom(*Mech::GetClassDerivations())) + return 0; + return ((Mech *)e)->IsDisabled() ? 1 : 0; +} static Scalar FUN_004b0d50(void *) { return 1.0f; }// PoweredSubsystem dt scale static int FUN_004ac9c8(void *) { return 0; } // heat-state query static void *FUN_00417ab4(void *) { return 0; } // follow SharedData link @@ -215,8 +228,13 @@ void DrawWeaponPip(aimTransform); // FUN_004b9728 } - // stash the beam endpoint for replication (world hit point) + // stash the beam endpoint for replication (world hit point) + the beam's + // TARGET (0x474 -- the discharge checks read it every frame: the beam cuts + // the moment this mech is destroyed). The owner's target slot is the same + // raw 0x388 the rest of the weapon path uses (HasActiveTarget/mech4). beamEndpoint = targetPoint; // 0x460 <- targetPoint + targetEntity = (owner != 0) + ? *(Entity **)((char *)owner + 0x388) : 0; // 0x474 <- mech target entity targetLocalFlag = 1; // 0x470 SetDirty(); // this+6 |= 1 (needs replication) @@ -259,8 +277,9 @@ void Logical fireEdge = CheckFireEdge(); // @004b9608 targetWithinRange = UpdateTargeting(); // @004b9bdc -> this+0x34c - // hard failure: powered-off, faulted, or destroyed -> drop everything - if (GetFlags() == 1 || GetFaultState() == 2 || FUN_0049fb54(0)) + // hard failure: powered-off, faulted, or the OWNING MECH destroyed (@004baa88 + // checks IsDestroyed on the owner -- a dead mech's weapons drop everything) + if (GetFlags() == 1 || GetFaultState() == 2 || BTMechDestroyed((Entity *)owner)) { ResetFiringState(); // @004ba9a8 currentLevel = 0.0f; // 0x414 @@ -382,7 +401,7 @@ void weaponAlarm.SetLevel(0); // Firing dischargeTimer -= time_slice; // 0x440 if (dischargeTimer <= 0.0f // _DAT_004ba9a4 - || (targetEntity != 0 && FUN_0049fb54(0))) // or the targeted subsystem is gone + || BTMechDestroyed(targetEntity)) // the beam CUTS when its target mech dies { firingActive = 0; weaponAlarm.SetLevel(2); // Loaded @@ -420,7 +439,7 @@ void { if (beamFlag != 0) // 0x46c { - if (targetEntity != 0 && FUN_0049fb54(0)) // targeted subsystem gone + if (BTMechDestroyed(targetEntity)) // the beam CUTS when its target mech dies { ResetFiringState(); // @004ba9a8 return;