fix(motion): restore retail object manager order

Process animation completion at the retail process_hooks boundary, then run targeting, movement, PartArray completion, and PositionManager in the named UpdateObjectInternal order for local, remote, hidden, and position-less animated objects. Retire TS-42 with deterministic conformance coverage.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-19 21:40:14 +02:00
parent 0f996db747
commit c5ab99081c
17 changed files with 493 additions and 98 deletions

View file

@ -127,6 +127,29 @@ public sealed class RetailLocalPlayerFrameControllerTests
Assert.Equal(1f / 60f, timeAfterOneTick);
}
[Fact]
public void TargetManager_ObservesPostTransitionPlayerPosition()
{
PlayerMovementController controller = CreateController();
Vector3 initial = controller.Position;
Vector3 positionSeenByTargetManager = new(float.NaN);
var local = new RetailLocalPlayerFrameController(
canPresentPlayer: () => true,
getController: () => controller,
captureInput: () => new MovementInput(Forward: true),
resolveLocalEntityId: () => 7u,
handleTargeting: () => positionSeenByTargetManager = controller.Position,
isHidden: () => false,
project: (_, _, _) => { },
sendPreNetwork: (_, _, _) => { },
sendPostNetwork: (_, _) => { });
local.AdvanceBeforeNetwork(PhysicsBody.MaxQuantum);
Assert.NotEqual(initial, controller.Position);
Assert.Equal(controller.Position, positionSeenByTargetManager);
}
private static PlayerMovementController CreateController()
{
var engine = new PhysicsEngine();

View file

@ -56,6 +56,35 @@ public sealed class RemotePhysicsUpdaterTests
Assert.Equal(entity.Position, root.Translation);
}
[Fact]
public void TickHidden_RunsPartArrayManagerTailWithoutAdvancingSequence()
{
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var motion = new GameWindow.RemoteMotion();
motion.Body.Orientation = Quaternion.Identity;
var entity = new WorldEntity
{
Id = 2u,
ServerGuid = 0x70000002u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
int partArrayTailCalls = 0;
updater.TickHidden(
motion,
entity,
0.1f,
() => partArrayTailCalls++);
Assert.Equal(1, partArrayTailCalls);
}
[Fact]
public void TickHidden_CrossCellCommitUpdatesCanonicalCellBeforeUnhide()
{

View file

@ -120,7 +120,7 @@ public sealed class EntityEffectPoseRegistryTests
}
[Fact]
public void AnimationHookQueue_CompletesMotionStateEvenWhenPoseWasRemoved()
public void AnimationHookQueue_CompletesMotionStateAtCaptureEvenWhenPoseWasRemoved()
{
var poses = new EntityEffectPoseRegistry();
var queue = new AnimationHookFrameQueue(new AnimationHookRouter(), poses);
@ -134,12 +134,31 @@ public sealed class EntityEffectPoseRegistryTests
11u,
sequencer,
new AnimationHook[] { new AnimationDoneHook() });
Assert.Empty(sequencer.Manager.PendingAnimations);
queue.Drain();
Assert.Empty(sequencer.Manager.PendingAnimations);
Assert.Equal(0, queue.Count);
}
[Fact]
public void AnimationHookQueue_DeferredDrainDoesNotOwnPartArrayUseTime()
{
var poses = new EntityEffectPoseRegistry();
var queue = new AnimationHookFrameQueue(new AnimationHookRouter(), poses);
var sequencer = new AnimationSequencer(
new Setup(),
new MotionTable(),
new NullAnimationLoader());
sequencer.Manager.AddToQueue(0x10000001u, ticks: 0u);
queue.Capture(12u, sequencer, Array.Empty<AnimationHook>());
queue.Drain();
Assert.Single(sequencer.Manager.PendingAnimations);
}
[Fact]
public void Publish_UsesOutdoorEffectCellWithoutChangingRenderParent()
{

View file

@ -0,0 +1,38 @@
using AcDream.Core.Physics;
namespace AcDream.Core.Tests.Physics;
public sealed class RetailObjectManagerTailTests
{
[Fact]
public void Run_UsesNamedRetailManagerOrder()
{
var calls = new List<string>();
RetailObjectManagerTail.Run(
checkDetection: () => calls.Add("detection"),
handleTargeting: () => calls.Add("target"),
movementUseTime: () => calls.Add("movement"),
partArrayHandleMovement: () => calls.Add("part-array"),
positionUseTime: () => calls.Add("position"));
Assert.Equal(
["detection", "target", "movement", "part-array", "position"],
calls);
}
[Fact]
public void Run_SkipsMissingManagersWithoutDuplicatingNeighbours()
{
var calls = new List<string>();
RetailObjectManagerTail.Run(
checkDetection: null,
handleTargeting: () => calls.Add("target"),
movementUseTime: null,
partArrayHandleMovement: () => calls.Add("part-array"),
positionUseTime: null);
Assert.Equal(["target", "part-array"], calls);
}
}