fix(physics): traverse prepared indoor portal topology
This commit is contained in:
parent
b12d94047c
commit
4d095be286
9 changed files with 400 additions and 5 deletions
|
|
@ -103,6 +103,33 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## #246 — Prepared indoor cells stopped traversing their portals
|
||||||
|
|
||||||
|
**Status:** DONE — 2026-07-26; connected house/dungeon gate passed
|
||||||
|
**Severity:** HIGH
|
||||||
|
**Filed:** 2026-07-26
|
||||||
|
**Component:** physics / cell membership / portal visibility
|
||||||
|
|
||||||
|
**Description:** Houses and dungeons could collide with empty space, expose
|
||||||
|
the background through walls, retain the wrong room, and fail on stairs after
|
||||||
|
the parsed collision graph was removed.
|
||||||
|
|
||||||
|
**Root cause / status:** The prepared package retained exact portal topology,
|
||||||
|
planes, and BSPs, but `CellTransit.FindTransitCellsSphere` still returned
|
||||||
|
immediately when the old parsed portal-polygon dictionary was absent. It now
|
||||||
|
consumes the prepared topology's direct polygon index, preserving retail's
|
||||||
|
portal traversal and failing loudly on corrupt topology. Synthetic graph-free
|
||||||
|
tests and the exact connected Holtburg cottage camera path pass, along with
|
||||||
|
1,778 physics tests / 1 skip.
|
||||||
|
|
||||||
|
**Research:**
|
||||||
|
[`research/2026-07-26-prepared-indoor-transit-regression.md`](research/2026-07-26-prepared-indoor-transit-regression.md).
|
||||||
|
|
||||||
|
**Acceptance:** PASSED — the user confirmed house/dungeon collision, doorway
|
||||||
|
entry, stairs, and interior rendering work in the corrected connected build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## #245 — Distant Use stayed busy without turning or walking
|
## #245 — Distant Use stayed busy without turning or walking
|
||||||
|
|
||||||
**Status:** DONE — 2026-07-25; connected gate passed
|
**Status:** DONE — 2026-07-25; connected gate passed
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,16 @@ This remains one `DatCollection`, one preparation algorithm, and one package
|
||||||
mmap. Evidence:
|
mmap. Evidence:
|
||||||
`docs/research/2026-07-25-slice-i7-closeout.md`.
|
`docs/research/2026-07-25-slice-i7-closeout.md`.
|
||||||
|
|
||||||
|
**Prepared indoor-transit consumer correction (2026-07-26).** The package
|
||||||
|
already retained exact portal planes through
|
||||||
|
`FlatEnvCellTopology.PolygonIndex` plus the aliased CellStruct portal-polygon
|
||||||
|
table. The production `CellTransit` consumer now reads that prepared
|
||||||
|
relationship directly; it no longer treats the intentionally absent parsed
|
||||||
|
`CellPhysics.PortalPolygons` dictionary as “this cell has no portals.” No
|
||||||
|
package schema, bake, DAT reader, collision formula, or render portal graph
|
||||||
|
changed. Evidence:
|
||||||
|
`docs/research/2026-07-26-prepared-indoor-transit-regression.md`.
|
||||||
|
|
||||||
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
|
**Retail VFX hook compatibility seam (2026-07-14).** Chorizite.DatReaderWriter
|
||||||
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
|
2.1.7 models `CreateBlockingParticleHook` as the common hook header only, while
|
||||||
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
|
retail inherits the complete `CreateParticleHook` payload. The narrow readers in
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,15 @@ GPU/frame-time and ordinary-process memory comparison remains a physical-
|
||||||
display measurement, not a Slice-I correctness blocker. Evidence:
|
display measurement, not a Slice-I correctness blocker. Evidence:
|
||||||
[`../research/2026-07-25-slice-i7-closeout.md`](../research/2026-07-25-slice-i7-closeout.md).
|
[`../research/2026-07-25-slice-i7-closeout.md`](../research/2026-07-25-slice-i7-closeout.md).
|
||||||
|
|
||||||
|
**Corrective gate 2026-07-26.** Later connected house/dungeon testing exposed
|
||||||
|
one I7 consumer omission: `CEnvCell::find_transit_cells` still required the
|
||||||
|
deleted parsed portal-polygon dictionary, even though the prepared topology
|
||||||
|
and polygon table retained the exact same plane. The production consumer now
|
||||||
|
uses that direct prepared index. Synthetic graph-free tests and the captured
|
||||||
|
Holtburg cottage camera path prove identical graph/prepared results; no
|
||||||
|
package rebake or parsed-graph restoration is needed. Evidence:
|
||||||
|
[`../research/2026-07-26-prepared-indoor-transit-regression.md`](../research/2026-07-26-prepared-indoor-transit-regression.md).
|
||||||
|
|
||||||
## 4. Commit order
|
## 4. Commit order
|
||||||
|
|
||||||
Each unit is independently buildable and reversible:
|
Each unit is independently buildable and reversible:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
# Prepared indoor transit regression
|
||||||
|
|
||||||
|
**Date:** 2026-07-26
|
||||||
|
**Scope:** Slice-I prepared collision consumer correction
|
||||||
|
**Behavioral boundary:** No collision formula, portal rule, or renderer
|
||||||
|
behavior changed. This restores the existing retail traversal over the
|
||||||
|
production flat representation.
|
||||||
|
|
||||||
|
## Symptom and connected evidence
|
||||||
|
|
||||||
|
After the Slice-I parsed-graph removal, entering houses or dungeons could:
|
||||||
|
|
||||||
|
- collide with empty space at a doorway or room boundary;
|
||||||
|
- retain the wrong indoor cell on stairs or upper floors;
|
||||||
|
- expose the world background through the interior portal view.
|
||||||
|
|
||||||
|
The connected capture at
|
||||||
|
`.test-out/indoor-visibility-20260726-220007/stdout.log` placed the player in
|
||||||
|
Holtburg cottage vestibule `0xA9B40150`, with the chase-camera eye outside the
|
||||||
|
doorway in outdoor landcell `0xA9B40029`. The original DAT graph advanced the
|
||||||
|
camera transition from `0xA9B40150` to `0xA9B40029`. The prepared production
|
||||||
|
path retained `0xA9B40150`, causing collision and portal rendering to share
|
||||||
|
the same wrong viewer-cell root.
|
||||||
|
|
||||||
|
## Retail oracle
|
||||||
|
|
||||||
|
Named retail:
|
||||||
|
|
||||||
|
- `CEnvCell::find_transit_cells @ 0x0052C820`
|
||||||
|
- `CObjCell::find_cell_list`
|
||||||
|
- `CEnvCell::find_visible_child_cell`
|
||||||
|
|
||||||
|
Consolidated pseudocode:
|
||||||
|
[`acclient_indoor_transitions_pseudocode.md`](acclient_indoor_transitions_pseudocode.md).
|
||||||
|
|
||||||
|
For each `CCellPortal`, retail reads that portal's polygon plane from the
|
||||||
|
owning `CCellStruct`. An exterior portal admits outdoor cells when any path
|
||||||
|
sphere straddles the plane. An interior portal tests the loaded neighbor's
|
||||||
|
cell-containment BSP, using the plane only as an unloaded-cell hint.
|
||||||
|
|
||||||
|
## Root cause
|
||||||
|
|
||||||
|
The prepared package was complete:
|
||||||
|
|
||||||
|
- `FlatEnvCellTopology.Portals[i].PolygonIndex` retained the direct portal
|
||||||
|
polygon index;
|
||||||
|
- `FlatCellStructureCollisionAsset.PortalPolygons` retained the exact plane;
|
||||||
|
- prepared containment and physics BSPs matched the original DAT graph.
|
||||||
|
|
||||||
|
`CellTransit.FindTransitCellsSphere`, however, still began with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
if currentCell.PortalPolygons is null: return
|
||||||
|
```
|
||||||
|
|
||||||
|
`PortalPolygons` is the old parsed-DAT dictionary and is intentionally null in
|
||||||
|
production after Slice I7. Therefore every prepared indoor cell skipped its
|
||||||
|
entire portal loop. The I4/I5 differential referee covered BSP queries and
|
||||||
|
complete resolver results, but did not exercise portal-plane lookup after the
|
||||||
|
parsed graph had been removed, allowing the representation-boundary defect to
|
||||||
|
escape.
|
||||||
|
|
||||||
|
## Correction
|
||||||
|
|
||||||
|
`CellTransit` now resolves the exact same portal plane from:
|
||||||
|
|
||||||
|
1. the parsed dictionary in explicit graph-oracle fixtures; or
|
||||||
|
2. the prepared topology's direct polygon index and flat polygon table in
|
||||||
|
production.
|
||||||
|
|
||||||
|
Prepared topology/list mismatches and invalid polygon indices fail loudly as
|
||||||
|
corrupt publication rather than silently disabling indoor traversal. Portal
|
||||||
|
iteration order, comparisons, sphere transforms, neighbor containment, and
|
||||||
|
outside admission remain unchanged.
|
||||||
|
|
||||||
|
## Automated evidence
|
||||||
|
|
||||||
|
- Synthetic prepared exterior-portal test with no parsed polygon dictionary.
|
||||||
|
- Synthetic prepared loaded-neighbor test with no parsed containment tree.
|
||||||
|
- Exact connected Holtburg cottage camera path:
|
||||||
|
graph and prepared paths both end in `0xA9B40029` at identical position.
|
||||||
|
- Installed-DAT graph/flat coverage expanded across the Holtburg cottage and
|
||||||
|
inn vestibules, connectors, stairwell, upper floor, and inn shell.
|
||||||
|
- Physics-focused Release tests: 1,778 passed / 1 skipped.
|
||||||
|
- Interaction/projection focused App tests: 127 passed.
|
||||||
|
- Client-object ordering tests: 82 passed.
|
||||||
|
- Complete Release build: passed with zero warnings.
|
||||||
|
|
||||||
|
No divergence-register row is required: the correction removes an accidental
|
||||||
|
production divergence and introduces no replacement behavior.
|
||||||
|
|
@ -101,12 +101,21 @@ public static class CellTransit
|
||||||
uint lbPrefix = currentCellId & 0xFFFF0000u;
|
uint lbPrefix = currentCellId & 0xFFFF0000u;
|
||||||
int sphereCount = EffectiveSphereCount(worldSpheres, numSpheres);
|
int sphereCount = EffectiveSphereCount(worldSpheres, numSpheres);
|
||||||
|
|
||||||
if (currentCell.PortalPolygons is null || sphereCount == 0) return;
|
if (sphereCount == 0) return;
|
||||||
|
|
||||||
foreach (var portal in currentCell.Portals)
|
for (int portalIndex = 0;
|
||||||
|
portalIndex < currentCell.Portals.Count;
|
||||||
|
portalIndex++)
|
||||||
{
|
{
|
||||||
if (!currentCell.PortalPolygons.TryGetValue(portal.PolygonId, out var poly))
|
PortalInfo portal = currentCell.Portals[portalIndex];
|
||||||
|
if (!TryGetPortalPlane(
|
||||||
|
currentCell,
|
||||||
|
portalIndex,
|
||||||
|
portal,
|
||||||
|
out Plane portalPlane))
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (portal.OtherCellId == 0xFFFF)
|
if (portal.OtherCellId == 0xFFFF)
|
||||||
{
|
{
|
||||||
|
|
@ -142,7 +151,9 @@ public static class CellTransit
|
||||||
float pad = sphere.Radius + FEpsilon;
|
float pad = sphere.Radius + FEpsilon;
|
||||||
var localCenter = Vector3.Transform(
|
var localCenter = Vector3.Transform(
|
||||||
sphere.Origin, currentCell.InverseWorldTransform);
|
sphere.Origin, currentCell.InverseWorldTransform);
|
||||||
float dist = Vector3.Dot(localCenter, poly.Plane.Normal) + poly.Plane.D;
|
float dist =
|
||||||
|
Vector3.Dot(localCenter, portalPlane.Normal) +
|
||||||
|
portalPlane.D;
|
||||||
if (dist > -pad && dist < pad)
|
if (dist > -pad && dist < pad)
|
||||||
{
|
{
|
||||||
exitOutside = true;
|
exitOutside = true;
|
||||||
|
|
@ -190,7 +201,9 @@ public static class CellTransit
|
||||||
float rad = sphere.Radius + EPSILON;
|
float rad = sphere.Radius + EPSILON;
|
||||||
var localCenter = Vector3.Transform(
|
var localCenter = Vector3.Transform(
|
||||||
sphere.Origin, currentCell.InverseWorldTransform);
|
sphere.Origin, currentCell.InverseWorldTransform);
|
||||||
float dist = Vector3.Dot(localCenter, poly.Plane.Normal) + poly.Plane.D;
|
float dist =
|
||||||
|
Vector3.Dot(localCenter, portalPlane.Normal) +
|
||||||
|
portalPlane.D;
|
||||||
bool hit = portal.PortalSide ? dist > -rad : dist < rad;
|
bool hit = portal.PortalSide ? dist > -rad : dist < rad;
|
||||||
if (hit)
|
if (hit)
|
||||||
{
|
{
|
||||||
|
|
@ -201,6 +214,64 @@ public static class CellTransit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the portal plane from whichever immutable representation owns
|
||||||
|
/// this cell. Graph fixtures retain the DAT polygon dictionary; production
|
||||||
|
/// cells intentionally retain only the prepared topology's direct polygon
|
||||||
|
/// index and flat polygon table.
|
||||||
|
/// </summary>
|
||||||
|
private static bool TryGetPortalPlane(
|
||||||
|
CellPhysics cell,
|
||||||
|
int portalIndex,
|
||||||
|
PortalInfo portal,
|
||||||
|
out Plane plane)
|
||||||
|
{
|
||||||
|
if (cell.PortalPolygons is not null &&
|
||||||
|
cell.PortalPolygons.TryGetValue(
|
||||||
|
portal.PolygonId,
|
||||||
|
out ResolvedPolygon? polygon))
|
||||||
|
{
|
||||||
|
plane = polygon.Plane;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatEnvCellTopology? topology = cell.FlatTopology;
|
||||||
|
FlatPolygonTable? polygonTable = cell.FlatPortalPolygons;
|
||||||
|
if (topology is null || polygonTable is null)
|
||||||
|
{
|
||||||
|
plane = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((uint)portalIndex >= (uint)topology.Portals.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Cell 0x{cell.SourceId:X8} portal {portalIndex} is absent " +
|
||||||
|
"from its prepared topology.");
|
||||||
|
}
|
||||||
|
|
||||||
|
FlatEnvCellPortal flatPortal = topology.Portals[portalIndex];
|
||||||
|
if (flatPortal.OtherCellId != portal.OtherCellId ||
|
||||||
|
flatPortal.PolygonId != portal.PolygonId ||
|
||||||
|
flatPortal.Flags != portal.Flags)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Cell 0x{cell.SourceId:X8} portal {portalIndex} does not " +
|
||||||
|
"match its prepared topology.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int polygonIndex = flatPortal.PolygonIndex;
|
||||||
|
if ((uint)polygonIndex >= (uint)polygonTable.Polygons.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"Cell 0x{cell.SourceId:X8} portal {portalIndex} references " +
|
||||||
|
$"invalid prepared polygon index {polygonIndex}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
plane = polygonTable.Polygons[polygonIndex].Plane;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Outdoor neighbour expansion. Ported from
|
/// Outdoor neighbour expansion. Ported from
|
||||||
/// <c>CLandCell::add_all_outside_cells</c> (sphere variant,
|
/// <c>CLandCell::add_all_outside_cells</c> (sphere variant,
|
||||||
|
|
|
||||||
|
|
@ -290,4 +290,88 @@ public class CameraCornerSealReplayTests
|
||||||
"Viewer sweep through a verified-open doorway path was obstructed or cut short " +
|
"Viewer sweep through a verified-open doorway path was obstructed or cut short " +
|
||||||
"(camera would wrongly pull in at openings) for: " + string.Join(", ", failures));
|
"(camera would wrongly pull in at openings) for: " + string.Join(", ", failures));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HoltburgCottageExit_ViewerSweep_AdvancesFromAlcoveToOutdoor()
|
||||||
|
{
|
||||||
|
var datDir = ConformanceDats.ResolveDatDir();
|
||||||
|
if (datDir is null) return;
|
||||||
|
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
var graphCache = new PhysicsDataCache();
|
||||||
|
var preparedCache = PhysicsDataCache.CreateProduction();
|
||||||
|
for (uint low = 0x013Fu; low <= 0x0150u; low++)
|
||||||
|
{
|
||||||
|
uint cellId = ConformanceDats.HoltburgLandblock | low;
|
||||||
|
ConformanceDats.LoadEnvCell(
|
||||||
|
dats,
|
||||||
|
graphCache,
|
||||||
|
cellId);
|
||||||
|
|
||||||
|
CellPhysics graphCell = Assert.IsType<CellPhysics>(
|
||||||
|
graphCache.GetCellStruct(cellId));
|
||||||
|
FlatCellCollisionAsset prepared =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCell(graphCell);
|
||||||
|
var datCell = Assert.IsType<DatReaderWriter.DBObjs.EnvCell>(
|
||||||
|
dats.Get<DatReaderWriter.DBObjs.EnvCell>(cellId));
|
||||||
|
preparedCache.CacheCellStruct(
|
||||||
|
cellId,
|
||||||
|
datCell,
|
||||||
|
graphCell.WorldTransform,
|
||||||
|
prepared.Structure,
|
||||||
|
prepared.Topology);
|
||||||
|
}
|
||||||
|
|
||||||
|
var graphEngine = new PhysicsEngine { DataCache = graphCache };
|
||||||
|
var preparedEngine = new PhysicsEngine { DataCache = preparedCache };
|
||||||
|
|
||||||
|
var heights = new byte[81];
|
||||||
|
var heightTable = new float[256];
|
||||||
|
for (int i = 0; i < heightTable.Length; i++)
|
||||||
|
heightTable[i] = -1000f;
|
||||||
|
foreach (PhysicsEngine engine in new[] { graphEngine, preparedEngine })
|
||||||
|
{
|
||||||
|
engine.AddLandblock(
|
||||||
|
ConformanceDats.HoltburgLandblock,
|
||||||
|
new TerrainSurface(heights, heightTable),
|
||||||
|
Array.Empty<CellSurface>(),
|
||||||
|
Array.Empty<PortalPlane>(),
|
||||||
|
0f,
|
||||||
|
0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connected capture indoor-visibility-20260726-220007. The player is
|
||||||
|
// just inside the narrow 0x0150 vestibule, facing into the cottage.
|
||||||
|
// Its chase camera is behind it, outside the open doorway. Retail's
|
||||||
|
// viewer CTransition crosses 0x0150's 0xFFFF portal and carries the
|
||||||
|
// outdoor landcell as viewer_cell.
|
||||||
|
var player = new Vector3(132.70f, 16.80f, 94.00f);
|
||||||
|
var pivot = player + new Vector3(0f, 0f, PivotHeight);
|
||||||
|
var eye = new Vector3(131.30f, 19.97f, 96.25f);
|
||||||
|
|
||||||
|
var graphPivot = graphEngine.AdjustPosition(0xA9B40150u, pivot);
|
||||||
|
var preparedPivot = preparedEngine.AdjustPosition(0xA9B40150u, pivot);
|
||||||
|
Assert.True(graphPivot.found);
|
||||||
|
Assert.True(preparedPivot.found);
|
||||||
|
Assert.Equal(0xA9B40150u, graphPivot.cellId);
|
||||||
|
Assert.Equal(graphPivot.cellId, preparedPivot.cellId);
|
||||||
|
|
||||||
|
ResolveResult graphResult = SweepViewer(
|
||||||
|
graphEngine,
|
||||||
|
pivot,
|
||||||
|
eye,
|
||||||
|
0xA9B40150u);
|
||||||
|
ResolveResult preparedResult = SweepViewer(
|
||||||
|
preparedEngine,
|
||||||
|
pivot,
|
||||||
|
eye,
|
||||||
|
0xA9B40150u);
|
||||||
|
|
||||||
|
_out.WriteLine(FormattableString.Invariant(
|
||||||
|
$"graph=0x{graphResult.CellId:X8} prepared=0x{preparedResult.CellId:X8} graphPos=({graphResult.Position.X:F3},{graphResult.Position.Y:F3},{graphResult.Position.Z:F3}) preparedPos=({preparedResult.Position.X:F3},{preparedResult.Position.Y:F3},{preparedResult.Position.Z:F3})"));
|
||||||
|
|
||||||
|
Assert.Equal(0xA9B40029u, graphResult.CellId);
|
||||||
|
Assert.Equal(graphResult.CellId, preparedResult.CellId);
|
||||||
|
Assert.Equal(graphResult.Position, preparedResult.Position);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ public class CellTransitFindTransitCellsSphereTests
|
||||||
Root = new CellBSPNode
|
Root = new CellBSPNode
|
||||||
{
|
{
|
||||||
// Local x >= 0 is inside this synthetic cell.
|
// Local x >= 0 is inside this synthetic cell.
|
||||||
|
Type = DatReaderWriter.Enums.BSPNodeType.BPIn,
|
||||||
SplittingPlane = new Plane(new Vector3(1f, 0f, 0f), 0f),
|
SplittingPlane = new Plane(new Vector3(1f, 0f, 0f), 0f),
|
||||||
PosNode = leaf,
|
PosNode = leaf,
|
||||||
}
|
}
|
||||||
|
|
@ -28,6 +29,7 @@ public class CellTransitFindTransitCellsSphereTests
|
||||||
// Portal poly at local x=2.5 (right wall), normal +X.
|
// Portal poly at local x=2.5 (right wall), normal +X.
|
||||||
var portalPolyA = new ResolvedPolygon
|
var portalPolyA = new ResolvedPolygon
|
||||||
{
|
{
|
||||||
|
Id = 10,
|
||||||
Vertices = new[]
|
Vertices = new[]
|
||||||
{
|
{
|
||||||
new Vector3(2.5f, -2.5f, 0f),
|
new Vector3(2.5f, -2.5f, 0f),
|
||||||
|
|
@ -54,6 +56,24 @@ public class CellTransitFindTransitCellsSphereTests
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CellPhysics PrepareCell(CellPhysics graphCell, uint sourceId)
|
||||||
|
{
|
||||||
|
FlatCellCollisionAsset prepared =
|
||||||
|
FlatCollisionAssetBuilder.FlattenCell(graphCell);
|
||||||
|
return new CellPhysics
|
||||||
|
{
|
||||||
|
SourceId = sourceId,
|
||||||
|
WorldTransform = graphCell.WorldTransform,
|
||||||
|
InverseWorldTransform = graphCell.InverseWorldTransform,
|
||||||
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
|
FlatPhysicsBsp = prepared.Structure.PhysicsBsp,
|
||||||
|
FlatContainmentBsp = prepared.Structure.ContainmentBsp,
|
||||||
|
FlatPortalPolygons = prepared.Structure.PortalPolygons,
|
||||||
|
FlatTopology = prepared.Topology,
|
||||||
|
Portals = graphCell.Portals,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SphereInsideCellA_NearPortal_AddsCellB()
|
public void SphereInsideCellA_NearPortal_AddsCellB()
|
||||||
{
|
{
|
||||||
|
|
@ -216,4 +236,73 @@ public class CellTransitFindTransitCellsSphereTests
|
||||||
|
|
||||||
Assert.True(exitOutside);
|
Assert.True(exitOutside);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedExitPortal_UsesIndexedPlaneWithoutGraphDictionary()
|
||||||
|
{
|
||||||
|
const uint cellId = 0xA9B40100u;
|
||||||
|
CellPhysics graphCell = MakeCellWithPortalAtRightWall(
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
otherCellId: 0xFFFF,
|
||||||
|
flags: 0);
|
||||||
|
CellPhysics preparedCell = PrepareCell(graphCell, cellId);
|
||||||
|
var cache = PhysicsDataCache.CreateProduction();
|
||||||
|
cache.RegisterCellStructForTest(cellId, preparedCell);
|
||||||
|
|
||||||
|
var candidates = new HashSet<uint>();
|
||||||
|
CellTransit.FindTransitCellsSphere(
|
||||||
|
cache,
|
||||||
|
preparedCell,
|
||||||
|
cellId,
|
||||||
|
new Vector3(2.0f, 0f, 2.5f),
|
||||||
|
sphereRadius: 0.5f,
|
||||||
|
candidates,
|
||||||
|
out bool exitOutside);
|
||||||
|
|
||||||
|
Assert.Null(preparedCell.PortalPolygons);
|
||||||
|
Assert.True(exitOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreparedLoadedNeighbor_UsesFlatContainmentWithoutGraphObjects()
|
||||||
|
{
|
||||||
|
const uint cellAId = 0xA9B40100u;
|
||||||
|
const uint cellBId = 0xA9B40101u;
|
||||||
|
CellPhysics graphCellA = MakeCellWithPortalAtRightWall(
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
otherCellId: 0x0101,
|
||||||
|
flags: 2);
|
||||||
|
|
||||||
|
var cellBTransform = Matrix4x4.CreateTranslation(
|
||||||
|
new Vector3(5f, 0f, 0f));
|
||||||
|
Matrix4x4.Invert(cellBTransform, out Matrix4x4 cellBInverse);
|
||||||
|
var graphCellB = new CellPhysics
|
||||||
|
{
|
||||||
|
WorldTransform = cellBTransform,
|
||||||
|
InverseWorldTransform = cellBInverse,
|
||||||
|
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||||
|
CellBSP = SinglePlaneCellBsp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
CellPhysics preparedCellA = PrepareCell(graphCellA, cellAId);
|
||||||
|
CellPhysics preparedCellB = PrepareCell(graphCellB, cellBId);
|
||||||
|
var cache = PhysicsDataCache.CreateProduction();
|
||||||
|
cache.RegisterCellStructForTest(cellAId, preparedCellA);
|
||||||
|
cache.RegisterCellStructForTest(cellBId, preparedCellB);
|
||||||
|
|
||||||
|
var candidates = new HashSet<uint>();
|
||||||
|
CellTransit.FindTransitCellsSphere(
|
||||||
|
cache,
|
||||||
|
preparedCellA,
|
||||||
|
cellAId,
|
||||||
|
new Vector3(4.75f, 0f, 2.5f),
|
||||||
|
sphereRadius: 0.5f,
|
||||||
|
candidates,
|
||||||
|
out bool exitOutside);
|
||||||
|
|
||||||
|
Assert.Null(preparedCellA.PortalPolygons);
|
||||||
|
Assert.Null(preparedCellB.CellBSP);
|
||||||
|
Assert.Contains(cellBId, candidates);
|
||||||
|
Assert.False(exitOutside);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,13 @@ public sealed class FlatBspQueryDifferentialTests
|
||||||
0x8A02_016Eu,
|
0x8A02_016Eu,
|
||||||
0x8A02_017Au,
|
0x8A02_017Au,
|
||||||
0xA9B4_013Fu,
|
0xA9B4_013Fu,
|
||||||
|
0xA9B4_0150u,
|
||||||
|
0xA9B4_0159u,
|
||||||
|
0xA9B4_015Au,
|
||||||
|
0xA9B4_0161u,
|
||||||
|
0xA9B4_0162u,
|
||||||
|
0xA9B4_0164u,
|
||||||
|
0xA9B4_0166u,
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
0x8A02_016Eu, // Facility Hub corridor
|
0x8A02_016Eu, // Facility Hub corridor
|
||||||
0x8A02_017Au, // asymmetric adjoining corridor
|
0x8A02_017Au, // asymmetric adjoining corridor
|
||||||
0xA9B4_013Fu, // Holtburg cottage
|
0xA9B4_013Fu, // Holtburg cottage
|
||||||
|
0xA9B4_0150u, // Holtburg cottage exterior vestibule
|
||||||
|
0xA9B4_0159u, // Holtburg inn south room
|
||||||
|
0xA9B4_015Au, // Holtburg inn ground-floor connector
|
||||||
|
0xA9B4_0161u, // Holtburg inn upper-floor connector
|
||||||
|
0xA9B4_0162u, // Holtburg inn main room / stairwell
|
||||||
|
0xA9B4_0164u, // Holtburg inn exterior vestibule
|
||||||
|
0xA9B4_0166u, // Holtburg inn upper floor
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
var cache = new PhysicsDataCache();
|
var cache = new PhysicsDataCache();
|
||||||
|
|
@ -118,6 +125,7 @@ public sealed class FlatCollisionInstalledDatTests
|
||||||
foreach (uint gfxObjId in new[]
|
foreach (uint gfxObjId in new[]
|
||||||
{
|
{
|
||||||
0x0100_0A2Bu, // Holtburg cottage shell
|
0x0100_0A2Bu, // Holtburg cottage shell
|
||||||
|
0x0100_0C17u, // Holtburg inn shell
|
||||||
0x0100_0AC5u, // outdoor stair/ramp fixture
|
0x0100_0AC5u, // outdoor stair/ramp fixture
|
||||||
0x0100_44B5u, // multipart door shell
|
0x0100_44B5u, // multipart door shell
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue