acdream/tests/AcDream.Core.Tests/Conformance/Issue112MembershipTests.cs
Erik 414c3deaf4 fix(phys): #112 residual - retail straddle gate for outdoor-cell admission (live-binary verified)
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>
2026-06-10 16:52:24 +02:00

141 lines
6.7 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.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);
}
}