Treat the equipped-child controller's composed-pose callback as the authoritative attached projection presence edge. Portal transit can withdraw presentation while retaining the accepted parent relation, so the first destination pose must register the child again. Release gate: 3,734 App tests / 3 skips; 8,218 complete-solution tests / 5 skips. Co-Authored-By: OpenAI Codex <codex@openai.com>
447 lines
17 KiB
C#
447 lines
17 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Scene.Arch;
|
|
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.Rendering;
|
|
|
|
public sealed class LiveRenderProjectionJournalTests
|
|
{
|
|
private const uint LandblockId = 0x1234FFFFu;
|
|
private const uint CellId = 0x12340100u;
|
|
private const uint Guid = 0x80000001u;
|
|
|
|
[Fact]
|
|
public void EntityReady_RegistersExactRootOnceAfterResourcesCommit()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 7);
|
|
LiveEntityReadyCandidate candidate =
|
|
LiveEntityReadyCandidate.Capture(record);
|
|
|
|
Assert.True(harness.Projections.OnEntityReady(candidate));
|
|
Assert.True(harness.Projections.OnEntityReady(candidate));
|
|
|
|
Assert.Equal(1, harness.Journal.Count);
|
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
|
RenderProjectionDelta registered = harness.Journal.Pending[0];
|
|
Assert.Equal(RenderProjectionDeltaKind.Register, registered.Kind);
|
|
Assert.Equal(RenderProjectionClass.LiveDynamicRoot,
|
|
registered.Record.ProjectionClass);
|
|
Assert.Equal(record.LocalEntityId, registered.Record.Source.LocalEntityId);
|
|
Assert.Equal(Guid, registered.Record.Source.ServerGuid);
|
|
Assert.Equal(LandblockId, registered.Record.Residency.OwnerLandblockId);
|
|
Assert.True((registered.Record.Flags & RenderProjectionFlags.Draw) != 0);
|
|
}
|
|
|
|
[Fact]
|
|
public void PendingToLoadedAndLoadedToLoaded_RebucketWithoutLogicalRecreate()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: false);
|
|
BindVisibility(harness);
|
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 1);
|
|
Assert.False(record.IsSpatiallyVisible);
|
|
harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record));
|
|
RenderOwnerIncarnation incarnation =
|
|
harness.Journal.Pending[0].Record.OwnerIncarnation;
|
|
|
|
Load(harness.State, LandblockId);
|
|
|
|
Assert.True(record.IsSpatiallyVisible);
|
|
Assert.Equal(2, harness.Journal.Count);
|
|
Assert.Equal(
|
|
RenderProjectionDeltaKind.UpdateFlags,
|
|
harness.Journal.Pending[1].Kind);
|
|
|
|
const uint secondLandblock = 0x1235FFFFu;
|
|
const uint secondCell = 0x12350100u;
|
|
Load(harness.State, secondLandblock);
|
|
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, secondCell));
|
|
harness.Projections.SynchronizeActiveSources();
|
|
|
|
Assert.Equal(3, harness.Journal.Count);
|
|
Assert.Equal(
|
|
RenderProjectionDeltaKind.Rebucket,
|
|
harness.Journal.Pending[2].Kind);
|
|
Assert.Equal(
|
|
incarnation,
|
|
harness.Journal.Pending[2].Record.OwnerIncarnation);
|
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void SynchronizeActiveSources_MirrorsFinalTransformAppearanceAndHiddenFlags()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 2);
|
|
harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record));
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
WorldEntity entity = record.WorldEntity!;
|
|
|
|
entity.SetPosition(new Vector3(20, 30, 40));
|
|
entity.Rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
|
|
entity.ApplyAppearance(
|
|
[new MeshRef(0x01010077u, Matrix4x4.Identity)],
|
|
new PaletteOverride(0x04000001u, []),
|
|
[new PartOverride(0, 0x01010077u)]);
|
|
entity.IsDrawVisible = false;
|
|
|
|
harness.Projections.SynchronizeActiveSources();
|
|
|
|
Assert.Equal(
|
|
[
|
|
RenderProjectionDeltaKind.UpdateTransform,
|
|
RenderProjectionDeltaKind.UpdateAppearance,
|
|
RenderProjectionDeltaKind.UpdateFlags,
|
|
],
|
|
harness.Journal.Pending.ToArray()
|
|
.Select(delta => delta.Kind)
|
|
.ToArray());
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
Assert.True(harness.Scene.OpenQuery().TryGet(
|
|
LiveRenderProjectionJournal.ProjectionId(entity.Id),
|
|
out var projected));
|
|
Assert.Equal(entity.Position, projected.Transform.Position);
|
|
Assert.True((projected.Flags & RenderProjectionFlags.Hidden) != 0);
|
|
Assert.True((projected.Flags & RenderProjectionFlags.Draw) == 0);
|
|
Assert.Equal(1, projected.MeshSet.MeshCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void EquippedPoseAndRemoval_UseSameIncarnationWithoutGuidOwnershipMap()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(
|
|
harness,
|
|
Guid,
|
|
instance: 3,
|
|
projectionKind: LiveEntityProjectionKind.Attached);
|
|
harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record));
|
|
RenderOwnerIncarnation incarnation =
|
|
harness.Journal.Pending[0].Record.OwnerIncarnation;
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
|
|
record.WorldEntity!.SetPosition(new Vector3(9, 8, 7));
|
|
harness.Projections.OnProjectionPoseReady(Guid);
|
|
|
|
Assert.Single(harness.Journal.Pending.ToArray());
|
|
Assert.Equal(
|
|
RenderProjectionDeltaKind.UpdateTransform,
|
|
harness.Journal.Pending[0].Kind);
|
|
Assert.Equal(
|
|
incarnation,
|
|
harness.Journal.Pending[0].Record.OwnerIncarnation);
|
|
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
|
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
|
Assert.Equal(2, harness.Journal.Count);
|
|
Assert.Equal(
|
|
RenderProjectionDeltaKind.Unregister,
|
|
harness.Journal.Pending[1].Kind);
|
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerationReplacement_UnregistersOldAndRejectsStaleReadyCandidate()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord first = Materialize(harness, Guid, instance: 4);
|
|
LiveEntityReadyCandidate stale = LiveEntityReadyCandidate.Capture(first);
|
|
harness.Projections.OnEntityReady(stale);
|
|
uint firstLocalId = first.WorldEntity!.Id;
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
|
|
LiveEntityRegistrationResult replacement =
|
|
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
|
|
LiveEntityRecord second = replacement.Record!;
|
|
WorldEntity secondEntity = harness.Runtime.MaterializeLiveEntity(
|
|
Guid,
|
|
CellId,
|
|
localId => Entity(localId, Guid))!;
|
|
second.InitialHydrationCompleted = true;
|
|
LiveEntityReadyCandidate current = LiveEntityReadyCandidate.Capture(second);
|
|
Assert.True(harness.Projections.OnEntityReady(current));
|
|
Assert.False(harness.Projections.OnEntityReady(stale));
|
|
|
|
Assert.Equal(2, harness.Journal.Count);
|
|
Assert.Equal(
|
|
[
|
|
RenderProjectionDeltaKind.Unregister,
|
|
RenderProjectionDeltaKind.Register,
|
|
],
|
|
harness.Journal.Pending.ToArray()
|
|
.Select(delta => delta.Kind)
|
|
.ToArray());
|
|
Assert.NotEqual(firstLocalId, secondEntity.Id);
|
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
Assert.False(harness.Scene.OpenQuery().TryGet(
|
|
LiveRenderProjectionJournal.ProjectionId(firstLocalId),
|
|
out _));
|
|
Assert.True(harness.Scene.OpenQuery().TryGet(
|
|
LiveRenderProjectionJournal.ProjectionId(secondEntity.Id),
|
|
out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void DuplicateCreateAndReady_DoNotRegisterProjectionTwice()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 5);
|
|
harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record));
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
|
|
LiveEntityRegistrationResult duplicate =
|
|
harness.Runtime.RegisterLiveEntity(Spawn(Guid, instance: 5));
|
|
Assert.Same(record, duplicate.Record);
|
|
Assert.True(harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record)));
|
|
|
|
Assert.Equal(0, harness.Journal.Count);
|
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
|
Assert.Equal(1, harness.Scene.Counts.Total);
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionClear_UnregistersEveryTrackedProjectionExactlyOnce()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 6);
|
|
harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record));
|
|
|
|
harness.Runtime.Clear();
|
|
|
|
Assert.Equal(2, harness.Journal.Count);
|
|
Assert.Equal(
|
|
RenderProjectionDeltaKind.Unregister,
|
|
harness.Journal.Pending[1].Kind);
|
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
|
Assert.Equal(0, harness.Runtime.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResetTracking_DropsDerivedSourcesWithoutTouchingGameplayRuntime()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 7);
|
|
harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record));
|
|
|
|
harness.Projections.ResetTracking();
|
|
|
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
|
Assert.Equal(1, harness.Runtime.Count);
|
|
Assert.Same(record, harness.Runtime.Records.Single());
|
|
}
|
|
|
|
[Fact]
|
|
public void SynchronizeActiveSources_RecoversCurrentRootFromCanonicalWorkset()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(harness, Guid, instance: 8);
|
|
Assert.True(harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record)));
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
harness.Projections.ResetTracking();
|
|
|
|
harness.Projections.SynchronizeActiveSources();
|
|
|
|
RenderProjectionDelta recovered =
|
|
Assert.Single(harness.Journal.Pending.ToArray());
|
|
Assert.Equal(RenderProjectionDeltaKind.Register, recovered.Kind);
|
|
Assert.Equal(
|
|
RenderProjectionClass.LiveDynamicRoot,
|
|
recovered.Record.ProjectionClass);
|
|
Assert.Equal(record.LocalEntityId, recovered.Record.Source.LocalEntityId);
|
|
}
|
|
|
|
[Fact]
|
|
public void SynchronizeActiveSources_DoesNotResurrectRemovedAttachment()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(
|
|
harness,
|
|
Guid,
|
|
instance: 9,
|
|
projectionKind: LiveEntityProjectionKind.Attached);
|
|
Assert.True(harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record)));
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
|
|
harness.Projections.SynchronizeActiveSources();
|
|
|
|
Assert.Equal(0, harness.Journal.Count);
|
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProjectionPoseReady_RecoversCurrentAttachmentAfterPresentationRemoval()
|
|
{
|
|
Harness harness = CreateHarness(loadLandblock: true);
|
|
LiveEntityRecord record = Materialize(
|
|
harness,
|
|
Guid,
|
|
instance: 10,
|
|
projectionKind: LiveEntityProjectionKind.Attached);
|
|
Assert.True(harness.Projections.OnEntityReady(
|
|
LiveEntityReadyCandidate.Capture(record)));
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
|
harness.Journal.DrainTo(harness.Scene);
|
|
|
|
harness.Projections.OnProjectionPoseReady(record.ServerGuid);
|
|
|
|
RenderProjectionDelta recovered =
|
|
Assert.Single(harness.Journal.Pending.ToArray());
|
|
Assert.Equal(RenderProjectionDeltaKind.Register, recovered.Kind);
|
|
Assert.Equal(
|
|
RenderProjectionClass.EquippedChild,
|
|
recovered.Record.ProjectionClass);
|
|
Assert.Equal(record.LocalEntityId, recovered.Record.Source.LocalEntityId);
|
|
}
|
|
|
|
private static Harness CreateHarness(bool loadLandblock)
|
|
{
|
|
var state = new GpuWorldState();
|
|
if (loadLandblock)
|
|
Load(state, LandblockId);
|
|
|
|
ILiveRenderProjectionSink? sink = null;
|
|
var resources = new DelegateLiveEntityResourceLifecycle(
|
|
static _ => { },
|
|
entity => sink?.OnResourceUnregister(entity));
|
|
var runtime = new LiveEntityRuntime(state, resources);
|
|
var journal = new RenderProjectionJournal(
|
|
RenderSceneGeneration.FromRaw(1));
|
|
var projections = new LiveRenderProjectionJournal(runtime, journal);
|
|
sink = projections;
|
|
var scene = new ArchRenderScene(RenderSceneGeneration.FromRaw(1));
|
|
return new Harness(state, runtime, journal, projections, scene);
|
|
}
|
|
|
|
private static void BindVisibility(Harness harness) =>
|
|
harness.Runtime.ProjectionVisibilityChanged +=
|
|
harness.Projections.OnProjectionVisibilityChanged;
|
|
|
|
private static LiveEntityRecord Materialize(
|
|
Harness harness,
|
|
uint guid,
|
|
ushort instance,
|
|
LiveEntityProjectionKind projectionKind =
|
|
LiveEntityProjectionKind.World)
|
|
{
|
|
LiveEntityRecord record =
|
|
harness.Runtime.RegisterLiveEntity(Spawn(guid, instance)).Record!;
|
|
WorldEntity? entity = harness.Runtime.MaterializeLiveEntity(
|
|
guid,
|
|
CellId,
|
|
localId => Entity(localId, guid),
|
|
projectionKind);
|
|
Assert.NotNull(entity);
|
|
record.InitialHydrationCompleted = true;
|
|
return record;
|
|
}
|
|
|
|
private static void Load(GpuWorldState state, uint landblockId) =>
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>(),
|
|
PhysicsDatBundle.Empty));
|
|
|
|
private static WorldEntity Entity(uint localId, uint guid) => new()
|
|
{
|
|
Id = localId,
|
|
ServerGuid = guid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = new Vector3(1, 2, 3),
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs =
|
|
[
|
|
new MeshRef(0x01010001u, Matrix4x4.Identity),
|
|
],
|
|
ParentCellId = CellId,
|
|
};
|
|
|
|
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
|
|
{
|
|
var position = new CreateObject.ServerPosition(
|
|
CellId,
|
|
10f,
|
|
10f,
|
|
5f,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f);
|
|
var timestamps = new PhysicsTimestamps(
|
|
Position: 1,
|
|
Movement: 1,
|
|
State: 1,
|
|
Vector: 1,
|
|
Teleport: 0,
|
|
ServerControlledMove: 1,
|
|
ForcePosition: 0,
|
|
ObjDesc: 1,
|
|
Instance: instance);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
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,
|
|
0x09000001u,
|
|
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
InstanceSequence: instance,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: 1,
|
|
Physics: physics);
|
|
}
|
|
|
|
private sealed record Harness(
|
|
GpuWorldState State,
|
|
LiveEntityRuntime Runtime,
|
|
RenderProjectionJournal Journal,
|
|
LiveRenderProjectionJournal Projections,
|
|
ArchRenderScene Scene);
|
|
}
|