using System; using System.Collections.Generic; using System.IO; using System.Numerics; using System.Text.Json; using System.Text.Json.Serialization; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.Core.Physics; /// /// A6.P3 issue #98 (2026-05-23 evening v2) — JSON-serializable snapshot of /// a single instance, capturing the polygons /// the engine actually queries during collision. Parallel to /// but for GfxObj-owned geometry (landblock-baked /// statics like the cottage building 0x01000A2B in this investigation). /// /// /// Polygons are captured in OBJECT-LOCAL frame — the same frame the /// engine uses at collision time after applying the ShadowEntry's /// world transform. The harness loads the dump, attaches a synthetic /// single-leaf BSP, and registers the GfxObj at the appropriate world /// transform via the test's ShadowObjects.Register call. /// /// /// /// What's intentionally NOT captured: the raw DAT /// PhysicsBSPTree (the dump's consumer wraps polygons in a /// single-leaf BSP), the original PhysicsPolygons + raw /// VertexArray dicts (the engine uses /// during collision; the raw dicts are placeholders post-hydrate). /// /// public sealed record GfxObjDump( uint GfxObjId, Vector3Dto BoundingSphereOrigin, float BoundingSphereRadius, IReadOnlyList ResolvedPolygons); public static class GfxObjDumpSerializer { private static readonly JsonSerializerOptions WriteOptions = new() { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, }; private static readonly JsonSerializerOptions ReadOptions = new() { NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals, }; /// /// Snapshot a instance into a /// JSON-friendly DTO. Polygon vertices /// stay in object-local frame. /// public static GfxObjDump Capture(uint gfxObjId, GfxObjPhysics gfx) { var resolved = new List(gfx.Resolved.Count); foreach (var (id, poly) in gfx.Resolved) { var verts = new List(poly.Vertices.Length); foreach (var v in poly.Vertices) verts.Add(Vector3Dto.From(v)); resolved.Add(new PolygonDump( Id: id, NumPoints: poly.NumPoints, SidesType: (int)poly.SidesType, Plane: PlaneDto.From(poly.Plane), Vertices: verts)); } // Fall back to (0,0,0) + 0 when BoundingSphere is unset — the // hydrate path recomputes a covering sphere from the polygon // vertices in that case. var bsOrigin = gfx.BoundingSphere?.Origin ?? Vector3.Zero; var bsRadius = gfx.BoundingSphere?.Radius ?? 0f; return new GfxObjDump( GfxObjId: gfxObjId, BoundingSphereOrigin: Vector3Dto.From(bsOrigin), BoundingSphereRadius: bsRadius, ResolvedPolygons: resolved); } public static void Write(GfxObjDump dump, string filePath) { var dir = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) Directory.CreateDirectory(dir); using var stream = File.Create(filePath); JsonSerializer.Serialize(stream, dump, WriteOptions); } public static GfxObjDump Read(string filePath) { using var stream = File.OpenRead(filePath); var dump = JsonSerializer.Deserialize(stream, ReadOptions); if (dump is null) throw new InvalidDataException( $"GfxObj dump deserialized to null: {filePath}"); return dump; } /// /// Re-hydrate a from a /// . Constructs a synthetic single-leaf /// referencing every polygon — sufficient /// for BSPQuery traversal at collision time. PhysicsPolygons and /// Vertices are placeholders (the engine reads /// during collision, not the raw dat dicts). /// public static GfxObjPhysics Hydrate(GfxObjDump dump) { var resolved = new Dictionary( dump.ResolvedPolygons.Count); foreach (var p in dump.ResolvedPolygons) { var verts = new Vector3[p.Vertices.Count]; for (int i = 0; i < verts.Length; i++) verts[i] = p.Vertices[i].ToVector3(); resolved[p.Id] = new ResolvedPolygon { Vertices = verts, Plane = p.Plane.ToPlane(), NumPoints = p.NumPoints, SidesType = (DatReaderWriter.Enums.CullMode)p.SidesType, Id = p.Id, }; } // Bounding sphere: use the captured value, OR fall back to a sphere // that covers every polygon (computed from the centroid + max // distance to any vertex). var bsOrigin = dump.BoundingSphereOrigin.ToVector3(); var bsRadius = dump.BoundingSphereRadius; if (bsRadius <= 0f && resolved.Count > 0) { (bsOrigin, bsRadius) = ComputeCoveringSphere(resolved); } var leaf = new PhysicsBSPNode { Type = BSPNodeType.Leaf, BoundingSphere = new Sphere { Origin = bsOrigin, Radius = bsRadius, }, }; foreach (var id in resolved.Keys) leaf.Polygons.Add(id); var bspTree = new PhysicsBSPTree { Root = leaf }; return new GfxObjPhysics { BSP = bspTree, PhysicsPolygons = new Dictionary(), Vertices = new VertexArray(), Resolved = resolved, BoundingSphere = leaf.BoundingSphere, }; } private static (Vector3 origin, float radius) ComputeCoveringSphere( Dictionary resolved) { var sum = Vector3.Zero; int count = 0; foreach (var poly in resolved.Values) foreach (var v in poly.Vertices) { sum += v; count++; } if (count == 0) return (Vector3.Zero, 0f); var centroid = sum / count; float maxDistSq = 0f; foreach (var poly in resolved.Values) foreach (var v in poly.Vertices) { float distSq = Vector3.DistanceSquared(centroid, v); if (distSq > maxDistSq) maxDistSq = distSq; } return (centroid, MathF.Sqrt(maxDistSq)); } }