feat(vfx): port retail hidden and teleport presentation

Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
Erik 2026-07-14 14:59:48 +02:00
parent a51ebc66e9
commit 1e98d81448
46 changed files with 4883 additions and 127 deletions

View file

@ -0,0 +1,166 @@
using System.Numerics;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Physics;
using AcDream.Core.World;
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_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);
}
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;
}
}