The two remaining flagged workarounds retired, per the BR-7 plan +
the WF1 [MEDIUM] correction (re-gate, do NOT delete the outside-add):
1. A6.P5 hasExitPortal topology widening DELETED. Outdoor cells enter the
collision cell array ONLY on the retail straddle gate - |dist| <
radius + F_EPSILON against an exterior portal plane
(CEnvCell::find_transit_cells Ghidra 0x0052c820, gate 0052c9d6,
live-binary verified) - the same flag that already gated the
membership pick (#112 rider). The widening existed so outdoor-
registered doors stayed findable from indoor cells under the old flat
registry query; with per-cell shadow lists the door is found in the
straddle-admitted outdoor cell's own list (tick-13558 pin holds).
The hasExitPortal out-param + plumbing deleted from
FindTransitCellsSphere; the AddAllOutsideCells call in
BuildCellSetAndPickContaining re-gated on exitOutsideStraddle
(once-per-walk = retail CELLARRAY.added_outside).
2. #90 ResolveCellId sphere-overlap stickiness REMOVED (the 4ca3596
workaround, deferred-to-A6.P4 in the physics digest). It was dead
code: the method's only caller is FindEnvCollisions' cache-null TEST
fallback, and the indoor branch (where the stickiness lived) required
a non-null DataCache. Production membership flows exclusively through
the collide-then-pick advance whose ordered-array hysteresis (current
cell at index 0, interior-wins-break) is the retail mechanism the
workaround approximated. ResolveCellId reduced to the bare
prefix-preserving outdoor re-derive, documented test-only.
Test updates (pins of the deleted behaviors inverted to retail):
- A6P5_BuildCellSetFromIndoorStart_ReachesDoorOutdoorCell (asserted the
topology widening verbatim) -> DeepInteriorSphere_NoStraddle_
AddsNoOutdoorCells: a deep-interior sphere admits NO outdoor cells.
- A6P5_BuildCellSetFromAlcove... -> AlcoveSphere_StraddlesExitPortal_
ReachesDoorOutdoorCell (the captured alcove position genuinely
straddles - the retail-positive half).
- Issue112MembershipTests straddle pin + the second-sphere straddle test
updated to the single-flag signature.
Suites: Core 1416/0/2, App 225, UI 420, Net 294 - green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
142 lines
6.8 KiB
C#
142 lines
6.8 KiB
C#
using System.Collections.Generic;
|
||
using System.Numerics;
|
||
using AcDream.Core.Physics;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.Options;
|
||
using Xunit;
|
||
using Xunit.Abstractions;
|
||
|
||
namespace AcDream.Core.Tests.Conformance;
|
||
|
||
/// <summary>
|
||
/// #112 (2026-06-10): the A9B3 hill cottage has a real containment GAP inside
|
||
/// the house (world (184.9, −109.5, 116) / A9B3-local (184.9, 82.5) is in NO
|
||
/// interior cell while points ~0.7 m away are inside 0x100/0x103). The
|
||
/// 6dbbf95 escape hatch demoted the walker to the outdoor column there,
|
||
/// stranding them outdoor-classified deep indoors (no containment-based
|
||
/// re-promotion) → the outdoor flood rendered the interior transparent.
|
||
/// Retail find_cell_list KEEPS curr_cell when nothing contains the centre
|
||
/// (pc:308788-308825). These tests pin the replacement semantics against the
|
||
/// real dats.
|
||
/// </summary>
|
||
public sealed class Issue112MembershipTests
|
||
{
|
||
private readonly ITestOutputHelper _out;
|
||
public Issue112MembershipTests(ITestOutputHelper output) => _out = output;
|
||
|
||
private const float FootRadius = 0.48f;
|
||
|
||
private static PhysicsDataCache LoadLandblockInteriors(DatCollection dats, uint lbPrefix)
|
||
{
|
||
var cache = new PhysicsDataCache();
|
||
for (uint low = 0x0100; low <= 0x01FF; low++)
|
||
{
|
||
try { ConformanceDats.LoadEnvCell(dats, cache, lbPrefix | low); }
|
||
catch { }
|
||
}
|
||
return cache;
|
||
}
|
||
|
||
[Fact]
|
||
public void A9B3CottageGap_AtDoorway_StraddlesExitPlane_DemotesRetailFaithfully()
|
||
{
|
||
var datDir = ConformanceDats.ResolveDatDir();
|
||
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
|
||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||
var cache = LoadLandblockInteriors(dats, 0xA9B30000u);
|
||
|
||
// The gap point (A9B3-local frame, footcenter height): contained by NO
|
||
// interior cell (dat-scan fact from the live capture issue111-verify7).
|
||
var gap = new Vector3(184.915f, 82.464f, 116.48f);
|
||
|
||
uint picked = CellTransit.FindCellList(cache, gap, FootRadius, 0xA9B30104u);
|
||
_out.WriteLine($"pick(seed 0x104) at gap -> 0x{picked:X8}");
|
||
|
||
// RESOLVED 2026-06-10 (#112 rider, live-binary oracle): retail's
|
||
// CEnvCell::find_transit_cells admits outdoor cells IFF a path sphere
|
||
// STRADDLES an exterior portal's plane (|dist| < radius + F_EPSILON;
|
||
// acclient.exe 0052c8e5-0052c9f0). The gap point sits 0.23 m from
|
||
// 0x104's exit-door plane (x=184.684) with foot radius 0.48 — it
|
||
// STRADDLES, so retail admits the outdoor column and demotes here too.
|
||
// This at-doorway demote is RETAIL-FAITHFUL, not a divergence; it
|
||
// self-heals one step inward via doorway re-promotion. The former
|
||
// "DocumentsResidual" framing is closed — see the deep-gap test below
|
||
// for the behavior that DID change with the straddle gate.
|
||
Assert.Equal(0xA9B3003Cu, picked);
|
||
}
|
||
|
||
[Fact]
|
||
public void A9B3Cottage_GapBeyondStraddleDistance_KeepsCurrCell_RetailGate()
|
||
{
|
||
var datDir = ConformanceDats.ResolveDatDir();
|
||
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
|
||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||
var cache = LoadLandblockInteriors(dats, 0xA9B30000u);
|
||
|
||
// A no-cell point past the straddle window: 0.66 m beyond 0x104's
|
||
// exit-door plane (x=184.684 + 0.48 + 0.18), still in no interior cell
|
||
// (inside the shell-wall band). Pre-gate, the A6.P5 topology widening
|
||
// let the outdoor column WIN the pick here → outdoor demote deep in a
|
||
// containment gap (#112's transparent-interior shape). Retail keeps
|
||
// curr_cell: no sphere straddles any exterior portal plane, so the
|
||
// outdoor cells never become pick candidates (live-binary verified).
|
||
var deepGap = new Vector3(185.345f, 82.464f, 116.48f);
|
||
|
||
uint picked = CellTransit.FindCellList(cache, deepGap, FootRadius, 0xA9B30104u);
|
||
_out.WriteLine($"pick(seed 0x104) at deep gap -> 0x{picked:X8}");
|
||
Assert.Equal(0xA9B30104u, picked);
|
||
}
|
||
|
||
[Fact]
|
||
public void FindTransitCellsSphere_ExitPortalStraddleGate_MatchesRetail()
|
||
{
|
||
var datDir = ConformanceDats.ResolveDatDir();
|
||
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
|
||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||
var cache = LoadLandblockInteriors(dats, 0xA9B30000u);
|
||
|
||
// Cottage north room 0x102 has an exterior door at x=186 (portal poly
|
||
// plane n=(±1,0,0), |d|=186 — dat dump 2026-06-10). Function-level pin
|
||
// of the retail gate semantics on real dat geometry:
|
||
var cell102 = cache.GetCellStruct(0xA9B30102u)!;
|
||
|
||
// (a) Deep inside the room, 3 m from the door plane: the cell HAS an
|
||
// exterior portal but no straddle → no outdoor admission flag
|
||
// (retail: var_44 stays 0, add_all_outside skipped). (BR-7 C4
|
||
// deleted the non-retail hasExitPortal topology output — the
|
||
// straddle flag is the only outdoor-admission signal, like
|
||
// retail.)
|
||
var farCandidates = new List<uint>();
|
||
CellTransit.FindTransitCellsSphere(
|
||
cache, cell102, 0xA9B30102u, new Vector3(183.0f, 86.5f, 117.0f),
|
||
FootRadius, farCandidates, out bool farStraddle);
|
||
Assert.False(farStraddle);
|
||
|
||
// (b) At the door plane (0.30 m away < 0.48 radius): straddle fires.
|
||
var nearCandidates = new List<uint>();
|
||
CellTransit.FindTransitCellsSphere(
|
||
cache, cell102, 0xA9B30102u, new Vector3(185.70f, 85.5f, 117.0f),
|
||
FootRadius, nearCandidates, out bool nearStraddle);
|
||
Assert.True(nearStraddle);
|
||
}
|
||
|
||
[Fact]
|
||
public void ThresholdCottage_AdjacentClaim_LaterallyRecovers_ViaStabGraph()
|
||
{
|
||
var datDir = ConformanceDats.ResolveDatDir();
|
||
if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; }
|
||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||
var cache = LoadLandblockInteriors(dats, 0xA9B40000u);
|
||
|
||
// The #111 gate shape: claim 0x172 (adjacent room), position deep
|
||
// inside 0x171 (the captured spawn). The sphere does not overlap
|
||
// 0x172's volume from there → the new lateral recovery searches the
|
||
// claim's stab list (retail find_visible_child_cell :311444) and
|
||
// self-heals to 0x171 — instead of the old outdoor demote.
|
||
var spawnFootCenter = new Vector3(155.390f, 11.020f, 94.0f + FootRadius);
|
||
|
||
uint picked = CellTransit.FindCellList(cache, spawnFootCenter, FootRadius, 0xA9B40172u);
|
||
_out.WriteLine($"pick(seed 0x172) at 0x171-interior -> 0x{picked:X8}");
|
||
Assert.Equal(0xA9B40171u, picked);
|
||
}
|
||
}
|