Member-wise deletion of the three legacy resolver members named in docs/research/2026-08-05-c5a-contract.md: PhysicsEngine.Resolve, PhysicsEngine.HasCellSurface, and PhysicsEngine.ResolvePlacement. An exhaustive receiver census over src/ found zero production callers of any of the three — every production placement writer already reaches the canonical PhysicsEngine.SetPosition transaction exclusively through RuntimeSetPositionState (three call sites total). The deletion is purely member-wise: IsSpawnCellReady and AdjustPosition, which shared the same source region as the deleted members, are preserved byte-identical — every remaining production caller of either (including PhysicsCameraCollisionProbe, AdjustPosition's sole surviving production caller) is unaffected. Companion changes: - PlayerMovementController's 3-argument SetPosition test overload is renamed to SeedPlacementForTest (internal) and CommitPreparedPosition is deleted; 83 call sites across 19 test files were mechanically renamed to match. - Seven pinned test dispositions from the contract are executed: 3.1 (PhysicsEngineTests.cs: 11 legacy-resolver tests deleted, 6 ResolveWithTransition tests kept), 3.2/3.3/3.4 (re-point to canonical SetPosition, with TransitionScratchDifferentialTests.cs additionally gaining positive IsCommitted assertions after each bitwise comparison so the differential proves a placement actually committed, not just that two possibly-uncommitted results match), 3.5 (Runtime rename), and 3.6 (PlayerMovementPlacementTransactionTests.cs rewritten — its xmldoc now states plainly that the render-root publish moved to RuntimeSetPositionState.cs, but the sticky-release relocation claim was false and is retracted; this disposition's coverage loss is the sticky release path, not silently absorbed elsewhere). - Stale `PhysicsEngine.Resolve`/`Resolve` doc citations in CellTransit.cs, PlayerMovementController.cs, and HeadlessSessionWorldProjection.cs are corrected to name the surviving canonical entry points by symbol (SetPosition, AdjustSetPosition/AdjustPosition, ResolveWithTransition) rather than fragile line numbers. Retires AP-1 and AD-1 in docs/architecture/retail-divergence-register.md: both rows described production zero-delta placement routing remaining on the legacy resolver pending the Slice 4B2/4B route cutover; that resolver no longer exists, so the condition each row tracked is now structurally false rather than merely narrowed. AP-145 (routed through the prior commit) and this commit's AP-1/AD-1 together bring the section counts to 101 AP / 47 AD active rows. Builds on the AP-145 fix (previous commit) — this commit's staged tree was independently rebuilt and its four suites independently rerun on top of that commit before this commit was created, in addition to the combined rebuild/rerun below. Full-solution build: 0 errors (21 pre-existing warnings, all unrelated). Suite results (combined tree): Core 4270/4271 passed (1 skip; the single DatSoundCacheTests concurrent-decode-dedup failure is a known load-sensitive race, confirmed passing standalone and unrelated to this change), Runtime 1176/1176, Headless 86/86, App 4132/4135 (3 skips). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
179 lines
8.7 KiB
C#
179 lines
8.7 KiB
C#
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) — a validated dungeon claim's landblock prefix is
|
||
/// AUTHORITATIVE; a candidate world position that also falls inside a
|
||
/// neighbouring RESIDENT landblock's <c>[0,192)</c> local bounds must NOT
|
||
/// re-stamp the claim with that neighbour's prefix.
|
||
///
|
||
/// <para>
|
||
/// C5a (2026-08-05) re-point: the legacy <c>PhysicsEngine.Resolve</c> this
|
||
/// test originally drove is deleted (zero production callers — every
|
||
/// production placement writer reaches canonical
|
||
/// <c>PhysicsEngine.SetPosition</c> through <c>RuntimeSetPositionState</c>).
|
||
/// The defect this test pins lived entirely inside <c>Resolve</c>'s own
|
||
/// <c>lbPrefix</c> resident-landblock scan (find a resident block whose
|
||
/// <c>[0,192)</c> bounds contain the candidate XY, then stamp
|
||
/// <c>lbPrefix | (cellId & 0xFFFF)</c> — 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 <c>SetPosition</c>/<c>SetPositionInternal</c> pipeline has no
|
||
/// equivalent lbPrefix scan: a validated claim (<c>AdjustSeedCell</c>
|
||
/// 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
|
||
/// <c>0x00070143</c> at dungeon-local <c>(70,-60,0.01)</c>, with a
|
||
/// still-resident Holtburg neighbour block whose <c>[0,192)</c> bounds
|
||
/// contain the same world XY) through canonical
|
||
/// <c>PhysicsEngine.SetPosition</c> and assert the committed cell keeps
|
||
/// the <c>0x0007</c> prefix.
|
||
/// </para>
|
||
///
|
||
/// <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
|
||
/// legacy scan instead matched a still-resident NEIGHBOURING block (a
|
||
/// Holtburg landblock whose world bounds happen to contain the same XY) and
|
||
/// set <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>
|
||
/// </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 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<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 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<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 } },
|
||
// 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<ushort, ResolvedPolygon>(),
|
||
VisibleCellIds = new HashSet<uint>(),
|
||
};
|
||
}
|
||
}
|