fix(physics #145): Slice 2a — canonicalize outdoor seed + re-derive cell index every tick
The wire local is LANDBLOCK-relative [0,192); the cell low word = floor(local/24), so a consistent (cell,local) pair must keep them in lockstep. Two retail-faithful corrections vs the first pass: - SnapToCell canonicalizes the OUTDOOR seed via AdjustToOutside (retail SetPositionInternal/adjust_to_outside @0x00504A40 — the #107 'never trust a server (cell,pos) pair' protection). Indoor seeds stay verbatim (BSP-validated). - SyncCellPositionDelta calls AdjustToOutside on EVERY delta, not just on 192 m crossings, so intra-landblock 24 m cell-index changes track (needed by Slice 3 membership). Idempotent within a cell. Tests rewritten to verify both (the earlier test paired an inconsistent cell+local). Core 1527 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
afe495b9e6
commit
7928b445ab
2 changed files with 59 additions and 42 deletions
|
|
@ -117,46 +117,46 @@ public sealed class PhysicsBody
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Placement: set the world position AND seed <see cref="CellPosition"/> from the
|
/// Placement: set the world position AND seed <see cref="CellPosition"/> from the
|
||||||
/// wire's (cell, local) directly — NO streaming center, NO delta. Stores the wire
|
/// wire's (cell, local) — NO streaming center, NO delta. For an OUTDOOR cell the
|
||||||
/// values verbatim; the caller has already decoded the wire into a consistent
|
/// seed is canonicalized via <see cref="LandDefs.AdjustToOutside"/> (retail
|
||||||
/// (cellId, cellLocal) pair. Caller (Slice 2b) supplies the wire (cell, localX/Y/Z)
|
/// <c>SetPositionInternal</c>/<c>adjust_to_outside</c> @0x00504A40): the cell index
|
||||||
/// from the inbound position update / teleport arrival.
|
/// is re-derived from the landblock-local position (low word = floor(local/24)) and
|
||||||
|
/// the origin wrapped into [0,192). This is the #107 protection — never trust a
|
||||||
|
/// server (cell, pos) pair without re-deriving the cell. Indoor EnvCell claims
|
||||||
|
/// (low word >= 0x100) are validated by the BSP/spawn-gate path — seeded verbatim.
|
||||||
|
/// Caller (Slice 2b) supplies the wire (cell, localX/Y/Z) from the inbound position
|
||||||
|
/// update / teleport arrival. <paramref name="worldPos"/> stays authoritative for
|
||||||
|
/// the world frame; canonicalizing the (cell, local) decomposition leaves the world
|
||||||
|
/// point unchanged for an in-range local.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SnapToCell(uint cellId, Vector3 worldPos, Vector3 cellLocal)
|
public void SnapToCell(uint cellId, Vector3 worldPos, Vector3 cellLocal)
|
||||||
{
|
{
|
||||||
_position = worldPos;
|
_position = worldPos;
|
||||||
CellPosition = new Position(cellId, new CellFrame(cellLocal, Orientation));
|
uint cell = cellId;
|
||||||
|
Vector3 local = cellLocal;
|
||||||
|
if ((cellId & 0xFFFFu) is >= 1u and <= 0x40u)
|
||||||
|
LandDefs.AdjustToOutside(ref cell, ref local);
|
||||||
|
CellPosition = new Position(cell, new CellFrame(local, Orientation));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mirror a world-position translation into the cell-relative frame. Velocity is
|
// Mirror a world-position translation into the cell-relative frame. Velocity is
|
||||||
// frame-invariant under translation, so the same delta applies to the local origin;
|
// frame-invariant under translation, so the same delta applies to the local origin.
|
||||||
// AdjustToOutside re-wraps into [0,192) and bumps the cell on a landblock crossing.
|
// AdjustToOutside then recomputes the cell index from the local (intra-landblock 24 m
|
||||||
// Only for a SEEDED OUTDOOR cell (player). Unseeded bodies (ObjCellId==0, e.g. remote
|
// cell crossings) AND wraps + bumps the landblock on a 192 m crossing — the outdoor
|
||||||
// entities) and indoor cells are left untouched here.
|
// membership + canonicalization in one call (get_outside_lcoord + lcoord_to_gid +
|
||||||
// We only invoke AdjustToOutside when the local coordinate leaves [0,192) — i.e. on
|
// [0,192) wrap). Idempotent within a cell. Runs only for a SEEDED OUTDOOR cell;
|
||||||
// an actual landblock-boundary crossing. Within the same landblock the cell id is left
|
// unseeded bodies (ObjCellId==0, e.g. remote entities) and indoor cells are skipped.
|
||||||
// unchanged (the wire seed is authoritative for which cell index we occupy).
|
|
||||||
private void SyncCellPositionDelta(Vector3 delta)
|
private void SyncCellPositionDelta(Vector3 delta)
|
||||||
{
|
{
|
||||||
uint cell = CellPosition.ObjCellId;
|
uint cell = CellPosition.ObjCellId;
|
||||||
if ((cell & 0xFFFFu) is not (>= 1u and <= 0x40u))
|
if ((cell & 0xFFFFu) is not (>= 1u and <= 0x40u))
|
||||||
return;
|
return;
|
||||||
Vector3 local = CellPosition.Frame.Origin + delta;
|
Vector3 local = CellPosition.Frame.Origin + delta;
|
||||||
// Only call AdjustToOutside on a landblock-boundary crossing (local X or Y
|
uint adjusted = cell;
|
||||||
// leaves [0, BlockLength)). Within the block the cell index is kept as-is.
|
if (LandDefs.AdjustToOutside(ref adjusted, ref local))
|
||||||
if (local.X < 0f || local.X >= LandDefs.BlockLength ||
|
CellPosition = new Position(adjusted, new CellFrame(local, CellPosition.Frame.Orientation));
|
||||||
local.Y < 0f || local.Y >= LandDefs.BlockLength)
|
// else: map edge (AdjustToOutside failed) — leave CellPosition unchanged.
|
||||||
{
|
// Slice 3 owns proper map-edge membership; this slice is behaviour-neutral.
|
||||||
uint adjusted = cell;
|
|
||||||
if (LandDefs.AdjustToOutside(ref adjusted, ref local))
|
|
||||||
CellPosition = new Position(adjusted, new CellFrame(local, CellPosition.Frame.Orientation));
|
|
||||||
// else: map-edge (AdjustToOutside failed) — leave CellPosition unchanged.
|
|
||||||
// Slice 3 owns proper map-edge membership; this slice is behaviour-neutral.
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CellPosition = new Position(cell, new CellFrame(local, CellPosition.Frame.Orientation));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Orientation quaternion (struct offsets 0x60–0x80 column matrix).</summary>
|
/// <summary>Orientation quaternion (struct offsets 0x60–0x80 column matrix).</summary>
|
||||||
|
|
|
||||||
|
|
@ -4,46 +4,63 @@ using Xunit;
|
||||||
|
|
||||||
namespace AcDream.Core.Tests.Physics;
|
namespace AcDream.Core.Tests.Physics;
|
||||||
|
|
||||||
|
// #145 Slice 2a — PhysicsBody carries a cell-relative CellPosition that rides alongside
|
||||||
|
// the world Vector3 Position. The wire's local is LANDBLOCK-relative [0,192); the cell id
|
||||||
|
// low word (which 24 m cell) = floor(local/24), so a consistent pair has
|
||||||
|
// cell.lowword == cellIndex(local). Placement canonicalizes the outdoor seed (retail
|
||||||
|
// SetPositionInternal/adjust_to_outside); motion mirrors the world delta into the local
|
||||||
|
// and re-derives the cell every tick via AdjustToOutside.
|
||||||
public class PhysicsBodyCellSyncTests
|
public class PhysicsBodyCellSyncTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SnapToCell_SeedsCellPosition_AndWorldPosition()
|
public void SnapToCell_ConsistentOutdoorPair_SeedsAsGiven()
|
||||||
{
|
{
|
||||||
var body = new PhysicsBody();
|
var body = new PhysicsBody();
|
||||||
body.SnapToCell(0xC95B0001u, worldPos: new Vector3(1000f, 2000f, 12f), cellLocal: new Vector3(14.8f, 0.3f, 12f));
|
// cell 0x23 = (lx=4, ly=2); local (100,50) is in that cell (floor(100/24)=4, floor(50/24)=2).
|
||||||
Assert.Equal(0xC95B0001u, body.CellPosition.ObjCellId);
|
body.SnapToCell(0xC95B0023u, worldPos: new Vector3(1000f, 2000f, 12f), cellLocal: new Vector3(100f, 50f, 12f));
|
||||||
Assert.Equal(new Vector3(14.8f, 0.3f, 12f), body.CellPosition.Frame.Origin);
|
Assert.Equal(0xC95B0023u, body.CellPosition.ObjCellId);
|
||||||
|
Assert.Equal(new Vector3(100f, 50f, 12f), body.CellPosition.Frame.Origin);
|
||||||
Assert.Equal(new Vector3(1000f, 2000f, 12f), body.Position);
|
Assert.Equal(new Vector3(1000f, 2000f, 12f), body.Position);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PositionDelta_MirrorsIntoCellPosition_WithinLandblock()
|
public void SnapToCell_InconsistentOutdoorPair_CanonicalizesCellFromLocal()
|
||||||
{
|
{
|
||||||
|
// #107 protection: retail re-derives the cell from the position on placement.
|
||||||
|
// (cell 0x01, local in cell 0x23) is fixed to 0x23.
|
||||||
var body = new PhysicsBody();
|
var body = new PhysicsBody();
|
||||||
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(100f, 50f, 12f));
|
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(100f, 50f, 12f));
|
||||||
body.Position += new Vector3(5f, 3f, 0f); // move within the landblock
|
Assert.Equal(0xC95B0023u, body.CellPosition.ObjCellId);
|
||||||
Assert.Equal(new Vector3(105f, 53f, 12f), body.CellPosition.Frame.Origin);
|
Assert.Equal(new Vector3(100f, 50f, 12f), body.CellPosition.Frame.Origin);
|
||||||
Assert.Equal(0xC95B0001u, body.CellPosition.ObjCellId); // no [0,192) crossing → same cell
|
}
|
||||||
Assert.Equal(new Vector3(1005f, 2003f, 12f), body.Position); // world still authoritative + moved
|
|
||||||
|
[Fact]
|
||||||
|
public void PositionDelta_WithinLandblock_UpdatesCellIndex()
|
||||||
|
{
|
||||||
|
var body = new PhysicsBody();
|
||||||
|
body.SnapToCell(0xC95B0023u, new Vector3(1000f, 2000f, 12f), new Vector3(100f, 50f, 12f));
|
||||||
|
body.Position += new Vector3(0f, 24f, 0f); // local (100,74) → cell (4,3)
|
||||||
|
Assert.Equal(new Vector3(100f, 74f, 12f), body.CellPosition.Frame.Origin);
|
||||||
|
// low word for (lx=4, ly=3): (ly&7) + ((lx&7)<<3) + 1 = 3 + 32 + 1 = 0x24
|
||||||
|
Assert.Equal(0xC95B0024u, body.CellPosition.ObjCellId);
|
||||||
|
Assert.Equal(new Vector3(1000f, 2024f, 12f), body.Position); // world still authoritative + moved
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PositionDelta_AcrossSouthLandblockEdge_BumpsCellAndRewraps()
|
public void PositionDelta_AcrossSouthLandblockEdge_BumpsCellAndRewraps()
|
||||||
{
|
{
|
||||||
var body = new PhysicsBody();
|
var body = new PhysicsBody();
|
||||||
// local Y just above 0 in 0xC95B (lbY=0x5B); move south past 0 → south neighbour 0xC95A, local Y near 192.
|
|
||||||
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(100f, 0.3f, 12f));
|
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(100f, 0.3f, 12f));
|
||||||
body.Position += new Vector3(0f, -1f, 0f);
|
body.Position += new Vector3(0f, -1f, 0f); // local Y → -0.7 → south neighbour, ~191.3
|
||||||
int lbY = (int)((body.CellPosition.ObjCellId >> 16) & 0xFFu);
|
int lbY = (int)((body.CellPosition.ObjCellId >> 16) & 0xFFu);
|
||||||
Assert.Equal(0x5A, lbY); // bumped to south neighbour
|
Assert.Equal(0x5A, lbY); // bumped to south neighbour 0xC95A
|
||||||
Assert.InRange(body.CellPosition.Frame.Origin.Y, 190f, 192f); // re-wrapped near top of southern block
|
Assert.InRange(body.CellPosition.Frame.Origin.Y, 190f, 192f); // re-wrapped near top of southern block
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PositionDelta_WithoutSeed_LeavesCellPositionDefault()
|
public void PositionDelta_WithoutSeed_LeavesCellPositionDefault()
|
||||||
{
|
{
|
||||||
// A body that was never SnapToCell'd (e.g. a remote entity) must NOT get
|
// A body never SnapToCell'd (e.g. a remote entity) gets no synthesized cell.
|
||||||
// a synthesized cell — CellPosition stays default, no AdjustToOutside.
|
|
||||||
var body = new PhysicsBody();
|
var body = new PhysicsBody();
|
||||||
body.Position += new Vector3(10f, 10f, 0f);
|
body.Position += new Vector3(10f, 10f, 0f);
|
||||||
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue