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);

View file

@ -103,6 +103,26 @@ public class ShadowShapeBuilderTests
Assert.Equal(1, bspCount);
}
[Fact]
public void FromSetup_EffectivePartIdentitiesControlPhysicsBspSelection()
{
const uint replacementWithBsp = 0x0100AA01u;
const uint replacementWithoutBsp = 0x0100AA02u;
var setup = new Setup
{
Parts = { 0x010044B5u, 0x010044B6u },
};
var shapes = ShadowShapeBuilder.FromSetup(
setup,
entScale: 1f,
hasPhysicsBsp: id => id == replacementWithBsp,
effectivePartGfxObjIds: [replacementWithBsp, replacementWithoutBsp]);
ShadowShape shape = Assert.Single(shapes);
Assert.Equal(replacementWithBsp, shape.GfxObjId);
}
[Fact]
public void FromSetup_CreatureWithCylSpheres_OnlyEmitsCylinders()
{