Files
BT411/scratchpad/night13/close148.py
T

82 lines
4.1 KiB
Python

"""Night 13: close #148 as not-a-bug with the measured chain. ASCII only."""
import sys
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
import gitea
BODY = """**NOT A BUG -- closing (2026-08-08).** There was nothing to fix here. The bench was lying,
and this ticket's own premise was wrong twice over.
## The answer
`Entity::Execute` (`ENTITY.cpp:556`, real engine source [T0]) calls `PerformAndWatch` **only** when
```cpp
application->GetApplicationState() == Application::RunningMission
|| application->GetApplicationState() == Application::EndingMission
|| IsPreRunnable()
```
and otherwise merely `WriteSimulationUpdate()`s. `Entity::DefaultFlags` is
`DynamicFlag|MasterInstance` -- **no `PreRunFlag`**. Only `Player` and `Director` add it in their
DefaultFlags, 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, no matter how
much correctly-replicated data is arriving for it. Measured on the observer node:
```
235 [perf-first] mech 3:161 master <- own mech, immediately
402 [torso-rec-rx] <- peer's torso records start 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 it does.
## So the symptom was a BENCH ARTIFACT
`BT_AUTOFIRE` starts shooting immediately, during `WaitingForLaunch` -- something no player can do
in a real match. Those leading salvos measured a peer whose torso, gait and subsystems had never
run. Every `ZZZZ...XXXX` prefix in this investigation was that, and the first `X` lands within a
few lines of the state transition.
**#141 is unaffected and stays fixed** -- its segment-cache defect was real and mid-match.
## Ruled out along the way (all measured, all recorded so nobody repeats them)
* **The record cadence is authentic.** My "only 13 records in 5 minutes" premise was wrong. The
payloads are the sweep EXTREMES with `rate` flipping sign at each one -- the master sends on
**rate change** and the peer dead-reckons `atUpd + rate * elapsed` between them. 12 records for
12 direction reversals is correct, not starved.
* **The `ComputeTargetTwist` clamp.** The copy's limits load correctly (`limL=2.44346
limR=-2.44346 enab=1`), so `Min/Max` was not pinning `targetTwist` to zero.
* **The torso's own executable flag.** `Entity::Perform` picks its predicate by instance
(`IsNonReplicantExecutable` vs `IsReplicantExecutable`, differing on
`|| lastUpdate >= lastPerformance`), and Mech's tick loop had dropped that branch. Restoring it
(`f36f013`) is a genuine fidelity fix and is kept -- but it moved this bug by nothing.
* **The scheduler.** The replicant entity IS offered to the performer with `executable=1` from
line 171, ~2500 lines before its first `PerformAndWatch`. The gate was inside `Execute`, not in
who gets offered.
## What came out of it
* `[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 exactly why this took
so long to see in a 2-node log.
* `[torso-copy]` now prints `limL/limR/enab`; `[launchframe]` prints the shooter's live torso twist.
* **Gotcha #29** in `context/reconstruction-gotchas.md`: judge a 2-node bench by PREFIX vs
INTERLEAVED, never by raw percentage; check `[ent-exec] state=` before suspecting replication.
`missileframe.sh` carries the same warning inline.
## Still worth checking separately
`#37` (MadCat torso BACKWARDS) and `#70` (torso twist stops after respawn) were flagged here as
possibly sharing a cause. They do **not** share this one -- it is not a defect. They should be
re-tested against the #141 segment-cache fix instead, which is a real mid-match change."""
gitea.comment(148, BODY)
gitea.call("/issues/148", method="PATCH", payload={"state": "closed"})
print("closed #148")