using System.Collections.Generic;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
///
/// A6.P3 issue #98 (2026-05-23 evening v2). Sanity check for
/// : hand-construct a small
/// , snapshot it, serialize → deserialize →
/// hydrate, then assert the round-trip preserves every field the harness's
/// RegisterCottageGfxObj path depends on.
///
///
/// Mirrors in shape — sanity-only,
/// no engine wiring. The fixture's polygons are in OBJECT-LOCAL frame
/// (matching the production capture path's frame), so the hydrated
/// instance is suitable for placement under any world transform via
/// ShadowObjects.Register.
///
///
public class GfxObjDumpRoundTripTests
{
[Fact]
public void Capture_Then_Hydrate_PreservesPolygons()
{
var original = MakeFixtureGfx();
var dump = GfxObjDumpSerializer.Capture(0x01000A2Bu, original);
var hydrated = GfxObjDumpSerializer.Hydrate(dump);
Assert.Equal(original.Resolved.Count, hydrated.Resolved.Count);
foreach (var (id, originalPoly) in original.Resolved)
{
Assert.True(hydrated.Resolved.TryGetValue(id, out var rehydrated));
Assert.Equal(originalPoly.NumPoints, rehydrated!.NumPoints);
Assert.Equal(originalPoly.SidesType, rehydrated.SidesType);
Assert.Equal(originalPoly.Plane.Normal, rehydrated.Plane.Normal);
Assert.Equal(originalPoly.Plane.D, rehydrated.Plane.D);
Assert.Equal(originalPoly.Vertices.Length, rehydrated.Vertices.Length);
for (int i = 0; i < originalPoly.Vertices.Length; i++)
Assert.Equal(originalPoly.Vertices[i], rehydrated.Vertices[i]);
Assert.Equal(originalPoly.Id, rehydrated.Id);
}
}
[Fact]
public void Hydrate_ConstructsSyntheticSingleLeafBspWithAllPolygons()
{
var original = MakeFixtureGfx();
var dump = GfxObjDumpSerializer.Capture(0x01000A2Bu, original);
var hydrated = GfxObjDumpSerializer.Hydrate(dump);
// Synthetic BSP is single-leaf, references every resolved poly id.
Assert.NotNull(hydrated.BSP);
Assert.NotNull(hydrated.BSP!.Root);
Assert.Equal(BSPNodeType.Leaf, hydrated.BSP.Root.Type);
Assert.Equal(original.Resolved.Count, hydrated.BSP.Root.Polygons.Count);
foreach (var id in original.Resolved.Keys)
Assert.Contains(id, hydrated.BSP.Root.Polygons);
}
[Fact]
public void WriteRead_OnDisk_PreservesContent()
{
var original = MakeFixtureGfx();
var dump = GfxObjDumpSerializer.Capture(0x01000A2Bu, original);
var path = Path.Combine(Path.GetTempPath(),
$"acdream-gfxobjdump-test-{System.Guid.NewGuid():N}.json");
try
{
GfxObjDumpSerializer.Write(dump, path);
Assert.True(File.Exists(path));
var readBack = GfxObjDumpSerializer.Read(path);
Assert.Equal(dump.GfxObjId, readBack.GfxObjId);
Assert.Equal(dump.ResolvedPolygons.Count, readBack.ResolvedPolygons.Count);
var hydrated = GfxObjDumpSerializer.Hydrate(readBack);
Assert.Equal(original.Resolved.Count, hydrated.Resolved.Count);
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}
[Fact]
public void Hydrate_RecomputesCoveringSphereWhenDumpHasZeroRadius()
{
// Force the hydrate-side covering-sphere computation by capturing
// a GfxObj whose BoundingSphere is null. The hydrate path should
// compute a sphere that encloses every vertex.
var resolved = new Dictionary
{
[0x0001] = new ResolvedPolygon
{
Id = 0x0001,
NumPoints = 3,
SidesType = CullMode.Clockwise,
Plane = new Plane(new Vector3(0, 0, 1), 0f),
Vertices = new[]
{
new Vector3(0f, 0f, 0f),
new Vector3(4f, 0f, 0f),
new Vector3(0f, 3f, 0f),
},
},
};
var leaf = new PhysicsBSPNode { Type = BSPNodeType.Leaf };
leaf.Polygons.Add(0x0001);
var src = new GfxObjPhysics
{
BSP = new PhysicsBSPTree { Root = leaf },
PhysicsPolygons = new Dictionary(),
Vertices = new VertexArray(),
Resolved = resolved,
BoundingSphere = null,
};
var dump = GfxObjDumpSerializer.Capture(0x42u, src);
// BoundingSphere=null in source → radius 0 in dump.
Assert.Equal(0f, dump.BoundingSphereRadius);
var hydrated = GfxObjDumpSerializer.Hydrate(dump);
Assert.NotNull(hydrated.BoundingSphere);
Assert.True(hydrated.BoundingSphere!.Radius > 0f);
// Each vertex must lie within the covering sphere (with float-EPS slack).
foreach (var v in resolved[0x0001].Vertices)
{
float dist = Vector3.Distance(hydrated.BoundingSphere.Origin, v);
Assert.True(dist <= hydrated.BoundingSphere.Radius + 1e-4f,
$"Vertex {v} at distance {dist} exceeds covering radius {hydrated.BoundingSphere.Radius}");
}
}
private static GfxObjPhysics MakeFixtureGfx()
{
// Two polygons modelling a fragment of the cottage GfxObj (object-
// local frame): a downward-facing horizontal "cottage floor" and
// the cellar ramp (the polys live capture pinpointed).
var resolved = new Dictionary
{
[0x0004] = new ResolvedPolygon
{
Id = 0x0004,
NumPoints = 3,
SidesType = CullMode.Clockwise,
// Downward-facing floor at object-Z=1.5 (in local frame the
// cottage floor sits 1.5m above the building origin).
Plane = new Plane(new Vector3(0, 0, -1), 1.5f),
Vertices = new[]
{
new Vector3(-6.2f, 7.6f, 1.5f),
new Vector3(-10f, 7.6f, 1.5f),
new Vector3(-10f, 2.8f, 1.5f),
},
},
[0x0008] = new ResolvedPolygon
{
Id = 0x0008,
NumPoints = 4,
SidesType = CullMode.Clockwise,
Plane = new Plane(new Vector3(0f, -0.7190f, 0.6950f), -0.1007f),
Vertices = new[]
{
new Vector3( 0.8f, -1.59f, -1.5f),
new Vector3( 0.8f, 1.31f, 1.5f),
new Vector3(-0.8f, 1.31f, 1.5f),
new Vector3(-0.8f, -1.59f, -1.5f),
},
},
};
var leaf = new PhysicsBSPNode
{
Type = BSPNodeType.Leaf,
BoundingSphere = new Sphere
{
Origin = new Vector3(-5f, 4f, 0f),
Radius = 14f,
},
};
leaf.Polygons.Add(0x0004);
leaf.Polygons.Add(0x0008);
return new GfxObjPhysics
{
BSP = new PhysicsBSPTree { Root = leaf },
PhysicsPolygons = new Dictionary(),
Vertices = new VertexArray(),
Resolved = resolved,
BoundingSphere = leaf.BoundingSphere,
};
}
}