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);
}
}