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

@ -356,9 +356,44 @@ public static class CellTransit
Vector3 worldSphereCenter,
float sphereRadius,
ICollection<uint> candidates)
=> CheckBuildingTransit(
cache, building,
new[] { new Sphere { Origin = worldSphereCenter, Radius = sphereRadius } },
1, candidates, out _);
/// <summary>
/// Multi-sphere form matching retail's call shape: every path/flood
/// sphere is tested and the FIRST one intersecting the interior cell's
/// BSP admits the cell (<c>CEnvCell::check_building_transit</c>,
/// Ghidra 0x0052c5d0 — per-sphere loop at 0052c5fe, break-on-hit).
/// </summary>
/// <param name="hitsInteriorCell">True when at least one interior cell
/// was admitted — retail writes <c>SPHEREPATH.hits_interior_cell = 1</c>
/// at 0052c650 the moment a sphere lands a building-transit cell. Feeds
/// the building-shell <c>bldg_check</c> weakening in
/// <c>BSPTREE::find_collisions</c> (0x0053a440).</param>
public static void CheckBuildingTransit(
PhysicsDataCache cache,
BuildingPhysics building,
IReadOnlyList<Sphere> worldSpheres,
int numSpheres,
ICollection<uint> candidates,
out bool hitsInteriorCell)
{
hitsInteriorCell = false;
int sphereCount = EffectiveSphereCount(worldSpheres, numSpheres);
if (sphereCount == 0) return;
foreach (var portal in building.Portals)
{
// BR-7 / A6.P4 (2026-06-11): retail's first gate — the whole
// transit is rejected when other_portal_id is negative
// (`if (arg2 >= 0)` at 0x0052c5dc; arg2 is the SIGNED
// sign-extended CBldPortal.other_portal_id, acclient.h:32098).
// Wire 0xFFFF = -1 = "no reciprocal portal".
if (portal.OtherPortalId < 0)
continue;
var otherCell = cache.GetCellStruct(portal.OtherCellId);
if (otherCell?.CellBSP?.Root is null)
{
@ -381,17 +416,26 @@ public static class CellTransit
// it, login-inside-the-inn keeps the player classified outdoor
// until they walk further in (sphere center crosses), letting
// them run through exterior walls on the way out.
var localCenter = Vector3.Transform(worldSphereCenter, otherCell.InverseWorldTransform);
bool inside = BSPQuery.SphereIntersectsCellBsp(otherCell.CellBSP.Root, localCenter, sphereRadius);
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
bool inside = false;
for (int i = 0; i < sphereCount && !inside; i++)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[check-bldg] portal->0x{portal.OtherCellId:X8} wpos=({worldSphereCenter.X:F3},{worldSphereCenter.Y:F3},{worldSphereCenter.Z:F3}) lpos=({localCenter.X:F3},{localCenter.Y:F3},{localCenter.Z:F3}) r={sphereRadius:F3} inside={inside}"));
var sphere = worldSpheres[i];
var localCenter = Vector3.Transform(sphere.Origin, otherCell.InverseWorldTransform);
inside = BSPQuery.SphereIntersectsCellBsp(
otherCell.CellBSP.Root, localCenter, sphere.Radius);
if (PhysicsDiagnostics.ProbeIndoorBspEnabled)
{
Console.WriteLine(System.FormattableString.Invariant(
$"[check-bldg] portal->0x{portal.OtherCellId:X8} sphere#{i} wpos=({sphere.Origin.X:F3},{sphere.Origin.Y:F3},{sphere.Origin.Z:F3}) lpos=({localCenter.X:F3},{localCenter.Y:F3},{localCenter.Z:F3}) r={sphere.Radius:F3} inside={inside}"));
}
}
if (inside)
{
// Retail sets SPHEREPATH.hits_interior_cell the moment a
// building-transit sphere lands an interior cell (0052c650).
hitsInteriorCell = true;
candidates.Add(portal.OtherCellId);
}
}