using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Enums; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// #133 (Bug A) — a validated dungeon claim's landblock prefix is /// AUTHORITATIVE; a candidate world position that also falls inside a /// neighbouring RESIDENT landblock's [0,192) local bounds must NOT /// re-stamp the claim with that neighbour's prefix. /// /// /// C5a (2026-08-05) re-point: the legacy PhysicsEngine.Resolve this /// test originally drove is deleted (zero production callers — every /// production placement writer reaches canonical /// PhysicsEngine.SetPosition through RuntimeSetPositionState). /// The defect this test pins lived entirely inside Resolve's own /// lbPrefix resident-landblock scan (find a resident block whose /// [0,192) bounds contain the candidate XY, then stamp /// lbPrefix | (cellId & 0xFFFF) — the scan has no notion that /// the claim's OWN landblock can legitimately fail its own bounds test, /// which is exactly what a dungeon EnvCell's negative local Y does). The /// canonical SetPosition/SetPositionInternal pipeline has no /// equivalent lbPrefix scan: a validated claim (AdjustSeedCell /// confirms the seed cell contains the point) is committed with ITS OWN /// cell id, full stop — the defect class cannot recur there by /// construction. This test now proves that positively: drive the SAME /// geometry as the original #133 capture (dungeon claim /// 0x00070143 at dungeon-local (70,-60,0.01), with a /// still-resident Holtburg neighbour block whose [0,192) bounds /// contain the same world XY) through canonical /// PhysicsEngine.SetPosition and assert the committed cell keeps /// the 0x0007 prefix. /// /// /// /// lbPrefix is found by scanning resident landblocks for one whose /// [0,192) local bounds contain the candidate XY. A dungeon EnvCell's /// local Y can be NEGATIVE relative to its own landblock (the live capture: /// server teleport to dungeon cell 0x00070143 at local (70,-60,0.01)). /// The dungeon landblock fails the localY >= 0 bounds test, so the /// legacy scan instead matched a still-resident NEIGHBOURING block (a /// Holtburg landblock whose world bounds happen to contain the same XY) and /// set lbPrefix = 0xA9B30000. The old code then returned /// 0xA9B30000 | 0x0143 = 0xA9B30143, re-stamping the validated dungeon /// claim with the wrong landblock — the client mis-resolved the player into /// Holtburg and spammed ACE with rejected moves /// (movement pre-validation failed from 00070143 to A9B30143). /// /// public class Issue133DungeonTeleportPrefixTests { private const uint DungeonLandblock = 0x00070000u; private const uint DungeonCellId = 0x00070143u; // indoor (low 0x0143 ≥ 0x0100) private const uint HoltburgLandblock = 0xA9B30000u; // a neighbouring resident block // The capture: dungeon cell 0x00070143 at dungeon-local (70, -60, 0.01). // We place the Holtburg block at world origin so its [0,192) bounds contain // the candidate XY, and the dungeon block at world Y-offset 130 so the SAME // world XY lands at dungeon-local Y = 70 - 130 = -60 (the captured negative). private static readonly Vector3 SpawnPos = new(70f, 70f, 0.01f); [Fact] public void ValidatedDungeonClaim_KeepsItsLandblockPrefix_NotTheNeighbour() { var engine = BuildEngine(); // Zero-delta teleport arrival — canonical SetPosition's placement // shape for a server-restored (cell, position) pair. The dungeon // claim is the seed cell; the candidate XY also falls inside the // resident Holtburg block's [0,192) bounds. PhysicsSetPositionResult result = engine.SetPosition( new PhysicsSetPositionRequest( Position: SpawnPos, Orientation: Quaternion.Identity, CellId: DungeonCellId, CellLocalPosition: SpawnPos, Spheres: default, Scale: 1f, StepUpHeight: 0.4f, StepDownHeight: 0.4f, Flags: PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Teleport | PhysicsSetPositionFlags.Slide)); Assert.True(result.IsCommitted); // The validated claim's prefix is authoritative — high word stays 0x0007, // NOT re-stamped to the neighbouring Holtburg 0xA9B3. Canonical // SetPosition has no lbPrefix scan to get this wrong in the first // place; this assertion is the positive proof of that. Assert.Equal(DungeonCellId, result.CellId); Assert.Equal(DungeonLandblock, result.CellId & 0xFFFF0000u); } // ── fixture ────────────────────────────────────────────────────────────── private static PhysicsEngine BuildEngine() { var cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; // The dungeon cell: a Leaf CellBSP contains any point, so AdjustPosition // validates the claim (returns it with found=true). Its Resolved set has // one walkable floor polygon at z=0 under the spawn XY so placement // grounds onto it. cache.RegisterCellStructForTest(DungeonCellId, MakeDungeonCell()); // Resident Holtburg block at world origin: its [0,192) bounds CONTAIN the // candidate XY (70,70). This is the block the legacy lbPrefix loop wrongly // matched. engine.AddLandblock( landblockId: HoltburgLandblock, terrain: FlatTerrain(), cells: Array.Empty(), portals: Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); // The dungeon's own landblock, offset so the candidate XY produces a // NEGATIVE dungeon-local Y (70 - 130 = -60) → it FAILS the [0,192) bounds // test, which is exactly why the legacy code fell through to the Holtburg // prefix. Registered so the scenario is faithful (a resident dungeon block // whose local bounds don't cover the EnvCell's negative-Y position). engine.AddLandblock( landblockId: DungeonLandblock, terrain: FlatTerrain(), cells: Array.Empty(), portals: Array.Empty(), worldOffsetX: 0f, worldOffsetY: 130f); return engine; } /// Flat 81-vertex stub terrain (all zero heights). private static TerrainSurface FlatTerrain() => new(new byte[81], new float[256]); private static CellPhysics MakeDungeonCell() { // One floor polygon: a 200×200 square at z=0 centred so it covers the // spawn XY. Normal (0,0,1) → normal.Z = 1 ≥ FloorZ (0.6642) → walkable. // Identity transform: cell-local == world, so the plane d = 0 (z + d = 0). var floor = new ResolvedPolygon { Vertices = new[] { new Vector3(-100f, -100f, 0f), new Vector3( 200f, -100f, 0f), new Vector3( 200f, 200f, 0f), new Vector3(-100f, 200f, 0f), }, Plane = new Plane(new Vector3(0f, 0f, 1f), 0f), NumPoints = 4, SidesType = CullMode.None, }; return new CellPhysics { BSP = new PhysicsBSPTree { Root = new PhysicsBSPNode { Type = BSPNodeType.Leaf } }, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity, Resolved = new Dictionary { [0] = floor }, // Leaf root → point_in_cell true for any point → AdjustPosition // validates the claim (found=true, cell unchanged). CellBSP = new CellBSPTree { Root = new CellBSPNode { Type = BSPNodeType.Leaf } }, // Retail CEnvCell::point_in_cell rejects cells with no portal // array before consulting the containment BSP. The synthetic // cell is intended to exercise an eligible loaded dungeon cell. Portals = [new PortalInfo(0xFFFF, 0, 0)], PortalPolygons = new Dictionary(), VisibleCellIds = new HashSet(), }; } }