Chased to the bottom instead of stopping. The answer is that there was
nothing to fix, and my bench was lying to me.
Entity::Execute (ENTITY.cpp:556, real engine source [T0]) calls PerformAndWatch
ONLY when the app state is RunningMission/EndingMission or the entity
IsPreRunnable(); otherwise it merely WriteSimulationUpdate()s.
Entity::DefaultFlags is DynamicFlag|MasterInstance -- no PreRunFlag. Only
Player and Director add it, and Mech::Reset sets it for a reset MASTER ("a
reset master must tick"). A REPLICANT mech never gets it.
So a peer mech performs ZERO subsystem ticks until the round actually starts,
however much correctly-replicated data is arriving. Measured on the observer:
235 [perf-first] mech 3:161 master <- own mech, immediately
402 [torso-rec-rx] <- peer torso records arriving
2754 [perf-first] mech 2:55 REPLICANT <- peer's FIRST performance
2758 [torso] PushTwist COPY <- its torso ticks 4 lines later
2761 [ent-exec] state=5 <- RunningMission
The peer starts performing exactly at the RunningMission transition. That is
the engine doing what it says.
WHICH MEANS THE PREFIX WAS A BENCH ARTIFACT. BT_AUTOFIRE starts shooting
immediately, during WaitingForLaunch -- something no player can do in a real
match -- so those 60 leading salvos measured a peer whose torso had never run.
Every "ZZZZ...XXXX" pattern in this investigation was that, and the first X
lands within a few lines of the state transition. #141's fix is unaffected and
remains verified: the segment-cache defect was real and mid-match.
Chain of things ruled out on the way, all measured:
* record CADENCE is authentic -- sends on RATE CHANGE (payloads are the sweep
extremes, rate flips sign), peer dead-reckons between them. 12 records for
12 reversals is correct, not starved. My "only 13 records" premise was wrong.
* ComputeTargetTwist clamp -- limits load fine on the copy (+/-2.44346).
* the torso's own executable flag -- restoring the engine's instance branch
(f36f013) is a genuine fidelity fix but moved this by nothing.
* the replicant entity IS offered to the performer, executable=1, from line
171 -- 2500 lines before its first PerformAndWatch. The gate was inside
Execute, not the scheduler.
Adds [perf-first]: a ONE-SHOT per-mech receipt naming entity ID + instance at a
mech's first performance. Every other per-frame receipt in mech4 is anonymous,
which is precisely why this took so long in a 2-node log -- master and replicant
lines were indistinguishable. Name the mech.
Gotcha #29 records the bench-design rule this cost: judge a 2-node bench by
PREFIX vs INTERLEAVED, never by raw percentage, and check [ent-exec] state=
before suspecting replication. missileframe.sh carries the same warning.
#148 to be closed as not-a-bug.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
122 lines
6.0 KiB
Bash
122 lines
6.0 KiB
Bash
#!/usr/bin/env bash
|
|
# =========================================================================
|
|
# #141 -- "missiles launch along the LEG/FOOT facing, then curve to the
|
|
# target -- PEER POV ONLY" (Oracle, night 13: "the emitter is following the
|
|
# foot facing"; shooter's own view correct).
|
|
#
|
|
# WHAT THE CODE SAYS SO FAR. Both sides already pass the mount segment:
|
|
# MissileLauncher::FireWeapon (mislanch.cpp:363) and the REPLICANT salvo
|
|
# mirror (mislanch.cpp:478) each hand BTPushProjectile
|
|
# `GetSegmentIndex() /*task #67 mount frame*/`. BTPushProjectile then
|
|
# rotates the authored MuzzleVelocity through
|
|
# `seg->GetSegmentToEntity() * localToWorld` -- so the launch direction IS
|
|
# the segment's world frame on BOTH nodes. Task #67 fixed exactly this
|
|
# symptom once already, master-side ("missiles fire out of his back").
|
|
#
|
|
# So if the peer report is real, the difference is NOT which frame is asked
|
|
# for -- it is whether the replicant's SEGMENT actually carries the torso
|
|
# twist. Torso pushes currentTwist into the skeleton on both paths
|
|
# (TorsoSimulation and TorsoCopySimulation both call UpdateJoints), so this
|
|
# has to be measured, not reasoned about.
|
|
#
|
|
# THE MEASUREMENT. New [launchframe] receipt (BT_PROJ_LOG) prints, on both
|
|
# nodes, the yaw of the launch forward vs the BODY forward:
|
|
# [launchframe] master seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
# [launchframe] REPLICANT seg=N segResolved=1 segYaw=.. bodyYaw=.. twistDelta=..
|
|
# twistDelta is the torso twist expressed in the launch direction.
|
|
#
|
|
# BUG CONFIRMED: A (master) shows |twistDelta| sweeping well away from 0
|
|
# while B (replicant mirror) stays pinned near 0
|
|
# -- or B shows segResolved=0 (fell back to the body basis).
|
|
# NOT REPRODUCED: both sides show the same twistDelta spread.
|
|
#
|
|
# ⚠ READ THE PREFIX, NOT THE PERCENTAGE (learned the hard way, 2026-08-08).
|
|
# BT_AUTOFIRE starts shooting IMMEDIATELY, while the app is still in
|
|
# WaitingForLaunch/LaunchingMission -- and Entity::Execute (ENTITY.cpp:556,
|
|
# real engine source) only calls PerformAndWatch when the state is
|
|
# RunningMission/EndingMission or the entity IsPreRunnable(). A REPLICANT mech
|
|
# is not pre-runnable (Entity::DefaultFlags has no PreRunFlag; only Player /
|
|
# Director add it, and Mech::Reset sets it for a reset MASTER), so a peer mech
|
|
# does not tick at all until the round actually starts. Every salvo fired
|
|
# before that reads twistDelta=0 legitimately -- the peer has no twist yet.
|
|
# That is a BENCH artifact (nobody can fire pre-round in a real match), not a
|
|
# defect: it showed up as a clean leading run of zeros, e.g.
|
|
# ZZZZ...(60)...ZZZZXXXX...(105)...XXXX
|
|
# and the first X lands within a few lines of the RunningMission transition.
|
|
# So: judge this bench by whether the failures are a PREFIX (fine) or
|
|
# INTERLEAVED (real), never by the raw percentage.
|
|
#
|
|
# Only A fires and only A sweeps its torso, so every REPLICANT line in B's
|
|
# log is a mirror of an A salvo and the comparison is unambiguous.
|
|
# =========================================================================
|
|
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 mf_a.log mf_b.log mf_relay.log
|
|
bt_expert_egg MP.EGG MF.EGG
|
|
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF.EGG
|
|
|
|
# B: the OBSERVER. Drives at A so it stays in range, but does NOT fire and
|
|
# does NOT sweep -- so every [launchframe] REPLICANT line in mf_b.log is a
|
|
# mirror of one of A's salvos.
|
|
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
export BT_PROJ_LOG=1 BT_MP_LOG=1
|
|
bt_launch mf_b.log MF.EGG 0x0C -net 1601 )
|
|
sleep 2
|
|
# A: the SHOOTER. Sweeps the torso hard so twistDelta is unmistakably
|
|
# non-zero at fire time, and autofires missiles at the designated enemy.
|
|
( export BT_GOTO=enemy BT_GOTO_STOP=150
|
|
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=7
|
|
export BT_LOCK_SWEEP=0.35
|
|
export BT_PROJ_LOG=1 BT_TORSO_LOG=1 BT_MP_LOG=1
|
|
bt_launch mf_a.log MF.EGG 0x03 -net 1501 )
|
|
sleep 5
|
|
python ../tools/btconsole.py MF.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf_relay.log 2>&1 &
|
|
RELAY=$!
|
|
sleep 300
|
|
kill $RELAY 2>/dev/null
|
|
sleep 3
|
|
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
|
|
|
|
echo "=================== #141 MISSILE LAUNCH FRAME ==================="
|
|
echo "--- did A fire, and did B mirror? ---"
|
|
echo -n " A [launchframe] master lines ....... "; grep -ac "launchframe\] master" mf_a.log
|
|
echo -n " B [launchframe] REPLICANT lines .... "; grep -ac "launchframe\] REPLICANT" mf_b.log
|
|
echo
|
|
echo "--- did the segment RESOLVE on each side? (segResolved=0 would be the bug) ---"
|
|
echo -n " A segResolved=0 ... "; grep -a "launchframe\] master" mf_a.log | grep -ac "segResolved=0"
|
|
echo -n " B segResolved=0 ... "; grep -a "launchframe\] REPLICANT" mf_b.log | grep -ac "segResolved=0"
|
|
echo
|
|
echo "--- THE COMPARISON: twistDelta spread on each side ---"
|
|
python - <<'PY'
|
|
import re, io
|
|
def stats(path, tag):
|
|
v = []
|
|
try:
|
|
for ln in io.open(path, encoding="latin-1", errors="replace"):
|
|
if "[launchframe] " + tag in ln:
|
|
m = re.search(r"twistDelta=([-\d.e+]+)", ln)
|
|
if m:
|
|
try: v.append(float(m.group(1)))
|
|
except ValueError: pass
|
|
except IOError:
|
|
print(" %s: no log" % tag); return
|
|
if not v:
|
|
print(" %-9s no samples" % tag); return
|
|
a = [abs(x) for x in v]
|
|
big = sum(1 for x in a if x > 0.10) # ~5.7 deg -- clearly twisted
|
|
print(" %-9s n=%-4d |twistDelta| max=%.4f mean=%.4f >0.10rad: %d (%.0f%%)"
|
|
% (tag, len(v), max(a), sum(a)/len(a), big, 100.0*big/len(a)))
|
|
stats(r"C:\git\bt411\content\mf_a.log", "master")
|
|
stats(r"C:\git\bt411\content\mf_b.log", "REPLICANT")
|
|
print()
|
|
print(" VERDICT: master twisted + REPLICANT pinned near 0 => #141 CONFIRMED.")
|
|
print(" both twisted alike => NOT reproduced.")
|
|
PY
|
|
echo
|
|
echo "--- sample lines, both sides ---"
|
|
grep -a "launchframe\] master" mf_a.log | head -4
|
|
grep -a "launchframe\] REPLICANT" mf_b.log | head -4
|