using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using Xunit; using Xunit.Abstractions; using Env = System.Environment; namespace AcDream.Core.Tests.Physics; /// /// #186 diagnostic (report-only): dump the render-shell + collision geometry of the /// connecting-room grey-flap cells (0xF6820116 player room, 0xF6820117 next room, /// 0xF6820118 the connector root where the frame greys). Answers: is 0118 a CLOSED /// shell (walls/floor/ceiling covering every direction bar the portal apertures) or a /// bare connector? And does it carry collision geometry (so the camera sweep would /// hard-stop) or none (so the boom coasts to a degenerate spot)? Skips without the dat. /// public class Issue186ConnectorCellGeometryInspectionTests { private readonly ITestOutputHelper _out; public Issue186ConnectorCellGeometryInspectionTests(ITestOutputHelper output) => _out = output; private static string? DatDir() { var d = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); return Directory.Exists(d) ? d : null; } [Fact] public void Dump_ConnectorCells_ShellAndCollision() { var datDir = DatDir(); if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; } using var dats = new DatCollection(datDir, DatAccessType.Read); foreach (uint cellId in new[] { 0xF6820116u, 0xF6820117u, 0xF6820118u }) { var env = dats.Get(cellId); if (env is null) { _out.WriteLine($"cell 0x{cellId:X8}: EnvCell NOT FOUND"); continue; } var environment = dats.Get(0x0D000000u | env.EnvironmentId); environment!.Cells.TryGetValue(env.CellStructure, out var cs); var portals = env.CellPortals?.Select(p => $"0x{(0xF6820000u | p.OtherCellId):X8}") ?? Enumerable.Empty(); _out.WriteLine($"=== cell 0x{cellId:X8} envId=0x{env.EnvironmentId:X4} struct={env.CellStructure} " + $"pos=({env.Position.Origin.X:F2},{env.Position.Origin.Y:F2},{env.Position.Origin.Z:F2}) ==="); _out.WriteLine($" CellPortals -> [{string.Join(",", portals)}] VisibleCells={env.VisibleCells?.Count ?? 0}"); if (cs is null) { _out.WriteLine(" CellStruct: NULL"); continue; } int renderPolys = cs.Polygons?.Count ?? 0; int physPolys = cs.PhysicsPolygons?.Count ?? 0; bool physBsp = cs.PhysicsBSP?.Root is not null; bool cellBsp = cs.CellBSP?.Root is not null; _out.WriteLine($" renderPolys={renderPolys} physicsPolys={physPolys} physicsBSP={(physBsp ? "YES" : "NO")} cellBSP={(cellBsp ? "YES" : "NO")}"); // Render-shell normal coverage: bucket each render poly's local normal by // dominant axis to see whether the shell encloses (has +/-X,+/-Y,+/-Z faces). if (cs.Polygons is not null && cs.VertexArray?.Vertices is not null) { var buckets = new Dictionary(); foreach (var poly in cs.Polygons.Values) { var n = PolyNormal(poly, cs.VertexArray); if (n is null) continue; buckets.TryGetValue(DomAxis(n.Value), out int c); buckets[DomAxis(n.Value)] = c + 1; } _out.WriteLine(" render-shell normal buckets: " + string.Join(" ", buckets.OrderBy(k => k.Key).Select(k => $"{k.Key}={k.Value}"))); } } } private static Vector3? PolyNormal(DatReaderWriter.Types.Polygon poly, DatReaderWriter.Types.VertexArray va) { if (poly.VertexIds is null || poly.VertexIds.Count < 3) return null; if (!va.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var a)) return null; if (!va.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var b)) return null; if (!va.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var c)) return null; var n = Vector3.Cross( new Vector3(b.Origin.X, b.Origin.Y, b.Origin.Z) - new Vector3(a.Origin.X, a.Origin.Y, a.Origin.Z), new Vector3(c.Origin.X, c.Origin.Y, c.Origin.Z) - new Vector3(a.Origin.X, a.Origin.Y, a.Origin.Z)); return n.LengthSquared() < 1e-9f ? (Vector3?)null : Vector3.Normalize(n); } private static string DomAxis(Vector3 n) { float ax = MathF.Abs(n.X), ay = MathF.Abs(n.Y), az = MathF.Abs(n.Z); if (ax >= ay && ax >= az) return n.X >= 0 ? "+X" : "-X"; if (ay >= ax && ay >= az) return n.Y >= 0 ? "+Y" : "-Y"; return n.Z >= 0 ? "+Z" : "-Z"; } }