acdream/tests/AcDream.App.Tests/Physics/RemotePhysicsUpdaterTests.cs
Erik c5ab99081c 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>
2026-07-19 21:40:14 +02:00

312 lines
10 KiB
C#

using System.Numerics;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Physics;
public sealed class RemotePhysicsUpdaterTests
{
[Fact]
public void TickHidden_AppliesPositionManagerOffsetWithoutAdvancingPhysics()
{
var motion = new GameWindow.RemoteMotion();
motion.Body.Position = Vector3.Zero;
motion.Body.Orientation = Quaternion.Identity;
motion.Body.Velocity = new Vector3(4f, 0f, 5f);
motion.Body.Acceleration = new Vector3(0f, 0f, -9.8f);
motion.CellId = 0x01010001u;
motion.Interp.Enqueue(
new Vector3(1f, 0f, 0f),
heading: 0f,
isMovingTo: false,
currentBodyPosition: Vector3.Zero);
var entity = new WorldEntity
{
Id = 1u,
ServerGuid = 0x70000001u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var poses = new EntityEffectPoseRegistry();
poses.Publish(entity, Array.Empty<Matrix4x4>());
updater.TickHidden(motion, entity, 0.1f);
Assert.True(poses.UpdateRoot(entity));
Assert.InRange(motion.Body.Position.X, 0.01f, 1f);
Assert.Equal(0f, motion.Body.Position.Z);
Assert.Equal(new Vector3(4f, 0f, 5f), motion.Body.Velocity);
Assert.Equal(motion.Body.Position, entity.Position);
Assert.Equal(motion.CellId, entity.ParentCellId);
Assert.True(poses.TryGetRootPose(entity.Id, out Matrix4x4 root));
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()
{
var engine = BuildBoundaryEngine();
var updater = new RemotePhysicsUpdater(
engine,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var motion = new GameWindow.RemoteMotion();
motion.Body.Position = new Vector3(191f, 10f, 50f);
motion.Body.Orientation = Quaternion.Identity;
motion.Body.TransientState = TransientStateFlags.Contact
| TransientStateFlags.OnWalkable;
uint canonicalCell = 0xA9B40039u;
int canonicalWrites = 0;
motion.BindCanonicalCell(
() => canonicalCell,
value =>
{
canonicalCell = value;
canonicalWrites++;
});
motion.CellId = canonicalCell;
canonicalWrites = 0;
motion.Interp.Enqueue(
new Vector3(193f, 10f, 50f),
heading: 0f,
isMovingTo: false,
currentBodyPosition: motion.Body.Position);
var entity = new WorldEntity
{
Id = 1u,
ServerGuid = 0x70000002u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = motion.Body.Position,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
updater.TickHidden(motion, entity, 2f);
Assert.Equal(0xAAB40001u, canonicalCell);
Assert.Equal(canonicalCell, entity.ParentCellId);
Assert.True(canonicalWrites > 0);
Assert.True(entity.Position.X > 192f);
}
[Fact]
public void TickHidden_LandingSynchronizesRemoteAirborneState()
{
var engine = BuildBoundaryEngine();
var updater = new RemotePhysicsUpdater(
engine,
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var motion = new GameWindow.RemoteMotion
{
Airborne = true,
};
motion.Body.Position = new Vector3(10f, 10f, 50f);
motion.Body.Orientation = Quaternion.Identity;
motion.Body.TransientState = TransientStateFlags.Active;
motion.CellId = 0xA9B40001u;
motion.Interp.Enqueue(
new Vector3(11f, 10f, 50f),
heading: 0f,
isMovingTo: false,
currentBodyPosition: motion.Body.Position);
var entity = new WorldEntity
{
Id = 3u,
ServerGuid = 0x70000003u,
SourceGfxObjOrSetupId = 0x02000001u,
Position = motion.Body.Position,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
};
updater.TickHidden(motion, entity, 0.1f);
Assert.True(motion.Body.InContact);
Assert.True(motion.Body.OnWalkable);
Assert.False(motion.Airborne);
}
[Fact]
public void TickHiddenEntities_DeletionCallbackCannotAdvanceStaleSnapshotOwner()
{
const uint firstGuid = 0x70000011u;
const uint secondGuid = 0x70000012u;
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
0x0101FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
var live = new LiveEntityRuntime(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
BindHiddenRemote(live, firstGuid);
BindHiddenRemote(live, secondGuid);
Assert.Equal(2, live.SpatialRemoteMotionRuntimes.Count);
var updater = new RemotePhysicsUpdater(
new PhysicsEngine(),
(_, _) => (0.48f, 1.835f),
(_, _, _, _) => { });
var published = new List<uint>();
updater.TickHiddenEntities(
live,
localPlayerServerGuid: 0x50000001u,
dt: 0.1f,
entity =>
{
published.Add(entity.ServerGuid);
uint other = entity.ServerGuid == firstGuid ? secondGuid : firstGuid;
Assert.True(live.UnregisterLiveEntity(
new DeleteObject.Parsed(other, InstanceSequence: 1),
isLocalPlayer: false));
});
Assert.Single(published);
Assert.Single(live.SpatialRemoteMotionRuntimes);
}
private static PhysicsEngine BuildBoundaryEngine()
{
static TerrainSurface FlatTerrain()
{
var heights = new byte[81];
var table = new float[256];
Array.Fill(table, 50f);
return new TerrainSurface(heights, table);
}
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
0xA9B4FFFFu,
FlatTerrain(),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
0f,
0f);
engine.AddLandblock(
0xAAB4FFFFu,
FlatTerrain(),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
192f,
0f);
return engine;
}
private static void BindHiddenRemote(LiveEntityRuntime live, uint guid)
{
const uint cellId = 0x01010001u;
PhysicsStateFlags state = PhysicsStateFlags.Hidden
| PhysicsStateFlags.IgnoreCollisions;
var position = new CreateObject.ServerPosition(
cellId, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
var physics = new PhysicsSpawnData(
RawState: (uint)state,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
live.RegisterLiveEntity(new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"hidden fixture",
null,
null,
0x09000001u,
PhysicsState: (uint)state,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics));
WorldEntity entity = live.MaterializeLiveEntity(
guid,
cellId,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 10f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = cellId,
})!;
var remote = new GameWindow.RemoteMotion();
remote.Body.Position = entity.Position;
remote.Body.Orientation = entity.Rotation;
remote.CellId = cellId;
remote.Interp.Enqueue(
entity.Position + Vector3.UnitX,
heading: 0f,
isMovingTo: false,
currentBodyPosition: entity.Position);
live.SetRemoteMotionRuntime(guid, remote);
}
}