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

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,