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>
This commit is contained in:
parent
e7058caa79
commit
2ce5e5c862
2 changed files with 157 additions and 1 deletions
|
|
@ -638,9 +638,23 @@ public sealed class PhysicsEngine
|
||||||
{
|
{
|
||||||
Console.WriteLine(System.FormattableString.Invariant(
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
$"[snap] claim=0x{cellId:X8} pos=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) VALIDATED -> grounded to its walkable floor z={claimFloorZ.Value:F3}"));
|
$"[snap] claim=0x{cellId:X8} pos=({currentPos.X:F3},{currentPos.Y:F3},{currentPos.Z:F3}) VALIDATED -> grounded to its walkable floor z={claimFloorZ.Value:F3}"));
|
||||||
|
// #133 (2026-06-13): 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 — but a dungeon EnvCell's local Y can be NEGATIVE
|
||||||
|
// (server teleport to 0x00070143 at local (70,-60,0.01)). The dungeon
|
||||||
|
// landblock fails the localY>=0 bounds test, so the loop matches a
|
||||||
|
// neighbouring still-resident block (e.g. Holtburg 0xA9B3), re-stamping
|
||||||
|
// the validated claim 0x00070143 -> 0xA9B30143. The client then
|
||||||
|
// mis-resolves the player into the wrong landblock and spams ACE with
|
||||||
|
// rejected moves. The validated claim's prefix is AUTHORITATIVE; a
|
||||||
|
// position falling in a neighbouring resident landblock must not
|
||||||
|
// re-stamp it. Byte-identical for the login case (the position lies in
|
||||||
|
// the claim's own landblock, so lbPrefix == cellId & 0xFFFF0000);
|
||||||
|
// diverges only — and correctly — in the far-teleport dungeon case.
|
||||||
return new ResolveResult(
|
return new ResolveResult(
|
||||||
new Vector3(candidatePos.X, candidatePos.Y, claimFloorZ.Value),
|
new Vector3(candidatePos.X, candidatePos.Y, claimFloorZ.Value),
|
||||||
lbPrefix | (cellId & 0xFFFFu),
|
cellId,
|
||||||
IsOnGround: true);
|
IsOnGround: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
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 & 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 >= 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>(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue