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

@ -216,6 +216,92 @@ public sealed class ShadowObjectRegistry
CollisionType: ShadowCollisionType.BSP, CylHeight: 0f, Scale: 1f);
}
/// <summary>
/// Replaces an existing live PartArray collision payload in its current
/// shadow-cell membership. Retail <c>CPartArray::SetPart</c> changes the
/// part read by later collision tests; it does not recalculate cross-cells.
/// A suspended owner updates only its retained payload. A newly shaped
/// owner may perform its first flood, then immediately suspend when its
/// canonical projection is Hidden, attached, or cell-less.
/// </summary>
public void ReplaceMultiPartPayload(
uint entityId,
Vector3 entityWorldPos,
Quaternion entityWorldRot,
System.Collections.Generic.IReadOnlyList<ShadowShape> shapes,
uint state,
EntityCollisionFlags flags,
float worldOffsetX,
float worldOffsetY,
uint landblockId,
uint seedCellId = 0u,
bool isStatic = false,
bool suspendIfNew = false)
{
if (!_entityReg.TryGetValue(entityId, out RegistrationRecord? prior)
|| !prior.IsMultiPart)
{
if (shapes.Count == 0)
return;
RegisterMultiPart(
entityId,
entityWorldPos,
entityWorldRot,
shapes,
state,
flags,
worldOffsetX,
worldOffsetY,
landblockId,
seedCellId,
isStatic);
if (suspendIfNew)
Suspend(entityId);
return;
}
bool suspended = _suspendedEntities.Contains(entityId);
_entityShapes[entityId] = shapes;
_entityReg[entityId] = prior with
{
EntityWorldPos = entityWorldPos,
EntityWorldRot = entityWorldRot,
State = state,
Flags = flags,
};
if (suspended || !_entityToCells.TryGetValue(entityId, out List<uint>? cells))
return;
foreach (uint cellId in cells)
{
if (_cells.TryGetValue(cellId, out List<ShadowEntry>? entries))
entries.RemoveAll(entry => entry.EntityId == entityId);
}
foreach (ShadowShape shape in shapes)
{
Vector3 partWorldPos = entityWorldPos
+ Vector3.Transform(shape.LocalPosition, entityWorldRot);
Quaternion partWorldRot = entityWorldRot * shape.LocalRotation;
var entry = new ShadowEntry(
EntityId: entityId,
GfxObjId: shape.GfxObjId,
Position: partWorldPos,
Rotation: partWorldRot,
Radius: shape.Radius,
CollisionType: shape.CollisionType,
CylHeight: shape.CylHeight,
Scale: shape.Scale,
State: state,
Flags: flags,
LocalPosition: shape.LocalPosition,
LocalRotation: shape.LocalRotation);
foreach (uint cellId in cells)
AddEntryToCell(entry, cellId);
}
}
/// <summary>
/// Retail flood-sphere rule (CylSphere overload, Ghidra 0x0052b9f0):
/// when the object has cylinder shapes, each contributes one sphere at