using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; // Component D — cell-march landblock preservation (#145 residual, 2026-06-22 spec §3 D). // // Scenario: a player arrives via teleport at outdoor cell 0xA9B40019 (lbX=0xA9, lbY=0xB4). // The destination landblock has NOT yet been applied to the physics engine (priority-apply // is in flight). The body's CarriedBlockOrigin is null (either the body has no seeded outdoor // cell yet, or the incoming cell differs from the body's last-known cell). // // Without the fix: BuildCellSetAndPickContaining's TryGetTerrainOrigin returns false with // blockOrigin=(0,0,0). AddAllOutsideCells treats worldSphereCenter as block-local and marches // the cell one block in whatever direction the player is moving — lbX→lbX−1 or lbY→lbY−1 // per tick, cascading until lbX or lbY underflows to 0x00. The outbound wire then encodes // the marched cell and ACE rejects every move (MOVEMENT SPEED / failed transition). // // With the fix: when TryGetTerrainOrigin returns false (block not resident), return the // seed currentCellId verbatim — "no block-local frame → preserve" (same contract as // PhysicsEngine.Resolve's NO-LANDBLOCK verbatim branch). The player cell stays correct // until the landblock applies. // // The entry point exercised here is CellTransit.FindCellSet — the same function that // RunCheckOtherCellsAndAdvance (TransitionTypes.cs) calls on every per-frame physics tick. public class CellMarchLandblockPreservationTests { private static int LbX(uint cellId) => (int)((cellId >> 24) & 0xFFu); private static int LbY(uint cellId) => (int)((cellId >> 16) & 0xFFu); // ── West-edge crossing: no anchor, unstreamed terrain → must NOT march lbX ────────────── [Fact] public void WestEdge_UnstreamedLandblock_NoAnchor_PreservesSeedCell() { // Empty cache — landblock 0xA9B4 has not been applied (TryGetTerrainOrigin → false). var cache = new PhysicsDataCache(); // Sphere just west of 0xA9B4's origin (world X = −0.088 → block-local X = −0.088 // relative to (0,0) origin → floor(−0.088/24) = −1 → lbX marches without fix). var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(-0.088f, 14.8f, 12f), Radius = 0.48f } }; const uint currentCell = 0xA9B40019u; // No carried anchor (the teleport-arrival first tick: body still carries an old // cell or is null-seeded, so ResolveWithTransition passes null). uint result = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null); // The cell must be preserved unchanged — lbX must NOT drop below 0xA9. Assert.Equal(0xA9, LbX(result)); // The full cell id must equal the seed (no march at all). Assert.Equal(currentCell, result); } // ── South-edge crossing: no anchor, unstreamed terrain → must NOT march lbY ───────────── [Fact] public void SouthEdge_UnstreamedLandblock_NoAnchor_PreservesSeedCell() { var cache = new PhysicsDataCache(); var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(14.8f, -0.088f, 12f), Radius = 0.48f } }; const uint currentCell = 0xA9B40001u; // south-west landcell of 0xA9B4 uint result = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null); // Without fix: lbY drops from 0xB4 to 0xB3 on the first tick, then cascades. Assert.Equal(0xB4, LbY(result)); Assert.Equal(currentCell, result); } // ── Carried anchor STILL works: if the anchor is supplied, the correct neighbour ──────── // landblock cell is returned even when terrain is unstreamed (the original #145 fix). [Fact] public void WithCarriedAnchor_UnstreamedNeighbour_ReturnsCorrectCell() { var cache = new PhysicsDataCache(); // Player crossed the south edge into 0xA9B3 (origin (0, −192)). var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(14.8f, -0.088f, 12f), Radius = 0.48f } }; const uint currentCell = 0xA9B30001u; var anchor = new Vector3(0f, -192f, 0f); // body.Position − CellPosition.Origin uint result = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor); // The anchor gives the correct block-local frame → stays in 0xA9B3 (no march back to B4). Assert.Equal(0xB3, LbY(result)); } }