Facility Hub corridor repro (probe + dat evidence, both mechanisms
pinned):
1. PortalSide portal polygons live IN CellStruct.PhysicsPolygons and
acdream collides with them as plain solid geometry. Cell
0x8A02016E's portals to 0x011E (polys 1/3/5, flags=PortalSide) are
present in the physics set while every ExactMatch portal in the
same cell is absent — the cell's rotation maps those portal planes
to the world -X wall the player hit mid-corridor
(launch-175-verify2.log:42858, n=(-1,0,0)). Retail must honor the
portal side; oracle grep required before fixing.
2. After the single seam hit, the body-persisted SlidingNormal
projects every subsequent forward offset to exactly zero in
AdjustOffset - the step aborts BEFORE any collision test can
update state (ok=False hit=no, zero advance), an absorbing wedge
escaped only by strafing ("push through on the side"). The #116
slide-response family: retail re-derives slide state per frame
(get_object_info pc:279992).
Inspection fixture: Issue137CorridorSeamInspectionTests (dumps
physics-vs-portal polygon membership for the two corridor cells).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
90 lines
4.5 KiB
C#
90 lines
4.5 KiB
C#
using System;
|
||
using System.IO;
|
||
using DatReaderWriter;
|
||
using DatReaderWriter.DBObjs;
|
||
using DatReaderWriter.Options;
|
||
using Xunit;
|
||
using Xunit.Abstractions;
|
||
using Env = System.Environment;
|
||
|
||
namespace AcDream.Core.Tests.Physics;
|
||
|
||
/// <summary>
|
||
/// #137 corridor-seam inspection (2026-07-05, Facility Hub). Live probe
|
||
/// evidence (launch-175-verify2.log:42858): crossing corridor cells
|
||
/// 0x8A02016E → 0x8A02017A at world x≈85.25 records a wall hit with normal
|
||
/// (−1,0,0) — pointing straight back against the movement — after which the
|
||
/// stale sliding normal wedges all forward motion (ok=False hit=no, offset
|
||
/// projected to zero). Question this dump answers: does cell 0x8A02017A's
|
||
/// PHYSICS polygon set contain a portal-spanning polygon at its entry plane
|
||
/// (normal ≈ ±X at the portal's local X) — i.e., are portal-sealing polys in
|
||
/// our collision set where retail filters them?
|
||
/// </summary>
|
||
public class Issue137CorridorSeamInspectionTests
|
||
{
|
||
private readonly ITestOutputHelper _out;
|
||
public Issue137CorridorSeamInspectionTests(ITestOutputHelper output) => _out = output;
|
||
|
||
[Theory]
|
||
[InlineData(0x8A02016Eu)]
|
||
[InlineData(0x8A02017Au)]
|
||
public void CorridorCell_PhysicsPolysAndPortals_DatInspection(uint envCellId)
|
||
{
|
||
var datDir = Env.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||
?? Path.Combine(Env.GetFolderPath(Env.SpecialFolder.UserProfile),
|
||
"Documents", "Asheron's Call");
|
||
if (!Directory.Exists(datDir))
|
||
{
|
||
_out.WriteLine($"SKIP: dat directory not found at {datDir}");
|
||
return;
|
||
}
|
||
|
||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||
|
||
var envCell = dats.Get<EnvCell>(envCellId);
|
||
Assert.NotNull(envCell);
|
||
_out.WriteLine($"=== EnvCell 0x{envCellId:X8} ===");
|
||
_out.WriteLine($" pos=({envCell!.Position.Origin.X:F2},{envCell.Position.Origin.Y:F2},{envCell.Position.Origin.Z:F2}) " +
|
||
$"rot=({envCell.Position.Orientation.X:F3},{envCell.Position.Orientation.Y:F3},{envCell.Position.Orientation.Z:F3},{envCell.Position.Orientation.W:F3})");
|
||
_out.WriteLine($" EnvironmentId=0x{envCell.EnvironmentId:X4} CellStructure={envCell.CellStructure}");
|
||
_out.WriteLine($" CellPortals={envCell.CellPortals.Count}");
|
||
foreach (var p in envCell.CellPortals)
|
||
_out.WriteLine($" portal poly={p.PolygonId} other=0x{p.OtherCellId:X4} flags={p.Flags}");
|
||
|
||
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(0x0D000000u | envCell.EnvironmentId);
|
||
Assert.NotNull(environment);
|
||
Assert.True(environment!.Cells.TryGetValue(envCell.CellStructure, out var cs));
|
||
|
||
_out.WriteLine($" PhysicsPolygons={cs!.PhysicsPolygons.Count} (portal-relevant normals below)");
|
||
foreach (var (id, poly) in cs.PhysicsPolygons)
|
||
{
|
||
// Compute the face normal from the vertex fan (same math as
|
||
// PhysicsDataCache.ResolvePolygons).
|
||
var verts = poly.VertexIds;
|
||
if (verts.Count < 3) continue;
|
||
if (!cs.VertexArray.Vertices.TryGetValue((ushort)verts[0], out var v0)) continue;
|
||
if (!cs.VertexArray.Vertices.TryGetValue((ushort)verts[1], out var v1)) continue;
|
||
if (!cs.VertexArray.Vertices.TryGetValue((ushort)verts[2], out var v2)) continue;
|
||
var n = System.Numerics.Vector3.Normalize(System.Numerics.Vector3.Cross(
|
||
v1.Origin - v0.Origin, v2.Origin - v0.Origin));
|
||
|
||
// Only print near-horizontal-normal polys (walls) — the seam wall
|
||
// candidates; floors/ceilings are noise here.
|
||
if (MathF.Abs(n.Z) > 0.3f) continue;
|
||
_out.WriteLine($" poly {id}: n=({n.X:F2},{n.Y:F2},{n.Z:F2}) v0=({v0.Origin.X:F2},{v0.Origin.Y:F2},{v0.Origin.Z:F2}) verts={verts.Count} sides={poly.SidesType} stip={poly.Stippling}");
|
||
}
|
||
|
||
// The portal polygons live in the VISUAL polygon set — print their
|
||
// ids so overlap with the physics set (same id space?) is visible.
|
||
_out.WriteLine($" VisualPolygons={cs.Polygons.Count}");
|
||
foreach (var p in envCell.CellPortals)
|
||
{
|
||
if (cs.Polygons.TryGetValue((ushort)p.PolygonId, out var vp))
|
||
{
|
||
_out.WriteLine($" portal-poly {p.PolygonId} IS in the visual set (verts={vp.VertexIds.Count})");
|
||
bool inPhysics = cs.PhysicsPolygons.ContainsKey((ushort)p.PolygonId);
|
||
_out.WriteLine($" portal-poly {p.PolygonId} in PHYSICS set: {inPhysics}");
|
||
}
|
||
}
|
||
}
|
||
}
|