#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:
co-authored by
Claude Opus 5
parent
05d7b5890a
commit
f01de8cbfa
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user