using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using Xunit; using Env = System.Environment; namespace AcDream.Core.Tests.Physics; /// /// Tests for portal-graph expansion. /// /// /// History: A6.P5 (2026-05-25) widened the collision cell set on exit- /// portal TOPOLOGY (outdoor cells added whenever an overlapped cell had a /// 0xFFFF portal) so outdoor-registered doors stayed findable from indoor /// cells under the flat registry query. BR-7 / A6.P4 C4 (2026-06-11) /// removed the widening: outdoor cells enter the array only on the retail /// STRADDLE gate (|dist| < radius + F_EPSILON vs the exterior portal /// plane — CEnvCell::find_transit_cells, Ghidra 0x0052c820, live-binary /// verified), and doors are found per-cell via registration-time /// membership instead. These tests pin BOTH halves of the retail /// semantics on real Holtburg dat geometry. /// /// public class CellTransitTests { [Fact] public void DeepInteriorSphere_NoStraddle_AddsNoOutdoorCells() { var datDir = ResolveDatDir(); if (datDir is null) return; // Hydrate the cells in the portal chain. // 0xA9B4013F — deep interior cell (the old over-penetration tick) // 0xA9B40150 — alcove cell adjacent to the doorway (exit portal) var cache = new PhysicsDataCache(); HydrateCell(cache, datDir, 0xA9B4013Fu); HydrateCell(cache, datDir, 0xA9B40150u); Assert.NotNull(cache.GetCellStruct(0xA9B4013Fu)); Assert.NotNull(cache.GetCellStruct(0xA9B40150u)); // Sphere deep in 0x13F's volume — no path sphere straddles // 0x150's exit portal plane from here (the A6.P5 pin's own // message documented exactly that). Retail adds NO outdoor cells // in this state; the door (outdoor 0xA9B40029) is reached only // when the sphere actually straddles the threshold — pinned by // the alcove test below and the tick-13558 door pin. var sphereWorld = new Vector3(132.5935f, 16.350428f, 94.48f); const float sphereRadius = 0.48f; const uint startCellId = 0xA9B4013Fu; _ = CellTransit.FindCellSet( cache, sphereWorld, sphereRadius, startCellId, out var cellSet); Assert.DoesNotContain(cellSet, c => (c & 0xFFFFu) < 0x0100u); Assert.Contains(startCellId, cellSet); } [Fact] public void AlcoveSphere_StraddlesExitPortal_ReachesDoorOutdoorCell() { var datDir = ResolveDatDir(); if (datDir is null) return; // The alcove cell 0xA9B40150 IS the cell with the exit portal, and // at this captured position the foot sphere straddles its plane // (|dist| < 0.48 + ε — same geometry the tick-13558 straddle pin // proves). Retail admits the outdoor cells exactly here, which is // how the indoor-side sphere reaches the outdoor-registered door's // per-cell shadow list. var cache = new PhysicsDataCache(); HydrateCell(cache, datDir, 0xA9B40150u); var sphereWorld = new Vector3(132.4014f, 16.761757f, 94.48f); const float sphereRadius = 0.48f; _ = CellTransit.FindCellSet( cache, sphereWorld, sphereRadius, 0xA9B40150u, out var cellSet); Assert.True( cellSet.Contains(0xA9B40029u), $"Straddle-gated outside-add: the alcove sphere straddles " + $"0xA9B40150's exit portal, so the door's outdoor cell " + $"0xA9B40029 must enter the set (retail " + $"CEnvCell::find_transit_cells straddle, Ghidra 0x0052c820). " + $"Actual: {string.Join(",", cellSet.Select(c => $"0x{c:X8}"))}"); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private static string? ResolveDatDir() { var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); return Directory.Exists(datDir) ? datDir : null; } private static void HydrateCell(PhysicsDataCache cache, string datDir, uint cellId) { const uint EnvCellPrefix = 0x0D000000u; using var dats = new DatCollection(datDir, DatAccessType.Read); var envCell = dats.Get(cellId); if (envCell is null) throw new InvalidOperationException( $"Cell 0x{cellId:X8} missing from dat at {datDir}."); var environment = dats.Get( EnvCellPrefix | envCell.EnvironmentId); if (environment is null) throw new InvalidOperationException( $"Environment 0x{EnvCellPrefix | envCell.EnvironmentId:X8} missing."); if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct)) throw new InvalidOperationException( $"CellStructure {envCell.CellStructure} missing in environment."); var worldTransform = Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * Matrix4x4.CreateTranslation(envCell.Position.Origin); cache.CacheCellStruct(cellId, envCell, cellStruct, worldTransform); } }