The #141 bug class, written up so it is not re-introduced. GetSegmentToEntity recomputes ONLY when segmentModified is set; JointedMover::GetSegmentToWorld is what sets it -- and the binary's own GetMuzzlePoint @004b9948 goes through it (FUN_00424da8), so every muzzle query in the 1995 image performs the joints->segments refresh. Four port sites hand-composed instead, one of them commented "the faithful FUN_004b9948". Records the four rules the investigation actually cost: (a) never hand-compose; call GetSegmentToWorld (b) never force the dirty flag to fix a stale read -- that stand-in scored IDENTICALLY to the faithful fix while patching only one consumer (c) "peer POV only" geometry bugs = suspect a cache the local render pass refreshes for free, before suspecting replication (it was provably fine) (d) a partial-looking score: check PREFIX vs interleaved before calling it partial -- these were a clean prefix ending when the peer first had a twist to carry, so the fix was complete and "64% fixed" was wrong (e) the probe trap: one shared static sampled every Nth call hides one of two alternating instances entirely #141 closed with the full write-up; #148 filed for the torso replication cadence (13 records in a 5-minute run) which is a separate, real problem and likely bears on #37 and #70. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
148 lines
6.7 KiB
Python
148 lines
6.7 KiB
Python
"""Night 13: report #141 (fixed) and file the torso-cadence follow-up. ASCII only."""
|
|
import sys
|
|
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
|
import gitea
|
|
|
|
BODY_141 = """**ROOT CAUSE FOUND AND FIXED** (`f01de8c` + sweep `e6c5ac9`), reproduced and measured on a
|
|
2-node bench (`scratchpad/night13/missileframe.sh`).
|
|
|
|
## Reproduced
|
|
|
|
Only A sweeps its torso and only A fires, so every REPLICANT line in B's log mirrors one A salvo.
|
|
New `[launchframe]` receipt (`BT_PROJ_LOG`) prints the yaw of the launch forward vs the BODY forward:
|
|
|
|
| | n | max abs(twistDelta) | mean | >0.1 rad |
|
|
|---|---|---|---|---|
|
|
| master | 165 | 2.2962 | 1.2283 | **100%** |
|
|
| REPLICANT | 165 | **0.0000** | 0.0000 | **0%** |
|
|
|
|
`segResolved=1` on both, and `segYaw == bodyYaw` EXACTLY on the peer -- the launch frame was the
|
|
bind pose.
|
|
|
|
## What it was NOT
|
|
|
|
Everything upstream was already correct, which is why it looked like netcode. Both sides pass the
|
|
mount segment (`GetSegmentIndex()`, task #67 -- master `mislanch.cpp:363`, replicant mirror `:478`).
|
|
The peer's torso data is fine end to end: records arrive (`atUpd=2.44/-2.39`, `rate=0.305`), the
|
|
copy extrapolates correctly (`cur=-2.13987 target=-2.13987 copy=1`), and the copy torso
|
|
demonstrably writes its joint (`PushTwist COPY twist=-1.49601`). Hierarchy identical on both nodes
|
|
-- same seg 18, same `parentIdx=4`, non-null parent and joint subsystem.
|
|
|
|
The twist reached the joint and died at the **segment cache**.
|
|
|
|
## Root cause
|
|
|
|
`MechWeapon::GetMuzzlePoint` `@004b9948` ends in `FUN_00424da8(owner, segment, out)`, which is
|
|
`JointedMover::GetSegmentToWorld` instruction-for-instruction:
|
|
|
|
```c
|
|
iVar1 = FUN_00417ab4(param_1 + 0x31c); // GetJointSubsystem()
|
|
if (*(int *)(iVar1 + 0xfc) != 0) { // AreJointsModified() <- TESTED, never set
|
|
... walk owner+0x300, seg+0xc = 1 ... // ModifySegment() on every segment
|
|
*(int *)(iVar1 + 0xfc) = 0; // ModifyJoints(False)
|
|
}
|
|
FUN_0040b104(out, FUN_004244dc(seg), owner+0xd0); // x localToWorld
|
|
```
|
|
|
|
**In the 1995 image every muzzle query performs the joints->segments refresh.** Our
|
|
`BTResolveWeaponMuzzle` -- labelled "the faithful FUN_004b9948" -- hand-composed
|
|
`GetSegmentToEntity() x localToWorld` and skipped it. `GetSegmentToEntity` only recomputes when
|
|
`segmentModified` is already set (`SEGMENT.cpp:262`), so it returned a stale cache. On the MASTER
|
|
that was invisible (the render pass refreshes the local mech every frame, after its torso pushes
|
|
the joint); a REPLICANT gets no such refresh, so peer muzzles sat at the bind pose.
|
|
|
|
## Fix
|
|
|
|
Route the muzzle path through the engine accessor, where the binary puts it. **No forced dirty
|
|
flag** -- an earlier attempt set `ModifyJoints(True)` and scored the same, so it bought nothing and
|
|
was removed; the binary only ever tests that flag.
|
|
|
|
Swept the same unfaithful pattern at three more sites: the generic segment->world bridge, the
|
|
damage-effect anchor, and **the energy-beam gun port** -- a peer's BEAM had the identical exposure
|
|
and would also have originated from the untwisted gun port. Repo-wide there is now exactly one
|
|
`GetSegmentToEntity` call outside `SEGMENT.cpp`: inside `GetSegmentToWorld` itself, after the
|
|
refresh.
|
|
|
|
## Result
|
|
|
|
| | n | max | mean | >0.1 rad |
|
|
|---|---|---|---|---|
|
|
| master | 165 | 2.1719 | 1.2781 | 100% |
|
|
| REPLICANT | 165 | **2.0907** | 0.8201 | **64%** |
|
|
|
|
**The 64% is not a partial fix.** The failures are a contiguous PREFIX with zero interleaved cases:
|
|
|
|
```
|
|
ZZZZ...(60)...ZZZZXXXX...(105)...XXXX
|
|
```
|
|
|
|
and they end exactly when the peer acquires a twist to carry:
|
|
|
|
```
|
|
first torso RECORD received : line 206
|
|
first copy currentTwist != 0 : line 1016
|
|
first CORRECT launch frame : line 1054 (38 lines = probe sampling granularity)
|
|
```
|
|
|
|
Those 60 salvos fired while the replicated twist was genuinely 0, so launching along the body
|
|
facing was CORRECT. Once the peer has a twist, 100% of launches carry it.
|
|
|
|
## Field-verify
|
|
|
|
Unreleased. Next playtest: have a peer watch a twisted mech fire missiles -- rounds should leave
|
|
along the torso, not the feet. Also worth checking beams for the same reason (same fix).
|
|
|
|
Follow-up filed separately: the peer's torso takes far too long to FIRST acquire the master's
|
|
twist."""
|
|
|
|
TITLE_NEW = "Peer torso twist takes far too long to first sync -- only 13 update records across a 5-minute run"
|
|
|
|
BODY_NEW = """Split out of #141, whose launch-frame defect is fixed. This is a separate, measured
|
|
problem in the torso REPLICATION CADENCE.
|
|
|
|
## Measurement
|
|
|
|
From the #141 bench (`scratchpad/night13/missileframe.sh` / `missileframe2.sh`, 2 nodes, node A
|
|
sweeping its torso continuously at 0.35 rad/s for the whole run):
|
|
|
|
```
|
|
first torso RECORD received on the peer : line 206
|
|
first copy currentTwist != 0 : line 1016
|
|
```
|
|
|
|
and across the entire ~5 minute run the peer received only **13** `[torso-rec-rx]` records, despite
|
|
the master's twist changing continuously the whole time.
|
|
|
|
So the master was twisted from very early on, while the peer's copy torso reported `currentTwist`
|
|
of exactly 0 for a long stretch afterwards. The extrapolator itself is fine once fed --
|
|
`ComputeTargetTwist` predicts `twistAtUpdate + twistRate * elapsed` and the copy tracks its target
|
|
exactly (`cur=-2.13987 target=-2.13987 copy=1`). The problem is how rarely it is fed, and how late
|
|
the first useful feed arrives.
|
|
|
|
## Why it matters
|
|
|
|
* It is the entire reason #141's fix reads 64% instead of 100% on the bench -- 60 salvos fired
|
|
before the peer had any twist to carry.
|
|
* A peer's torso will visibly LAG or sit straight while the mech is actually twisted. That is
|
|
plausibly relevant to **#37** (MadCat torso is BACKWARDS) and **#70** (torso twist stops working
|
|
after respawn) -- worth re-testing both against this once it is understood.
|
|
|
|
## Not yet investigated
|
|
|
|
Whether 13 records is the authentic cadence (the binary may deliberately send torso updates rarely
|
|
and lean on `twistRate` extrapolation to cover the gaps -- in which case the bug is that our
|
|
extrapolation is not running or not seeded until late), or whether our send-side gate is simply too
|
|
conservative. `Torso::WriteUpdateRecord` snapshots `twistAtUpdate = currentTwist` at send, so the
|
|
send trigger is the thing to read first.
|
|
|
|
Diagnostics already in place: `BT_TORSO_LOG` gives `[torso-rec-rx]` (receive), `[torso-copy]`
|
|
(the copy's cur/target/atUpd/rate), and `[torso] PushTwist master|COPY` (per instance-kind -- note
|
|
that probe previously sampled one shared static every 30th call, which with two torsos ticking 1:1
|
|
always reported the SAME instance and hid the copy entirely; fixed in `05d7b58`)."""
|
|
|
|
gitea.comment(141, BODY_141)
|
|
gitea.call("/issues/141", method="PATCH", payload={"state": "closed"})
|
|
print("commented + closed #141")
|
|
num = gitea.create(TITLE_NEW, BODY_NEW)
|
|
print("created #%d" % num["number"])
|