acdream/tests/AcDream.Core.Tests/Conformance/Issue112MembershipTests.cs
Erik ca4b482f8b T6 (BR-7) C4: straddle-only outside-add (A6.P5 widening DELETED) + #90 stickiness removed
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>
2026-06-11 14:44:49 +02:00

142 lines
6.8 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 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);
}
}