The oracle read the #112 residual was waiting on, settled against the LIVE 2013 client (cdb attach, CEnvCell::find_transit_cells @ 0052c820; BN pseudo-C was ambiguous and partly wrong per feedback_bn_decomp_field_names - it invented portal_side tests in this branch): retail admits outdoor transit cells from an indoor cell IFF a path sphere STRADDLES an exterior portal polygon plane, |dist| < radius + F_EPSILON(0.000199999995, @ 007c8c70). The flag at [esp+18h] (set 0052c925, x87 decode fcompp/test ah,41h + fcomp/test ah,5/jp) gates the add_all_outside_cells call (0052c9d6 je). Graph reachability alone NEVER admits outdoor cells in retail. Port (CellTransit): - FindTransitCellsSphere: exitOutside now carries the retail straddle semantics; new hasExitPortal out carries the old topology-only flag. - BuildCellSetAndPickContaining: the collision cell SET keeps the A6.P5 topology widening on hasExitPortal (outdoor-registered doors must stay findable from indoor cells until #99/A6.P4 ships per-cell shadow lists - the 2026-05-25 door capture scenario), but the membership PICK's outdoor branch is gated on the retail flag. Membership is now retail-identical in both regimes: straddle -> outdoor candidates valid; no straddle -> outdoor ignored -> retail keep-curr. This is what stops deep-interior containment gaps in ANY house from demoting to outdoor (the #112 transparent-interior shape) - the systemic protection the user asked for, without house-by-house verification. The at-doorway A9B3 gap demote is RETAIL-FAITHFUL (gap point is 0.23m from 0x104s door plane < 0.48 foot radius -> retail straddles + demotes + self-heals inward): DocumentsResidual renamed to ...DemotesRetailFaithfully, expectation unchanged. New conformance pins: deep-gap keep-curr (A9B3Cottage_GapBeyondStraddleDistance_KeepsCurrCell) + function-level gate semantics on real dat geometry (FindTransitCellsSphere_ExitPortalStraddleGate_MatchesRetail). Tests: Core 1391 green (+2) / App 224 / UI 420 / Net 294; pre-existing 4 #99-era failures unchanged; P1 membership goldens + A6.P5 door-set tests explicitly green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
141 lines
6.7 KiB
C#
141 lines
6.7 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 (topology flag) but no straddle → no outdoor
|
||
// admission flag (retail: var_44 stays 0, add_all_outside skipped).
|
||
var farCandidates = new List<uint>();
|
||
CellTransit.FindTransitCellsSphere(
|
||
cache, cell102, 0xA9B30102u, new Vector3(183.0f, 86.5f, 117.0f),
|
||
FootRadius, farCandidates, out bool farStraddle, out bool farHasExit);
|
||
Assert.True(farHasExit);
|
||
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, out bool nearHasExit);
|
||
Assert.True(nearHasExit);
|
||
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);
|
||
}
|
||
}
|