acdream/tests/AcDream.Core.Tests/Physics/TeleportFarTownRunawayTests.cs
Erik 6349ba49aa fix(physics #145): Slice 3 — carried-anchor membership; closes the far-town cascade
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>
2026-06-21 13:51:04 +02:00

63 lines
3.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}");
}
}