diff --git a/context/combat-damage.md b/context/combat-damage.md index abc43f1..91debbc 100644 --- a/context/combat-damage.md +++ b/context/combat-damage.md @@ -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 diff --git a/context/locomotion.md b/context/locomotion.md index d556211..39522f6 100644 --- a/context/locomotion.md +++ b/context/locomotion.md @@ -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)` — diff --git a/context/rendering.md b/context/rendering.md index 9e0408b..def0d84 100644 --- a/context/rendering.md +++ b/context/rendering.md @@ -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 diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 8a5bc9c..038ad81 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -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, diff --git a/scratchpad/night7/mp_rampricing.sh b/scratchpad/night7/mp_rampricing.sh new file mode 100644 index 0000000..6d5dc89 --- /dev/null +++ b/scratchpad/night7/mp_rampricing.sh @@ -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) ==="