#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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
This commit is contained in:
Joe DiPrima
2026-08-08 08:30:01 -05:00
co-authored by Claude Opus 5
parent 05d7b5890a
commit f01de8cbfa
+28 -10
View File
@@ -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);