using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using AcDream.Core.World.Cells; using DatReaderWriter; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; namespace AcDream.Core.Tests.Conformance; /// /// #107 (2026-06-10): the indoor-login spawn wedge. The live capture /// (resolve-107-login1.jsonl) showed ACE restoring the player with a POISONED /// (cell, position) pair — cell id 0xA9B40162 (a different building, ~55 m /// away) with a position inside cell 0xA9B40171 (the threshold cottage). The /// unvalidated claim left the player fake-grounded (no contact plane, no /// walkable polygon); the first movement demoted them to outdoor mid-building /// and they fell 2.4 m through the cottage floor onto the terrain underneath. /// /// These tests pin the dat-level facts that drove the fix (the retail /// AdjustPosition validation at the player snap, PhysicsEngine.Resolve head): /// the position is genuinely inside 0x171, genuinely NOT inside 0x162's /// visible graph, and the production pick recovers correctly from a GOOD seed /// while demoting to outdoor from the poisoned one (retail-identical: retail's /// find_visible_child_cell also cannot recover a cross-building claim and /// falls to seen_outside → adjust_to_outside, acclient :280037). /// public sealed class Issue107SpawnDiagnosticTests { private readonly ITestOutputHelper _out; public Issue107SpawnDiagnosticTests(ITestOutputHelper output) => _out = output; private const float FootRadius = 0.48f; // capture input.sphereRadius private const uint PoisonedClaim = 0xA9B40162u; // the inn-side cell ACE reported private const uint ActualCell = 0xA9B40171u; // the cell that contains the position /// The captured spawn position (A9B4-local == capture world frame). private static readonly Vector3 Spawn = new(156.53314f, 11.775104f, 96.5f); [Fact] public void SpawnPosition_IsInside0171_NotInside0162_PickRecoversFromGoodSeed() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = new PhysicsDataCache(); var containing = new List(); int loaded = 0; for (uint low = 0x0100; low <= 0x01FF; low++) { uint cellId = 0xA9B40000u | low; EnvCell cell; try { cell = ConformanceDats.LoadEnvCell(dats, cache, cellId); } catch { continue; } loaded++; if (cell.PointInCell(Spawn + new Vector3(0, 0, FootRadius))) { containing.Add(cellId); _out.WriteLine($"cell 0x{cellId:X8} CONTAINS spawn footCenter"); } } Assert.True(loaded > 0, "expected Holtburg interior cells to load"); // The poisoned-pair facts: position inside 0x171, NOT inside 0x162. Assert.Contains(ActualCell, containing); Assert.DoesNotContain(PoisonedClaim, containing); var footCenter = Spawn + new Vector3(0, 0, FootRadius); // Production pick with the GOOD seed keeps the player indoor. uint pickGood = CellTransit.FindCellList(cache, footCenter, FootRadius, ActualCell); _out.WriteLine($"FindCellList(seed=0x{ActualCell:X8}) -> 0x{pickGood:X8}"); Assert.Equal(ActualCell, pickGood); // Production pick with the POISONED seed demotes to the outdoor column — // retail-identical for a cross-building claim (0x171 is not in 0x162's // stab list). The login-side fix is AdjustPosition at the snap, which // demotes IMMEDIATELY instead of after the first movement. uint pickBad = CellTransit.FindCellList(cache, footCenter, FootRadius, PoisonedClaim); _out.WriteLine($"FindCellList(seed=0x{PoisonedClaim:X8}) -> 0x{pickBad:X8}"); Assert.Equal(0xA9B40031u, pickBad); } [Fact] public void SeenOutside_IsPopulated_ForGroundFloorInteriors() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = new PhysicsDataCache(); ConformanceDats.LoadEnvCell(dats, cache, ActualCell); ConformanceDats.LoadEnvCell(dats, cache, PoisonedClaim); var room = cache.GetCellStruct(ActualCell); Assert.NotNull(room); // 0x171 is a ground-floor cottage room with an exterior doorway — // retail marks it seen_outside; AdjustPosition's indoor not-found // fallback (acclient :280037) depends on this flag being cached. Assert.True(room!.SeenOutside, "0xA9B40171 should be seen_outside"); _out.WriteLine($"0x{ActualCell:X8}.SeenOutside={room.SeenOutside}"); var inn = cache.GetCellStruct(PoisonedClaim); Assert.NotNull(inn); _out.WriteLine($"0x{PoisonedClaim:X8}.SeenOutside={inn!.SeenOutside}"); } [Fact] public void AdjacentRoomClaim_CorrectsToContainingRoom_ViaStabList() { // 2026-06-10 gate-run regression: ACE saved the cell one room off // (claim 0xA9B40172, position inside 0xA9B40171 — adjacent rooms of the // threshold cottage, connected via the 20 cm threshold cell 0x173). // With every cell hydrated, retail's find_visible_child_cell stab-list // search (acclient :311444) must correct the claim. The live failure // was the auto-entry hold opening before 0x171 hydrated (the // mid-population "any cell present" disambiguator, since removed) — // this test pins the hydrated-path correctness. var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var cache = new PhysicsDataCache(); for (uint low = 0x0100; low <= 0x01FF; low++) { try { ConformanceDats.LoadEnvCell(dats, cache, 0xA9B40000u | low); } catch { } } // The gate-run spawn point (ACE save: cell 0x172, position in 0x171). var spawnFootCenter = new Vector3(155.390f, 11.020f, 94.0f + FootRadius); uint corrected = CellTransit.FindVisibleChildCell( cache, 0xA9B40172u, spawnFootCenter, useStabList: true); _out.WriteLine($"FindVisibleChildCell(claim 0x172, stab) -> 0x{corrected:X8}"); Assert.Equal(ActualCell, corrected); } [Fact] public void PoisonedClaim_IsADifferentBuilding_55mAway() { var datDir = ConformanceDats.ResolveDatDir(); if (datDir is null) { _out.WriteLine("dats unavailable — skipped"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var claim = dats.Get(PoisonedClaim); var actual = dats.Get(ActualCell); Assert.NotNull(claim); Assert.NotNull(actual); float dist = Vector3.Distance(claim!.Position.Origin, actual!.Position.Origin); _out.WriteLine( $"claim origin=({claim.Position.Origin.X:F1},{claim.Position.Origin.Y:F1}) " + $"actual origin=({actual.Position.Origin.X:F1},{actual.Position.Origin.Y:F1}) dist={dist:F1} m"); // The pair is not a one-tick-stale neighbour confusion — the buildings // are tens of metres apart. The poisoning happened on the WIRE side // (the old position-derived-landblock wireCellId composition) or in a // prior wedged session's chaos, not in the per-tick membership pick. Assert.True(dist > 20f, $"expected cross-building distance, got {dist:F1} m"); } }