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) — the validated-claim placement branch of /// must return the VALIDATED claim's own /// full cell id, NOT lbPrefix | (cellId & 0xFFFF). /// /// /// 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 loop /// instead matches a still-resident NEIGHBOURING block (a Holtburg landblock /// whose world bounds happen to contain the same XY) and sets /// 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). /// /// /// /// The validated claim's prefix is authoritative; a position falling in a /// neighbouring resident landblock must not re-stamp it. This test reproduces /// the exact geometry of the capture (dungeon claim in landblock 0x0007, /// candidate XY also inside resident Holtburg 0xA9B3) and asserts the /// returned cell keeps its 0x0007 prefix. /// /// 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 = the snap shape (teleport arrival). cellId is the dungeon // claim; the candidate XY also falls inside the resident Holtburg block. var result = engine.Resolve(SpawnPos, DungeonCellId, delta: Vector3.Zero, stepUpHeight: 0.5f); Assert.True(result.IsOnGround); // The validated claim's prefix is authoritative — high word stays 0x0007, // NOT re-stamped to the neighbouring Holtburg 0xA9B3. 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 the #111 // validated-claim branch 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 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 old 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 } }, Portals = Array.Empty(), PortalPolygons = new Dictionary(), VisibleCellIds = new HashSet(), }; } }