collision-pricing audit: restore the binary's crash economy -- two fixes

A. KNOCKDOWN PACING: the 0.4s gBlockCooldown contact-hysteresis is gone.
It suppressed the crash knockdown for as long as contact was held, letting
the kinematic drive re-slam the obstacle at full commanded speed EVERY
FRAME -- continuous full-price crash self-damage, a healthy 65-tonner dead
in ~2-3s of grinding (the night-7 SAURON ram death, 110 priced frames at
|v|~34.7).  The binary needs no guard (crash threshold @0x4ab178 read from
the exe = 0.0, dispatch per blocked frame is authentic): the bmp clip
zeroes the drive, and a pressed mech re-triggers the knockdown at v>6.3
long BEFORE its grind can price anything -- the type-0 divert's 0.5-pt
free floor needs v>~18.5 at 65t.  Wall grinding is free taps + paced
staggers; only genuine fast arrivals pay.

B. RAW RAM DISPATCH: the *0.001 "ram economy normalization" (2026-07-12)
predated the #83 type-0 divert and became a double-normalization -- the
victim's divert scale (~4e-5) compounded with it, so NO physically
possible ram could clear the 0.5-pt free floor: rams were a no-op against
the victim while the rammer's own un-scaled crash path rattled for real.
The binary dispatches raw [T1 @part_012:15324]; the divert IS the
normalizer.  Also un-starves cultural-icon crunches (trucks now crush on
contact per their authored armor, not after ~19 bumps).

Bench (scratchpad/night7/mp_rampricing.sh):
- wall leg: 150s full-throttle wall push = 125 paced knockdown binds
  (~1.2s cadence, iv2 40-43), 4767 crash frames ALL under the free floor,
  0 rattle, 0 deaths (pre-fix: dead in seconds).
- ram leg: BT_GOTO=enemy + BT_GOTO_STOP=2 rams the peer; raw amount
  arrives byte-identical on the victim (66239.1 tx == rx), victim prices
  2.55 pts internal rattle (first ram damage to a victim in the port's
  history), pressed follow-ups free, 0 deaths both nodes, 108 knockdowns
  -> exactly 108 type-5 records on the observer (no skating regression).

