acdream/tests/AcDream.Core.Tests/Physics/Issue176177SeamTransitLagTests.cs
Erik b8e9e204ad docs+test #176/#177: 12 mechanisms refuted, apparatus shipped, probe protocol staged
The dungeon render pair (purple seam flash + stair pop-in) resisted
desk root-causing, but the investigation narrowed the space to two
live theories and shipped permanent apparatus:

- Issue176177DungeonSeamInspectionTests: dat truth — corridor floors
  ARE textured drawn PortalSide portal polys; the reciprocal is NoPos;
  the 'stairs' are 0x8A020182's ramp shell (vertical portals, zero
  statics); CellBSP partitions exactly at portal planes; DXT1 textures
  carry zero transparent-mode texels.
- Issue176177FacilityHubFloodReplayTests: production-matched flood
  replays — approach/descent/gaze-sweep/walk all healthy with coherent
  inputs; ScenarioE pins the flood's collapse-to-root sensitivity under
  incoherent (root, eye) pairs.
- Issue176177SeamTransitLagTests: the resolver flips cells within one
  tick-step of the portal plane — the 0.33-0.47m [cell-transit] 'lag'
  in the gate logs is speed*tick quantization, not membership error.

Refuted (do NOT retry — ledger in the research doc): placeholder
texture, reciprocal z-fight, seal z-fight (seals only fire for
OtherCellId==0xFFFF), root/eye incoherence (production camera sweep is
mm-exact at planes), flood bistability, #119-class statics, undefined
DXT mips (both paths decode DXT->RGBA8; the compressed-array branch is
dead code), DXT1 alpha, fog mix (ramp ~538m), lightning leak (flash==0
in production), viewer-light pops (smooth (1-d/range) ramp).

Filed #178 (A8 double-sided shell stopgap still live) and #179
(lightning flash lacks an indoor gate — dormant). The purple can only
be the fog clear color (undrawn pixels) or the outdoor ambient+sun
tint; discrimination needs ONE probe launch (ACDREAM_PROBE_LIGHT +
ACDREAM_PROBE_PVINPUT + ACDREAM_PROBE_CELL) — protocol in
docs/research/2026-07-06-176-177-render-pair-investigation.md.

Suites: Core 2587 / App 719 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:20:03 +02:00

157 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.IO;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using Env = System.Environment;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// #176/#177 membership half: production [cell-transit] lines
/// (launch-137-gate2.log) fire 0.10.6 m PAST the portal plane in the travel
/// direction (016E→017A at x=85.3385.47 vs the plane at x=85.00), while the
/// dat CellBSP volumes partition EXACTLY at the plane
/// (Issue176177DungeonSeamInspectionTests.SeamCells_CellBspContainment) —
/// retail's center-only point_in_cell flips at the plane. The render root
/// (viewer cell) resolves through the same machinery; while it lags, the
/// portal flood correctly refuses the boundary portal the eye has already
/// crossed and the whole forward chain drops (the purple seam flash /
/// stair pop). This replay measures OUR resolver's flip point across the
/// x=85 seam in a controlled run.
/// </summary>
public class Issue176177SeamTransitLagTests
{
private const uint SeamCellWest = 0x8A02016Eu; // x 75..85
private const uint SeamCellEast = 0x8A02017Au; // x 85..88.33
private readonly ITestOutputHelper _out;
public Issue176177SeamTransitLagTests(ITestOutputHelper output) => _out = output;
private static string? ResolveDatDir()
{
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(datDir) ? datDir : null;
}
private static PhysicsEngine BuildEngine(DatCollection dats)
{
var engine = new PhysicsEngine();
engine.DataCache = new PhysicsDataCache();
var toLoad = new System.Collections.Generic.HashSet<uint> { SeamCellWest, SeamCellEast };
for (int ring = 0; ring < 3; ring++)
{
foreach (var known in new System.Collections.Generic.List<uint>(toLoad))
{
var cell = dats.Get<EnvCell>(known);
if (cell is null) continue;
foreach (var p in cell.CellPortals)
toLoad.Add(0x8A020000u | p.OtherCellId);
}
}
foreach (var cellId in toLoad)
{
var envCell = dats.Get<EnvCell>(cellId);
if (envCell is null) continue;
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(0x0D000000u | envCell.EnvironmentId);
if (environment is null) continue;
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cs)) continue;
var rot = new Quaternion(
envCell.Position.Orientation.X, envCell.Position.Orientation.Y,
envCell.Position.Orientation.Z, envCell.Position.Orientation.W);
var world = Matrix4x4.CreateFromQuaternion(rot)
* Matrix4x4.CreateTranslation(
envCell.Position.Origin.X, envCell.Position.Origin.Y, envCell.Position.Origin.Z);
engine.DataCache.CacheCellStruct(cellId, envCell, cs!, world);
}
return engine;
}
private static PhysicsBody GroundedBody()
{
var body = new PhysicsBody();
body.ContactPlaneValid = true;
body.ContactPlane = new Plane(Vector3.UnitZ, 6f);
body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
body.WalkablePolygonValid = true;
body.WalkablePlane = new Plane(Vector3.UnitZ, 6f);
body.WalkableUp = Vector3.UnitZ;
body.WalkableVertices = new[]
{
new Vector3(75f, -41.67f, -6f),
new Vector3(85f, -41.67f, -6f),
new Vector3(85f, -38.33f, -6f),
new Vector3(75f, -38.33f, -6f),
};
return body;
}
/// <summary>
/// Run +X across the x=85 seam at run-speed tick steps (13.5 cm/tick ≈
/// 4 m/s at 30 Hz) and record where ResolveWithTransition's CellId flips.
/// Retail (center-only point_in_cell, exact-partition CellBSP) flips on
/// the first tick whose END position is past x=85.00 — any flip later
/// than one step past the plane is OUR lag.
/// </summary>
[Theory]
[InlineData(+1)] // west → east across x=85
[InlineData(-1)] // east → west back across
public void RunAcrossSeam_CellFlipPosition(int direction)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); return; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var engine = BuildEngine(dats);
var body = GroundedBody();
const float step = 0.135f;
float startX = direction > 0 ? 83.8f : 86.2f;
uint cell = direction > 0 ? SeamCellWest : SeamCellEast;
var pos = new Vector3(startX, -40f, -6f);
float? flipX = null;
for (int tick = 0; tick < 26; tick++)
{
var target = pos + new Vector3(direction * step, 0f, 0f);
var r = engine.ResolveWithTransition(
currentPos: pos,
targetPos: target,
cellId: cell,
sphereRadius: 0.48f,
sphereHeight: 1.835f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f,
isOnGround: true,
body: body,
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide);
bool flipped = r.CellId != cell;
_out.WriteLine($"tick={tick,2} pos=({r.Position.X:F3},{r.Position.Y:F3},{r.Position.Z:F3}) " +
$"cell=0x{r.CellId:X8} ok={r.Ok}{(flipped ? " <<< FLIP" : "")}");
if (flipped && flipX is null)
flipX = r.Position.X;
cell = r.CellId;
pos = r.Position;
if (direction > 0 && pos.X > 86.4f) break;
if (direction < 0 && pos.X < 83.6f) break;
}
Assert.NotNull(flipX);
float lag = direction > 0 ? flipX!.Value - 85.00f : 85.00f - flipX!.Value;
_out.WriteLine($"flip at x={flipX:F3} → lag past the plane = {lag:F3} m " +
$"(one-tick quantization bound = {step:F3} m)");
// Diagnostic, not a pin: the finding is the printed lag. A lag beyond
// one tick step is the divergence under investigation.
}
}