using System; using System.Collections.Generic; using System.IO; using System.Numerics; using AcDream.App.Rendering; using AcDream.Core.Physics; using AcDream.Core.Rendering; using DatReaderWriter; using DatReaderWriter.Options; using DatEnvCell = DatReaderWriter.DBObjs.EnvCell; using DatEnvironment = DatReaderWriter.DBObjs.Environment; using Xunit; using Xunit.Abstractions; namespace AcDream.App.Tests.Rendering; /// /// #181 excitation, isolated headlessly — the WALL-PRESS equilibrium. Live /// evidence: a camera pressed into corridor walls/openings never reaches a /// fixed point (sought steps α·gap into the wall per frame; the sweep clips it /// back within adjust_to_plane's parametric 0.02 window) → the published eye /// wanders ~1 mm/frame, and when the wander straddles a cell boundary the /// VIEWER CELL flaps (launch-181-pressed.log: viewer≠player on 85.5% of /// frames, one-frame A→B→A root flips) — each flip re-roots the whole /// visibility frame (the #176/#181 flicker). /// /// This test runs the REAL RetailChaseCamera + the REAL /// PhysicsCameraCollisionProbe against the REAL Facility Hub BSP with a /// static player backed against the corridor wall, and measures the /// steady-state eye wander + ViewerCellId stability over 20k frames. /// Diagnostic (reporting) first; the equilibrium fix turns the wander/flap /// numbers into hard pins. /// public class Issue181WallPressEquilibriumTests { private const uint FacilityHubLandblock = 0x8A020000u; private readonly ITestOutputHelper _out; public Issue181WallPressEquilibriumTests(ITestOutputHelper output) => _out = output; // Mirrors AcDream.Core.Tests Conformance.ConformanceDats (not referencable // from App.Tests): resolve the dat dir + load real EnvCells into the cache. private static string? ResolveDatDir() { var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) return fromEnv; var def = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); return Directory.Exists(def) ? def : null; } private static (PhysicsEngine, PhysicsDataCache) BuildCorridorEngine(DatCollection dats) { var cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; for (uint low = 0x0100u; low <= 0x01FFu; low++) { uint id = FacilityHubLandblock | low; var datCell = dats.Get(id); if (datCell is null) continue; var environment = dats.Get(0x0D000000u | datCell.EnvironmentId); if (environment is null) continue; if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct) || cellStruct is null) continue; var world = Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation) * Matrix4x4.CreateTranslation(datCell.Position.Origin); cache.CacheCellStruct(id, datCell, cellStruct, world); } var heights = new byte[81]; var heightTable = new float[256]; for (int i = 0; i < 256; i++) heightTable[i] = -1000f; engine.AddLandblock(FacilityHubLandblock, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), 0f, 0f); return (engine, cache); } [Fact] public void Diagnostic_WallPressedCamera_EyeWanderAndViewerCellStability() { var datDir = ResolveDatDir(); if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); var (engine, _) = BuildCorridorEngine(dats); bool savedAlign = CameraDiagnostics.AlignToSlope; bool savedColl = CameraDiagnostics.CollideCamera; float savedT = CameraDiagnostics.TranslationStiffness; float savedR = CameraDiagnostics.RotationStiffness; try { CameraDiagnostics.AlignToSlope = true; CameraDiagnostics.CollideCamera = true; CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f; // The live parked spot from the leak-fix log: player at the corridor // spawn (cell 0x0142), backed near the +Y wall so the full boom is // blocked (live [resolve]: hit=yes n=(0,-1,0) every frame). var playerPos = new Vector3(50.331f, -39.357f, -5.90f); // Live [resolve]: the sweep target headed (-2.13,+1.32,+0.75) from the // pivot and hit the n=(0,-1,0) wall — so the player faces (+X,-Y)-ish // and the boom presses -X+Y into that wall. yaw = atan2(-0.53, 0.85). float yaw = -0.556f; uint cellId = 0x8A020142u; float dt = 1f / 1500f; var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), }; void Step() => cam.Update( playerPosition: playerPos, playerYaw: yaw, playerVelocity: Vector3.Zero, isOnGround: true, contactPlaneNormal: Vector3.UnitZ, dt: dt, cellId: cellId, selfEntityId: 0x5); // Settle into the wall-press equilibrium. for (int i = 0; i < 5000; i++) Step(); // Measure 20k steady-state frames. var eyes = new List(20000); var cells = new HashSet(); int cellTransitions = 0; uint prevCell = cam.ViewerCellId; Vector3 prevEye = cam.Position; float maxStep = 0f; double sumStep = 0; for (int i = 0; i < 20000; i++) { Step(); float d = Vector3.Distance(cam.Position, prevEye); maxStep = MathF.Max(maxStep, d); sumStep += d; prevEye = cam.Position; eyes.Add(cam.Position); cells.Add(cam.ViewerCellId); if (cam.ViewerCellId != prevCell) { cellTransitions++; prevCell = cam.ViewerCellId; } } // Wander bounding box. Vector3 mn = eyes[0], mx = eyes[0]; foreach (var e in eyes) { mn = Vector3.Min(mn, e); mx = Vector3.Max(mx, e); } var span = mx - mn; _out.WriteLine(FormattableString.Invariant( $"steady-state: avgStep={sumStep / 20000 * 1e6:F1}um maxStep={maxStep * 1e6:F1}um wanderBox=({span.X * 1000:F2},{span.Y * 1000:F2},{span.Z * 1000:F2})mm")); _out.WriteLine(FormattableString.Invariant( $"viewer cells seen: {cells.Count} transitions={cellTransitions} eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6}) cell=0x{cam.ViewerCellId:X8}")); // Orbit structure: 16 consecutive frames at 6dp, with the sweep's // own [flap-sweep] lines captured for the same frames. bool savedFlap = RenderingDiagnostics.ProbeFlapEnabled; var savedOut = Console.Out; try { RenderingDiagnostics.ProbeFlapEnabled = true; using var writer = new StringWriter(); Console.SetOut(writer); for (int i = 0; i < 16; i++) { Step(); writer.WriteLine(FormattableString.Invariant( $"orbit[{i:D2}] eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6})")); } Console.SetOut(savedOut); foreach (var line in writer.ToString().Split('\n')) if (line.Length > 1) _out.WriteLine(line.TrimEnd()); } finally { Console.SetOut(savedOut); RenderingDiagnostics.ProbeFlapEnabled = savedFlap; } } finally { CameraDiagnostics.AlignToSlope = savedAlign; CameraDiagnostics.CollideCamera = savedColl; CameraDiagnostics.TranslationStiffness = savedT; CameraDiagnostics.RotationStiffness = savedR; } } }