KB swept: combat-damage.md (the 1.3e6 moverMass mis-attribution corrected
to the measured 60-90k tonnage scale), locomotion.md, rendering.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Zh7PTkFy4KwTzVighLR9J
This commit is contained in:
Joe DiPrima
2026-07-31 02:48:35 -05:00
co-authored by Claude Fable 5
parent 054c3f2eee
commit 37dd7f9663
5 changed files with 131 additions and 56 deletions
+14 -7
View File
@@ -336,13 +336,20 @@ Three-layer story, measured live + decomp-verified:
application regardless of burstCount (burstCount is read only by the gyro-bounce math + the
SplashDamage falloff). A cluster missile therefore lands its damageAmount ONCE — see the
SALVO-LEAD FIX below for why the port's N-round missiles must damage once per salvo. ✓
2. **StaticBounce prices rams with the AUTHORED moverMass ≈ 1.3e6 units** → ~600×v² ≈ 59,000
points for a 10 m/s ram (measured: 59221@9.94, 398@0.81; `[collide-tx]` now logs mass/e).
The binary DISPATCHES it raw (@part_012:15324-15358) — but in pod MP it landed on the local
REPLICANT of the victim (the master never heard it; MECH.CPP:986 warns and proceeds) —
**ram damage was network-inert in the pod**. Our task-#47 replicant forwarding (required
for MP weapon damage) surfaced it as a one-shot. Port: `BTDispatchCollisionDamage`
normalizes ×1e-3 to the armor-point economy (bump = points, charge = tens) [T3].
2. **StaticBounce prices rams at ~600×v² raw** (measured: 59221@9.94, 66239@9.59, 398@0.81;
`[collide-tx]` logs mass/e — mech moverMass is the tonnage-scale 60000-90000, NOT the old
"authored ≈1.3e6" mis-attribution; the ~24·v²·0.0005·mass factor is the head-on (vn·vp)
algebra). The binary DISPATCHES it raw (@part_012:15324-15358) — but in pod MP it landed
on the local REPLICANT of the victim (the master never heard it; MECH.CPP:986 warns and
proceeds) — **ram damage was network-inert in the pod**. Our task-#47 replicant forwarding
surfaced it as a one-shot, and a ×1e-3 normalization was patched in (2026-07-12 [T3]).
**That normalization is GONE (pricing audit 2026-07-31): the #83 type-0 divert is the
authentic normalizer** — the victim's TakeDamage hub routes collision damage into
`DistributeCollisionDamage` (scale ≈ 3.9-4.5e-5), so with the 0.001 still applied no
physically possible ram could clear the divert's 0.5-pt free floor (rams were a no-op
against the victim). Raw restored [T1]: a 9.6 m/s ram = 2.55 pts of internal rattle on the
victim (bench-measured, `scratchpad/night7/mp_rampricing.sh`); pressed follow-up contact
prices under the floor = free.
3. **The contact EDGE**: the binary's bounce reversed the mover's velocity so the frame after
a bump read as separating; our gait re-derives velocity per frame (still closing while
pressed / respawn-overlapped) → sustained contact re-priced full rams at 60Hz (the respawn
+11 -5
View File
@@ -206,11 +206,17 @@ master mech needs a **CollisionAssistant** (GetCurrentCollisions iterates it unc
ship ZERO tiles (h never negative). [T2]
## Knockdown / crash + the desync/stutter fixes
Wall impacts iv²>40 (~6.3 u/s) bind the `bmp` stagger clip. Two documented bug fixes: (a) the
knockdown must stagger BOTH channels (`SetBodyAnimation(0x20)` + `SetLegAnimation(0x20)`) or display
+ travel split permanently (foot-slip); (b) a contact-hysteresis gate (0.4 s `gBlockCooldown`)
stops the knockdown re-firing on sustained/glancing contact. `localVelocity` is zeroed while crashed
(spec-faithful) so a knocked mech can't keep advancing. [T2]
Wall impacts iv²>40 (~6.3 u/s) bind the `bmp` stagger clip. The knockdown must stagger BOTH
channels (`SetBodyAnimation(0x20)` + `SetLegAnimation(0x20)`) or display + travel split permanently
(foot-slip). `localVelocity` is zeroed while crashed (spec-faithful) so a knocked mech can't keep
advancing. **The 0.4 s `gBlockCooldown` contact-hysteresis is GONE (pricing audit 2026-07-31)**: it
held the knockdown off for as long as contact persisted, letting the drive pay full-commanded-speed
crash self-damage EVERY FRAME — a healthy mech dead in ~2-3 s of grinding (the night-7 SAURON ram
death). The binary needs no such gate: the clip zeroes the drive, and a pressed mech re-triggers
the knockdown at v>6.3 long before its grind can price anything (the type-0 divert's 0.5-pt floor
needs v>~18.5 at 65 t) — wall grinding = free taps + ~1.2 s-paced staggers, only fast ARRIVALS pay.
Bench: 150 s full-throttle wall push = 125 paced binds, 4,767 free-tap frames, 0 rattle, 0 deaths
(`scratchpad/night7/mp_rampricing.sh`). The mid-clip rebind guard (`legState != 0x20`) stays. [T2]
**⚠ The WALL-GRIND KNOCKDOWN STORM = the surviving "#82 peers see skating" — ROOT-CAUSED + FIXED
(2026-07-30) [T1 disasm / T2 verified].** Each stagger bind also broadcasts `ForceUpdate(0x20)`
+4 -2
View File
@@ -255,8 +255,10 @@ streamed via INTEREST.cpp; ARENA1 = 124 icons, each with a damage zone, `Explosi
TakeDamageMessageHandler` → zone `damageLevel ≥ 1`**Explosion entity** (Crunch variant for
collision) + `WaitingToBurn` (+`timeDelay`) → `BurningState` + `UpdateCollisionVolumes` (boxes
DELETED on `removeOnDeath`, else sunk by `destroyedYTranslation`). The mech-side crunch dispatch
(`mech4.cpp ProcessCollision``BTDispatchCollisionDamage`, ×0.001 ram economy) reaches icons
(≈19 walking-speed bumps kill the spawn-yard truck). What was BROKEN (the "no state change"
(`mech4.cpp ProcessCollision``BTDispatchCollisionDamage`, RAW since the 2026-07-31 pricing
audit — the ×0.001 era needed ≈19 walking-speed bumps to kill the spawn-yard truck; raw is the
binary's own dispatch [T1], so a mech contact now crunches a truck-scale icon outright, per the
authored icon armor). What was BROKEN (the "no state change"
report): the 2007 port fully stubbed **`StateInstanceSwitchRenderable`** ("STUBBED: DPL RB
1/14/07") — the intact→rubble visual swap NEVER fired. Revived D3D9-native: the switch now
controls the draw component via the **`SetDrawObj` in-place drawable swap** (the mech
+31 -42
View File
@@ -641,20 +641,17 @@ static int gShotCount = 0;
// = {0,0,-adv/dt} is ZEROED while crashed) so a knocked-down mech cannot creep back
// into the obstacle and re-trigger every frame. We reproduce that: while the crash
// clip (legState 0x20) plays, hold the velocity at zero.
// The knockdown must also only fire on a FRESH impact -- the frame the mech first
// strikes something -- NOT every frame it grinds against an obstacle it is already
// pressed into. Otherwise holding throttle into a wall/mech re-knocks-down every
// recovery (the stuttery leg flip-flop the user sees). gWasBlocked tracks last
// frame's contact so a continuous press just BLOCKS (stand/walk-in-place) and only a
// separate-then-re-approach produces a new knockdown.
// Block hysteresis: a knockdown fires only when the mech has been OUT of contact
// for kBlockHysteresis seconds (a genuine fresh approach). A 1-frame edge is too
// brittle -- a glancing / sliding contact alternates blocked/not-blocked (the frame
// rejection pushes the mech out each blocked frame), which would flicker the edge
// back to "fresh" and re-knockdown. The window bridges those gaps so any sustained
// OR intermittent press just BLOCKS; only separating for real re-arms the knockdown.
static Scalar gBlockCooldown = 0.0f; // seconds since the last blocking contact (counts down)
static const Scalar kBlockHysteresis = 0.4f;
// (The former gBlockCooldown "fresh strike" hysteresis is GONE -- collision-pricing
// audit, 2026-07-31. It suppressed the knockdown for as long as contact was held,
// which let the drive re-slam the obstacle at full commanded speed EVERY FRAME:
// continuous full-price crash self-damage, a healthy mech dead in ~2-3s of grinding
// (night-7 field death, SAURON's Loki vs the Sunder wreck). The binary's own pacing
// needs no guard: the knockdown zeroes the drive, and a pressed mech re-accelerating
// from stand re-triggers the knockdown at v>6.3 (iv2>40) long BEFORE its grind can
// price anything (the divert's 0.5-pt free floor needs v>~18.5 at 65t) -- so wall
// grinding is free taps + periodic staggers, and only genuine fast ARRIVALS pay.
// The mid-clip rebind guard below provides the flicker protection the hysteresis
// was really buying.)
// E8 weapon triggers (bring-up): pulsed per player frame; read by the weapon
// sims so CheckFireEdge sees rising edges. Non-static (the weapons extern
@@ -6790,16 +6787,6 @@ void
Point3D savedPos = localOrigin.linearPosition;
BoxedSolidCollisionList *cols = GetCurrentCollisions();
Vector3D impactVel = localVelocity.linearMotion;
// CONTACT EDGE (player only): a knockdown fires only on a FRESH strike (not blocked
// last frame). While the mech stays pressed against the obstacle it just BLOCKS --
// no repeated knockdowns. gWasBlocked is a single global, so gate it to the
// viewpoint mech; other masters (the stationary dummy, MP replicant-driven masters)
// keep the original per-fresh-block behavior and never touch the shared state.
const bool isPlayer = (application != 0
&& (Entity *)this == application->GetViewpointEntity());
const int freshBlock = isPlayer ? (gBlockCooldown <= 0.0f ? 1 : 0) : 1;
if (isPlayer && gBlockCooldown > 0.0f)
gBlockCooldown -= dt; // decay the out-of-contact window
// Per-frame collision-state reset (@4aa741): the walk below re-accumulates it
// per resolved contact, then it's pushed into the collisionState StateIndicator.
collisionTemporaryState = 0;
@@ -6857,8 +6844,6 @@ void
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
localOrigin.linearPosition = old_position;
MoveCollisionVolume();
if (isPlayer)
gBlockCooldown = kBlockHysteresis; // (re)arm the in-contact window
//
// CRASH BOOKKEEPING + SELF-DAMAGE (binary @4aa89f-4aaab4; raw disasm
// scratchpad/night6/gap_4a9770.txt -- the export gap). This block was
@@ -6929,11 +6914,14 @@ void
// (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.)
// armor. The mid-clip guard stays as a cheap belt-and-braces
// port addition; the freshBlock HYSTERESIS is GONE (pricing audit
// 2026-07-31): it held the knockdown off during sustained contact,
// letting the drive pay full-speed crash price every frame -- the
// 2-3s grind death. See the pacing note at gBlockCooldown's old
// site (~line 640).
if (animationClips[0x20] != 0
&& legStateAlarm.GetLevel() != 0x20
&& freshBlock) // only on a FRESH strike, not continuous grinding
&& legStateAlarm.GetLevel() != 0x20) // not mid-clip (natural spacing)
{
SetLegAnimation(0x20); // FUN_004a7fc4(this, 0x20)
// SYNC THE DISPLAY CHANNEL (fix the persistent post-bump foot-slip): the
@@ -7060,18 +7048,19 @@ static void
{
Damage dmg;
dmg.damageType = Damage::CollisionDamageType;
// RAM ECONOMY NORMALIZATION [T3, evidence-forced 2026-07-12]: StaticBounce
// prices the kinetic loss with the AUTHORED moverMass (~1.3e6 units) --
// ~59,000 "points" for a 10 m/s ram, against zones whose armor absorbs
// 1 point per point (scale = 1/armorPoints, armor 50-140: the [zone-armor]
// dump). The BINARY dispatches this raw number [T1 @part_012:15324] --
// but on the pod network it only ever hit the local REPLICANT of the
// victim, where it evaporated (the master never heard it; MECH.CPP:986
// merely warns). Our MP forwards replicant damage (task #47, required
// for weapons), which surfaced the dormant 59K as a one-shot ram-kill.
// Normalize by 1e-3 (the mass-unit/armor-point scale): a walking bump =
// a few points, a full charge = tens -- the armor-table economy.
dmg.damageAmount = resolved->damageAmount * 0.001f;
// RAW DISPATCH RESTORED [T1 @part_012:15324] -- collision-pricing audit
// 2026-07-31. The old *0.001 "ram economy normalization" (2026-07-12)
// predated the #83 type-0 divert: back then this amount hit the victim's
// ARMOR zones directly, and the raw kinetic number one-shot people. Since
// 14ff351 the victim's TakeDamage hub diverts type-0 into
// DistributeCollisionDamage, whose authentic (2000/mass)/(100km/h)^2 scale
// (~4.5e-5 at 65t) IS the normalizer -- with the 0.001 still applied the
// two compounded and NO physically possible ram could clear the divert's
// 0.5-pt free-tap floor: rams were a no-op against the victim while the
// rammer's own (un-scaled) crash path rattled for real. Raw through the
// divert prices a full-speed 65t ram at ~1.7 pts of internal rattle --
// small-but-real, the manual's "collision damage" technician setting.
dmg.damageAmount = resolved->damageAmount;
dmg.surfaceNormal = resolved->surfaceNormal;
dmg.impactPoint = Point3D(
(collision.collisionSlice.minX + collision.collisionSlice.maxX) * 0.5f,
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Collision-pricing audit bench (2026-07-31) -- verifies the two fixes:
# A. knockdown-paced crash economy (freshBlock hysteresis removed): a mech
# held against a wall must STAGGER periodically and survive, not pay
# full-speed crash price every frame and die in seconds.
# B. raw ram dispatch (the *0.001 double-normalization removed): a fast
# mech-vs-mech ram must land REAL internal rattle on the VICTIM
# ([colldmg] on the victim's node), which was impossible before.
#
# usage: mp_rampricing.sh wall (leg A: solo, arena, BT_GOTO beeline into a wall)
# mp_rampricing.sh ram (leg B: 2-node, BT_GOTO=enemy ram + observer)
set -x
. /c/git/bt411/scratchpad/night6/bench_common.sh
cd /c/git/bt411/content || exit 1
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
LEG="${1:-wall}"
if [ "$LEG" = "wall" ]; then
# Solo arena: beeline far past the arena wall -> full-speed strike, then a
# sustained push. PASS: [knock] BIND repeats (paced), crashdmg has no
# constant-|v| full-price ramp, ZERO deaths.
rm -f rp_wall.log
BT_GROUND_LOG=1 BT_DMG_LOG=1 BT_GOTO="5000 0" \
bt_launch rp_wall.log WALLTEST.EGG 0x03
sleep 150
bt_kill_ours
echo "=== WALL LEG (rp_wall.log) ==="
echo "-- deaths (MUST be 0):"; grep -c "DESTROYED" rp_wall.log
echo "-- knockdown binds:"; grep -c "knock] BIND" rp_wall.log
grep "knock] BIND" rp_wall.log | head -8
echo "-- crashdmg events:"; grep -c "crashdmg" rp_wall.log
echo "-- rattle (colldmg) events:"; grep -c "colldmg" rp_wall.log
grep "colldmg" rp_wall.log | head -5
else
# 2-node ram: A chases + rams B (stationary master). PASS: RAM lines show
# amt == raw (no *0.001), B's node prices [colldmg] rattle > 0 on arrival
# speed, and neither mech dies from a few rams.
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > RM.EGG
rm -f rp_ram_victim.log rp_ram_rammer.log
# victim (B): stationary master, DMG log for the divert verdict
BT_MP_LOG=1 BT_DMG_LOG=1 BT_MP_NET=1 BT_REPL_LOG=1 \
bt_launch rp_ram_victim.log RM.EGG 0x0C -net 1601
sleep 2
# rammer (A): chase the peer and hit it
BT_MP_LOG=1 BT_DMG_LOG=1 BT_MP_NET=1 BT_GROUND_LOG=1 \
BT_GOTO=enemy BT_GOTO_STOP=2 \
bt_launch rp_ram_rammer.log RM.EGG 0x03 -net 1501
sleep 4
python ../tools/btconsole.py RM.EGG 127.0.0.1:1501 127.0.0.1:1601 &
PC=$!
sleep 150
kill $PC 2>/dev/null
bt_kill_ours
rm -f RM.EGG
echo "=== RAM LEG ==="
echo "-- rammer RAM dispatches (raw vs amt -- MUST match):"
grep -o "RAM.*" rp_ram_rammer.log | head -6
grep "collide-tx" rp_ram_rammer.log | head -4
echo "-- victim rattle ([colldmg] MUST appear for fast rams):"
grep "colldmg" rp_ram_victim.log | head -6
echo "-- victim received type-0 ([mp-hdlr] type=0):"
grep "mp-hdlr" rp_ram_victim.log | grep "type=0" | head -4
echo "-- deaths on each node (a few rams must NOT kill):"
grep -c "DESTROYED" rp_ram_rammer.log
grep -c "DESTROYED" rp_ram_victim.log
echo "-- rammer knockdown pacing:"
grep -c "knock] BIND" rp_ram_rammer.log
fi
echo "=== DONE ($LEG) ==="