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>
312 lines
10 KiB
C#
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);
|
|
}
|
|
}
|