fix(render): Phase U.2b — resolve reciprocal portal by other_portal_id (retail 433557)

Code review caught a CRITICAL under-inclusion: ApplyReciprocalClip scanned for the
first OtherCellId match, so a cell with two portals to the same neighbour clipped both
near-side openings against the FIRST reciprocal polygon — hiding geometry through the
second opening (real on Holtburg cellar cells 0x148<->0x149). Plumb the dat's
OtherPortalId back-link through CellPortalInfo + BuildLoadedCell and index the reciprocal
directly (retail arg2->other_portal_id, 433557). Skip (degrade to over-include) when the
index is unresolvable — never clip against a guessed polygon. Adds a disjoint two-back-
portal regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-30 16:56:00 +02:00
parent 3916b2b23e
commit 65781f5768
7 changed files with 184 additions and 66 deletions

View file

@ -88,8 +88,17 @@ public sealed class LoadedCell
/// <summary>
/// Portal connection to a neighbouring cell.
/// OtherCellId == 0xFFFF indicates an exit portal to the outdoor world.
/// <para>
/// <see cref="OtherPortalId"/> is the dat's reciprocal back-link: the index of
/// the portal WITHIN the neighbour cell's portal list that points back through
/// this same opening. Retail indexes the reciprocal directly via this field
/// (<c>arg2-&gt;other_portal_id</c>, decomp:433557) rather than scanning — which
/// is what lets a cell with TWO portals to the same neighbour resolve each
/// opening against its OWN reciprocal polygon instead of the first match.
/// </para>
/// </summary>
public readonly record struct CellPortalInfo(ushort OtherCellId, ushort PolygonId, ushort Flags);
public readonly record struct CellPortalInfo(
ushort OtherCellId, ushort PolygonId, ushort Flags, ushort OtherPortalId);
/// <summary>
/// Clip plane derived from a portal polygon, in cell-local space.