refactor(world): extract live projection mechanics

Move appearance rebinding, collision construction, default-pose resolution, local shadow ownership, and exact leave-world presentation into focused owners. Preserve retail parent ordering with staged validation, committed recovery, recursive attached-subtree withdrawal, and retryable exact teardown across parent, pickup, position, unwield, and pose-loss edges.

Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-21 16:17:03 +02:00
parent c87bac31a0
commit 69a2ca0c6d
26 changed files with 4172 additions and 430 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,10 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Physics;
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;
@ -8,6 +13,50 @@ namespace AcDream.App.Tests.Rendering;
public sealed class LiveAppearanceAnimationTests
{
[Fact]
public void Capture_UsesCanonicalRecordEntityAndAnimationOwner()
{
const uint guid = 0x70000002u;
var runtime = new AcDream.App.World.LiveEntityRuntime(
new AcDream.App.Streaming.GpuWorldState(),
new AcDream.App.World.DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
var record = runtime.RegisterLiveEntity(new AcDream.Core.Net.WorldSession.EntitySpawn(
guid,
Position: null,
SetupTableId: null,
AnimPartChanges: Array.Empty<AcDream.Core.Net.Messages.CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<AcDream.Core.Net.Messages.CreateObject.TextureChange>(),
SubPalettes: Array.Empty<AcDream.Core.Net.Messages.CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "appearance fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: 1)).Record!;
WorldEntity entity = Entity(0x70000002u, 0x01000001u);
var state = new LiveEntityAnimationState
{
Entity = entity,
Setup = new Setup(),
Animation = new Animation(),
LowFrame = 0,
HighFrame = 0,
Framerate = 0f,
Scale = 1f,
PartTemplate = Array.Empty<LiveAnimationPartTemplate>(),
PartAvailability = Array.Empty<bool>(),
};
record.WorldEntity = entity;
record.AnimationRuntime = state;
LiveEntityAppearanceUpdateState captured = Assert.IsType<LiveEntityAppearanceUpdateState>(
LiveEntityAppearanceBinding.Capture(runtime, guid));
Assert.Same(entity, captured.Entity);
Assert.Same(state, captured.Animation);
}
[Fact]
public void RebindAppearance_PreservesEntitySequencerAndPlaybackState()
{
@ -36,7 +85,7 @@ public sealed class LiveAppearanceAnimationTests
];
oldEntity.ApplyAppearance(dressedParts, paletteOverride: null, partOverrides: []);
GameWindow.RebindAnimatedEntityForAppearance(
LiveEntityAppearanceBinding.RebindAnimation(
state,
oldEntity,
setup,
@ -57,6 +106,88 @@ public sealed class LiveAppearanceAnimationTests
state.PartTemplate.Select(part => part.GfxObjId));
}
[Fact]
public void CollisionCommit_DelayedOldIncarnationCannotMutateGuidReplacement()
{
const uint guid = 0x70000010u;
const uint cell = 0x01010001u;
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
0x0101FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
var runtime = new LiveEntityRuntime(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
WorldSession.EntitySpawn first = Spawn(guid, cell, instance: 1);
runtime.RegisterLiveEntity(first);
WorldEntity oldEntity = runtime.MaterializeLiveEntity(
guid,
cell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000010u,
Position = Vector3.One,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = cell,
})!;
var registry = new ShadowObjectRegistry();
registry.Register(
oldEntity.Id,
0x01000010u,
oldEntity.Position,
oldEntity.Rotation,
radius: 0.5f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: cell,
seedCellId: cell,
isStatic: false);
var builder = new LiveEntityCollisionBuilder(
_ => false,
_ => null,
new LiveEntityDefaultPoseResolver(
_ => null,
new NullLoader(),
dumpMotion: false));
LiveEntityAppearanceCollisionUpdate update =
Assert.IsType<LiveEntityAppearanceCollisionUpdate>(
LiveEntityAppearanceBinding.PrepareCollision(
runtime,
builder,
oldEntity,
new Setup(),
Array.Empty<uint>(),
first,
Vector3.Zero));
runtime.RegisterLiveEntity(Spawn(guid, cell, instance: 2));
Assert.False(LiveEntityAppearanceBinding.CommitCollision(
runtime,
registry,
update));
Assert.Equal(1, registry.RetainedRegistrationCount);
}
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell, ushort instance) => new(
guid,
new CreateObject.ServerPosition(cell, 1f, 1f, 1f, 1f, 0f, 0f, 0f),
0x02000010u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "appearance collision fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: instance);
private static WorldEntity Entity(uint id, uint gfxObjId) => new()
{
Id = id,

View file

@ -134,7 +134,7 @@ public sealed class LiveEntityAnimationPresenterTests
replacementSetup.DefaultScale.Add(Vector3.One);
fixture.Entity.MeshRefs = [new MeshRef(0x01000009u, Matrix4x4.Identity)];
fixture.Entity.SetIndexedPartPoses([Matrix4x4.Identity], [true]);
GameWindow.RebindAnimatedEntityForAppearance(
LiveEntityAppearanceBinding.RebindAnimation(
fixture.State,
fixture.Entity,
replacementSetup,
@ -169,7 +169,7 @@ public sealed class LiveEntityAnimationPresenterTests
var replacement = new Setup();
replacement.Parts.Add(0x01000099u);
replacement.DefaultScale.Add(Vector3.One);
GameWindow.RebindAnimatedEntityForAppearance(
LiveEntityAppearanceBinding.RebindAnimation(
fixture.State,
fixture.Entity,
replacement,

View file

@ -453,6 +453,78 @@ public sealed class EntitySpawnAdapterLifetimeTests
Assert.Equal(0, meshes.TotalReferenceCount);
}
[Fact]
public void AppearanceChange_AfterPublicationRunsBeforeOldRetirement()
{
const uint guid = 0x7400C100u;
const ulong oldMesh = 0x01000A10u;
const ulong newMesh = 0x01000A11u;
var meshes = new CallbackMeshAdapter();
var adapter = new EntitySpawnAdapter(
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
WorldEntity entity = MakeEntity(152u, guid);
entity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
adapter.OnCreate(entity);
Assert.True(adapter.SetPresentationResident(entity, resident: true));
meshes.Operations.Clear();
MeshRef[] replacement = [new MeshRef((uint)newMesh, Matrix4x4.Identity)];
Assert.True(adapter.OnAppearanceChanged(
entity,
replacement,
[],
() =>
{
meshes.Operations.Add("publish");
entity.ApplyAppearance(replacement, paletteOverride: null, []);
},
() =>
{
Assert.Same(replacement, entity.MeshRefs);
Assert.Equal(1, meshes.ReferenceCounts[oldMesh]);
Assert.Equal(1, meshes.ReferenceCounts[newMesh]);
meshes.Operations.Add("after-publication");
}));
int afterPublication = meshes.Operations.IndexOf("after-publication");
Assert.True(meshes.Operations.IndexOf("publish") < afterPublication);
Assert.True(meshes.Operations.IndexOf($"decrement:{oldMesh:X8}") > afterPublication);
Assert.True(adapter.OnRemove(entity));
Assert.Equal(0, meshes.TotalReferenceCount);
}
[Fact]
public void AppearanceChange_AfterPublicationFailureKeepsCommittedSetAndRetryRetiresOld()
{
const uint guid = 0x7400C200u;
const ulong oldMesh = 0x01000A20u;
const ulong newMesh = 0x01000A21u;
var meshes = new RefCountingMeshAdapter();
var adapter = new EntitySpawnAdapter(
new RecordingTextureLifetime(), _ => MakeSequencer(), meshes);
WorldEntity entity = MakeEntity(153u, guid);
entity.MeshRefs = [new MeshRef((uint)oldMesh, Matrix4x4.Identity)];
adapter.OnCreate(entity);
Assert.True(adapter.SetPresentationResident(entity, resident: true));
MeshRef[] replacement = [new MeshRef((uint)newMesh, Matrix4x4.Identity)];
Assert.Throws<InvalidOperationException>(() => adapter.OnAppearanceChanged(
entity,
replacement,
[],
() => entity.ApplyAppearance(replacement, paletteOverride: null, []),
() => throw new InvalidOperationException("injected dependent publication failure")));
Assert.Same(replacement, entity.MeshRefs);
Assert.Equal(1, meshes.ReferenceCounts[oldMesh]);
Assert.Equal(1, meshes.ReferenceCounts[newMesh]);
Assert.True(adapter.SetPresentationResident(entity, resident: true));
Assert.False(meshes.ReferenceCounts.ContainsKey(oldMesh));
Assert.Equal(1, meshes.ReferenceCounts[newMesh]);
Assert.True(adapter.OnRemove(entity));
Assert.Equal(0, meshes.TotalReferenceCount);
}
[Fact]
public void AppearanceChange_WhileSuspended_OnlyReplacementSetIsAcquiredOnResume()
{