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:
parent
fca0a13217
commit
d5ffb0331b
2 changed files with 63 additions and 5 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue