From f01de8cbfad21a4595f451aac64d40cc419e9db8 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sat, 8 Aug 2026 08:30:01 -0500 Subject: [PATCH] #141 follow-up: do it the BINARY's way -- the muzzle query IS the segment refresh The previous commit's fix worked but was NOT faithful: it set ModifyJoints(True) to force the engine's dirty flag before reading the segment. The binary never does that. Called out by the user; corrected. WHAT THE BINARY ACTUALLY DOES. MechWeapon::GetMuzzlePoint @004b9948 ends in `FUN_00424da8(owner, segment, out)`, which is JointedMover::GetSegmentToWorld instruction-for-instruction: iVar1 = FUN_00417ab4(param_1 + 0x31c); // GetJointSubsystem() if (*(int *)(iVar1 + 0xfc) != 0) { // AreJointsModified() <- TESTED ... walk owner+0x300, seg+0xc = 1 ... // ModifySegment() *(int *)(iVar1 + 0xfc) = 0; // ModifyJoints(False) } FUN_0040b104(out, FUN_004244dc(seg), owner+0xd0); // x localToWorld So in the 1995 image EVERY muzzle query performs the joints->segments refresh, and the flag is only ever TESTED, never set. THE REAL DEFECT. BTResolveWeaponMuzzle -- labelled "the faithful FUN_004b9948" -- hand-composed `seg->GetSegmentToEntity() x localToWorld` and skipped @00424da8 entirely. GetSegmentToEntity only recomputes when segmentModified is already set (SEGMENT.cpp:262), so it returned a stale cache. On the MASTER the render pass refreshes the local mech every frame and hid it; a REPLICANT got no refresh, so peer muzzles sat at the BIND POSE and the missile left along the leg facing. Fixed at the muzzle path, where the binary puts it -- and the forced flag in BTPushProjectile is REMOVED (the launcher calls GetMuzzlePoint just above, so the cache is already current when the launch frame is composed). MEASURED -- the faithful path scores exactly what the hack did, so the hack bought nothing and is gone: master n=165 max 2.1389 mean 1.2718 >0.1rad 100% REPLICANT n=165 max 1.9426 mean 0.8051 >0.1rad 64% AND THE 64% IS NOT A PARTIAL FIX -- I called that wrong last commit. The failures are a contiguous PREFIX, not interleaved: 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 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. #141 is fixed. SEPARATE ISSUE FOUND, not fixed here: the peer's copy torso takes far too long to first reflect the master's twist -- the master was twisted from the start, only 13 torso records arrived across the whole run, and the copy's twist stayed 0 until line 1016. That is a torso REPLICATION CADENCE problem, and it would also make peer torsos visibly lag -- likely relevant to #37 (MadCat torso backwards) and #70 (twist stops after respawn). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC --- game/reconstructed/mech4.cpp | 38 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 91df319..32498bd 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -790,8 +790,26 @@ void EntitySegment *seg = m->GetSegment(segIndex); // owner+0x300 table, GetNth(index) if (seg != 0) { - AffineMatrix mw; - mw.Multiply(seg->GetSegmentToEntity(), m->localToWorld); // segment -> world (== mech4 gun-port path) + // #141 -- THE BINARY GOES THROUGH FUN_00424da8, AND SO MUST WE. + // @004b9948 ends in `FUN_00424da8(owner, segment, out)`, which is + // JointedMover::GetSegmentToWorld instruction-for-instruction: + // iVar1 = FUN_00417ab4(param_1 + 0x31c); // GetJointSubsystem() + // if (*(int *)(iVar1 + 0xfc) != 0) { // AreJointsModified() + // ...walk owner+0x300 setting seg+0xc = 1... // ModifySegment() + // *(int *)(iVar1 + 0xfc) = 0; // ModifyJoints(False) + // } + // FUN_0040b104(out, FUN_004244dc(seg), owner+0xd0); // x localToWorld + // So in the 1995 image EVERY muzzle query performs the joints->segments + // refresh. This port hand-composed GetSegmentToEntity() x localToWorld + // and skipped it -- and 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); on a REPLICANT nothing did, so + // peer muzzles sat at the BIND POSE -- the missile launched along the + // leg facing (#141). Use the engine accessor; do NOT force the dirty + // flag, the binary does not. + LinearMatrix mw; + m->GetSegmentToWorld(*seg, &mw); out = mw; // Point3D = matrix W_Axis translation } else @@ -1534,14 +1552,14 @@ void // GetSegmentToWorld mark every segment dirty so the whole // chain re-derives from the CURRENT joint angles. Costs one // segment-table walk per salvo. - // MEASURED: this alone takes the replicant from 0% to 64% of - // salvos carrying the twist. Also forcing every per-joint - // `jointModified` flag (GetSegmentToParent's own gate, - // SEGMENT.cpp:196) was tried and moved the number by NOTHING - // -- 64% either way -- so the residual 36% is a different - // cause, not per-joint cache staleness. Kept the cheap form. - if (JointSubsystem *jsf = sm->GetJointSubsystem()) - jsf->ModifyJoints(True); + // NO forced dirty flag here. An earlier pass set + // ModifyJoints(True) before this read; it bought 64% of + // salvos but it is NOT what the binary does -- @00424da8 + // tests AreJointsModified() and never sets it. The authentic + // refresh happens in the MUZZLE query (GetMuzzlePoint -> + // @00424da8), which the launcher calls just above this, so by + // the time we compose the launch frame the segment cache is + // already current. See BTResolveWeaponMuzzle. LinearMatrix mw; sm->GetSegmentToWorld(*seg, &mw); mw.GetFromAxis(X_Axis, &ax);