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:
co-authored by
Claude Fable 5
parent
054c3f2eee
commit
37dd7f9663
@@ -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
@@ -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)` —
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) ==="
|
||||
Reference in New Issue
Block a user