T6 (BR-7) C1: signed OtherPortalId + the >=0 building-transit gate

Retail CEnvCell::check_building_transit (Ghidra 0x0052c5d0) opens with
`if (other_portal_id >= 0)` on the SIGNED sign-extended portal id
(CBldPortal.other_portal_id is int, acclient.h:32098). Our BldPortalInfo
carried the dat reader's raw ushort and CheckBuildingTransit had no gate
at all, so a portal whose dat value is 0xFFFF (-1, "no reciprocal
portal") could admit its interior cell. BN's pseudo-C renders the
comparison unsigned - the sign-extension is Ghidra-proven (BR-7 verified
corrections, wf1-interior-collision.md).

- BldPortalInfo.OtherPortalId: ushort -> short; GameWindow construction
  reinterprets the dat ushort via unchecked((short)).
- CheckBuildingTransit: negative-id portals rejected before any sphere
  test; new multi-sphere overload matching retail's per-sphere loop
  (0052c5fe, first-hit admits) with the hits_interior_cell output
  (0052c650) the BR-7 building channel consumes next.
- Tests: negative-id skip vs positive-id admit on a leaf-root CellBSP;
  multi-sphere plumbing + zero-sphere no-op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 13:56:16 +02:00
parent 85fe20f51d
commit 6ec4cde9a4
4 changed files with 192 additions and 14 deletions

View file

@ -24,7 +24,7 @@ public sealed class BuildingPhysics
/// </summary>
public readonly struct BldPortalInfo
{
public BldPortalInfo(uint otherCellId, ushort otherPortalId, ushort flags)
public BldPortalInfo(uint otherCellId, short otherPortalId, ushort flags)
{
OtherCellId = otherCellId;
OtherPortalId = otherPortalId;
@ -33,8 +33,20 @@ public readonly struct BldPortalInfo
/// <summary>Full id of the interior EnvCell this portal connects to.</summary>
public uint OtherCellId { get; }
/// <summary>The portal id within the destination EnvCell.</summary>
public ushort OtherPortalId { get; }
/// <summary>
/// The portal id within the destination EnvCell — SIGNED, like retail's
/// <c>CBldPortal.other_portal_id</c> (<c>int</c>, acclient.h:32098,
/// sign-extended from the dat's 16-bit field). <c>-1</c> (wire
/// <c>0xFFFF</c>) means "no reciprocal portal"; retail's
/// <c>CEnvCell::check_building_transit</c> (Ghidra 0x0052c5d0) rejects
/// the whole transit when this is negative — the <c>arg2 &gt;= 0</c>
/// gate is the first instruction. BN's pseudo-C renders the comparison
/// unsigned (wrong); the sign-extension is Ghidra-proven
/// (wf1-interior-collision.md, BR-7 verified corrections).
/// DatReaderWriter parses the field as <c>ushort</c>; construction
/// sites reinterpret via <c>unchecked((short)value)</c>.
/// </summary>
public short OtherPortalId { get; }
public ushort Flags { get; }
/// <summary>