From 9604f4c4921d1600278165e4879f3c661c44f7f7 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Wed, 5 Aug 2026 12:41:55 -0500 Subject: [PATCH] #52 SKATE detector (ungated field forensic) -- negative-verified; local repro eludes The night-12 field logs eliminated record starvation (zero [ghost] during three observed skating windows), so the bug lives in gait APPLICATION on peers. This adds the [skate] detector to the death-handler tick: a replicant moving >0.08 u/frame for 90+ frames with BOTH animation channels idle (legCycleSpeed + bodyCycleSpeed ~ 0) logs one line per episode + a SKATE matchlog record carrying the discriminating inputs (legCyc/bodyCyc/cmdSpd/destroyed/mode). Honest history: the first build keyed on legCycleSpeed alone and false-fired on every healthy movement phase -- the current peer architecture poses joints from the BODY channel (s_peerLegCh=0, AdvanceBodyAnimation mj=1), so legCycleSpeed==0 is NORMAL there. Caught same-session by the [gimpfeed] silence (AdvanceLegAnimation never runs on peers); corrected to channel-agnostic before anything shipped. Bench (skatebench2.sh, 2-node, autodrive walker + kill every ~40s): 7 death/respawn cycles, ZERO skate hits either side -- no false fires, and light local conditions do NOT reproduce the field skating. Next provocations: leg-GIMPED walker (the #82 family transition) and 6-player load; otherwise the detector rides the next cut and the field names the failing case for us. Co-Authored-By: Claude Fable 5 --- game/reconstructed/mechdmg.cpp | 61 ++++++++++++++++++++++++++++++- game/reconstructed/mechdmg.hpp | 5 +++ scratchpad/night12/skatebench.sh | 36 ++++++++++++++++++ scratchpad/night12/skatebench2.sh | 36 ++++++++++++++++++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 scratchpad/night12/skatebench.sh create mode 100644 scratchpad/night12/skatebench2.sh diff --git a/game/reconstructed/mechdmg.cpp b/game/reconstructed/mechdmg.cpp index e83a27a..e45fa58 100644 --- a/game/reconstructed/mechdmg.cpp +++ b/game/reconstructed/mechdmg.cpp @@ -1206,7 +1206,8 @@ void } MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984 - : owner(mech), prevMode(0), ghostFrames(0), ghostLogged(0) + : owner(mech), prevMode(0), ghostFrames(0), ghostLogged(0), + skatePrevX(0.0f), skatePrevZ(0.0f), skateFrames(0), skateLogged(0) { // per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c). int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0; @@ -1229,6 +1230,64 @@ void if (owner == 0) return; + // #52 SKATE DETECTOR (PORT, ungated -- the night-12 forensic): a REPLICANT + // whose position advances while its LEG CHANNEL is not cycling is the + // field "skating" signature. One line per episode, carrying the three + // inputs that split the failure: bodyTargetSpeed (the replicated commanded + // speed the leg SM feeds from -- ~0 means the RECORD side starved the + // gait), legCycleSpeed (~0 with a live demand means the SM is STUCK), and + // IsMechDestroyed (1 means the gait feed block was SKIPPED -- the stale + // post-respawn flag suspect). ~90 frames of sustained skate to trip; the + // threshold 0.08 u/frame ~ 2.4 u/s at 30 fps, well above reckoner jitter. + if (owner->GetInstance() == Entity::ReplicantInstance) + { + const float px = (float)owner->localOrigin.linearPosition.x; + const float pz = (float)owner->localOrigin.linearPosition.z; + const float dx = px - skatePrevX, dz = pz - skatePrevZ; + const float step = sqrtf(dx * dx + dz * dz); + skatePrevX = px; skatePrevZ = pz; + // CHANNEL-AGNOSTIC (corrected same-day): the peer's joints are posed by + // the BODY channel in the current authentic architecture (mech4 + // s_peerLegCh=0 -> AdvanceBodyAnimation mj=1), so legCycleSpeed==0 is + // NORMAL there -- the first detector build false-fired on every healthy + // movement phase. Skating = moving with BOTH channels idle. + const float cyc = fabsf((float)owner->legCycleSpeed) + + fabsf((float)owner->bodyCycleSpeed); + const int movingNoLegs = (step > 0.08f && step < 5.0f // 5+: teleport/warp + && cyc < 0.05f) ? 1 : 0; + if (movingNoLegs) + { + if (++skateFrames > 90 && !skateLogged) + { + skateLogged = 1; + DEBUG_STREAM << "[skate] replicant " << owner->GetEntityID() + << " SKATING: " << skateFrames << " frames moving (" + << step << " u/frame) with legCyc=" + << (float)owner->legCycleSpeed + << " bodyCyc=" << (float)owner->bodyCycleSpeed + << " bodyTargetSpeed=" << (float)owner->bodyTargetSpeed + << " destroyed=" << (int)owner->IsMechDestroyed() + << " mode=" << (int)owner->MovementMode() + << " at (" << px << "," << pz << ")\n" << std::flush; + if (BTMatchLogActive()) + BTMatchLog("SKATE", "mech=%d:%d frames=%d step=%.3f cyc=%.3f " + "cmdSpd=%.2f destroyed=%d mode=%d x=%.1f z=%.1f", + BTMatchHostOf(owner->GetEntityID()), (int)owner->GetEntityID(), + skateFrames, step, cyc, + (float)owner->bodyTargetSpeed, + (int)owner->IsMechDestroyed(), (int)owner->MovementMode(), + px, pz); + } + } + else if (skateFrames > 0) + { + if (skateLogged) + DEBUG_STREAM << "[skate] replicant " << owner->GetEntityID() + << " recovered after " << skateFrames << " frames\n" << std::flush; + skateFrames = 0; skateLogged = 0; + } + } + // #108 GHOST DETECTOR (PORT, ungated -- the night-9 forensic): a visible // REPLICANT that stops receiving update records is exactly what "everyone // shoots thin air" looks like from the other side. ReadUpdateRecord diff --git a/game/reconstructed/mechdmg.hpp b/game/reconstructed/mechdmg.hpp index c64cd05..d0bc33f 100644 --- a/game/reconstructed/mechdmg.hpp +++ b/game/reconstructed/mechdmg.hpp @@ -386,6 +386,11 @@ class MechDeathHandler // #108 ghost detector state (PORT) int ghostFrames; int ghostLogged; + // #52 skate detector state (PORT): position advancing while the leg + // channel is not cycling = the field "skating" signature. + float skatePrevX, skatePrevZ; + int skateFrames; + int skateLogged; }; #endif diff --git a/scratchpad/night12/skatebench.sh b/scratchpad/night12/skatebench.sh new file mode 100644 index 0000000..4ee5ce5 --- /dev/null +++ b/scratchpad/night12/skatebench.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# #52/#130 SKATE PROVOCATION -- transition-heavy 2-node run under the new +# ungated [skate] detector. A walks CONTINUOUSLY (autodrive) and gets killed +# every ~40s by B (force-dmg) -> respawn-while-moving cycles, the field's +# suspected trigger (skating clustered in the deaths-heavy final drop). +# B is the OBSERVER: its log carries [skate] lines for A's replicant. +# Negative control: sustained clean walking between kills must NOT trip it. +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 +rm -f sk_a.log sk_b.log +bt_expert_egg MP.EGG SK.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SK.EGG + +# B = observer + killer (back window) +BT_MP_FORCE_DMG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 \ + bt_launch sk_b.log SK.EGG 0x0C -net 1601 +sleep 2 +# A = walking victim: autodrive persists across respawns +BT_AUTODRIVE=0.8 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 \ + bt_launch sk_a.log SK.EGG 0x03 -net 1501 +sleep 5 +python ../tools/btconsole.py SK.EGG 127.0.0.1:1501 127.0.0.1:1601 > sk_relay.log 2>&1 & +RELAY=$! +sleep 260 +kill $RELAY 2>/dev/null +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 +echo "=== deaths/respawns on A:" +grep -cE "death cycle START" sk_a.log +echo "=== SKATE hits on B (A's replicant):" +grep -a "\[skate\]" sk_b.log | head -12 +echo "=== SKATE hits on A (B's replicant -- should be none, B stands still):" +grep -ac "\[skate\]" sk_a.log diff --git a/scratchpad/night12/skatebench2.sh b/scratchpad/night12/skatebench2.sh new file mode 100644 index 0000000..eb81929 --- /dev/null +++ b/scratchpad/night12/skatebench2.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# #52/#130 SKATE PROVOCATION -- transition-heavy 2-node run under the new +# ungated [skate] detector. A walks CONTINUOUSLY (autodrive) and gets killed +# every ~40s by B (force-dmg) -> respawn-while-moving cycles, the field's +# suspected trigger (skating clustered in the deaths-heavy final drop). +# B is the OBSERVER: its log carries [skate] lines for A's replicant. +# Negative control: sustained clean walking between kills must NOT trip it. +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 +rm -f sk_a.log sk_b.log +bt_expert_egg MP.EGG SK.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" SK.EGG + +# B = observer + killer (back window) +BT_MP_FORCE_DMG=1 BT_GIMPFEED=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 \ + bt_launch sk_b.log SK.EGG 0x0C -net 1601 +sleep 2 +# A = walking victim: autodrive persists across respawns +BT_AUTODRIVE=0.8 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 \ + bt_launch sk_a.log SK.EGG 0x03 -net 1501 +sleep 5 +python ../tools/btconsole.py SK.EGG 127.0.0.1:1501 127.0.0.1:1601 > sk_relay.log 2>&1 & +RELAY=$! +sleep 260 +kill $RELAY 2>/dev/null +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 +echo "=== deaths/respawns on A:" +grep -cE "death cycle START" sk_a.log +echo "=== SKATE hits on B (A's replicant):" +grep -a "\[skate\]" sk_b.log | head -12 +echo "=== SKATE hits on A (B's replicant -- should be none, B stands still):" +grep -ac "\[skate\]" sk_a.log