using System; using System.Collections.Immutable; using System.IO; using System.Numerics; using AcDream.Core.Physics; using Xunit; using Xunit.Abstractions; namespace AcDream.Core.Tests.Physics; /// /// Issue #273 — exact Holtburg tight-gap replay captured live on 2026-07-31. /// The player presses between building shell 0x01000F69 and the timber post /// at 0xCA9B4027. Retail blocks this passage; before the fix ACDream lets the /// post slide feed a displaced step-down probe into precipice-slide, which /// carries the player around the post and along the building's outer edge. /// /// The shell collision is a self-contained dump of the installed DAT's real /// PhysicsBSP. Post dimensions, building frame, player Setup spheres, and /// movement frames are copied from the live trace. /// public sealed class Issue273HoltburgTightGapReplayTests { private readonly ITestOutputHelper _output; public Issue273HoltburgTightGapReplayTests(ITestOutputHelper output) => _output = output; private const uint Landblock = 0xA9B40000u; private const uint Cell = 0xA9B40032u; private const uint ShellGfxObj = 0x01000F69u; private const uint PlayerEntity = 0x000F4243u; private static readonly Vector3 BuildingOrigin = new(158.178f, 37.7055f, 94f); private static readonly Quaternion BuildingRotation = Quaternion.Normalize(new Quaternion(0f, 0f, -0.343045f, 0.939319f)); private static readonly Matrix4x4 BuildingTransform = Matrix4x4.CreateFromQuaternion(BuildingRotation) * Matrix4x4.CreateTranslation(BuildingOrigin); private static readonly ImmutableArray PlayerSpheres = [ new FlatCollisionSphere(new Vector3(0f, 0f, 0.475f), 0.480f), new FlatCollisionSphere(new Vector3(0f, 0f, 1.350f), 0.480f), ]; private static PhysicsEngine BuildEngine( bool preparedFlat = false, bool includeShell = true, bool includeBlockingPost = true) { var cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; string dumpPath = Path.Combine( SolutionRoot(), "tests", "AcDream.Core.Tests", "Fixtures", "issue273", "0x01000F69.gfxobj.json"); Assert.True(File.Exists(dumpPath), $"Missing issue #273 fixture: {dumpPath}"); GfxObjPhysics physics = GfxObjDumpSerializer.Hydrate( GfxObjDumpSerializer.Read(dumpPath)); if (preparedFlat) { cache.CollisionTraversalMode = CollisionTraversalMode.Flat; cache.CacheGfxObj( ShellGfxObj, FlatCollisionAssetBuilder.FlattenGfxObj(physics)); } else { cache.RegisterGfxObjForTest(ShellGfxObj, physics); } // The shell is registered through retail's building channel, not as a // shadow object. Its one portal is irrelevant to this exterior sweep. if (includeShell) { cache.CacheBuilding( Cell, Array.Empty(), BuildingTransform, ShellGfxObj); } // Terrain is deliberately below the shell in the real fixture. The // no-shell counterfactual substitutes a featureless floor at the same // height so it isolates the removed shell wall/ledge while preserving // grounded movement around the remaining post. var heights = new byte[81]; var heightTable = new float[256]; Array.Fill(heightTable, includeShell ? -1000f : 96f); engine.AddLandblock( Landblock, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), 0f, 0f); if (includeBlockingPost) { RegisterPost( engine, 0xCA9B4027u, new Vector3(160.173f, 34.487f, 95.975f), radius: 0.282f); } RegisterPost( engine, 0xCA9B402Eu, new Vector3(158.282f, 34.610f, 95.975f), radius: 0.282f); RegisterPost( engine, 0xCA9B402Fu, new Vector3(157.952f, 32.239f, 96f), radius: 0.600f); return engine; } private static void RegisterPost( PhysicsEngine engine, uint entityId, Vector3 basePosition, float radius) { engine.ShadowObjects.Register( entityId, gfxObjId: 0u, worldPos: basePosition, rotation: Quaternion.Identity, radius, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: Landblock, collisionType: ShadowCollisionType.Cylinder, cylHeight: 5.564f, state: 0u, seedCellId: Cell, isStatic: true); } private static PhysicsBody GroundedBody(Vector3 position) { Vector3[] localWalkable = [ new(4f, 7.25f, 2f), new(3.3f, 6.5003f, 2f), new(3.3f, -2.0157f, 2f), new(4f, -4.7f, 2f), ]; var worldWalkable = new Vector3[localWalkable.Length]; for (int i = 0; i < localWalkable.Length; i++) worldWalkable[i] = Vector3.Transform(localWalkable[i], BuildingTransform); var floor = new Plane(Vector3.UnitZ, -96f); return new PhysicsBody { Position = position, Orientation = Quaternion.Identity, ContactPlaneValid = true, ContactPlane = floor, ContactPlaneCellId = Cell, WalkablePolygonValid = true, WalkablePlane = floor, WalkableUp = Vector3.UnitZ, WalkableVertices = worldWalkable, TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, }; } [Theory] [InlineData(1.239f, true)] [InlineData(1.241f, false)] public void RetailHalfRadiusSupport_RejectsCenterBeyondQuarterMeterEdge( float centerX, bool expected) { Vector3[] floor = [ new(0f, 0f, 0f), new(1f, 0f, 0f), new(1f, 1f, 0f), new(0f, 1f, 0f), ]; // The player's 0.48 m foot sphere becomes a 0.24 m support sphere in // SPHEREPATH::check_walkables. Just inside that boundary is supported; // just outside it is not, even though the full movement sphere still // overlaps the floor polygon. bool supported = BSPQuery.CheckWalkableSupport( new Plane(Vector3.UnitZ, 0f), floor, new Vector3(centerX, 0.5f, 0.48f), supportRadius: 0.24f, Vector3.UnitZ); Assert.Equal(expected, supported); } [Fact] public void CapturedRun_DoesNotSqueezeBetweenPostAndBuilding() { ReplayFrame[] trace = RunCapturedReplay(BuildEngine()); Vector3 position = trace[^1].Result.Position; // The captured broken run reached (163.234, 36.982) by this point, // already beyond the post and sliding along the building. Retail // blocks the passage before the player can cross the post's Y. Assert.True( position.Y < 34.487f, $"Player squeezed through the retail-blocked gap: " + $"final=({position.X:F3},{position.Y:F3},{position.Z:F3})."); } [Fact] public void CapturedRun_PreparedFlatMatchesGraphForEveryFrame() { ReplayFrame[] graph = RunCapturedReplay(BuildEngine()); ReplayFrame[] preparedFlat = RunCapturedReplay( BuildEngine(preparedFlat: true)); Assert.Equal(graph.Length, preparedFlat.Length); for (int frame = 0; frame < graph.Length; frame++) AssertFrameBitwise(graph[frame], preparedFlat[frame], frame); } [Theory] [InlineData(false, true, "shell")] [InlineData(true, false, "blocking post")] public void CapturedRun_RequiresBothShellAndBlockingPost( bool includeShell, bool includeBlockingPost, string omittedGeometry) { PhysicsEngine engine = BuildEngine( includeShell: includeShell, includeBlockingPost: includeBlockingPost); PhysicsBody body = GroundedBody(new Vector3(160.016f, 33.562f, 96.005f)); if (!includeShell) { // Do not retain the removed shell through the body's remembered // support polygon. The contact plane keeps the counterfactual at // the captured height while the shell geometry itself is absent. body.WalkablePolygonValid = false; body.WalkableVertices = null; } ReplayFrame[] trace = RunCapturedReplay(engine, body); Vector3 final = trace[^1].Result.Position; Assert.True( final.Y > 34.487f, $"Omitting the {omittedGeometry} still reproduced the complete " + $"fixture's block: final=({final.X:F3},{final.Y:F3},{final.Z:F3})."); } private ReplayFrame[] RunCapturedReplay( PhysicsEngine engine, PhysicsBody? body = null) { Vector3 position = new(160.016f, 33.562f, 96.005f); body ??= GroundedBody(position); uint cell = Cell; // First frame is copied verbatim from the live capture. Subsequent // held-forward frames use the stable displacement visible in that // same trace after input acceleration settles. Vector3[] offsets = [ new(1.396f, 1.123f, 0f), new(0.727f, 0.584f, 0f), new(0.624f, 0.501f, 0f), new(0.727f, 0.585f, 0f), new(0.728f, 0.585f, 0f), new(0.727f, 0.585f, 0f), new(0.727f, 0.585f, 0f), new(0.728f, 0.585f, 0f), ]; var trace = new ReplayFrame[offsets.Length]; for (int frame = 0; frame < offsets.Length; frame++) { ResolveResult result = engine.ResolveWithTransition( currentPos: position, targetPos: position + offsets[frame], cellId: cell, sphereRadius: 0.48f, sphereHeight: 1.835f, stepUpHeight: 0.6f, stepDownHeight: 1.5f, isOnGround: true, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: PlayerEntity, sphereList: PlayerSpheres, sphereScale: 1f); _output.WriteLine( $"f{frame}: in=({position.X:F3},{position.Y:F3},{position.Z:F3}) " + $"out=({result.Position.X:F3},{result.Position.Y:F3},{result.Position.Z:F3}) " + $"hit={result.CollisionNormalValid} " + $"normal=({result.CollisionNormal.X:F3}," + $"{result.CollisionNormal.Y:F3},{result.CollisionNormal.Z:F3})"); position = result.Position; cell = result.CellId; body.Position = position; trace[frame] = new ReplayFrame( result, body.TransientState, body.ContactPlaneValid, body.ContactPlane, body.ContactPlaneCellId, body.ContactPlaneIsWater, body.WalkablePolygonValid, body.WalkablePlane, body.WalkableUp, body.SlidingNormal); } return trace; } private static void AssertFrameBitwise( ReplayFrame expected, ReplayFrame actual, int frame) { string context = $"frame {frame}"; AssertVectorBitwise(expected.Result.Position, actual.Result.Position, context); Assert.Equal(expected.Result.CellId, actual.Result.CellId); Assert.Equal(expected.Result.IsOnGround, actual.Result.IsOnGround); Assert.Equal( expected.Result.CollisionNormalValid, actual.Result.CollisionNormalValid); AssertVectorBitwise( expected.Result.CollisionNormal, actual.Result.CollisionNormal, context); Assert.Equal(expected.Result.Ok, actual.Result.Ok); AssertQuaternionBitwise( expected.Result.Orientation, actual.Result.Orientation, context); Assert.Equal(expected.Result.InContact, actual.Result.InContact); Assert.Equal(expected.Result.OnWalkable, actual.Result.OnWalkable); AssertPlaneBitwise( expected.Result.ContactPlane, actual.Result.ContactPlane, context); Assert.Equal( expected.Result.ContactPlaneCellId, actual.Result.ContactPlaneCellId); Assert.Equal( expected.Result.ContactPlaneIsWater, actual.Result.ContactPlaneIsWater); Assert.Equal(expected.TransientState, actual.TransientState); Assert.Equal(expected.ContactPlaneValid, actual.ContactPlaneValid); AssertPlaneBitwise(expected.ContactPlane, actual.ContactPlane, context); Assert.Equal(expected.ContactPlaneCellId, actual.ContactPlaneCellId); Assert.Equal(expected.ContactPlaneIsWater, actual.ContactPlaneIsWater); Assert.Equal(expected.WalkablePolygonValid, actual.WalkablePolygonValid); AssertPlaneBitwise(expected.WalkablePlane, actual.WalkablePlane, context); AssertVectorBitwise(expected.WalkableUp, actual.WalkableUp, context); AssertVectorBitwise(expected.SlidingNormal, actual.SlidingNormal, context); } private static void AssertVectorBitwise( Vector3 expected, Vector3 actual, string context) { AssertFloatBitwise(expected.X, actual.X, $"{context}.X"); AssertFloatBitwise(expected.Y, actual.Y, $"{context}.Y"); AssertFloatBitwise(expected.Z, actual.Z, $"{context}.Z"); } private static void AssertQuaternionBitwise( Quaternion expected, Quaternion actual, string context) { AssertFloatBitwise(expected.X, actual.X, $"{context}.X"); AssertFloatBitwise(expected.Y, actual.Y, $"{context}.Y"); AssertFloatBitwise(expected.Z, actual.Z, $"{context}.Z"); AssertFloatBitwise(expected.W, actual.W, $"{context}.W"); } private static void AssertPlaneBitwise( Plane expected, Plane actual, string context) { AssertVectorBitwise(expected.Normal, actual.Normal, $"{context}.Normal"); AssertFloatBitwise(expected.D, actual.D, $"{context}.D"); } private static void AssertFloatBitwise( float expected, float actual, string context) => Assert.True( BitConverter.SingleToInt32Bits(expected) == BitConverter.SingleToInt32Bits(actual), $"{context}: expected 0x{BitConverter.SingleToInt32Bits(expected):X8}, " + $"actual 0x{BitConverter.SingleToInt32Bits(actual):X8}"); private readonly record struct ReplayFrame( ResolveResult Result, TransientStateFlags TransientState, bool ContactPlaneValid, Plane ContactPlane, uint ContactPlaneCellId, bool ContactPlaneIsWater, bool WalkablePolygonValid, Plane WalkablePlane, Vector3 WalkableUp, Vector3 SlidingNormal); private static string SolutionRoot() { string? directory = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(directory)) { if (File.Exists(Path.Combine(directory, "AcDream.slnx"))) return directory; directory = Path.GetDirectoryName(directory); } throw new InvalidOperationException( $"Could not locate AcDream.slnx from {AppContext.BaseDirectory}."); } }