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

@ -160,6 +160,110 @@ public class ShadowObjectRegistryTests
Assert.Equal(0x44u, restored.State);
}
[Fact]
public void ReplaceMultiPartPayload_VisibleOwnerKeepsExistingCellMembership()
{
const uint entityId = 25u;
var reg = new ShadowObjectRegistry();
Vector3 position = new(12f, 12f, 50f);
reg.RegisterMultiPart(
entityId,
position,
Quaternion.Identity,
[BspShape(0x01000011u, radius: 0.5f)],
state: 0u,
flags: EntityCollisionFlags.None,
OffX,
OffY,
LbId,
seedCellId: LbId | 1u,
isStatic: false);
reg.ReplaceMultiPartPayload(
entityId,
position,
Quaternion.Identity,
[BspShape(0x01000012u, radius: 40f)],
state: 0u,
flags: EntityCollisionFlags.None,
OffX,
OffY,
landblockId: 0u,
seedCellId: 0u,
isStatic: false);
ShadowEntry replaced = Assert.Single(reg.GetObjectsInCell(LbId | 1u));
Assert.Equal(0x01000012u, replaced.GfxObjId);
Assert.Empty(reg.GetObjectsInCell(LbId | 9u));
}
[Fact]
public void ReplaceMultiPartPayload_SuspendedShapedEmptyShaped_RemainsSuspended()
{
const uint entityId = 26u;
var reg = new ShadowObjectRegistry();
Vector3 position = new(12f, 12f, 50f);
reg.RegisterMultiPart(
entityId,
position,
Quaternion.Identity,
[BspShape(0x01000021u, radius: 0.5f)],
state: 0u,
flags: EntityCollisionFlags.None,
OffX,
OffY,
LbId,
seedCellId: LbId | 1u,
isStatic: false);
Assert.True(reg.Suspend(entityId));
reg.ReplaceMultiPartPayload(
entityId, position, Quaternion.Identity, [], 0u,
EntityCollisionFlags.None, OffX, OffY, LbId, LbId | 1u);
reg.ReplaceMultiPartPayload(
entityId,
position,
Quaternion.Identity,
[BspShape(0x01000022u, radius: 0.5f)],
0u,
EntityCollisionFlags.None,
OffX,
OffY,
LbId,
LbId | 1u);
Assert.Equal(1, reg.RetainedRegistrationCount);
Assert.Equal(1, reg.SuspendedRegistrationCount);
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
reg.UpdatePosition(
entityId, position, Quaternion.Identity, OffX, OffY, LbId, LbId | 1u);
Assert.Equal(0x01000022u, Assert.Single(reg.GetObjectsInCell(LbId | 1u)).GfxObjId);
}
[Fact]
public void ReplaceMultiPartPayload_NewCelllessOwnerRegistersRetainedButSuspended()
{
const uint entityId = 27u;
var reg = new ShadowObjectRegistry();
reg.ReplaceMultiPartPayload(
entityId,
new Vector3(12f, 12f, 50f),
Quaternion.Identity,
[BspShape(0x01000031u, radius: 0.5f)],
0u,
EntityCollisionFlags.None,
OffX,
OffY,
LbId,
seedCellId: LbId | 1u,
isStatic: false,
suspendIfNew: true);
Assert.Equal(1, reg.RetainedRegistrationCount);
Assert.Equal(1, reg.SuspendedRegistrationCount);
Assert.Empty(reg.GetObjectsInCell(LbId | 1u));
}
// -----------------------------------------------------------------------
// RemoveLandblock
// -----------------------------------------------------------------------
@ -652,6 +756,15 @@ public class ShadowObjectRegistryTests
};
}
private static ShadowShape BspShape(uint gfxObjId, float radius) => new(
gfxObjId,
Vector3.Zero,
Quaternion.Identity,
Scale: 1f,
CollisionType: ShadowCollisionType.BSP,
Radius: radius,
CylHeight: 0f);
private static CellPhysics BuildShadowCellSetTests_MakeLeafCell(Matrix4x4 worldTransform)
{
Matrix4x4.Invert(worldTransform, out var inv);