#82 ROOT FIX: reconstruct the crash SELF-DAMAGE -- wall-grinding now hurts,

the knockdown storm self-limits, peers keep the limp

The 'peers see a limping mech skating' report decomposed into a wall-grind
KNOCKDOWN STORM: a gimped mech held against a blocking solid re-crashes each
time its gg cycle rebuilds speed (the gg ceiling is authentically the clip's
natural speed -- bhk1 ~14.9 u/s, ceiling+divisor @0x544/0x548, loader formula
byte-matched -- so iv2 ~222 >> the 40.0 threshold @0x4ab184), and every
type-5 KNOCKDOWN broadcast floors all peers' replicants into knockdown/trn
churn: gliding + leg-lifts = skating.  Turning and crushable cars were
A/B-exonerated (grass circling: 198 replicant gimp entries, 17 crunches,
zero broadcasts).

What the binary has that the port lacked -- recovered from the EXPORT GAP by
raw disasm (scratchpad/night6/gap_4a9770.txt, @4aa89f-4aaab4) -- is the crash
branch's tail: fallDirection = worldToLocal(damageForce) (FUN_0040879c, M^T v,
un-normalized), fallScalar = -(fallDirection . localVelocity.linearMotion),
and a self-dispatched TakeDamageMessage{0x64, zone=-1, the engine-computed
collision Damage verbatim}.  A wall crash COSTS ARMOR, so the grind degrades
the mech instead of looping forever.  That dispatch was the long-standing
'DEFERRED' note in the response policy; now reconstructed.

Falsified en route (comments + KB corrected, do not revive): the stagger's
FUN_004a4c54(1)/(0x20) 'action-request flags' feed NO drive suppressor --
image-wide sweep + full gap disasm show word[this+0x18] is read only by the
net record emitter; the bmp clip applies NO root motion (adv=0 measured); the
'gimp cap 5.8' figure was the BACK-cycle stride printed under a misleading
label (now ggCapL/R; @0x52c has no binary consumer).

