acdream/src/AcDream.App/Physics/RemoteInboundMotionDispatcher.cs
Erik f961d70023 feat(physics): port retail complete object frame pipeline
Restore the named-retail object update order across local, remote, static, projectile, animation, shadow, teleport, and effect lifetimes. Separate authoritative root commits from spatial rebucketing, preserve per-owner hook/FIFO ordering, and remove update-path allocations with exact lifecycle and residency gates.

Add deterministic conformance, adversarial lifetime, GUID-reuse, pending-cell, quaternion, timestamp, and allocation coverage. Release build is warning-free and all 6,446 tests pass with five intentional skips; retail, architecture, and adversarial reviews are clean.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-20 09:10:31 +02:00

145 lines
5.4 KiB
C#

using AcDream.Core.Net;
using AcDream.Core.Physics;
using AcDream.Core.Physics.Motion;
namespace AcDream.App.Physics;
/// <summary>
/// Applies the remote-object half of retail
/// <c>MovementManager::unpack_movement</c> (<c>0x00524440</c>) after the
/// packet timestamp gate has accepted an UpdateMotion event.
/// </summary>
/// <remarks>
/// The packet owner is deliberately independent of a render PartArray. A
/// normal animated object supplies a <see cref="MotionTableDispatchSink"/>;
/// an animation-less object supplies <see langword="null"/> and
/// <c>CMotionInterp</c> retains the same state on its canonical physics body.
/// This keeps the interrupt/style/type-0/sticky ordering identical for both
/// object shapes instead of maintaining a second headless protocol funnel.
/// </remarks>
internal sealed class RemoteInboundMotionDispatcher
{
private readonly Func<
MovementManager,
uint,
WorldSession.EntityMotionUpdate,
bool> _routeServerMoveTo;
private readonly Action<IPhysicsObjHost?, uint> _stickToObject;
public RemoteInboundMotionDispatcher(
Func<MovementManager, uint, WorldSession.EntityMotionUpdate, bool>
routeServerMoveTo,
Action<IPhysicsObjHost?, uint> stickToObject)
{
_routeServerMoveTo = routeServerMoveTo
?? throw new ArgumentNullException(nameof(routeServerMoveTo));
_stickToObject = stickToObject
?? throw new ArgumentNullException(nameof(stickToObject));
}
public RemoteInboundMotionDispatchResult Apply(
WorldSession.EntityMotionUpdate update,
MovementManager movement,
IInterpretedMotionSink? animationSink,
IPhysicsObjHost? host,
uint cellId,
uint fallbackForwardClass,
Func<bool>? isCurrent = null)
{
ArgumentNullException.ThrowIfNull(movement);
bool Current() => isCurrent?.Invoke() ?? true;
MotionInterpreter motion = movement.Minterp;
uint previousForward = motion.InterpretedState.ForwardCommand;
RemoteInboundMotionDispatchResult Superseded() => new(
RoutedMoveTo: false,
AppliedInterpretedState: false,
PreviousForwardCommand: previousForward,
CurrentForwardCommand: motion.InterpretedState.ForwardCommand,
Superseded: true);
if (!Current())
return Superseded();
// MovementManager::unpack_movement @00524440: these two calls occur
// before the movement-type switch for every accepted packet.
motion.InterruptCurrentMovement?.Invoke();
if (!Current())
return Superseded();
motion.UnstickFromObject?.Invoke();
if (!Current())
return Superseded();
uint wireStyle = update.MotionState.Stance != 0
? 0x80000000u | update.MotionState.Stance
: 0x8000003Du;
if (motion.InterpretedState.CurrentStyle != wireStyle)
{
motion.DoMotion(
wireStyle,
new MovementParameters());
if (!Current())
return Superseded();
}
// Cases 6/7/8/9 are owned by MoveToManager and return directly.
bool routedMoveTo = _routeServerMoveTo(movement, cellId, update);
if (!Current())
return Superseded();
if (routedMoveTo)
{
return new RemoteInboundMotionDispatchResult(
RoutedMoveTo: true,
AppliedInterpretedState: false,
PreviousForwardCommand: previousForward,
CurrentForwardCommand: motion.InterpretedState.ForwardCommand);
}
// Retail's ten-way switch returns zero for every unrecognised type;
// only case 0 enters the wholesale interpreted-state funnel.
if (update.MotionState.MovementType != 0)
{
return new RemoteInboundMotionDispatchResult(
RoutedMoveTo: false,
AppliedInterpretedState: false,
PreviousForwardCommand: previousForward,
CurrentForwardCommand: motion.InterpretedState.ForwardCommand);
}
InboundInterpretedState interpreted =
InboundInterpretedMotionFactory.Create(
update.MotionState,
fallbackForwardClass);
motion.MoveToInterpretedState(interpreted, animationSink);
if (!Current())
return Superseded();
// Case-0 tail order @00524583-0052458E: stick after the wholesale
// state copy, then write standing_longjump unconditionally.
if (update.MotionState.StickyObjectGuid is { } stickyGuid
&& stickyGuid != 0)
{
_stickToObject(host, stickyGuid);
if (!Current())
return Superseded();
}
motion.StandingLongJump = update.MotionState.StandingLongJump;
return new RemoteInboundMotionDispatchResult(
RoutedMoveTo: false,
AppliedInterpretedState: true,
PreviousForwardCommand: previousForward,
CurrentForwardCommand: interpreted.ForwardCommand);
}
}
internal readonly record struct RemoteInboundMotionDispatchResult(
bool RoutedMoveTo,
bool AppliedInterpretedState,
uint PreviousForwardCommand,
uint CurrentForwardCommand,
bool Superseded = false)
{
public bool ForwardCommandChanged =>
AppliedInterpretedState
&& PreviousForwardCommand != CurrentForwardCommand;
}