Followed up the geometry-gap diagnosis with a wider polygon search. Result: the cottage's north exterior wall east of doorway DOES exist in cottage GfxObj 0x01000A2B (polys 0x0032, 0x0033) at world X=[133.5, 136.3], Y=17.10, Z=[94, 97], normal +Y. Symmetric polys cover the west side and above the doorway lintel. The wall SHOULD block sphere at X=133.655 (sphere west edge at 133.175 overlaps wall X range; sphere south edge at 17.11 aligns with wall at Y=17.10). New hypothesis: the bug is sphere-vs-corner collision at the meeting point of cell 0x0150's east wall (X=133.5, Y=[16.5, 17.1]) and the cottage's north exterior wall (X=[133.5, 136.3], Y=17.10). Cell transit data shows sphere going from X=132.859 entering alcove to X=134.022 leaving alcove — sphere reached X=134.022 INSIDE cottage geometry somehow. The sliding along the slab east face (cn=(+1,0,0) in captured tick 3254) gradually pushes sphere east. Eventually it shifts past X=133.5 — the corner where alcove east wall meets cottage north wall. The corner-handling in our BSP collision may incorrectly let the sphere slide past, or the alcove cell's east wall and cottage GfxObj's north wall don't compose correctly at the corner. Diagnostic apparatus extensions: - HoltburgLandblockStatics_DatInspection: dumps LandBlockInfo for landblock 0xA9B4. Shows 114 stabs + 12 buildings. The cottage IS Building[6] with modelId=0x01000A2B (the GfxObj we already loaded). - Diagnostic_CottagePolys_NearWalkthroughPosition: widened search reveals the cottage's full north exterior wall geometry. - HoltburgCottage_CellPortals_DatInspection: extended with cell PhysicsPolygon world-frame dump (already in prior commit). Full updated handoff: docs/research/2026-05-25-door-bug-inside-out-geometry-gap.md Next-session move: add a "sphere walks +Y from inside alcove at X near 133" test. If harness slides past the corner like production, investigate BSPQuery's sphere-vs-edge case. If harness blocks at corner, the bug is elsewhere (cell 0x0150 BSP not queried, or cottage GfxObj BSP traversal misses the wall poly). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
444 lines
21 KiB
C#
444 lines
21 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Options;
|
|
using Xunit;
|
|
using Xunit.Abstractions;
|
|
using Env = System.Environment;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
/// <summary>
|
|
/// A6.P4 door fix (2026-05-24) — read-only dat inspection. Opens
|
|
/// the real client dat collection and reports the raw state of door
|
|
/// Setup 0x020019FF + its three referenced GfxObjs (0x010044B5 and
|
|
/// 0x010044B6 used twice). Answers the question "does the leaf GfxObj
|
|
/// 0x010044B6 actually have a non-null PhysicsBSP in the dat, or is it
|
|
/// genuinely visual-only?" without going through PhysicsDataCache's
|
|
/// four early-return filters, and without launching the live client.
|
|
///
|
|
/// <para>
|
|
/// SKIP if ACDREAM_DAT_DIR (or the default
|
|
/// %USERPROFILE%\Documents\Asheron's Call directory) isn't present —
|
|
/// keeps CI green. Local developer runs always have it.
|
|
/// </para>
|
|
/// </summary>
|
|
public class DoorSetupGfxObjInspectionTests
|
|
{
|
|
private readonly ITestOutputHelper _out;
|
|
public DoorSetupGfxObjInspectionTests(ITestOutputHelper output) => _out = output;
|
|
|
|
private const uint DoorSetupId = 0x020019FFu;
|
|
|
|
/// <summary>
|
|
/// Read door setup 0x020019FF and every GfxObj id it references.
|
|
/// Emits a structured report so we can decide whether the "BSP=null
|
|
/// on 0x010044B6" handoff finding is a data fact or a load-order /
|
|
/// loader-bug artifact.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DoorSetup_AndParts_DatInspection()
|
|
{
|
|
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 setup = dats.Get<Setup>(DoorSetupId);
|
|
Assert.NotNull(setup);
|
|
|
|
_out.WriteLine($"=== Setup 0x{DoorSetupId:X8} ===");
|
|
_out.WriteLine($" Flags = {setup!.Flags} (0x{(uint)setup.Flags:X8})");
|
|
_out.WriteLine($" Radius = {setup.Radius:F4}");
|
|
_out.WriteLine($" Height = {setup.Height:F4}");
|
|
_out.WriteLine($" StepUp = {setup.StepUpHeight:F4}");
|
|
_out.WriteLine($" StepDown = {setup.StepDownHeight:F4}");
|
|
_out.WriteLine($" CylSpheres = {setup.CylSpheres.Count}");
|
|
for (int i = 0; i < setup.CylSpheres.Count; i++)
|
|
{
|
|
var c = setup.CylSpheres[i];
|
|
_out.WriteLine($" [{i}] r={c.Radius:F4} h={c.Height:F4} origin=({c.Origin.X:F3},{c.Origin.Y:F3},{c.Origin.Z:F3})");
|
|
}
|
|
_out.WriteLine($" Spheres = {setup.Spheres.Count}");
|
|
for (int i = 0; i < setup.Spheres.Count; i++)
|
|
{
|
|
var s = setup.Spheres[i];
|
|
_out.WriteLine($" [{i}] r={s.Radius:F4} origin=({s.Origin.X:F3},{s.Origin.Y:F3},{s.Origin.Z:F3})");
|
|
}
|
|
_out.WriteLine($" Parts = {setup.Parts.Count}");
|
|
for (int i = 0; i < setup.Parts.Count; i++)
|
|
{
|
|
_out.WriteLine($" [{i}] gfxObj=0x{setup.Parts[i]:X8}");
|
|
}
|
|
_out.WriteLine($" PlacementFrames = {setup.PlacementFrames.Count}");
|
|
foreach (var kv in setup.PlacementFrames)
|
|
{
|
|
_out.WriteLine($" [{kv.Key}] frameCount={kv.Value.Frames.Count}");
|
|
for (int i = 0; i < kv.Value.Frames.Count; i++)
|
|
{
|
|
var f = kv.Value.Frames[i];
|
|
_out.WriteLine($" frame[{i}] pos=({f.Origin.X:F3},{f.Origin.Y:F3},{f.Origin.Z:F3}) " +
|
|
$"rot=({f.Orientation.X:F3},{f.Orientation.Y:F3},{f.Orientation.Z:F3},{f.Orientation.W:F3})");
|
|
}
|
|
}
|
|
_out.WriteLine($" HoldingLocations = {setup.HoldingLocations.Count}");
|
|
|
|
// Inspect each unique part GfxObj.
|
|
var uniqueGfxIds = setup.Parts.Distinct().ToList();
|
|
foreach (uint gfxId in uniqueGfxIds)
|
|
{
|
|
_out.WriteLine("");
|
|
InspectGfxObj(dats, gfxId);
|
|
}
|
|
|
|
_out.WriteLine("");
|
|
_out.WriteLine("=== Cache predicate would skip a part if: ===");
|
|
_out.WriteLine(" !Flags.HasFlag(HasPhysics) OR PhysicsBSP?.Root is null OR VertexArray is null");
|
|
_out.WriteLine("(See src/AcDream.Core/Physics/PhysicsDataCache.cs:44-47)");
|
|
}
|
|
|
|
private void InspectGfxObj(DatCollection dats, uint gfxId)
|
|
{
|
|
_out.WriteLine($"=== GfxObj 0x{gfxId:X8} ===");
|
|
var gfx = dats.Get<GfxObj>(gfxId);
|
|
if (gfx is null)
|
|
{
|
|
_out.WriteLine($" Get<GfxObj>(0x{gfxId:X8}) returned NULL — dat reader couldn't load");
|
|
return;
|
|
}
|
|
|
|
_out.WriteLine($" Flags = {gfx.Flags} (0x{(uint)gfx.Flags:X8})");
|
|
_out.WriteLine($" HasPhysics = {gfx.Flags.HasFlag(GfxObjFlags.HasPhysics)}");
|
|
_out.WriteLine($" HasDIDDegrade = {gfx.Flags.HasFlag(GfxObjFlags.HasDIDDegrade)}");
|
|
|
|
_out.WriteLine($" VertexArray = {(gfx.VertexArray is null ? "NULL" : $"non-null, {gfx.VertexArray.Vertices?.Count ?? 0} vertices")}");
|
|
_out.WriteLine($" PhysicsPolygons = {(gfx.PhysicsPolygons is null ? "NULL" : $"{gfx.PhysicsPolygons.Count} polys")}");
|
|
_out.WriteLine($" PhysicsBSP = {(gfx.PhysicsBSP is null ? "NULL" : "non-null")}");
|
|
if (gfx.PhysicsBSP is not null)
|
|
{
|
|
_out.WriteLine($" PhysicsBSP.Root = {(gfx.PhysicsBSP.Root is null ? "NULL" : "non-null")}");
|
|
if (gfx.PhysicsBSP.Root is { } root)
|
|
{
|
|
_out.WriteLine($" Root.Type = {root.Type}");
|
|
_out.WriteLine($" Root.Polygons = {root.Polygons?.Count ?? 0}");
|
|
_out.WriteLine($" Root.PosNode = {(root.PosNode is null ? "null" : "non-null")}");
|
|
_out.WriteLine($" Root.NegNode = {(root.NegNode is null ? "null" : "non-null")}");
|
|
var bs = root.BoundingSphere;
|
|
if (bs is null)
|
|
_out.WriteLine(" Root.BoundingSphere = null");
|
|
else
|
|
_out.WriteLine($" Root.BoundingSphere = ({bs.Origin.X:F3},{bs.Origin.Y:F3},{bs.Origin.Z:F3}) r={bs.Radius:F3}");
|
|
|
|
// Walk the tree to count total polygons.
|
|
int totalPolys = CountTotalPolys(root);
|
|
_out.WriteLine($" BSP tree total polys (including children) = {totalPolys}");
|
|
}
|
|
}
|
|
|
|
_out.WriteLine($" Polygons (visual) = {(gfx.Polygons is null ? "NULL" : $"{gfx.Polygons.Count} polys")}");
|
|
if (gfx.DrawingBSP is null)
|
|
{
|
|
_out.WriteLine($" DrawingBSP = NULL");
|
|
}
|
|
else
|
|
{
|
|
_out.WriteLine($" DrawingBSP = non-null");
|
|
_out.WriteLine($" DrawingBSP.Root = {(gfx.DrawingBSP.Root is null ? "NULL" : "non-null")}");
|
|
}
|
|
|
|
// If physics data is present, dump first few polygon AABBs in local frame
|
|
// — these tell us WHERE collision-bearing geometry sits relative to the
|
|
// door entity origin (helps distinguish "frame top above doorway" vs
|
|
// "leaf spanning the threshold").
|
|
if (gfx.PhysicsPolygons is { Count: > 0 } polys
|
|
&& gfx.VertexArray?.Vertices is { } verts && verts.Count > 0)
|
|
{
|
|
_out.WriteLine($" PhysicsPolygon AABB sweep (first 6 polys):");
|
|
int dumped = 0;
|
|
foreach (var (pid, p) in polys)
|
|
{
|
|
if (dumped++ >= 6) break;
|
|
float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue;
|
|
float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue;
|
|
int nVerts = p.VertexIds.Count;
|
|
for (int i = 0; i < nVerts; i++)
|
|
{
|
|
if (!verts.TryGetValue((ushort)p.VertexIds[i], out var sv)) { nVerts = -1; break; }
|
|
var v = sv.Origin;
|
|
if (v.X < minX) minX = v.X;
|
|
if (v.Y < minY) minY = v.Y;
|
|
if (v.Z < minZ) minZ = v.Z;
|
|
if (v.X > maxX) maxX = v.X;
|
|
if (v.Y > maxY) maxY = v.Y;
|
|
if (v.Z > maxZ) maxZ = v.Z;
|
|
}
|
|
if (nVerts < 0) { _out.WriteLine($" [0x{pid:X4}] vertex lookup failed"); continue; }
|
|
_out.WriteLine($" [0x{pid:X4}] nVerts={nVerts} sides={p.SidesType} " +
|
|
$"min=({minX:F3},{minY:F3},{minZ:F3}) max=({maxX:F3},{maxY:F3},{maxZ:F3})");
|
|
}
|
|
if (polys.Count > 6)
|
|
_out.WriteLine($" ... ({polys.Count - 6} more)");
|
|
|
|
// Total physics polygon AABB
|
|
float gMinX = float.MaxValue, gMinY = float.MaxValue, gMinZ = float.MaxValue;
|
|
float gMaxX = float.MinValue, gMaxY = float.MinValue, gMaxZ = float.MinValue;
|
|
foreach (var (_, p) in polys)
|
|
{
|
|
int nVerts = p.VertexIds.Count;
|
|
for (int i = 0; i < nVerts; i++)
|
|
{
|
|
if (!verts.TryGetValue((ushort)p.VertexIds[i], out var sv)) continue;
|
|
var v = sv.Origin;
|
|
if (v.X < gMinX) gMinX = v.X;
|
|
if (v.Y < gMinY) gMinY = v.Y;
|
|
if (v.Z < gMinZ) gMinZ = v.Z;
|
|
if (v.X > gMaxX) gMaxX = v.X;
|
|
if (v.Y > gMaxY) gMaxY = v.Y;
|
|
if (v.Z > gMaxZ) gMaxZ = v.Z;
|
|
}
|
|
}
|
|
_out.WriteLine($" PhysicsPolygons combined AABB: min=({gMinX:F3},{gMinY:F3},{gMinZ:F3}) max=({gMaxX:F3},{gMaxY:F3},{gMaxZ:F3})");
|
|
_out.WriteLine($" size=({gMaxX - gMinX:F3},{gMaxY - gMinY:F3},{gMaxZ - gMinZ:F3})");
|
|
}
|
|
}
|
|
|
|
private static int CountTotalPolys(DatReaderWriter.Types.PhysicsBSPNode node)
|
|
{
|
|
int n = node.Polygons?.Count ?? 0;
|
|
if (node.PosNode is not null) n += CountTotalPolys(node.PosNode);
|
|
if (node.NegNode is not null) n += CountTotalPolys(node.NegNode);
|
|
return n;
|
|
}
|
|
|
|
// ── A6.P4 door bug (2026-05-24, apparatus-replay extension) ──
|
|
// Dat-direct EnvCell portal inspection. The trajectory-replay
|
|
// harness (DoorBugTrajectoryReplayTests) proved the door BSP is
|
|
// never queried at indoor primary cell 0xA9B40150 — no [bsp-test]
|
|
// line fires because GetNearbyObjects returns zero shadows. The
|
|
// CellTransit.FindCellSet portal traversal isn't surfacing
|
|
// outdoor cell 0xA9B40029 from indoor cell 0xA9B40150. This test
|
|
// reads the relevant cells' raw portal lists from the dat so we
|
|
// can determine whether:
|
|
// (a) cell 0xA9B40150 has NO portal with OtherCellId=0xFFFF
|
|
// (no exit-portal → exitOutside is never set → bug is in
|
|
// the building-shell-transit path, not the cell-graph)
|
|
// (b) cell 0xA9B40150 HAS such a portal but FindTransitCellsSphere's
|
|
// sphere-vs-plane test rejects it at the player's position
|
|
// (c) the portal exists and fires correctly, but AddAllOutsideCells
|
|
// computes a different outdoor cell than 0xA9B40029
|
|
// Findings drive the fix direction.
|
|
[Fact]
|
|
public void HoltburgCottage_CellPortals_DatInspection()
|
|
{
|
|
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);
|
|
|
|
// Cells from the captured walkthrough:
|
|
// 0xA9B40150 — indoor doorway cell (failing tick primary cell)
|
|
// 0xA9B4013F — indoor cottage interior cell (player started here)
|
|
// 0xA9B40029 — outdoor cell where door is registered
|
|
var cellIds = new uint[] { 0xA9B40150u, 0xA9B4013Fu, 0xA9B40029u };
|
|
foreach (uint cellId in cellIds)
|
|
{
|
|
_out.WriteLine("");
|
|
InspectCell(dats, cellId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A6.P4 door inside-out (2026-05-25 late) — inspects LandBlockInfo
|
|
/// 0xA9B4FFFE to identify all static entities at the Holtburg
|
|
/// cottage doorway area. The captured walkthrough has sphere at
|
|
/// world (133.655, 17.59) — no cottage GfxObj polys exist there.
|
|
/// Maybe a different entity (stab object, second cottage GfxObj,
|
|
/// building wall sub-piece) lives at that XY.
|
|
/// </summary>
|
|
[Fact]
|
|
public void HoltburgLandblockStatics_DatInspection()
|
|
{
|
|
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);
|
|
|
|
// Landblock 0xA9B4 — the captured Holtburg cottage area.
|
|
// LandBlockInfo id = (landblockId & 0xFFFF0000) | 0xFFFE
|
|
var lbInfo = dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(0xA9B4FFFEu);
|
|
if (lbInfo is null)
|
|
{
|
|
_out.WriteLine("LandBlockInfo 0xA9B4FFFE: NULL");
|
|
return;
|
|
}
|
|
|
|
_out.WriteLine($"=== LandBlockInfo 0xA9B4FFFE ===");
|
|
_out.WriteLine($" NumCells = {lbInfo.NumCells}");
|
|
_out.WriteLine($" Objects = {lbInfo.Objects.Count} (landblock stabs)");
|
|
_out.WriteLine($" Buildings = {lbInfo.Buildings.Count}");
|
|
|
|
// The captured walkthrough sphere position.
|
|
const float SphereX = 133.655f;
|
|
const float SphereY = 17.59f;
|
|
const float Window = 4f; // search window
|
|
|
|
_out.WriteLine("");
|
|
_out.WriteLine($"=== Stabs (Objects) within {Window} m of sphere XY ({SphereX:F2}, {SphereY:F2}) ===");
|
|
int stabHits = 0;
|
|
for (int i = 0; i < lbInfo.Objects.Count; i++)
|
|
{
|
|
var stab = lbInfo.Objects[i];
|
|
float dx = stab.Frame.Origin.X - SphereX;
|
|
float dy = stab.Frame.Origin.Y - SphereY;
|
|
float dist = MathF.Sqrt(dx * dx + dy * dy);
|
|
if (dist > Window) continue;
|
|
stabHits++;
|
|
_out.WriteLine($" [{i}] id=0x{stab.Id:X8} pos=({stab.Frame.Origin.X:F3},{stab.Frame.Origin.Y:F3},{stab.Frame.Origin.Z:F3}) dist={dist:F3}");
|
|
}
|
|
_out.WriteLine($" Total stab hits: {stabHits}");
|
|
|
|
_out.WriteLine("");
|
|
_out.WriteLine($"=== ALL Buildings (sorted by distance to sphere) ===");
|
|
var buildings = lbInfo.Buildings
|
|
.Select((b, i) => new {
|
|
Idx = i, B = b,
|
|
Dist = MathF.Sqrt(
|
|
(b.Frame.Origin.X - SphereX) * (b.Frame.Origin.X - SphereX) +
|
|
(b.Frame.Origin.Y - SphereY) * (b.Frame.Origin.Y - SphereY))
|
|
})
|
|
.OrderBy(x => x.Dist)
|
|
.Take(6)
|
|
.ToList();
|
|
foreach (var x in buildings)
|
|
{
|
|
_out.WriteLine($" [{x.Idx}] modelId=0x{x.B.ModelId:X8} pos=({x.B.Frame.Origin.X:F3},{x.B.Frame.Origin.Y:F3},{x.B.Frame.Origin.Z:F3}) dist={x.Dist:F3} portals={x.B.Portals.Count} numLeaves={x.B.NumLeaves}");
|
|
}
|
|
}
|
|
|
|
private void InspectCell(DatCollection dats, uint cellId)
|
|
{
|
|
string typeLabel = (cellId & 0xFFFFu) >= 0x0100u ? "indoor" : "outdoor";
|
|
_out.WriteLine($"=== Cell 0x{cellId:X8} (low=0x{cellId & 0xFFFFu:X4}, type={typeLabel}) ===");
|
|
|
|
bool ok = dats.TryGet<EnvCell>(cellId, out var envCell);
|
|
if (!ok || envCell is null)
|
|
{
|
|
_out.WriteLine($" TryGet<EnvCell>(0x{cellId:X8}) returned false (not in dat or wrong type — outdoor cells use LandBlockInfo, not EnvCell)");
|
|
return;
|
|
}
|
|
|
|
_out.WriteLine($" Flags = 0x{(uint)envCell.Flags:X8}");
|
|
_out.WriteLine($" Position.Origin= ({envCell.Position.Origin.X:F3},{envCell.Position.Origin.Y:F3},{envCell.Position.Origin.Z:F3})");
|
|
var q = envCell.Position.Orientation;
|
|
_out.WriteLine($" Position.Rot = ({q.X:F4},{q.Y:F4},{q.Z:F4},{q.W:F4})");
|
|
_out.WriteLine($" EnvironmentId = 0x{envCell.EnvironmentId:X8}");
|
|
_out.WriteLine($" CellStructure = 0x{envCell.CellStructure:X8}");
|
|
_out.WriteLine($" StaticObjects = {envCell.StaticObjects.Count}");
|
|
_out.WriteLine($" VisibleCells = {envCell.VisibleCells.Count}");
|
|
_out.WriteLine($" CellPortals = {envCell.CellPortals.Count}");
|
|
for (int i = 0; i < envCell.CellPortals.Count; i++)
|
|
{
|
|
var p = envCell.CellPortals[i];
|
|
string otherIdStr = p.OtherCellId == 0xFFFFu
|
|
? "0xFFFF (EXIT-OUTSIDE)"
|
|
: $"0x{p.OtherCellId:X4} (other-indoor)";
|
|
_out.WriteLine($" [{i}] otherCellId={otherIdStr} polyId=0x{p.PolygonId:X4} flags=0x{(uint)p.Flags:X4}");
|
|
}
|
|
|
|
// Resolve the CellStruct (via Environment) so we can inspect the
|
|
// portal polygons' plane equations. The plane equation is what
|
|
// CellTransit.FindTransitCellsSphere tests the sphere against; if
|
|
// the test wrongly rejects, the door is invisible.
|
|
// Environment dat IDs are prefixed with 0x0D000000 — see
|
|
// GameWindow.cs:5388 (`0x0D000000u | envCell.EnvironmentId`).
|
|
bool envOk = dats.TryGet<DatReaderWriter.DBObjs.Environment>(
|
|
0x0D000000u | envCell.EnvironmentId, out var environment);
|
|
if (!envOk || environment is null)
|
|
{
|
|
_out.WriteLine($" Environment 0x{envCell.EnvironmentId:X8} NOT loadable");
|
|
return;
|
|
}
|
|
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct))
|
|
{
|
|
_out.WriteLine($" Environment.Cells[0x{envCell.CellStructure:X8}] NOT present");
|
|
return;
|
|
}
|
|
_out.WriteLine($" CellStruct verts = {cellStruct.VertexArray?.Vertices.Count ?? 0}");
|
|
_out.WriteLine($" CellStruct polygons = {cellStruct.Polygons?.Count ?? 0} (visible)");
|
|
_out.WriteLine($" CellStruct physicsPolys = {cellStruct.PhysicsPolygons?.Count ?? 0}");
|
|
|
|
// Dump ALL physics polygons (collision walls/floor) in world frame
|
|
// so we can see what blocks a sphere at world (133.655, 17.59).
|
|
_out.WriteLine($" CellStruct PHYSICS polys (world frame):");
|
|
var cellOrigin = envCell.Position.Origin;
|
|
var cellRot = envCell.Position.Orientation;
|
|
for (int pi = 0; pi < cellStruct.PhysicsPolygons.Count; pi++)
|
|
{
|
|
var (pid, poly) = (cellStruct.PhysicsPolygons.Keys.ElementAt(pi),
|
|
cellStruct.PhysicsPolygons.Values.ElementAt(pi));
|
|
float wxMin = float.MaxValue, wxMax = float.MinValue;
|
|
float wyMin = float.MaxValue, wyMax = float.MinValue;
|
|
float wzMin = float.MaxValue, wzMax = float.MinValue;
|
|
foreach (var vid in poly.VertexIds)
|
|
{
|
|
if (!cellStruct.VertexArray.Vertices.TryGetValue((ushort)vid, out var sv)) continue;
|
|
var rotated = System.Numerics.Vector3.Transform(sv.Origin, cellRot);
|
|
var world = cellOrigin + rotated;
|
|
if (world.X < wxMin) wxMin = world.X; if (world.X > wxMax) wxMax = world.X;
|
|
if (world.Y < wyMin) wyMin = world.Y; if (world.Y > wyMax) wyMax = world.Y;
|
|
if (world.Z < wzMin) wzMin = world.Z; if (world.Z > wzMax) wzMax = world.Z;
|
|
}
|
|
_out.WriteLine($" [{pi}=0x{pid:X4}] sides={poly.SidesType} " +
|
|
$"X=[{wxMin:F3},{wxMax:F3}] Y=[{wyMin:F3},{wyMax:F3}] Z=[{wzMin:F3},{wzMax:F3}]");
|
|
}
|
|
|
|
// Dump the plane of each portal polygon (the planes
|
|
// FindTransitCellsSphere tests the sphere center against).
|
|
for (int i = 0; i < envCell.CellPortals.Count; i++)
|
|
{
|
|
var p = envCell.CellPortals[i];
|
|
if (cellStruct.Polygons is null
|
|
|| !cellStruct.Polygons.TryGetValue(p.PolygonId, out var poly))
|
|
{
|
|
_out.WriteLine($" PortalPoly[{i}=0x{p.PolygonId:X4}] NOT present in CellStruct.Polygons");
|
|
continue;
|
|
}
|
|
// Resolve vertex positions.
|
|
var vs = poly.VertexIds.Select(vid => cellStruct.VertexArray.Vertices[(ushort)vid].Origin).ToList();
|
|
string vlist = string.Join(",", vs.Select(v => $"({v.X:F2},{v.Y:F2},{v.Z:F2})"));
|
|
|
|
// Compute the plane using the first three verts (normal = (v1-v0) x (v2-v0), d = -dot(n, v0)).
|
|
if (vs.Count >= 3)
|
|
{
|
|
var n = System.Numerics.Vector3.Normalize(System.Numerics.Vector3.Cross(vs[1] - vs[0], vs[2] - vs[0]));
|
|
float d = -System.Numerics.Vector3.Dot(n, vs[0]);
|
|
_out.WriteLine($" PortalPoly[{i}=0x{p.PolygonId:X4}] n_local=({n.X:F4},{n.Y:F4},{n.Z:F4}) d_local={d:F4} verts={vlist}");
|
|
}
|
|
else
|
|
{
|
|
_out.WriteLine($" PortalPoly[{i}=0x{p.PolygonId:X4}] <3 verts, plane n/a verts={vlist}");
|
|
}
|
|
}
|
|
}
|
|
}
|