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(),
};
}
}