Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Fires only on the stuck-tick predicate (>=1mm XY requested, <=0.1mm achieved), buffering per-tick phase outcomes cheaply and flushing only on a stuck tick: per-insert-attempt phase/state/normal/source, step-up enter/exit verdicts, every ValidateWalkable branch with dist/waterDepth and both SetCollisionNormal guards evaluated, and the tick's final AdjustOffset pair. Zero cost when off (flag before any allocation — the I1 zero-alloc gate stays green), mover id on every line, [ThreadStatic] buffer per the referee-safety rule. Two tests: fires on a synthetic wall-stuck tick, silent on ordinary movement. Diagnostics only; no behavioral change. Suite 11,271 / 6 / 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
7.8 KiB
C#
211 lines
7.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// #345 mechanism-session probe gate
|
|
/// (<c>docs/research/2026-08-08-345-mechanism-contract.md</c> "probe"
|
|
/// section). <c>ACDREAM_DUMP_TRANSIT_FAIL</c> /
|
|
/// <see cref="PhysicsDiagnostics.DumpTransitFailEnabled"/> 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.
|
|
///
|
|
/// <para>
|
|
/// The synthetic fixture reuses <see cref="BSPStepUpFixtures.TallWall"/> (a
|
|
/// floor at z=0 plus a 5 m wall at x=0.5, "too tall to step over" by design
|
|
/// — the same fixture <c>TransitionAllocationBaselineTests</c> 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.
|
|
/// </para>
|
|
/// </summary>
|
|
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<ushort, ResolvedPolygon> 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<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
0f,
|
|
0f);
|
|
|
|
var cache = new PhysicsDataCache();
|
|
cache.RegisterGfxObjForTest(GfxObjId, new GfxObjPhysics
|
|
{
|
|
BSP = new PhysicsBSPTree { Root = root },
|
|
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
|
|
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;
|
|
}
|
|
}
|