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>
145 lines
5.4 KiB
C#
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;
|
|
}
|