feat(phys): UpdatePosition handles multi-part entities

Multi-part entities cached via RegisterMultiPart's _entityShapes now
recompose all part transforms on UpdatePosition (called when the server
broadcasts UpdatePosition (0xF748) for a moving entity). Legacy
single-shape path preserved unchanged for tests + entities that never
went through RegisterMultiPart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-24 15:21:35 +02:00
parent fca0a13217
commit d5ffb0331b
2 changed files with 63 additions and 5 deletions

View file

@ -219,8 +219,36 @@ public sealed class ShadowObjectRegistry
public void UpdatePosition(uint entityId, Vector3 worldPos, Quaternion rotation, public void UpdatePosition(uint entityId, Vector3 worldPos, Quaternion rotation,
float worldOffsetX, float worldOffsetY, uint landblockId) float worldOffsetX, float worldOffsetY, uint landblockId)
{ {
// Find the existing entry (any cell holds a copy with the same // A6.P4 door fix (2026-05-24): if the entity was registered via
// entity-scoped state + flags + shape). // RegisterMultiPart, we have its full shape list cached. Use that
// to recompose all part transforms instead of finding one template entry.
if (_entityShapes.TryGetValue(entityId, out var shapes))
{
// Pull the entity-scoped state + flags from the first matching entry
// (they're shared across all parts of a logical entity).
uint state = 0u;
EntityCollisionFlags flags = EntityCollisionFlags.None;
if (_entityToCells.TryGetValue(entityId, out var existingCells)
&& existingCells.Count > 0
&& _cells.TryGetValue(existingCells[0], out var firstList))
{
foreach (var e in firstList)
{
if (e.EntityId == entityId)
{
state = e.State;
flags = e.Flags;
break;
}
}
}
RegisterMultiPart(entityId, worldPos, rotation, shapes,
state, flags, worldOffsetX, worldOffsetY, landblockId);
return;
}
// Single-shape path (legacy compat for tests + entities that never
// went through RegisterMultiPart).
if (!_entityToCells.TryGetValue(entityId, out var oldCells) || oldCells.Count == 0) if (!_entityToCells.TryGetValue(entityId, out var oldCells) || oldCells.Count == 0)
return; return;
@ -240,10 +268,8 @@ public sealed class ShadowObjectRegistry
} }
if (template is not null) break; if (template is not null) break;
} }
if (template is null) if (template is null) return;
return;
// Preserve everything except position + rotation.
var t = template.Value; var t = template.Value;
Register(entityId, t.GfxObjId, worldPos, rotation, t.Radius, Register(entityId, t.GfxObjId, worldPos, rotation, t.Radius,
worldOffsetX, worldOffsetY, landblockId, worldOffsetX, worldOffsetY, landblockId,

View file

@ -161,4 +161,36 @@ public class ShadowObjectRegistryMultiPartTests
Assert.Single(reg.GetObjectsInCell(LbId | 1u), Assert.Single(reg.GetObjectsInCell(LbId | 1u),
e => e.EntityId == 42u); e => e.EntityId == 42u);
} }
[Fact]
public void UpdatePosition_MovesAllPartsWithEntity()
{
var reg = new ShadowObjectRegistry();
const uint movingEntityId = 0xA1u;
var shapes = new[]
{
new ShadowShape(0u, new Vector3(0f, 0f, 0f), Quaternion.Identity, 1f,
ShadowCollisionType.Cylinder, 0.5f, 1f),
new ShadowShape(0u, new Vector3(1f, 0f, 0f), Quaternion.Identity, 1f,
ShadowCollisionType.Cylinder, 0.5f, 1f),
};
reg.RegisterMultiPart(movingEntityId, new Vector3(10f, 10f, 50f),
Quaternion.Identity, shapes, 0u,
EntityCollisionFlags.None, OffX, OffY, LbId);
// Move entity to (50, 10, 50). Parts should be at (50, 10, 50) and (51, 10, 50).
reg.UpdatePosition(movingEntityId,
new Vector3(50f, 10f, 50f), Quaternion.Identity,
OffX, OffY, LbId);
Vector3 expectedPart0 = new(50f, 10f, 50f);
Vector3 expectedPart1 = new(51f, 10f, 50f);
var atNew = reg.AllEntriesForDebug().Where(e => e.EntityId == movingEntityId).ToList();
Assert.Equal(2, atNew.Count);
bool found0 = atNew.Any(e => Vector3.Distance(e.Position, expectedPart0) < 0.01f);
bool found1 = atNew.Any(e => Vector3.Distance(e.Position, expectedPart1) < 0.01f);
Assert.True(found0 && found1,
"Expected both parts at new world positions (50, 10, 50) and (51, 10, 50)");
}
} }