Adversarial review flagged a possible CellPosition staleness on indoor->outdoor transition. Verified against source: false positive (SnapToCell isn't called on building entry; the body is world-space so the delta is frame-invariant; the anchor disengages indoors). Added a test proving CellPosition tracks the outdoor cell under the world position across a multi-cell, cross-landblock walk and stays canonical. Core cell-sync 6/6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
5 KiB
C#
102 lines
5 KiB
C#
using System.Numerics;
|
||
using AcDream.Core.Physics;
|
||
using Xunit;
|
||
|
||
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
|
||
{
|
||
[Fact]
|
||
public void SnapToCell_ConsistentOutdoorPair_SeedsAsGiven()
|
||
{
|
||
var body = new PhysicsBody();
|
||
// cell 0x23 = (lx=4, ly=2); local (100,50) is in that cell (floor(100/24)=4, floor(50/24)=2).
|
||
body.SnapToCell(0xC95B0023u, worldPos: new Vector3(1000f, 2000f, 12f), cellLocal: new Vector3(100f, 50f, 12f));
|
||
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);
|
||
}
|
||
|
||
[Fact]
|
||
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();
|
||
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(100f, 50f, 12f));
|
||
Assert.Equal(0xC95B0023u, body.CellPosition.ObjCellId);
|
||
Assert.Equal(new Vector3(100f, 50f, 12f), body.CellPosition.Frame.Origin);
|
||
}
|
||
|
||
[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]
|
||
public void PositionDelta_AcrossSouthLandblockEdge_BumpsCellAndRewraps()
|
||
{
|
||
var body = new PhysicsBody();
|
||
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(100f, 0.3f, 12f));
|
||
body.Position += new Vector3(0f, -1f, 0f); // local Y → -0.7 → south neighbour, ~191.3
|
||
int lbY = (int)((body.CellPosition.ObjCellId >> 16) & 0xFFu);
|
||
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
|
||
}
|
||
|
||
[Fact]
|
||
public void PositionDelta_WithoutSeed_LeavesCellPositionDefault()
|
||
{
|
||
// A body never SnapToCell'd (e.g. a remote entity) gets no synthesized cell.
|
||
var body = new PhysicsBody();
|
||
body.Position += new Vector3(10f, 10f, 0f);
|
||
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
||
}
|
||
|
||
[Fact]
|
||
public void ContinuousTracking_LongWalkAcrossCellsAndBlock_NeverGoesStale()
|
||
{
|
||
// Refutes the "CellPosition goes stale" concern (incl. the indoor-transit case):
|
||
// because the Position setter re-derives the cell via AdjustToOutside on EVERY
|
||
// write, CellPosition continuously tracks the outdoor cell under the body's WORLD
|
||
// position. The body is always world-space, so the world delta is frame-invariant —
|
||
// there is no separate "interior" frame to desync. On any exit it is the correct
|
||
// cell, never stale.
|
||
var body = new PhysicsBody();
|
||
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(10f, 10f, 12f));
|
||
|
||
// A winding walk: east within the block, then north across the 192 m boundary.
|
||
foreach (var step in new[]
|
||
{
|
||
new Vector3(50f, 0f, 0f),
|
||
new Vector3(50f, 0f, 0f),
|
||
new Vector3(0f, 100f, 0f),
|
||
new Vector3(0f, 100f, 0f), // local Y 110 → 210, crosses north into 0xC95C
|
||
})
|
||
body.Position += step;
|
||
|
||
// local accum (10,10)+(100,200) = (110,210); Y 210 wraps to the north neighbour
|
||
// 0xC95C (lbY 0x5B → 0x5C), local Y = 210−192 = 18; cell (lx=4, ly=0) → low 0x21.
|
||
Assert.Equal(0xC95C0021u, body.CellPosition.ObjCellId);
|
||
Assert.Equal(new Vector3(110f, 18f, 12f), body.CellPosition.Frame.Origin);
|
||
|
||
// And the carried (cell, local) is already canonical — re-deriving does not march.
|
||
uint cell = body.CellPosition.ObjCellId;
|
||
var local = body.CellPosition.Frame.Origin;
|
||
Assert.True(LandDefs.AdjustToOutside(ref cell, ref local));
|
||
Assert.Equal(body.CellPosition.ObjCellId, cell);
|
||
}
|
||
}
|