acdream/tests/AcDream.Core.Tests/Physics/Issue133DungeonTeleportPrefixTests.cs
Erik 2ce5e5c862 fix(G.3a): validated-claim placement keeps the claim's landblock prefix (#133)
The #111 validated-claim branch returned lbPrefix | (cellId & 0xFFFF), where
lbPrefix is found by searching resident landblocks for one containing the
candidate position. A dungeon EnvCell's local Y can be negative, so the dungeon
landblock fails the [0,192) bounds test and the loop matches a neighbouring
(e.g. Holtburg) resident block -> the validated claim 0x00070143 got re-stamped
0xA9B30143, making the client mis-resolve the player to the wrong landblock and
spam ACE with rejected moves. The validated claim's full id is authoritative;
return it directly. Byte-identical for the login case (position in the claim's
own landblock); fixes the far-teleport dungeon case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:27:45 +02:00

142 lines
6.6 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;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// #133 (Bug A) — the validated-claim placement branch of
/// <see cref="PhysicsEngine.Resolve"/> must return the VALIDATED claim's own
/// full cell id, NOT <c>lbPrefix | (cellId &amp; 0xFFFF)</c>.
///
/// <para>
/// <c>lbPrefix</c> is found by scanning resident landblocks for one whose
/// <c>[0,192)</c> 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 <c>0x00070143</c> at local <c>(70,-60,0.01)</c>).
/// The dungeon landblock fails the <c>localY &gt;= 0</c> 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
/// <c>lbPrefix = 0xA9B30000</c>. The old code then returned
/// <c>0xA9B30000 | 0x0143 = 0xA9B30143</c>, re-stamping the validated dungeon
/// claim with the wrong landblock — the client mis-resolved the player into
/// Holtburg and spammed ACE with rejected moves
/// (<c>movement pre-validation failed from 00070143 to A9B30143</c>).
/// </para>
///
/// <para>
/// 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 <c>0x0007</c>,
/// candidate XY also inside resident Holtburg <c>0xA9B3</c>) and asserts the
/// returned cell keeps its <c>0x0007</c> prefix.
/// </para>
/// </summary>
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<CellSurface>(),
portals: Array.Empty<PortalPlane>(),
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<CellSurface>(),
portals: Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 130f);
return engine;
}
/// <summary>Flat 81-vertex stub terrain (all zero heights).</summary>
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<ushort, ResolvedPolygon> { [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<PortalInfo>(),
PortalPolygons = new Dictionary<ushort, ResolvedPolygon>(),
VisibleCellIds = new HashSet<uint>(),
};
}
}