The outdoor membership pick derived the landblock origin from the terrain registry, which returns (0,0) for an UNSTREAMED neighbour — so a fresh far-town teleport at a landblock edge marched the cell id one block per physics tick (the cascade; the 17410 ACE rejects is its wire artifact). Fix: thread the CARRIED cell-relative frame anchor (body.Position - body.CellPosition.Frame.Origin) into the pick via SpherePath.CarriedBlockOrigin. 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. - CellTransit.FindCellSet/BuildCellSetAndPickContaining: Vector3? carriedBlockOrigin (null default = legacy TryGetTerrainOrigin → every existing caller/test untouched). - PhysicsEngine.ResolveWithTransition: set the anchor from a SEEDED OUTDOOR body whose carried landblock matches the resolve cell (else null → legacy). - PlayerMovementController.SetPosition: 3-arg overload seeds CellPosition from the wire's (cell, local) via SnapToCell; 2-arg delegates with cellLocal=pos (anchor (0,0,0) == legacy → zero test churn). - GameWindow.CellLocalForSeed: the placement seam (_liveCenter used ONCE here to derive the cell-local; physics carries it forward without _liveCenter). Regression: TeleportFarTownRunawayTests (south + east edge, unstreamed neighbour). Core 1529 / App 480, zero regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
3.4 KiB
C#
63 lines
3.4 KiB
C#
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}");
|
||
}
|
||
}
|