#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:
co-authored by
Claude Fable 5
parent
6d7946a264
commit
9604f4c492
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user