#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 <noreply@anthropic.com>
This commit is contained in:
Joe DiPrima
2026-08-05 12:41:55 -05:00
co-authored by Claude Fable 5
parent 6d7946a264
commit 9604f4c492
4 changed files with 137 additions and 1 deletions
+60 -1
View File
@@ -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
+5
View File
@@ -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
+36
View File
@@ -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
+36
View File
@@ -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