Each of these was temporary apparatus added to chase one bug, and each was supposed to be deleted in the commit that fixed it. Fourteen closed issues later they were still here: #337's support/wire-mesh trio, #171's sticky timeline, #119's viewer and entity dumps, #113's phantom probe, and a dozen more. 3,493 lines removed; the client now reads 144 environment variables instead of 161, and 47 temporary probes remain instead of 64. This is not only tidying. Every probe leaves a branch on its hot path when unset, several re-read the environment per call rather than caching, and the volume buries the diagnostics that are actually load-bearing. It is also a headless correctness matter: HeadlessStaticStateAudit reflects over PhysicsDiagnostics' flags to refuse a multi-session host when any is set, and cannot see probes that live outside that owner. Four files went entirely — WalkMissDiagnostic.cs, CollisionMeshWireframe.cs and two test files whose only subject was a deleted probe. TransitionTypes.SetContactPlane also sheds its CallerMemberName / CallerLineNumber parameters, which existed solely for #337's cpSrc= attribution and carried the instruction to strip them with the probe family; no call site passed them, so no behavior changes. F2's collision overlay survives and reverts to its proxy-cylinder form, which is what removing the ACDREAM_WIRE_MESH upgrade means. LaunchOptionsDocumentationTests earned its keep here: it refused the deletion until docs/launch-options.md moved the 17 rows into Retired and the frozen direct-read counts came down (PhysicsEngine.cs to zero, TransitionTypes.cs 3 to 2). The documentation could not drift during a cleanup this wide. The 14 probes that name no owning issue are deliberately NOT deleted. Nothing records when they became safe to remove, and guessing is how a future investigation loses apparatus it needed; #435 stays open for their attribution. Full hermetic suite 15,321 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
207 lines
7.5 KiB
C#
207 lines
7.5 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 family must be completely silent on a healthy
|
|
// moving tick.
|
|
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;
|
|
}
|
|
}
|