using System; using System.Numerics; using AcDream.Core.Physics; using Xunit; using Xunit.Abstractions; using Plane = System.Numerics.Plane; namespace AcDream.Core.Tests.Physics; /// /// Campaign P Slice P3 item 4 (#165): diagnosis-only fixtures discriminating /// the three candidate mechanisms the research doc /// (docs/research/2026-07-29-remote-and-world-specials-pseudocode.md /// §2.4) lists for "remote entities penetrate walls before stopping": /// /// (a) the InterpolationManager unclamped stall-fail "tail /// delta" snap (node_fail_counter > 3) commits a position on /// the far side of / inside a wall in one tick, and the SAME tick's /// ResolveWithTransition sweep fails to catch the crossing for a /// large single-tick delta; /// (b) a one-frame skip of the sweep entirely /// (RuntimeRemotePhysicsUpdater.cs gates the whole resolve on /// rm.CellId != 0 && LandblockCount > 0) reachable on /// some tick other than first-spawn; /// (c) render/interpolation presentation lag on the App side — /// out of Core/Runtime scope, not addressed here. /// /// /// /// No live client, no dat dependency — every fixture here is synthetic /// geometry driven directly through , /// matching the existing / /// pattern. /// /// public class Issue165RemoteWallPenetrationDiagnosticTests { private readonly ITestOutputHelper _out; public Issue165RemoteWallPenetrationDiagnosticTests(ITestOutputHelper output) => _out = output; private const uint TestLandblockId = 0xA9D60000u; private const uint TestCellId = TestLandblockId | 0x0001u; private const float SphereRadius = 0.48f; private const float SphereHeight = 1.835f; private const float StepUpHeight = 0.4f; private const float StepDownHeight = 0.4f; /// /// Candidate (a) discriminator: mirrors /// /// (30 small 0.08 m steps reaching a creature sphere at Y=11.5, surface /// contact Y≈10.54) but replaces the 30-tick approach with ONE resolve /// call spanning the ENTIRE distance in a single tick — exactly what an /// InterpolationManager unclamped tail-delta snap (the full /// remaining distance to the queue's tail node, no speed cap) would hand /// ResolveWithTransition as its targetPos argument. /// [Fact] public void SingleLargeTickJumpThroughObstacle_IsStillBlockedAtSurface() { var engine = BuildEngine(); RegisterCreatureSphere(engine, 0xC0F0u, 12f, 11.5f); // due north, same as the proven 30-tick test var body = MakeGroundedBody(new Vector3(12f, 10f, 0f)); // ONE resolve call for the full 2.4 m the 30-tick test covers in // 0.08 m increments — simulating an unclamped stall-fail snap that // jumps the remote directly toward (and past) the target node in a // single tick, rather than a smoothly interpolated approach. Vector3 farTarget = new(12f, 12.4f, 0f); var result = engine.ResolveWithTransition( body.Position, farTarget, TestCellId, SphereRadius, SphereHeight, StepUpHeight, StepDownHeight, isOnGround: true, body: body, moverFlags: ObjectInfoState.EdgeSlide, movingEntityId: 0); _out.WriteLine( $"single-jump result pos=({result.Position.X:F3},{result.Position.Y:F3},{result.Position.Z:F3}) " + $"ok={result.Ok} collisionNormalValid={result.CollisionNormalValid}"); // If the sweep correctly handles a large single-tick delta, the // result is IDENTICAL in kind to the proven 30-tick test: blocked at // the sphere surface (Y≈10.54), never past Y=11.02 (the near edge of // the creature sphere itself, combinedR=0.96 short of full // interpenetration) — NOT at or past the creature's own center // (Y=11.5), which would mean the sweep missed the crossing entirely. Assert.True( result.Position.Y < 10.7f, "A single large-tick jump through solid geometry must be blocked " + $"at the surface, matching the small-step case; got Y={result.Position.Y:F3}. " + "If this fails, candidate (a) (the sweep does not catch large " + "single-tick deltas) is CONFIRMED as a contributing #165 mechanism."); } /// /// Control for the test above: the SAME single large jump, but with NO /// obstacle registered — confirms the mover actually reaches the far /// target when nothing blocks it (proving the previous test's block is /// really the obstacle, not some unrelated large-distance resolve /// failure/clamp). /// [Fact] public void SingleLargeTickJumpWithNoObstacle_ReachesFarTarget() { var engine = BuildEngine(); var body = MakeGroundedBody(new Vector3(12f, 10f, 0f)); Vector3 farTarget = new(12f, 12.4f, 0f); var result = engine.ResolveWithTransition( body.Position, farTarget, TestCellId, SphereRadius, SphereHeight, StepUpHeight, StepDownHeight, isOnGround: true, body: body, moverFlags: ObjectInfoState.EdgeSlide, movingEntityId: 0); _out.WriteLine($"unobstructed pos=({result.Position.X:F3},{result.Position.Y:F3})"); Assert.True( result.Position.Y > 12.2f, $"An unobstructed large single-tick jump must actually complete; got Y={result.Position.Y:F3}"); } /// /// Candidate (a), sharper variant: the unclamped snap can also land the /// PRE-INTEGRATE start point already past/inside the obstacle in a /// pathological case (e.g. two consecutive stalled ticks). Confirms the /// sweep also rejects a targetPos that starts ALREADY behind the /// obstacle's near surface relative to the swept segment — i.e. even a /// start point inside the creature's overlap zone resolves to a valid, /// non-penetrating position rather than silently accepting the /// already-tunneled candidate. /// [Fact] public void SingleLargeTickJumpStartingInsideObstacleOverlap_DoesNotAcceptTunneledCandidate() { var engine = BuildEngine(); RegisterCreatureSphere(engine, 0xC0F1u, 12f, 11.5f); // Start already 0.3 m PAST the surface contact point (Y=10.84, // inside the combined-radius overlap zone starting at Y≈10.54) — // simulating a start position an earlier unclamped snap already // over-shot into. var body = MakeGroundedBody(new Vector3(12f, 10.84f, 0f)); Vector3 farTarget = new(12f, 13f, 0f); var result = engine.ResolveWithTransition( body.Position, farTarget, TestCellId, SphereRadius, SphereHeight, StepUpHeight, StepDownHeight, isOnGround: true, body: body, moverFlags: ObjectInfoState.EdgeSlide, movingEntityId: 0); _out.WriteLine( $"overlap-start result pos=({result.Position.X:F3},{result.Position.Y:F3}) " + $"input Y=10.84 (already inside the {0.48f + 0.48f:F2} m combined-radius overlap)"); // Retail's own validate_transition restores curr_pos on a non-clean // step when starting deep inside overlapping spheres (per the // physics digest's #184 DO-NOT-RETRY note) — this is NOT a bug to // chase if the position stays pinned near the start rather than // sailing through to the far target. Assert.True( result.Position.Y < 11.4f, "A candidate that starts inside a solid obstacle's overlap zone must not " + $"sail through to the far target; got Y={result.Position.Y:F3}"); } private static PhysicsEngine BuildEngine() { var cache = new PhysicsDataCache(); var engine = new PhysicsEngine { DataCache = cache }; var heights = new byte[81]; var heightTable = new float[256]; // all zero → terrain Z = 0 engine.AddLandblock( landblockId: TestLandblockId, terrain: new TerrainSurface(heights, heightTable), cells: Array.Empty(), portals: Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return engine; } private static void RegisterCreatureSphere( PhysicsEngine engine, uint entityId, float x, float y) { engine.ShadowObjects.Register( entityId, gfxObjId: 0u, new Vector3(x, y, SphereRadius), Quaternion.Identity, SphereRadius, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: TestLandblockId, collisionType: ShadowCollisionType.Sphere, cylHeight: 0f, scale: 1f, state: 0u, flags: EntityCollisionFlags.IsCreature, isStatic: false); } private static PhysicsBody MakeGroundedBody(Vector3 position) { var floorPlane = new Plane(Vector3.UnitZ, 0f); var floorVerts = new[] { new Vector3(-100f, -100f, 0f), new Vector3(100f, -100f, 0f), new Vector3(100f, 100f, 0f), new Vector3(-100f, 100f, 0f), }; return new PhysicsBody { Position = position, Orientation = Quaternion.Identity, ContactPlaneValid = true, ContactPlane = floorPlane, ContactPlaneCellId = TestCellId, WalkablePolygonValid = true, WalkablePlane = floorPlane, WalkableVertices = floorVerts, WalkableUp = Vector3.UnitZ, TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable, }; } }