using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; // #145 — far-town teleport resolver runaway. A teleport to a far town places the // player at a landblock EDGE; the first physics tick crosses into a neighbour that // has NOT streamed in yet. Pre-fix, the membership pick derived the block origin from // the terrain registry, which returns (0,0) for an unstreamed neighbour, so // AdjustToOutside marched the cell id one landblock per tick (the cascade) until lbY // hit 0 and the outbound wire synthesized the bogus localY (17410) that ACE rejects. // // The fix (Slice 3): the resolve threads the CARRIED cell-relative frame anchor // (body.Position − body.CellPosition.Frame.Origin) into the pick. That anchor is the // TRUE landblock world origin — correct even for an unstreamed neighbour — so the pick // re-derives the SAME (consistent) cell and never marches. // // These tests exercise CellTransit.FindCellSet directly with an EMPTY cache (so the // neighbour landblock is unstreamed → the legacy path's (0,0) fallback fires) and // compare the carried-anchor pick against the legacy pick. The cascade is direction- // agnostic, so both a south and an east edge crossing are covered. public class TeleportFarTownRunawayTests { private static int LbX(uint cellId) => (int)((cellId >> 24) & 0xFFu); private static int LbY(uint cellId) => (int)((cellId >> 16) & 0xFFu); [Fact] public void SouthEdge_UnstreamedNeighbour_CarriedAnchor_DoesNotMarch() { // Streaming center is the far town 0xC95B; the player just crossed its south // edge (world Y ≈ −0.088) into 0xC95A (origin (0,−192), UNSTREAMED). 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 = 0xC95A0001u; var anchor = new Vector3(0f, -192f, 0f); // body.Position − CellPosition.Origin uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor); Assert.Equal(0x5A, LbY(withAnchor)); // stays in 0xC95A — no march // Legacy path (no anchor → TryGetTerrainOrigin returns (0,0) for the // unstreamed neighbour) marches the cell one landblock south. uint legacy = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null); Assert.True(LbY(legacy) < 0x5A, $"legacy should march south of 0x5A, got 0x{LbY(legacy):X2}"); } [Fact] public void EastEdge_UnstreamedNeighbour_CarriedAnchor_DoesNotMarch() { // Player crossed the center's east edge (world X ≈ 192.088) into 0xCA5B // (origin (+192,0), UNSTREAMED). lbX 0xCA > 0xC9. var cache = new PhysicsDataCache(); var spheres = new[] { new DatReaderWriter.Types.Sphere { Origin = new Vector3(192.088f, 14.8f, 12f), Radius = 0.48f } }; const uint currentCell = 0xCA5B0001u; var anchor = new Vector3(192f, 0f, 0f); uint withAnchor = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, anchor); Assert.Equal(0xCA, LbX(withAnchor)); // stays in 0xCA5B — no march uint legacy = CellTransit.FindCellSet(cache, spheres, 1, currentCell, out _, null); Assert.True(LbX(legacy) > 0xCA, $"legacy should march east of 0xCA, got 0x{LbX(legacy):X2}"); } }