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,273 @@
using System.Numerics;
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.World;
public sealed class LiveEntityPresentationControllerTests
{
[Fact]
public void InitialVisibleObject_DoesNotPlayUnHide()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.Empty(fixture.TypedPlays);
Assert.True(fixture.Entity.IsDrawVisible);
Assert.Equal(1, fixture.Shadows.TotalRegistered);
}
[Fact]
public void HiddenThenVisible_PlaysTypedEffectsMutatesChildrenAndRestoresShadow()
{
Fixture fixture = new(
PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.Equal(
[(fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f)],
fixture.TypedPlays);
Assert.Equal([(Fixture.Guid, true)], fixture.ChildNoDraw);
Assert.Equal([Fixture.Guid], fixture.InvalidTargets);
Assert.Equal(0, fixture.Shadows.TotalRegistered);
Assert.False(fixture.Entity.IsDrawVisible);
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(Fixture.Guid, 0u, 1, 2),
out _,
out _));
Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid));
Assert.Equal(
[
(fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f),
(fixture.Entity.Id, LiveEntityPresentationController.UnHideScriptType, 1f),
],
fixture.TypedPlays);
Assert.Equal([(Fixture.Guid, true), (Fixture.Guid, false)], fixture.ChildNoDraw);
Assert.Equal(1, fixture.Shadows.TotalRegistered);
Assert.True(fixture.Entity.IsDrawVisible);
Assert.Same(fixture.Entity, Assert.Single(fixture.Runtime.WorldEntities).Value);
}
[Fact]
public void IdenticalOrStaleState_DoesNotReplayHiddenEffect()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
fixture.Controller.OnLiveEntityReady(Fixture.Guid);
var hidden = new SetState.Parsed(
Fixture.Guid,
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
1,
2);
Assert.True(fixture.Runtime.TryApplyState(hidden, out _, out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.False(fixture.Runtime.TryApplyState(hidden, out _, out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.Single(fixture.TypedPlays);
Assert.Equal(LiveEntityPresentationController.HiddenScriptType,
fixture.TypedPlays[0].Type);
}
[Fact]
public void UnHideWhileLandblockPending_RestoresShadowWhenProjectionReturns()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
fixture.Controller.OnLiveEntityReady(Fixture.Guid);
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(
Fixture.Guid,
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
1,
2),
out _,
out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.Equal(0, fixture.Shadows.TotalRegistered);
Assert.True(fixture.Runtime.RebucketLiveEntity(Fixture.Guid, 0x02020001u));
Assert.False(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid));
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(Fixture.Guid, 0u, 1, 3),
out _,
out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.Equal(0, fixture.Shadows.TotalRegistered);
fixture.Spatial.AddLandblock(new LoadedLandblock(
0x0202FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Assert.True(fixture.Runtime.WorldEntities.ContainsKey(Fixture.Guid));
Assert.Equal(1, fixture.Shadows.TotalRegistered);
}
[Fact]
public void ActivePlacement_IsGenerationScopedAndClearsOnTeardownAndReset()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.True(fixture.Controller.BeginAuthoritativePlacement(Fixture.Guid, 1));
Assert.True(fixture.Controller.HasActivePlacement(Fixture.Guid));
Assert.True(fixture.Runtime.TryGetRecord(Fixture.Guid, out LiveEntityRecord oldRecord));
fixture.Controller.Forget(oldRecord);
Assert.True(fixture.Runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(Fixture.Guid, 1),
isLocalPlayer: false));
Assert.False(fixture.Controller.HasActivePlacement(Fixture.Guid));
fixture.Runtime.RegisterLiveEntity(Fixture.Spawn(
PhysicsStateFlags.ReportCollisions,
instanceSequence: 2));
fixture.Runtime.MaterializeLiveEntity(
Fixture.Guid,
0x01010001u,
id => new WorldEntity
{
Id = id,
ServerGuid = Fixture.Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 10f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
});
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.False(fixture.Controller.CompleteAuthoritativePlacement(
Fixture.Guid,
generation: 1,
deferShadowRestore: true));
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
Assert.True(fixture.Controller.BeginAuthoritativePlacement(Fixture.Guid, 2));
Assert.True(fixture.Controller.HasActivePlacement(Fixture.Guid));
fixture.Controller.Clear();
Assert.False(fixture.Controller.HasActivePlacement(Fixture.Guid));
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
}
private sealed class Fixture
{
public const uint Guid = 0x70000051u;
public List<(uint Owner, uint Type, float Intensity)> TypedPlays { get; } = [];
public List<(uint Parent, bool NoDraw)> ChildNoDraw { get; } = [];
public List<uint> InvalidTargets { get; } = [];
public GpuWorldState Spatial { get; }
public LiveEntityRuntime Runtime { get; }
public ShadowObjectRegistry Shadows { get; } = new();
public WorldEntity Entity { get; }
public LiveEntityPresentationController Controller { get; }
public Fixture(PhysicsStateFlags initialState)
{
Spatial = new GpuWorldState();
Spatial.AddLandblock(new LoadedLandblock(
0x0101FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Runtime = new LiveEntityRuntime(
Spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
WorldSession.EntitySpawn spawn = Spawn(initialState);
Runtime.RegisterLiveEntity(spawn);
Entity = Runtime.MaterializeLiveEntity(
Guid,
0x01010001u,
id => new WorldEntity
{
Id = id,
ServerGuid = Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 10f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
Shadows.Register(
Entity.Id,
0x01000001u,
Entity.Position,
Entity.Rotation,
1f,
0f,
0f,
0x01010000u,
state: (uint)initialState,
seedCellId: 0x01010001u);
Controller = new LiveEntityPresentationController(
Runtime,
Shadows,
(owner, type, intensity) =>
{
TypedPlays.Add((owner, type, intensity));
return true;
},
(parent, noDraw) => ChildNoDraw.Add((parent, noDraw)),
InvalidTargets.Add,
() => (1, 1));
}
internal static WorldSession.EntitySpawn Spawn(
PhysicsStateFlags state,
ushort instanceSequence = 1)
{
var position = new CreateObject.ServerPosition(
0x01010001u, 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);
return new WorldSession.EntitySpawn(
Guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"fixture",
null,
null,
MotionTableId: 0x09000001u,
PhysicsState: (uint)state,
InstanceSequence: instanceSequence,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}
}