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
|
|
@ -411,7 +411,18 @@ public sealed class GameWindow : IDisposable
|
|||
internal sealed class RemoteMotion // internal: the R2-Q5 sink callbacks (ObservedOmega seam) capture it
|
||||
{
|
||||
public AcDream.Core.Physics.PhysicsBody Body;
|
||||
public AcDream.Core.Physics.MotionInterpreter Motion;
|
||||
|
||||
/// <summary>R5-V5: retail <c>CPhysicsObj::movement_manager</c> — the
|
||||
/// ONE per-entity owner of the interp + moveto pair (acclient.h
|
||||
/// <c>/* 3463 */</c>). Constructed here with the interp; the
|
||||
/// MoveToManager side arrives via <c>MoveToFactory</c> +
|
||||
/// <c>MakeMoveToManager()</c> in EnsureRemoteMotionBindings.
|
||||
/// <see cref="Motion"/>/<see cref="MoveTo"/> below are views of its
|
||||
/// children (retail <c>get_minterp</c>-style access), kept so the
|
||||
/// dozens of existing call sites read unchanged.</summary>
|
||||
public AcDream.Core.Physics.Motion.MovementManager Movement;
|
||||
|
||||
public AcDream.Core.Physics.MotionInterpreter Motion => Movement.Minterp;
|
||||
/// <summary>R3-W4: the persistent per-remote dispatch sink (created
|
||||
/// once by EnsureRemoteMotionBindings; also bound as
|
||||
/// Motion.DefaultSink so LeaveGround/HitGround re-applies dispatch
|
||||
|
|
@ -432,8 +443,9 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
/// <summary>R4-V4: the entity's verbatim retail MoveToManager
|
||||
/// (server-directed movement), constructed + seam-bound by
|
||||
/// EnsureRemoteMotionBindings beside the R2-Q5/R3 binds.</summary>
|
||||
public AcDream.Core.Physics.Motion.MoveToManager? MoveTo;
|
||||
/// EnsureRemoteMotionBindings beside the R2-Q5/R3 binds. R5-V5:
|
||||
/// owned by <see cref="Movement"/>; this is the child view.</summary>
|
||||
public AcDream.Core.Physics.Motion.MoveToManager? MoveTo => Movement.MoveTo;
|
||||
|
||||
// R5-V2: the entity's CPhysicsObj stand-in — owns the TargetManager
|
||||
// voyeur system (retail CPhysicsObj::target_manager). Replaces the
|
||||
|
|
@ -607,15 +619,20 @@ public sealed class GameWindow : IDisposable
|
|||
// transition link (see PhysicsBody.InWorld).
|
||||
InWorld = true,
|
||||
};
|
||||
Motion = new AcDream.Core.Physics.MotionInterpreter(Body)
|
||||
{
|
||||
// R4-V5 #160 fix: retail remotes carry a weenie whose
|
||||
// InqRunRate FAILS, landing apply_run_to_command on
|
||||
// my_run_rate (the M13 wire feed). A null weenie took the
|
||||
// degenerate 1.0 branch — observer-side run movetos played
|
||||
// and moved in slow motion. See RemoteWeenie.
|
||||
WeenieObj = new AcDream.Core.Physics.RemoteWeenie(),
|
||||
};
|
||||
// R5-V5: the interp is owned by the MovementManager facade
|
||||
// (retail CPhysicsObj::movement_manager -> motion_interpreter).
|
||||
// acdream constructs it eagerly here — retail's lazy-create
|
||||
// window is never observable (see MovementManager's class doc).
|
||||
Movement = new AcDream.Core.Physics.Motion.MovementManager(
|
||||
new AcDream.Core.Physics.MotionInterpreter(Body)
|
||||
{
|
||||
// R4-V5 #160 fix: retail remotes carry a weenie whose
|
||||
// InqRunRate FAILS, landing apply_run_to_command on
|
||||
// my_run_rate (the M13 wire feed). A null weenie took the
|
||||
// degenerate 1.0 branch — observer-side run movetos played
|
||||
// and moved in slow motion. See RemoteWeenie.
|
||||
WeenieObj = new AcDream.Core.Physics.RemoteWeenie(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4282,6 +4299,11 @@ public sealed class GameWindow : IDisposable
|
|||
// space on both sides (getPosition + the MovementStruct positions
|
||||
// the UM router builds) — one consistent space, so the manager's
|
||||
// distance/heading math matches the harness exactly.
|
||||
// R5-V5: the construction is now the MovementManager facade's
|
||||
// MoveToFactory (the acdream stand-in for the physics_obj/weenie_obj
|
||||
// backpointers retail's MakeMoveToManager 0x00524000 constructs
|
||||
// from); MakeMoveToManager() below invokes it once, preserving the
|
||||
// pre-facade eager timing.
|
||||
var rmT = rm;
|
||||
var mtBody = rm.Body;
|
||||
// R5-V3 (#171): real setup cylsphere radii — retail CPartArray::
|
||||
|
|
@ -4297,35 +4319,50 @@ public sealed class GameWindow : IDisposable
|
|||
// TargetManager::SetTarget). Assigned right after the manager is built.
|
||||
EntityPhysicsHost host = null!;
|
||||
double NowSeconds() => (System.DateTime.UtcNow - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
rm.MoveTo = new AcDream.Core.Physics.Motion.MoveToManager(
|
||||
rm.Motion,
|
||||
stopCompletely: () => rmT.Motion.StopCompletely(),
|
||||
getPosition: () => new AcDream.Core.Physics.Position(
|
||||
rmT.CellId, mtBody.Position, mtBody.Orientation),
|
||||
getHeading: () => AcDream.Core.Physics.Motion.MoveToMath.GetHeading(
|
||||
mtBody.Orientation),
|
||||
setHeading: (h, _) => mtBody.Orientation =
|
||||
AcDream.Core.Physics.Motion.MoveToMath.SetHeading(mtBody.Orientation, h),
|
||||
getOwnRadius: () => GetSetupCylinder(serverGuid, selfEntity).Radius,
|
||||
getOwnHeight: () => GetSetupCylinder(serverGuid, selfEntity).Height,
|
||||
contact: () => mtBody.OnWalkable,
|
||||
isInterpolating: () => rmT.Interp.IsActive,
|
||||
getVelocity: () => mtBody.Velocity,
|
||||
getSelfId: () => serverGuid,
|
||||
// R5-V2: retail CPhysicsObj::set_target/clear_target/target_quantum
|
||||
// → the entity's TargetManager (replaces the AP-79 TrackedTarget*
|
||||
// poll). The manager passes (0, top_level_id, 0.5, 0.0).
|
||||
setTarget: (ctx, tlid, radius, q) => host.SetTarget(ctx, tlid, radius, q),
|
||||
clearTarget: () => host.ClearTarget(),
|
||||
getTargetQuantum: () => host.TargetManager.GetTargetQuantum(),
|
||||
setTargetQuantum: q => host.TargetManager.SetTargetQuantum(q),
|
||||
// R4-V5: real clock (same epoch-seconds base the per-remote tick uses).
|
||||
curTime: NowSeconds);
|
||||
rm.Movement.MoveToFactory = () =>
|
||||
{
|
||||
var mtm = new AcDream.Core.Physics.Motion.MoveToManager(
|
||||
rm.Motion,
|
||||
stopCompletely: () => rmT.Motion.StopCompletely(),
|
||||
getPosition: () => new AcDream.Core.Physics.Position(
|
||||
rmT.CellId, mtBody.Position, mtBody.Orientation),
|
||||
getHeading: () => AcDream.Core.Physics.Motion.MoveToMath.GetHeading(
|
||||
mtBody.Orientation),
|
||||
setHeading: (h, _) => mtBody.Orientation =
|
||||
AcDream.Core.Physics.Motion.MoveToMath.SetHeading(mtBody.Orientation, h),
|
||||
getOwnRadius: () => GetSetupCylinder(serverGuid, selfEntity).Radius,
|
||||
getOwnHeight: () => GetSetupCylinder(serverGuid, selfEntity).Height,
|
||||
contact: () => mtBody.OnWalkable,
|
||||
isInterpolating: () => rmT.Interp.IsActive,
|
||||
getVelocity: () => mtBody.Velocity,
|
||||
getSelfId: () => serverGuid,
|
||||
// R5-V2: retail CPhysicsObj::set_target/clear_target/target_quantum
|
||||
// → the entity's TargetManager (replaces the AP-79 TrackedTarget*
|
||||
// poll). The manager passes (0, top_level_id, 0.5, 0.0).
|
||||
setTarget: (ctx, tlid, radius, q) => host.SetTarget(ctx, tlid, radius, q),
|
||||
clearTarget: () => host.ClearTarget(),
|
||||
getTargetQuantum: () => host.TargetManager.GetTargetQuantum(),
|
||||
setTargetQuantum: q => host.TargetManager.SetTargetQuantum(q),
|
||||
// R4-V5: real clock (same epoch-seconds base the per-remote tick uses).
|
||||
curTime: NowSeconds);
|
||||
// R5-V3 (#171, retires TS-39): bind the sticky seams to the host's
|
||||
// PositionManager (host is constructed before MakeMoveToManager
|
||||
// invokes this factory) —
|
||||
// * BeginNextNode's sticky-arrival handoff: PositionManager::StickTo
|
||||
// (retail MoveToManager::BeginNextNode @0x00529d3a);
|
||||
// * PerformMovement's head unstick: unstick_from_object →
|
||||
// PositionManager::UnStick (MoveToManager.PerformMovement:414).
|
||||
mtm.StickTo = (tlid, radius, height) =>
|
||||
host.PositionManager.StickTo(tlid, radius, height);
|
||||
mtm.Unstick = host.PositionManager.UnStick;
|
||||
return mtm;
|
||||
};
|
||||
// TS-36 (remote side): the interp's interrupt seam is retail's
|
||||
// interrupt_current_movement → MovementManager::CancelMoveTo(0x36)
|
||||
// chain (V2's reentrancy tests prove the wiring is inert-safe).
|
||||
// chain (V2's reentrancy tests prove the wiring is inert-safe;
|
||||
// R5-V5: the chain now lands on the literal facade relay 0x005241b0).
|
||||
rm.Motion.InterruptCurrentMovement =
|
||||
() => rmT.MoveTo?.CancelMoveTo(
|
||||
() => rmT.Movement.CancelMoveTo(
|
||||
AcDream.Core.Physics.WeenieError.ActionCancelled);
|
||||
|
||||
// R5-V2: the entity's CPhysicsObj stand-in + its TargetManager. Position
|
||||
|
|
@ -4350,24 +4387,22 @@ public sealed class GameWindow : IDisposable
|
|||
curTime: NowSeconds,
|
||||
physicsTimerTime: NowSeconds,
|
||||
getObjectA: ResolvePhysicsHost,
|
||||
handleUpdateTarget: info => rmT.MoveTo?.HandleUpdateTarget(info),
|
||||
interruptCurrentMovement: () => rmT.MoveTo?.CancelMoveTo(
|
||||
// Retail CPhysicsObj::HandleUpdateTarget (0x00512bc0) fan head:
|
||||
// MovementManager::HandleUpdateTarget (@0x00512bf0 — the facade
|
||||
// relay to the moveto side); the host chains the PositionManager
|
||||
// leg after it.
|
||||
handleUpdateTarget: info => rmT.Movement.HandleUpdateTarget(info),
|
||||
interruptCurrentMovement: () => rmT.Movement.CancelMoveTo(
|
||||
AcDream.Core.Physics.WeenieError.ActionCancelled));
|
||||
rm.Host = host;
|
||||
_physicsHosts[serverGuid] = host;
|
||||
|
||||
// R5-V3 (#171, retires TS-39): bind the sticky seams to the host's
|
||||
// PositionManager —
|
||||
// * BeginNextNode's sticky-arrival handoff: PositionManager::StickTo
|
||||
// (retail MoveToManager::BeginNextNode @0x00529d3a);
|
||||
// * PerformMovement's head unstick: unstick_from_object →
|
||||
// PositionManager::UnStick (MoveToManager.PerformMovement:414);
|
||||
// * the UM funnel head's unstick: CPhysicsObj::unstick_from_object
|
||||
// (0x0050eaea) — invoked at the mt-0 routing sites (~4894) but
|
||||
// unbound until now.
|
||||
rm.MoveTo.StickTo = (tlid, radius, height) =>
|
||||
host.PositionManager.StickTo(tlid, radius, height);
|
||||
rm.MoveTo.Unstick = host.PositionManager.UnStick;
|
||||
// R5-V5: retail MakeMoveToManager (0x00524000) — constructs the
|
||||
// MoveToManager via the factory above (host now exists for the
|
||||
// sticky binds inside it). The UM funnel head's unstick
|
||||
// (CPhysicsObj::unstick_from_object 0x0050eaea, invoked at the mt-0
|
||||
// routing sites) binds on the interp beside it.
|
||||
rm.Movement.MakeMoveToManager();
|
||||
rm.Motion.UnstickFromObject = host.PositionManager.UnStick;
|
||||
return rm.Sink;
|
||||
}
|
||||
|
|
@ -4506,11 +4541,12 @@ public sealed class GameWindow : IDisposable
|
|||
// independently (r3-port-plan §4): the manager's (each pending
|
||||
// animation fires MotionDone(success:false) → the bound interp pops
|
||||
// in step) THEN the interp's own (flushes any remainder —
|
||||
// CMotionInterp::HandleExitWorld 0x00527f30).
|
||||
// MovementManager::HandleExitWorld 0x00524350, the R5-V5 facade
|
||||
// relay: interp ONLY → CMotionInterp::HandleExitWorld 0x00527f30).
|
||||
if (_animatedEntities.TryGetValue(existingEntity.Id, out var aeGone))
|
||||
aeGone.Sequencer?.Manager.HandleExitWorld();
|
||||
if (_remoteDeadReckon.TryGetValue(serverGuid, out var rmGone))
|
||||
rmGone.Motion.HandleExitWorld();
|
||||
rmGone.Movement.HandleExitWorld();
|
||||
// R5-V2: retail CPhysicsObj::exit_world tells every voyeur of this
|
||||
// entity that it left the world (TargetManager::NotifyVoyeurOfEvent
|
||||
// ExitWorld) — a watcher moving-to/stuck-to this entity drops the
|
||||
|
|
@ -4565,13 +4601,16 @@ public sealed class GameWindow : IDisposable
|
|||
/// builds the <c>MovementStruct</c> (mt 6/8 resolve the target guid
|
||||
/// against the entity table; unresolvable degrades to
|
||||
/// MoveToPosition(wire origin) / TurnToHeading(wire heading) per §2f —
|
||||
/// NOT an error), and calls <c>PerformMovement</c>. Returns true when
|
||||
/// the event was a type-6..9 moveto (consumed); false for every other
|
||||
/// movement type (caller falls through to its funnel / skip posture).
|
||||
/// NOT an error), and calls the R5-V5 facade's <c>PerformMovement</c>
|
||||
/// (<c>MovementManager::PerformMovement</c> 0x005240d0 — types 6-9
|
||||
/// MakeMoveToManager + delegate; retail's cases call MakeMoveToManager
|
||||
/// at each head, a no-op here since the bind sites create eagerly).
|
||||
/// Returns true when the event was a type-6..9 moveto (consumed); false
|
||||
/// for every other movement type (caller falls through to its funnel /
|
||||
/// skip posture).
|
||||
/// </summary>
|
||||
private bool RouteServerMoveTo(
|
||||
AcDream.Core.Physics.Motion.MoveToManager mgr,
|
||||
AcDream.Core.Physics.MotionInterpreter interp,
|
||||
AcDream.Core.Physics.Motion.MovementManager movement,
|
||||
uint cellId,
|
||||
AcDream.Core.Net.WorldSession.EntityMotionUpdate update)
|
||||
{
|
||||
|
|
@ -4580,7 +4619,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
// my_run_rate write (unpack_movement @300603).
|
||||
if (update.MotionState.MoveToRunRate is { } mtRunRate)
|
||||
interp.MyRunRate = mtRunRate;
|
||||
movement.Minterp.MyRunRate = mtRunRate;
|
||||
|
||||
var destWorld = AcDream.Core.Physics.Motion.MoveToMath
|
||||
.OriginToWorld(
|
||||
|
|
@ -4629,7 +4668,7 @@ public sealed class GameWindow : IDisposable
|
|||
cellId, destWorld,
|
||||
System.Numerics.Quaternion.Identity);
|
||||
}
|
||||
mgr.PerformMovement(ms);
|
||||
movement.PerformMovement(ms);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -4669,7 +4708,7 @@ public sealed class GameWindow : IDisposable
|
|||
mp.DesiredHeading = wireHeading;
|
||||
}
|
||||
}
|
||||
mgr.PerformMovement(ms);
|
||||
movement.PerformMovement(ms);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -4677,8 +4716,9 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-V5 / R5-V2: the per-tick <see cref="AcDream.Core.Physics.Motion.MoveToManager"/>
|
||||
/// drive (<c>UseTime()</c> — steering, arrival, fail-distance). The P4
|
||||
/// R4-V5 / R5-V2: the per-tick <see cref="AcDream.Core.Physics.Motion.MovementManager"/>
|
||||
/// drive (retail <c>MovementManager::UseTime</c> 0x005242f0 — the moveto
|
||||
/// side's steering, arrival, fail-distance; R5-V5 facade relay). The P4
|
||||
/// TargetTracker POLL is gone (R5-V2): target-position delivery now flows
|
||||
/// through the <see cref="AcDream.Core.Physics.Motion.TargetManager"/>
|
||||
/// voyeur system — the target's own <c>HandleTargetting</c> pushes updates
|
||||
|
|
@ -4690,7 +4730,7 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
private void TickRemoteMoveTo(RemoteMotion rm)
|
||||
{
|
||||
rm.MoveTo?.UseTime();
|
||||
rm.Movement.UseTime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -4975,14 +5015,14 @@ public sealed class GameWindow : IDisposable
|
|||
_playerController.Motion.DoMotion(wireStylePlayer,
|
||||
new AcDream.Core.Physics.Motion.MovementParameters());
|
||||
|
||||
if (_playerController.MoveTo is { } playerMoveTo
|
||||
&& RouteServerMoveTo(playerMoveTo, _playerController.Motion,
|
||||
if (_playerController.MoveTo is not null
|
||||
&& RouteServerMoveTo(_playerController.Movement,
|
||||
_playerController.CellId, update))
|
||||
{
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
{
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[autowalk-begin] mt=0x{update.MotionState.MovementType:X2} movingTo={playerMoveTo.IsMovingTo()} type={playerMoveTo.MovementTypeState}"));
|
||||
$"[autowalk-begin] mt=0x{update.MotionState.MovementType:X2} movingTo={_playerController.Movement.IsMovingTo()} type={_playerController.MoveTo.MovementTypeState}"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -5067,8 +5107,10 @@ public sealed class GameWindow : IDisposable
|
|||
// R4-V5: the type-6..9 routing body is shared with the
|
||||
// local player (RouteServerMoveTo) — behavior identical
|
||||
// to the R4-V4 inline blocks it was extracted from.
|
||||
if (remoteMot.MoveTo is { } moveMgr
|
||||
&& RouteServerMoveTo(moveMgr, remoteMot.Motion,
|
||||
// (MoveTo null = EnsureRemoteMotionBindings early-outed
|
||||
// on a sequencer-less entity — same skip as pre-V5.)
|
||||
if (remoteMot.MoveTo is not null
|
||||
&& RouteServerMoveTo(remoteMot.Movement,
|
||||
remoteMot.CellId, update))
|
||||
{
|
||||
return;
|
||||
|
|
@ -5657,21 +5699,21 @@ public sealed class GameWindow : IDisposable
|
|||
rmState.Body.Position = worldPos;
|
||||
|
||||
// #161: retail landing = MovementManager::HitGround
|
||||
// (minterp → moveto, 0x00524300) with the Gravity state
|
||||
// bit STILL SET (CMotionInterp::HitGround gates on
|
||||
// state&0x400). The re-apply dispatches the PRESERVED
|
||||
// pre-fall forward command → landing link → cycle. This
|
||||
// replaces the forced SetCycle, which read the
|
||||
// then-clobbered ForwardCommand (Falling) and re-set the
|
||||
// pose it meant to clear. See the twin block in
|
||||
// TickAnimations (VU.land).
|
||||
// (minterp → moveto, 0x00524300 — the R5-V5 facade
|
||||
// relay) with the Gravity state bit STILL SET
|
||||
// (CMotionInterp::HitGround gates on state&0x400). The
|
||||
// re-apply dispatches the PRESERVED pre-fall forward
|
||||
// command → landing link → cycle. This replaces the
|
||||
// forced SetCycle, which read the then-clobbered
|
||||
// ForwardCommand (Falling) and re-set the pose it meant
|
||||
// to clear. See the twin block in TickAnimations
|
||||
// (VU.land).
|
||||
if (_animatedEntities.TryGetValue(entity.Id, out var aeForLand)
|
||||
&& aeForLand.Sequencer is not null)
|
||||
{
|
||||
EnsureRemoteMotionBindings(rmState, aeForLand, update.Guid);
|
||||
}
|
||||
rmState.Motion.HitGround();
|
||||
rmState.MoveTo?.HitGround();
|
||||
rmState.Movement.HitGround();
|
||||
// DR bookkeeping only (partner of the jump-start
|
||||
// `State |= Gravity`).
|
||||
rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
|
|
@ -10449,15 +10491,15 @@ public sealed class GameWindow : IDisposable
|
|||
// then-clobbered InterpretedState.ForwardCommand
|
||||
// — 0x40000015 — and re-set the very Falling
|
||||
// cycle it meant to clear.)
|
||||
rm.Motion.HitGround();
|
||||
// R4-V5 (closes the V4 wiring-contract gap the
|
||||
// adversarial review caught): retail order —
|
||||
// minterp first, then moveto (MovementManager::
|
||||
// HitGround 0x00524300, §2d). Re-arms a moveto
|
||||
// suspended by the airborne UseTime contact gate;
|
||||
// without it a chasing NPC that lands stalls
|
||||
// until ACE's ~1 Hz re-emit.
|
||||
rm.MoveTo?.HitGround();
|
||||
// HitGround 0x00524300, §2d — the R5-V5 facade
|
||||
// relay). Re-arms a moveto suspended by the
|
||||
// airborne UseTime contact gate; without it a
|
||||
// chasing NPC that lands stalls until ACE's
|
||||
// ~1 Hz re-emit.
|
||||
rm.Movement.HitGround();
|
||||
// DR bookkeeping only (partner of the jump-start
|
||||
// `State |= Gravity`): stops the per-tick gravity
|
||||
// integration for the grounded body.
|
||||
|
|
@ -12750,7 +12792,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
private void InstallSpeculativeTurnToTarget(uint targetGuid)
|
||||
{
|
||||
if (_playerController?.MoveTo is not { } playerMoveTo) return;
|
||||
if (_playerController is not { } pc || pc.MoveTo is null) return;
|
||||
if (!_entitiesByServerGuid.TryGetValue(targetGuid, out var entity))
|
||||
return;
|
||||
|
||||
|
|
@ -12829,7 +12871,10 @@ public sealed class GameWindow : IDisposable
|
|||
// set it autonomous) and clobbers this moveto's dispatched motions
|
||||
// with the idle raw state.
|
||||
_playerController.SetLastMoveWasAutonomous(false);
|
||||
playerMoveTo.PerformMovement(ms);
|
||||
// R5-V5: through the facade (MovementManager::PerformMovement
|
||||
// 0x005240d0) — retail's client-initiated use flow reaches the same
|
||||
// manager the wire path uses.
|
||||
pc.Movement.PerformMovement(ms);
|
||||
}
|
||||
|
||||
private uint? SelectClosestCombatTarget(bool showToast)
|
||||
|
|
@ -13399,27 +13444,57 @@ public sealed class GameWindow : IDisposable
|
|||
// R5-V2: forward-declared so the player MoveToManager's target seams
|
||||
// route into the player's TargetManager (retail CPhysicsObj::set_target).
|
||||
EntityPhysicsHost playerHost = null!;
|
||||
var playerMoveTo = new AcDream.Core.Physics.Motion.MoveToManager(
|
||||
pcMoveTo.Motion,
|
||||
stopCompletely: () => pcMoveTo.Motion.StopCompletely(),
|
||||
getPosition: () => new AcDream.Core.Physics.Position(
|
||||
pcMoveTo.CellId, pcMoveTo.Position, pcMoveTo.BodyOrientation),
|
||||
getHeading: () => AcDream.Core.Physics.Motion.MoveToMath.HeadingFromYaw(pcMoveTo.Yaw),
|
||||
setHeading: (h, _) => pcMoveTo.Yaw =
|
||||
AcDream.Core.Physics.Motion.MoveToMath.YawFromHeading(h),
|
||||
getOwnRadius: () => GetSetupCylinder(_playerServerGuid, playerEntity).Radius,
|
||||
getOwnHeight: () => GetSetupCylinder(_playerServerGuid, playerEntity).Height,
|
||||
contact: () => pcMoveTo.BodyInContact,
|
||||
isInterpolating: () => false,
|
||||
getVelocity: () => pcMoveTo.BodyVelocity,
|
||||
getSelfId: () => _playerServerGuid,
|
||||
// R5-V2: retail CPhysicsObj::set_target/clear_target/quantum → the
|
||||
// player's TargetManager (replaces the AP-79 _playerMoveToTarget* poll).
|
||||
setTarget: (ctx, tlid, radius, q) => playerHost.SetTarget(ctx, tlid, radius, q),
|
||||
clearTarget: () => playerHost.ClearTarget(),
|
||||
getTargetQuantum: () => playerHost.TargetManager.GetTargetQuantum(),
|
||||
setTargetQuantum: q => playerHost.TargetManager.SetTargetQuantum(q),
|
||||
curTime: () => pcMoveTo.SimTimeSeconds);
|
||||
// R5-V5: the construction is the player MovementManager's
|
||||
// MoveToFactory (same facade shape as EnsureRemoteMotionBindings);
|
||||
// MakeMoveToManager() below invokes it once, after playerHost exists
|
||||
// for the sticky binds.
|
||||
_playerController.Movement.MoveToFactory = () =>
|
||||
{
|
||||
var playerMoveTo = new AcDream.Core.Physics.Motion.MoveToManager(
|
||||
pcMoveTo.Motion,
|
||||
stopCompletely: () => pcMoveTo.Motion.StopCompletely(),
|
||||
getPosition: () => new AcDream.Core.Physics.Position(
|
||||
pcMoveTo.CellId, pcMoveTo.Position, pcMoveTo.BodyOrientation),
|
||||
getHeading: () => AcDream.Core.Physics.Motion.MoveToMath.HeadingFromYaw(pcMoveTo.Yaw),
|
||||
setHeading: (h, _) => pcMoveTo.Yaw =
|
||||
AcDream.Core.Physics.Motion.MoveToMath.YawFromHeading(h),
|
||||
getOwnRadius: () => GetSetupCylinder(_playerServerGuid, playerEntity).Radius,
|
||||
getOwnHeight: () => GetSetupCylinder(_playerServerGuid, playerEntity).Height,
|
||||
contact: () => pcMoveTo.BodyInContact,
|
||||
isInterpolating: () => false,
|
||||
getVelocity: () => pcMoveTo.BodyVelocity,
|
||||
getSelfId: () => _playerServerGuid,
|
||||
// R5-V2: retail CPhysicsObj::set_target/clear_target/quantum → the
|
||||
// player's TargetManager (replaces the AP-79 _playerMoveToTarget* poll).
|
||||
setTarget: (ctx, tlid, radius, q) => playerHost.SetTarget(ctx, tlid, radius, q),
|
||||
clearTarget: () => playerHost.ClearTarget(),
|
||||
getTargetQuantum: () => playerHost.TargetManager.GetTargetQuantum(),
|
||||
setTargetQuantum: q => playerHost.TargetManager.SetTargetQuantum(q),
|
||||
curTime: () => pcMoveTo.SimTimeSeconds);
|
||||
|
||||
// AD-27 re-anchored (was the deleted AutoWalkArrived event): fire
|
||||
// the deferred close-range Use/PickUp action when the moveto
|
||||
// completes NATURALLY (MoveToComplete is the documented client
|
||||
// seam; it never fires on CancelMoveTo, so a user-input cancel
|
||||
// doesn't send the action — same contract the old "arrived"-only
|
||||
// event had).
|
||||
playerMoveTo.MoveToComplete = err =>
|
||||
{
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[autowalk-end] reason=complete err={err}");
|
||||
if (err == AcDream.Core.Physics.WeenieError.None)
|
||||
OnAutoWalkArrivedSendDeferredAction();
|
||||
};
|
||||
|
||||
// R5-V3 (#171, retires TS-39 — player side): bind the sticky
|
||||
// seams to the player host's PositionManager (same trio as the
|
||||
// remote bind: BeginNextNode arrival StickTo @0x00529d3a,
|
||||
// PerformMovement-head Unstick).
|
||||
playerMoveTo.StickTo = (tlid, radius, height) =>
|
||||
playerHost.PositionManager.StickTo(tlid, radius, height);
|
||||
playerMoveTo.Unstick = playerHost.PositionManager.UnStick;
|
||||
return playerMoveTo;
|
||||
};
|
||||
|
||||
// R5-V2: the player's CPhysicsObj stand-in + TargetManager. Position is
|
||||
// the player's WORLD position (WorldEntity.Position — what the AP-79
|
||||
|
|
@ -13441,52 +13516,41 @@ public sealed class GameWindow : IDisposable
|
|||
curTime: () => pcMoveTo.SimTimeSeconds,
|
||||
physicsTimerTime: () => pcMoveTo.SimTimeSeconds,
|
||||
getObjectA: ResolvePhysicsHost,
|
||||
handleUpdateTarget: info => _playerController?.MoveTo?.HandleUpdateTarget(info),
|
||||
interruptCurrentMovement: () => _playerController?.MoveTo?.CancelMoveTo(
|
||||
// Retail CPhysicsObj::HandleUpdateTarget (0x00512bc0) fan head:
|
||||
// MovementManager::HandleUpdateTarget (@0x00512bf0 — the facade
|
||||
// relay); the host chains the PositionManager leg after it.
|
||||
handleUpdateTarget: info => _playerController?.Movement.HandleUpdateTarget(info),
|
||||
interruptCurrentMovement: () => _playerController?.Movement.CancelMoveTo(
|
||||
AcDream.Core.Physics.WeenieError.ActionCancelled));
|
||||
_playerHost = playerHost;
|
||||
_physicsHosts[_playerServerGuid] = playerHost;
|
||||
|
||||
// AD-27 re-anchored (was the deleted AutoWalkArrived event): fire
|
||||
// the deferred close-range Use/PickUp action when the moveto
|
||||
// completes NATURALLY (MoveToComplete is the documented client
|
||||
// seam; it never fires on CancelMoveTo, so a user-input cancel
|
||||
// doesn't send the action — same contract the old "arrived"-only
|
||||
// event had).
|
||||
playerMoveTo.MoveToComplete = err =>
|
||||
{
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled)
|
||||
Console.WriteLine($"[autowalk-end] reason=complete err={err}");
|
||||
if (err == AcDream.Core.Physics.WeenieError.None)
|
||||
OnAutoWalkArrivedSendDeferredAction();
|
||||
};
|
||||
_playerController.MoveTo = playerMoveTo;
|
||||
|
||||
// R5-V3 (#171, retires TS-39 — player side): bind the sticky seams to
|
||||
// the player host's PositionManager (same trio as the remote bind in
|
||||
// EnsureRemoteMotionBindings: BeginNextNode arrival StickTo
|
||||
// @0x00529d3a, PerformMovement-head Unstick, UM-funnel-head
|
||||
// unstick_from_object 0x0050eaea) and hand the facade to the
|
||||
// controller, which drives AdjustOffset/UseTime at the retail
|
||||
// UpdatePositionInternal/UpdateObjectInternal points.
|
||||
playerMoveTo.StickTo = (tlid, radius, height) =>
|
||||
playerHost.PositionManager.StickTo(tlid, radius, height);
|
||||
playerMoveTo.Unstick = playerHost.PositionManager.UnStick;
|
||||
// R5-V5: retail MakeMoveToManager (0x00524000) — constructs the
|
||||
// player MoveToManager via the factory above (playerHost now exists
|
||||
// for the sticky binds inside it). The UM-funnel-head unstick
|
||||
// (CPhysicsObj::unstick_from_object 0x0050eaea) binds on the interp
|
||||
// beside it, and the controller takes the PositionManager handoff it
|
||||
// drives at the retail UpdatePositionInternal/UpdateObjectInternal
|
||||
// points (AdjustOffset/UseTime).
|
||||
var playerMovement = _playerController.Movement;
|
||||
playerMovement.MakeMoveToManager();
|
||||
_playerController.Motion.UnstickFromObject = playerHost.PositionManager.UnStick;
|
||||
_playerController.PositionManager = playerHost.PositionManager;
|
||||
|
||||
// TS-36 RETIRED: the interp's interrupt seam is retail's
|
||||
// interrupt_current_movement → MovementManager::CancelMoveTo(0x36)
|
||||
// chain (raw 278189-278200). Every DoMotion/StopMotion/
|
||||
// StopCompletely/jump/set_hold_run cancel site now genuinely
|
||||
// cancels a running moveto (V2's reentrancy tests prove the chain
|
||||
// is inert-safe).
|
||||
// chain (raw 278189-278200) — since R5-V5 the literal facade relay
|
||||
// (0x005241b0). Every DoMotion/StopMotion/StopCompletely/jump/
|
||||
// set_hold_run cancel site now genuinely cancels a running moveto
|
||||
// (V2's reentrancy tests prove the chain is inert-safe). Captures
|
||||
// THIS controller's facade (not the _playerController field) so a
|
||||
// Tab-toggle's stale interp keeps cancelling its own manager.
|
||||
_playerController.Motion.InterruptCurrentMovement = () =>
|
||||
{
|
||||
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled
|
||||
&& playerMoveTo.IsMovingTo())
|
||||
&& playerMovement.IsMovingTo())
|
||||
Console.WriteLine("[autowalk-end] reason=interrupt");
|
||||
playerMoveTo.CancelMoveTo(AcDream.Core.Physics.WeenieError.ActionCancelled);
|
||||
playerMovement.CancelMoveTo(AcDream.Core.Physics.WeenieError.ActionCancelled);
|
||||
};
|
||||
|
||||
// K-fix7 (2026-04-26): if PlayerDescription already arrived, the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue