using System; using System.Collections.Generic; using System.IO; using System.Numerics; using AcDream.Core.Physics; using DatReaderWriter.Types; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// #345 mechanism-session probe gate /// (docs/research/2026-08-08-345-mechanism-contract.md "probe" /// section). ACDREAM_DUMP_TRANSIT_FAIL / /// must fire on a /// tick that requests real XY movement and delivers none (the stuck-tick /// fingerprint from the #345 uphill capture: a resolve returning a position /// byte-identical to the input against a nonzero request), and must stay /// silent on an ordinary moving tick — a healthy session prints nothing at /// all. /// /// /// The synthetic fixture reuses (a /// floor at z=0 plus a 5 m wall at x=0.5, "too tall to step over" by design /// — the same fixture TransitionAllocationBaselineTests already /// drives with an identical player profile) with the sphere already resting /// flush against the wall and a purely perpendicular (no lateral component) /// movement request, so the whole requested displacement is expected to be /// absorbed by the wall's contact-plane projection with nothing left to /// slide along. /// /// public sealed class TransitFailProbeTests { private const uint CellId = 0xA9B40001u; private const uint GfxObjId = 0x0100F100u; [Fact] public void Probe_FiresOnSyntheticStuckTick_WallAbsorbsWholeRequest() { var (root, resolved) = BSPStepUpFixtures.TallWall(); var engine = BuildEngine(root, resolved); var body = new PhysicsBody(); ResetBody(body); PhysicsDiagnostics.DumpTransitFailEnabled = true; var saved = Console.Out; var sw = new StringWriter(); Console.SetOut(sw); ResolveResult result; try { // Sphere resting flush against the wall (wall at x=0.5, radius // 0.2 -> resting x=0.3), requesting a further 0.3 m straight // into it with zero lateral (Y) component — the wall's contact // normal is pure -X, so there is no crease direction for a // slide to preserve. result = engine.ResolveWithTransition( currentPos: new Vector3(0.30f, 0f, 0.20f), targetPos: new Vector3(0.60f, 0f, 0.20f), cellId: CellId, sphereRadius: BSPStepUpFixtures.SphereRadius, sphereHeight: 1.20f, stepUpHeight: 0.60f, stepDownHeight: 1.50f, isOnGround: true, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0x5000000Au); } finally { Console.SetOut(saved); PhysicsDiagnostics.DumpTransitFailEnabled = false; } string log = sw.ToString(); // The stuck-tick predicate must have fired: requested ~0.30 m of // XY, delivered essentially none. Assert.Contains("[transit-fail]", log); Assert.Contains("STUCK-TICK", log); Assert.Contains("mover=0x5000000A", log); // At least one buffered TransitionalInsert-attempt line must have // flushed with it — proves the buffer-then-flush plumbing actually // carried per-tick detail through to the stuck-tick report, not // just the summary line. Assert.Contains("[transit-fail-insert]", log); float actualDx = result.Position.X - 0.30f; float actualDy = result.Position.Y - 0f; float actualXYLen = MathF.Sqrt(actualDx * actualDx + actualDy * actualDy); Assert.True( actualXYLen < 0.01f, $"expected the wall to absorb ~all requested XY movement, " + $"actual XY delta length={actualXYLen:F5} (position=" + $"{result.Position.X:F4},{result.Position.Y:F4},{result.Position.Z:F4})"); } [Fact] public void Probe_StaysSilentOnOrdinaryMovingTick() { var (root, resolved) = BSPStepUpFixtures.TallWall(); var engine = BuildEngine(root, resolved); var body = new PhysicsBody(); ResetBody(body); PhysicsDiagnostics.DumpTransitFailEnabled = true; var saved = Console.Out; var sw = new StringWriter(); Console.SetOut(sw); ResolveResult result; try { // Same floor, same player profile, but walking parallel to the // wall (along -Y) far from x=0.5 — nothing should block this // move at all. result = engine.ResolveWithTransition( currentPos: new Vector3(-1.50f, 0.00f, 0.20f), targetPos: new Vector3(-1.50f, -0.30f, 0.20f), cellId: CellId, sphereRadius: BSPStepUpFixtures.SphereRadius, sphereHeight: 1.20f, stepUpHeight: 0.60f, stepDownHeight: 1.50f, isOnGround: true, body: body, moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide, movingEntityId: 0x5000000Au); } finally { Console.SetOut(saved); PhysicsDiagnostics.DumpTransitFailEnabled = false; } string log = sw.ToString(); // The probe's own families must be completely silent on a healthy // moving tick. (Console.Out may still carry unrelated one-shot // process diagnostics — e.g. the #338 AnnounceStepHeightProbeOnce // self-report, which fires unconditionally on the first IsPlayer // resolve in the process regardless of any flag — so this checks // the probe's own tag rather than asserting total silence.) Assert.DoesNotContain("[transit-fail", log); float actualDy = result.Position.Y - 0.00f; Assert.True( MathF.Abs(actualDy) > 0.20f, $"expected the open-floor move to actually advance in Y, " + $"actual Y={result.Position.Y:F4}"); } private static void ResetBody(PhysicsBody body) { body.State = PhysicsStateFlags.Gravity; body.TransientState = TransientStateFlags.Active; body.ContactPlaneValid = false; body.WalkablePolygonValid = false; body.WalkableVertices = null; body.SlidingNormal = Vector3.Zero; body.FramesStationaryFall = 0; } private static PhysicsEngine BuildEngine( PhysicsBSPNode root, Dictionary resolved) { var heights = new byte[81]; var heightTable = new float[256]; Array.Fill(heightTable, -50f); var engine = new PhysicsEngine(); engine.AddLandblock( 0xA9B4FFFFu, new TerrainSurface(heights, heightTable), Array.Empty(), Array.Empty(), 0f, 0f); var cache = new PhysicsDataCache(); cache.RegisterGfxObjForTest(GfxObjId, new GfxObjPhysics { BSP = new PhysicsBSPTree { Root = root }, PhysicsPolygons = new Dictionary(), Vertices = new VertexArray(), Resolved = resolved, BoundingSphere = new Sphere { Origin = new Vector3(0f, 0f, 2.5f), Radius = 10f, }, }); engine.DataCache = cache; engine.ShadowObjects.Register( entityId: GfxObjId, gfxObjId: GfxObjId, worldPos: Vector3.Zero, rotation: Quaternion.Identity, radius: 10f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: 0xA9B4FFFFu, collisionType: ShadowCollisionType.BSP, scale: 1f); return engine; } }