Verified (mp_gimpAB rigs): arena1 wall-grind now 4 bounded strikes, limper
dies of its crashes and respawns cleanly twice (START->RESET, no strand);
grass circling unchanged-clean (198 replicant gimp states, 0 knocks).
Rig: pid capture race fixed (a missed winpid left a stale node holding the
-net port -> null runs), per-config sweep added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-07-30 16:18:28 -05:00
co-authored by Claude Fable 5
parent 19fe1e5fa2
commit 9c83a468db
8 changed files with 353 additions and 36 deletions
+81 -10
View File
@@ -599,6 +599,13 @@ int gBTHudPrimary = 1; // PrimaryHudOn (element mask 0x20): full HUD vs si
int gBTGotoActive = 0;
float gBTGotoTurn = 0.0f;
float gBTGotoThrottle = 1.0f;
// BT_GOTO_RELEASE latch: the beeline has arrived and handed the mech to the
// pilot. It must be visible to BOTH goto drive paths -- the self-drive primer
// (which re-asserts forced/0.8 every frame purely because BT_GOTO is set in the
// env) and the beeline block itself. Without it the primer overrode the
// handover on the very next frame and the mech walked straight past its target
// at full throttle, un-steered.
int gBTGotoReleased = 0;
static int gDriveSeeded = 0;
static Scalar gDriveLogAccum = 0.0f; // 1 Hz position log throttle
static Scalar gTickLogAccum = 0.0f; // 1 Hz subsystem-tick log throttle
@@ -3214,12 +3221,14 @@ void
}
}
}
else if (sGotoDrive)
else if (sGotoDrive && !gBTGotoReleased)
{
// BT_GOTO is self-driving: enable forced mode so the goto beeline
// block (gated on gBTDrive.forced, below) runs on its own without
// requiring BT_AUTODRIVE. That block sets the actual throttle
// (drive toward the target, 0 inside the stop radius).
// gBTGotoReleased gates it: once BT_GOTO_RELEASE has handed the
// mech over, this primer must STOP re-arming forced drive.
gBTDrive.forced = 1;
gBTDrive.forcedThrottle = 0.8f;
}
@@ -3539,6 +3548,17 @@ void
{ bestG = ed2; gxG = (float)ep.x; gzG = (float)ep.z; foundG = 1; }
}
if (foundG) { s_gx = gxG; s_gz = gzG; }
else if (s_goto == 3)
{
// "gimp" mode with NOBODY limping yet: STAND STILL and wait
// for one. Falling back to the nearest peer made every
// observer chase another OBSERVER (each is someone's
// nearest), so they scattered to three different corners
// instead of gathering on the wounded mech.
s_gx = 0.0f; s_gz = 0.0f; // (0,0) => the drive guard below skips
gBTGotoActive = 0;
gBTDrive.forced = 0;
}
else if (found) { s_gx = gxN; s_gz = gzN; }
else { gBTGotoActive = 0; }
}
@@ -3633,7 +3653,15 @@ void
gBTGotoActive = 0;
gBTDrive.forced = 0;
gBTDrive.forcedThrottle = 0.0f;
DEBUG_STREAM << "[goto] ARRIVED -- released to manual control\n" << std::flush;
// ALL-STOP on handover. Clearing `forced` alone leaves the
// throttle LEVER wherever the beeline parked it (0.8), so the
// mech walked away from the target the instant it was handed
// over -- observers ended up 1000+ units past the mech they
// were sent to watch. Zero the lever so it comes to rest and
// waits for the pilot.
gBTDrive.allStop = 1;
gBTGotoReleased = 1; // stop the self-drive primer re-arming forced/0.8
DEBUG_STREAM << "[goto] ARRIVED -- all-stop, released to manual control\n" << std::flush;
}
}
turn = gBTGotoTurn; // non-real-controls fallback path
@@ -6810,8 +6838,45 @@ void
MoveCollisionVolume();
if (isPlayer)
gBlockCooldown = kBlockHysteresis; // (re)arm the in-contact window
// DEFERRED: self TakeDamageMessage{0x64, zone=-1} (inert today) +
// material/normal/approach caches + collisionState indicators.
//
// CRASH BOOKKEEPING + SELF-DAMAGE (binary @4aa89f-4aaab4; raw disasm
// scratchpad/night6/gap_4a9770.txt -- the export gap). This block was
// the long-standing "DEFERRED self TakeDamageMessage" note; the #82
// wall-grind dig (2026-07-30) recovered it byte-by-byte:
// @4aa8e2 fallDirection = the engine-filled dmg.damageForce rotated
// world->local (FUN_0040879c = M^T v on the localToWorld
// 3x3 rows; NOT normalized);
// @4aa8f3 fallScalar = -(fallDirection . localVelocity.linearMotion)
// (the [ebp-0x198] operand confirmed = mech+0x1c4 -- the
// same vector iv2 squares below);
// @4aa984-4aaab4 build TakeDamageMessage{0x64, zone=-1, the
// collision Damage VERBATIM} and Dispatch it at OUR OWN
// mech: a wall crash HURTS. This is what makes wall-
// grinding self-limiting in the binary -- each stagger
// costs armor, so the #82 knockdown storm degrades the
// mech instead of looping forever while the peers'
// replicants churn knockdown/trn ("skating").
// (TODO payload nicety: throttleState = fall-surface material,
// binary (**[mech+0x2f8])+0x24 -- feeds the type-5 reader's
// surface-specific fall handling; needs the assistant-record map.)
//
{
const Vector3D &ff = dmg.damageForce;
fallDirection.x = ff.x * localToWorld(0,0) + ff.y * localToWorld(1,0)
+ ff.z * localToWorld(2,0);
fallDirection.y = ff.x * localToWorld(0,1) + ff.y * localToWorld(1,1)
+ ff.z * localToWorld(2,1);
fallDirection.z = ff.x * localToWorld(0,2) + ff.y * localToWorld(1,2)
+ ff.z * localToWorld(2,2);
fallScalar = -(fallDirection.x * impactVel.x
+ fallDirection.y * impactVel.y
+ fallDirection.z * impactVel.z);
Entity::TakeDamageMessage td(
Entity::TakeDamageMessageID,
sizeof(Entity::TakeDamageMessage),
GetEntityID(), -1 /* no aimed zone -> cylinder lottery */, dmg);
Dispatch(&td);
}
Scalar iv2 = impactVel.x * impactVel.x + impactVel.y * impactVel.y
+ impactVel.z * impactVel.z;
if (iv2 > 40.0f) // crash-anim threshold @0x4ab184
@@ -6824,12 +6889,18 @@ void
// two action-request calls are the binary's FUN_004a4c54(this,1)
// then (this,0x20) -- TWO calls, not one OR'd (fidelity verdict).
// Guard 1: only when the bmp clip resolved (no substitute clip).
// Guard 2 (BRING-UP, marked): don't REBIND while the crash clip is
// already playing -- the bmp clip carries ~6.5 u/s of root motion
// that presses back into the wall, and without the actionRequestFlags
// consumers (bits 1/0x20 -- likely the drive suppressor during the
// stagger, NOT yet reconstructed) the re-trigger restarts the clip
// every frame. Remove this guard when the flag consumers land.
// Guard 2: don't REBIND while the crash clip is already playing.
// (2026-07-30 #82 dig CORRECTS the old rationale here: the
// "actionRequestFlags consumers / drive suppressor" theory is
// FALSIFIED -- an image-wide sweep + a full disasm of the export
// gap show word[this+0x18] is read ONLY by the net record emitter.
// The binary has NO rebind guard and needs none: the crash zeroes
// worldLinearVelocity and the bmp state advances with cycleSpeed 0
// (adv=0 measured), so iv2 collapses during the clip and re-binds
// are naturally spaced. What actually bounds the stagger LOOP in
// the binary is the crash SELF-DAMAGE above -- grinding costs
// armor. This guard + the freshBlock hysteresis stay as cheap
// belt-and-braces port additions.)
if (animationClips[0x20] != 0
&& legStateAlarm.GetLevel() != 0x20
&& freshBlock) // only on a FRESH strike, not continuous grinding