"""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"])