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:
Erik 2026-07-05 11:12:19 +02:00
parent 6feaab91e3
commit dccd700991
6 changed files with 833 additions and 200 deletions

View file

@ -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).