feat(physics): R5-V5 — MovementManager facade owns each entity's interp+moveto pair
Structural capstone of the R5 movement-manager arc; zero behavior change. Retail MovementManager (acclient.h /* 3463 */, 16 bytes / four pointers) gives every CPhysicsObj ONE owner for its motion_interpreter + moveto_manager. acdream carried them as loose per-entity objects wired by hand at three sites. This slice: - New src/AcDream.Core/Physics/Motion/MovementManager.cs — owns MotionInterpreter + lazy MoveToManager (MakeMoveToManager 0x00524000 via a MoveToFactory closure, the acdream stand-in for the physics_obj/ weenie_obj backpointers) + the relays with retail call shapes: PerformMovement 0x005240d0 (types 1-5 -> minterp, 6-9 -> MakeMoveToManager + moveto, (type-1)>8 -> 0x47), UseTime 0x005242f0 (moveto only), HitGround 0x00524300 (minterp FIRST then moveto), HandleExitWorld 0x00524350 (minterp only), CancelMoveTo 0x005241b0, HandleUpdateTarget 0x00524790, IsMovingTo 0x00524260. - RemoteMotion.Movement + PlayerMovementController.Movement hold the ONE facade; Motion/MoveTo become child views so the comment-dense call sites read unchanged. The three wiring sites (EnsureRemoteMotionBindings, EnterPlayerModeNow, the chase harness — same commit per the mirror rule) construct through MoveToFactory + MakeMoveToManager(), preserving the pre-facade eager timing (side-effect-free ctor = unobservable either way). - Relay call sites repointed: both remote landing HitGround pairs + the player landing pair, despawn HandleExitWorld, TickRemoteMoveTo + the player Update UseTime, RouteServerMoveTo (takes the facade; routes via the retail PerformMovement dispatch), InstallSpeculativeTurnToTarget, host HandleUpdateTarget/InterruptCurrentMovement closures (retail CPhysicsObj::HandleUpdateTarget @0x00512bf0 fan head + the TS-36 interrupt chain, now the literal facade relays). - NOT absorbed per the slice spec: unpack_movement stays App (RouteServerMoveTo + UM heads; Core.Net types stay out of Core.Physics); TS-42 per-tick order untouched (R6); #170/#171 gate-passed machinery untouched. PerformMovement's set_active(1) head not re-asserted (spawn asserts Active; status quo — no new register row). - Register: TS-41/TS-42 source wording freshened to the facade shape; AD-36 retire note corrected (facade half closed; residue = entities that never get a RemoteMotion). No new rows. - Conformance: 15 new MovementManagerTests pin the dispatch table, lazy create, relay targets/order, null tolerance. Suite 4052 green; the 183-case/funnel/moveto/chase/sticky suites UNMODIFIED (harness construction mirrors production, test bodies untouched). Decomp: docs/research/2026-07-03-r5-managers/r5-movementmanager-decomp.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6feaab91e3
commit
dccd700991
6 changed files with 833 additions and 200 deletions
341
tests/AcDream.Core.Tests/Physics/Motion/MovementManagerTests.cs
Normal file
341
tests/AcDream.Core.Tests/Physics/Motion/MovementManagerTests.cs
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics.Motion;
|
||||
|
||||
/// <summary>
|
||||
/// R5-V5 — <see cref="MovementManager"/> facade conformance (retail struct
|
||||
/// acclient.h /* 3463 */; methods 0x00524000-0x00524790, decomp extract
|
||||
/// <c>docs/research/2026-07-03-r5-managers/r5-movementmanager-decomp.md</c>).
|
||||
/// The facade is pure relay/ownership — these tests pin the retail call
|
||||
/// shapes: which child each method touches, the lazy MakeMoveToManager
|
||||
/// create, the PerformMovement type dispatch, and null-tolerance before the
|
||||
/// moveto manager exists. Behavior of the children themselves is covered by
|
||||
/// the MotionInterpreter / MoveToManager suites (UNMODIFIED by R5-V5).
|
||||
/// </summary>
|
||||
public sealed class MovementManagerTests
|
||||
{
|
||||
/// <summary>Non-creature weenie: silences the MotionInterpreter's
|
||||
/// HitGround/LeaveGround creature gates (retail IsCreature vtable
|
||||
/// +0x2c) so a test can isolate the facade's MOVETO relay leg.</summary>
|
||||
private sealed class NonCreatureWeenie : IWeenieObject
|
||||
{
|
||||
public bool InqJumpVelocity(float extent, out float vz) { vz = 0f; return false; }
|
||||
public bool InqRunRate(out float rate) { rate = 1f; return false; }
|
||||
public bool CanJump(float extent) => false;
|
||||
bool IWeenieObject.IsCreature() => false;
|
||||
}
|
||||
|
||||
/// <summary>Facade over the shared MoveToManagerHarness: the harness's
|
||||
/// REAL MotionInterpreter is the minterp child; the harness's REAL
|
||||
/// seam-scripted MoveToManager arrives via <see cref="MovementManager.MoveToFactory"/>
|
||||
/// (the acdream stand-in for retail's physics_obj/weenie_obj
|
||||
/// backpointers that MakeMoveToManager constructs from).</summary>
|
||||
private static (MovementManager Mm, MoveToManagerHarness H, int[] FactoryCalls) MakeFacade()
|
||||
{
|
||||
var h = new MoveToManagerHarness();
|
||||
var factoryCalls = new int[1];
|
||||
var mm = new MovementManager(h.Interp)
|
||||
{
|
||||
MoveToFactory = () => { factoryCalls[0]++; return h.Manager; },
|
||||
};
|
||||
return (mm, h, factoryCalls);
|
||||
}
|
||||
|
||||
// ── MakeMoveToManager — 0x00524000 ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void MakeMoveToManager_CreatesViaFactory_ExactlyOnce()
|
||||
{
|
||||
var (mm, h, calls) = MakeFacade();
|
||||
Assert.Null(mm.MoveTo);
|
||||
|
||||
mm.MakeMoveToManager();
|
||||
Assert.Same(h.Manager, mm.MoveTo);
|
||||
Assert.Equal(1, calls[0]);
|
||||
|
||||
// Retail: no-op if already present.
|
||||
mm.MakeMoveToManager();
|
||||
Assert.Same(h.Manager, mm.MoveTo);
|
||||
Assert.Equal(1, calls[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MakeMoveToManager_WithoutFactory_IsANoOp()
|
||||
{
|
||||
var mm = new MovementManager(new MotionInterpreter());
|
||||
mm.MakeMoveToManager();
|
||||
Assert.Null(mm.MoveTo);
|
||||
}
|
||||
|
||||
// ── PerformMovement — 0x005240d0 (the type-1..9 two-way dispatch) ───────
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_InterpTypes_RouteToMinterp_NotMoveTo()
|
||||
{
|
||||
var (mm, h, calls) = MakeFacade();
|
||||
|
||||
var result = mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.InterpretedCommand,
|
||||
Motion = MotionCommand.WalkForward,
|
||||
Speed = 1f,
|
||||
ModifyInterpretedState = true,
|
||||
});
|
||||
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
Assert.Equal(MotionCommand.WalkForward, h.Interp.InterpretedState.ForwardCommand);
|
||||
// Types 1-5 never touch the moveto side (jump table 0x0052415c).
|
||||
Assert.Equal(0, calls[0]);
|
||||
Assert.Null(mm.MoveTo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_MoveToTypes_LazyCreate_AndRouteToMoveTo()
|
||||
{
|
||||
var (mm, h, calls) = MakeFacade();
|
||||
|
||||
var result = mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.MoveToPosition,
|
||||
Pos = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity),
|
||||
Params = new MovementParameters(),
|
||||
});
|
||||
|
||||
// Retail cases 5-8 (types 6-9): MakeMoveToManager first, delegate,
|
||||
// and the MoveToManager path's return is NOT propagated (@0052414f
|
||||
// `return 0`) — the facade reports None regardless.
|
||||
Assert.Equal(WeenieError.None, result);
|
||||
Assert.Equal(1, calls[0]);
|
||||
Assert.Equal(MovementType.MoveToPosition, h.Manager.MovementTypeState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_InvalidAndOutOfRangeTypes_Fail0x47()
|
||||
{
|
||||
var (mm, _, calls) = MakeFacade();
|
||||
|
||||
// Retail head: (type - 1) > 8 → 0x47. Type 0 underflows unsigned →
|
||||
// always > 8; anything above 9 fails the same check.
|
||||
Assert.Equal(WeenieError.GeneralMovementFailure,
|
||||
mm.PerformMovement(new MovementStruct { Type = MovementType.Invalid }));
|
||||
Assert.Equal(WeenieError.GeneralMovementFailure,
|
||||
mm.PerformMovement(new MovementStruct { Type = (MovementType)10 }));
|
||||
Assert.Equal(0, calls[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerformMovement_MoveToType_WithoutFactory_Fails0x47()
|
||||
{
|
||||
// acdream-only guard for the unreachable-in-production ordering
|
||||
// (a type-6..9 event before the bind sites set MoveToFactory):
|
||||
// retail would MoveToManager::Create here; without a factory the
|
||||
// facade reports the same 0x47 the range check uses.
|
||||
var mm = new MovementManager(new MotionInterpreter());
|
||||
Assert.Equal(WeenieError.GeneralMovementFailure,
|
||||
mm.PerformMovement(new MovementStruct { Type = MovementType.TurnToHeading }));
|
||||
}
|
||||
|
||||
// ── UseTime — 0x005242f0 (moveto only; never lazy-creates) ──────────────
|
||||
|
||||
[Fact]
|
||||
public void UseTime_BeforeMoveToExists_IsANoOp_AndDoesNotCreate()
|
||||
{
|
||||
var (mm, _, calls) = MakeFacade();
|
||||
mm.UseTime();
|
||||
Assert.Equal(0, calls[0]);
|
||||
Assert.Null(mm.MoveTo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseTime_RelaysToMoveTo()
|
||||
{
|
||||
// The MoveToManagerUseTimeGateTests arrival shape, driven through
|
||||
// the facade: grounded, facing the target, arrived — one UseTime
|
||||
// completes the move.
|
||||
var (mm, h, _) = MakeFacade();
|
||||
h.ContactValue = true;
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
|
||||
mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.MoveToPosition,
|
||||
Pos = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity),
|
||||
Params = new MovementParameters { DistanceToObject = 0.6f, UseSpheres = false },
|
||||
});
|
||||
h.DrainPendingMotions();
|
||||
|
||||
h.WorldPosition = new Position(1u, new Vector3(19.7f, 0f, 0f), Quaternion.Identity);
|
||||
h.Advance(2.0);
|
||||
mm.UseTime();
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState);
|
||||
}
|
||||
|
||||
// ── HitGround — 0x00524300 (minterp FIRST, then moveto) ────────────────
|
||||
|
||||
[Fact]
|
||||
public void HitGround_RelaysToMinterp_AndToleratesNullMoveTo()
|
||||
{
|
||||
var (mm, h, _) = MakeFacade();
|
||||
h.Body.State |= PhysicsStateFlags.Gravity; // CMotionInterp::HitGround gates on state & 0x400
|
||||
bool minterpHit = false;
|
||||
h.Interp.RemoveLinkAnimations = () => minterpHit = true;
|
||||
|
||||
mm.HitGround(); // MoveTo still null — retail's if-present guard
|
||||
Assert.True(minterpHit);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HitGround_RelaysMinterpFirst_ThenMoveTo()
|
||||
{
|
||||
var (mm, h, _) = MakeFacade();
|
||||
h.Body.State |= PhysicsStateFlags.Gravity;
|
||||
h.ContactValue = true;
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.MoveToPosition,
|
||||
Pos = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity),
|
||||
Params = new MovementParameters(),
|
||||
});
|
||||
h.DrainPendingMotions();
|
||||
|
||||
// Order pin: the FIRST RemoveLinkAnimations firing belongs to the
|
||||
// minterp leg (CMotionInterp::HitGround invokes it before any
|
||||
// dispatch), at which point the moveto leg's BeginNextNode
|
||||
// re-dispatch has NOT happened yet — the pending queue is still
|
||||
// drained. Later firings (the moveto dispatch's own TS-40
|
||||
// detached-strip at the DoInterpretedMotion tail runs AFTER its
|
||||
// enqueue) must not overwrite the recording — ??= keeps the first.
|
||||
bool? queueEmptyAtMinterpLeg = null;
|
||||
h.Interp.RemoveLinkAnimations =
|
||||
() => queueEmptyAtMinterpLeg ??= !h.Interp.MotionsPending();
|
||||
|
||||
mm.HitGround();
|
||||
|
||||
Assert.True(queueEmptyAtMinterpLeg); // minterp leg ran first
|
||||
Assert.True(h.Interp.MotionsPending()); // a re-dispatch landed after it
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HitGround_ReachesMoveTo_WhenMinterpLegIsGated()
|
||||
{
|
||||
// Isolate the MOVETO leg: a non-creature weenie makes
|
||||
// CMotionInterp::HitGround a retail no-op (IsCreature gate), so any
|
||||
// re-dispatched pending motion can only come from
|
||||
// MoveToManager::HitGround → BeginNextNode.
|
||||
var (mm, h, _) = MakeFacade();
|
||||
h.Interp.WeenieObj = new NonCreatureWeenie();
|
||||
h.ContactValue = true;
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.MoveToPosition,
|
||||
Pos = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity),
|
||||
Params = new MovementParameters(),
|
||||
});
|
||||
h.DrainPendingMotions();
|
||||
|
||||
mm.HitGround();
|
||||
|
||||
Assert.True(h.Interp.MotionsPending());
|
||||
}
|
||||
|
||||
// ── HandleExitWorld — 0x00524350 (minterp ONLY) ─────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HandleExitWorld_DrainsMinterp_AndDoesNotTouchMoveTo()
|
||||
{
|
||||
var (mm, h, _) = MakeFacade();
|
||||
h.ContactValue = true;
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
h.Heading = 90f;
|
||||
mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.MoveToPosition,
|
||||
Pos = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity),
|
||||
Params = new MovementParameters(),
|
||||
});
|
||||
Assert.True(h.Interp.MotionsPending()); // the arm's dispatch is queued
|
||||
|
||||
mm.HandleExitWorld();
|
||||
|
||||
Assert.False(h.Interp.MotionsPending());
|
||||
// Retail HandleExitWorld does NOT touch moveto_manager — the armed
|
||||
// move survives (its teardown is CancelMoveTo / exit-world at the
|
||||
// CPhysicsObj layer, not here).
|
||||
Assert.Equal(MovementType.MoveToPosition, h.Manager.MovementTypeState);
|
||||
}
|
||||
|
||||
// ── CancelMoveTo — 0x005241b0 / IsMovingTo — 0x00524260 ────────────────
|
||||
|
||||
[Fact]
|
||||
public void CancelMoveTo_NullTolerant_AndRelaysToMoveTo()
|
||||
{
|
||||
var (mm, h, _) = MakeFacade();
|
||||
mm.CancelMoveTo(WeenieError.ActionCancelled); // no moveto yet — no throw
|
||||
|
||||
h.ContactValue = true;
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.MoveToPosition,
|
||||
Pos = new Position(1u, new Vector3(20f, 0f, 0f), Quaternion.Identity),
|
||||
Params = new MovementParameters(),
|
||||
});
|
||||
Assert.True(mm.IsMovingTo());
|
||||
|
||||
mm.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
|
||||
Assert.Equal(MovementType.Invalid, h.Manager.MovementTypeState);
|
||||
Assert.False(mm.IsMovingTo());
|
||||
Assert.True(h.StopCompletelyCalls > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMovingTo_FalseBeforeMoveToExists()
|
||||
{
|
||||
var (mm, _, _) = MakeFacade();
|
||||
Assert.False(mm.IsMovingTo());
|
||||
}
|
||||
|
||||
// ── HandleUpdateTarget — 0x00524790 (→ moveto) ──────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void HandleUpdateTarget_NullTolerant_AndFeedsMoveToDeferredStart()
|
||||
{
|
||||
var (mm, h, _) = MakeFacade();
|
||||
var info = new TargetInfo
|
||||
{
|
||||
ObjectId = 0x5000AAAAu,
|
||||
Status = TargetStatus.Ok,
|
||||
TargetPosition = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity),
|
||||
InterpolatedPosition = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity),
|
||||
};
|
||||
mm.HandleUpdateTarget(info); // no moveto yet — no throw
|
||||
|
||||
// The V2 "uninitialized type-6 stall": MoveToObject defers its node
|
||||
// build to the FIRST HandleUpdateTarget delivery.
|
||||
h.ContactValue = true;
|
||||
h.WorldPosition = new Position(1u, Vector3.Zero, Quaternion.Identity);
|
||||
mm.PerformMovement(new MovementStruct
|
||||
{
|
||||
Type = MovementType.MoveToObject,
|
||||
ObjectId = 0x5000AAAAu,
|
||||
TopLevelId = 0x5000AAAAu,
|
||||
Pos = new Position(1u, new Vector3(10f, 0f, 0f), Quaternion.Identity),
|
||||
Params = new MovementParameters(),
|
||||
});
|
||||
Assert.False(h.Manager.Initialized);
|
||||
|
||||
mm.HandleUpdateTarget(info);
|
||||
|
||||
Assert.True(h.Manager.Initialized);
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +99,16 @@ internal sealed class RemoteChaseHarness
|
|||
public readonly MotionInterpreter Interp;
|
||||
public readonly AnimationSequencer Seq;
|
||||
public readonly MotionTableDispatchSink Sink;
|
||||
public readonly MoveToManager Mgr;
|
||||
|
||||
/// <summary>R5-V5: GameWindow's RemoteMotion.Movement twin — the ONE
|
||||
/// per-entity MovementManager facade owning Interp + the MoveToManager
|
||||
/// (retail CPhysicsObj::movement_manager).</summary>
|
||||
public readonly MovementManager Movement;
|
||||
|
||||
/// <summary>The moveto child view (RemoteMotion.MoveTo twin) — non-null
|
||||
/// after the ctor's MakeMoveToManager, kept so test bodies read
|
||||
/// unchanged.</summary>
|
||||
public MoveToManager Mgr => Movement.MoveTo!;
|
||||
|
||||
/// <summary>R5-V3 (#171): the creature's PositionManager facade — the
|
||||
/// EntityPhysicsHost-owned twin (GameWindow binds MoveToManager.StickTo/
|
||||
|
|
@ -169,44 +178,60 @@ internal sealed class RemoteChaseHarness
|
|||
Interp.InitializeMotionTables = () => Seq.Manager.InitializeState();
|
||||
Interp.CheckForCompletedMotions = Seq.Manager.CheckForCompletedMotions;
|
||||
|
||||
Mgr = new MoveToManager(
|
||||
Interp,
|
||||
stopCompletely: () => Interp.StopCompletely(),
|
||||
getPosition: () => new CorePosition(1u, Body.Position, Body.Orientation),
|
||||
getHeading: () => MoveToMath.GetHeading(Body.Orientation),
|
||||
setHeading: (h, _) => Body.Orientation =
|
||||
MoveToMath.SetHeading(Body.Orientation, h),
|
||||
getOwnRadius: () => OwnRadius,
|
||||
getOwnHeight: () => 1f,
|
||||
contact: () => Body.OnWalkable,
|
||||
isInterpolating: () => false,
|
||||
getVelocity: () => Body.Velocity,
|
||||
getSelfId: () => CreatureGuid,
|
||||
setTarget: (ctx, tlid, radius, q) =>
|
||||
// ── R5-V5: the MovementManager facade owns Interp + the moveto —
|
||||
// GameWindow's RemoteMotion ctor + EnsureRemoteMotionBindings
|
||||
// factory shape verbatim (sticky binds inside the factory;
|
||||
// MakeMoveToManager after the host/Pm exist). ──
|
||||
Movement = new MovementManager(Interp)
|
||||
{
|
||||
MoveToFactory = () =>
|
||||
{
|
||||
_targetArmed = tlid == PlayerGuid;
|
||||
// TargetManager delivers the FIRST info synchronously on
|
||||
// SetTarget (live log: HandleUpdateTarget printed directly
|
||||
// after the arm, same network phase).
|
||||
DeliverTargetInfo();
|
||||
var mtm = new MoveToManager(
|
||||
Interp,
|
||||
stopCompletely: () => Interp.StopCompletely(),
|
||||
getPosition: () => new CorePosition(1u, Body.Position, Body.Orientation),
|
||||
getHeading: () => MoveToMath.GetHeading(Body.Orientation),
|
||||
setHeading: (h, _) => Body.Orientation =
|
||||
MoveToMath.SetHeading(Body.Orientation, h),
|
||||
getOwnRadius: () => OwnRadius,
|
||||
getOwnHeight: () => 1f,
|
||||
contact: () => Body.OnWalkable,
|
||||
isInterpolating: () => false,
|
||||
getVelocity: () => Body.Velocity,
|
||||
getSelfId: () => CreatureGuid,
|
||||
setTarget: (ctx, tlid, radius, q) =>
|
||||
{
|
||||
_targetArmed = tlid == PlayerGuid;
|
||||
// TargetManager delivers the FIRST info synchronously on
|
||||
// SetTarget (live log: HandleUpdateTarget printed directly
|
||||
// after the arm, same network phase).
|
||||
DeliverTargetInfo();
|
||||
},
|
||||
clearTarget: () => _targetArmed = false,
|
||||
getTargetQuantum: () => _quantum,
|
||||
setTargetQuantum: q => _quantum = q,
|
||||
curTime: () => Now);
|
||||
// R5-V3 (#171) sticky seam binds (BeginNextNode arrival
|
||||
// StickTo @0x00529d3a, PerformMovement-head Unstick).
|
||||
mtm.StickTo = (tlid, radius, height) => Pm.StickTo(tlid, radius, height);
|
||||
mtm.Unstick = Pm.UnStick;
|
||||
return mtm;
|
||||
},
|
||||
clearTarget: () => _targetArmed = false,
|
||||
getTargetQuantum: () => _quantum,
|
||||
setTargetQuantum: q => _quantum = q,
|
||||
curTime: () => Now);
|
||||
};
|
||||
|
||||
// TS-36: interrupt_current_movement → MovementManager::CancelMoveTo
|
||||
// (the facade relay 0x005241b0) — EnsureRemoteMotionBindings twin.
|
||||
Interp.InterruptCurrentMovement =
|
||||
() => Mgr.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
() => Movement.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
|
||||
// ── R5-V3 (#171): the PositionManager/sticky wiring — GameWindow's
|
||||
// V3 additions verbatim: host-owned facade + the three seam binds
|
||||
// (BeginNextNode arrival StickTo, PerformMovement-head Unstick,
|
||||
// UM-funnel-head unstick_from_object). ──
|
||||
// V3 additions verbatim: host-owned facade + the UM-funnel-head
|
||||
// unstick_from_object bind; then MakeMoveToManager (0x00524000)
|
||||
// invokes the factory above, mirroring the production bind order. ──
|
||||
_playerHost = new TargetHost(this);
|
||||
_creatureHost = new CreatureHost(this);
|
||||
Pm = new PositionManager(_creatureHost);
|
||||
Mgr.StickTo = (tlid, radius, height) => Pm.StickTo(tlid, radius, height);
|
||||
Mgr.Unstick = Pm.UnStick;
|
||||
Movement.MakeMoveToManager();
|
||||
Interp.UnstickFromObject = Pm.UnStick;
|
||||
|
||||
// ── The anim-loop MotionDone binding (GameWindow.cs:10266) ──
|
||||
|
|
@ -295,7 +320,9 @@ internal sealed class RemoteChaseHarness
|
|||
Radius = targetRadius,
|
||||
Height = targetHeight,
|
||||
};
|
||||
Mgr.PerformMovement(ms);
|
||||
// R5-V5: RouteServerMoveTo twin — through the facade
|
||||
// (MovementManager::PerformMovement 0x005240d0).
|
||||
Movement.PerformMovement(ms);
|
||||
}
|
||||
|
||||
private void DeliverTargetInfo()
|
||||
|
|
@ -311,9 +338,10 @@ internal sealed class RemoteChaseHarness
|
|||
InterpolatedPosition = pos,
|
||||
};
|
||||
// R5-V3 fan order (EntityPhysicsHost.HandleUpdateTarget — retail
|
||||
// CPhysicsObj::HandleUpdateTarget 0x00512bc0): MoveToManager first,
|
||||
// then PositionManager (the sticky consumer).
|
||||
Mgr.HandleUpdateTarget(info);
|
||||
// CPhysicsObj::HandleUpdateTarget 0x00512bc0): the MovementManager
|
||||
// relay (@0x00512bf0 → moveto) first, then PositionManager (the
|
||||
// sticky consumer).
|
||||
Movement.HandleUpdateTarget(info);
|
||||
Pm.HandleUpdateTarget(info);
|
||||
}
|
||||
|
||||
|
|
@ -338,12 +366,12 @@ internal sealed class RemoteChaseHarness
|
|||
|
||||
public void HandleUpdateTarget(TargetInfo info)
|
||||
{
|
||||
_h.Mgr.HandleUpdateTarget(info);
|
||||
_h.Movement.HandleUpdateTarget(info);
|
||||
_h.Pm.HandleUpdateTarget(info);
|
||||
}
|
||||
|
||||
public void InterruptCurrentMovement()
|
||||
=> _h.Mgr.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
=> _h.Movement.CancelMoveTo(WeenieError.ActionCancelled);
|
||||
|
||||
public void SetTarget(uint contextId, uint objectId, float radius, double quantum)
|
||||
{
|
||||
|
|
@ -399,8 +427,9 @@ internal sealed class RemoteChaseHarness
|
|||
if (_targetArmed && Now - _lastDeliveryTime >= _quantum)
|
||||
DeliverTargetInfo();
|
||||
|
||||
// 2. MoveToManager drive (TickRemoteMoveTo, GameWindow.cs:9994).
|
||||
Mgr.UseTime();
|
||||
// 2. MovementManager drive (TickRemoteMoveTo — the R5-V5 facade
|
||||
// relay, MovementManager::UseTime 0x005242f0).
|
||||
Movement.UseTime();
|
||||
|
||||
// 3. get_state_velocity → body velocity (the d2ccc80e refresh,
|
||||
// GameWindow.cs:10024).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue