using System.Numerics; namespace AcDream.App.Rendering.Walk; /// Retail CBldPortal: one building portal — the authored /// side gate, the destination interior cell, its reciprocal portal index, /// and the stab list of cells whose view stacks are pushed around the /// look-in (PView::add_views/remove_views). public struct WalkBldPortal { public int PortalSide; public uint OtherCellId; public int OtherPortalId; public bool ExactMatch; public uint[] StabList; } /// One CPortalPoly reference on a PORT BSP node: the index /// into the building's portal list plus the portal polygon (building-local). public struct WalkPortalRef { public int PortalIndex; public WalkPolygon Polygon; } /// A node of the building's drawing BSP as the portal-only walk /// sees it (retail tags: 'FAIL' leaf = stop, 'PORT' = portal node with /// in_portals, anything else = plain splitting node). public sealed class WalkBspNode { public WalkPlane SplittingPlane; public WalkBspNode? PosNode; public WalkBspNode? NegNode; public bool IsFail; public WalkPortalRef[]? InPortals; // non-null = PORT node public bool IsPortal => InPortals is not null; } /// One degrade-ladder level: the drawing BSP of that level's /// GfxObj (portal-only view) and the level's far distance bound. public readonly record struct WalkBuildingDegradeLevel(float MaxDist, WalkBspNode? DrawingBsp); /// The walk's building model (retail CBuildingObj + /// BuildInfo as the frame walk consumes them). public sealed class WalkBuilding { /// Position.objcell_id — the id the oracle's BLD events /// carry (CBuildingObj+0x4C). public uint PositionCellId; public WalkBldPortal[] Portals = []; /// The drawing BSP of part 0's BASE GfxObj (portal-only view). /// Used directly when the model has no degrade ladder. public WalkBspNode? DrawingBsp; /// The degrade ladder (near→far). Retail walks the CURRENT /// level's drawing BSP (part->gfxobj[deg_level]) — and building /// degrade models beyond the first band carry NO portal nodes, which is /// what limits look-in punches to nearby buildings (probed 2026-08-30: /// every Holtburg building has ports at level 0 only). public WalkBuildingDegradeLevel[] DegradeLevels = []; /// part->gfxobj[deg_level] != 0 — a degraded-out slot /// skips the whole building AFTER publishing the portal list. public bool HasGeometry = true; /// Level selection by viewer distance. Simplified band pick /// (first level whose MaxDist covers the distance) — the faithful /// hysteresis of CPhysicsPart::UpdateViewerDistance is a port /// TODO; still fixtures are insensitive to hysteresis. public WalkBspNode? SelectDrawingBsp(float viewerDistance) { if (DegradeLevels.Length == 0) return DrawingBsp; foreach (WalkBuildingDegradeLevel level in DegradeLevels) { if (viewerDistance <= level.MaxDist) return level.DrawingBsp; } return null; // degraded out entirely } } /// /// Campaign FW1 — the building look-in machinery, ported from the first /// decomp appendix report 3 (docs/research/2026-08-30-fw-walk-pseudocode-appendix.md): /// BSPTREE/BSPNODE::build_draw_portals_only @0x00539860/@0x0053c100, /// BSPPORTAL::portal_draw_portals_only @0x0053d870, /// PView::DrawPortal @0x005a5ab0, and the CBldPortal /// PView::ConstructView overload @0x005a59a0. /// /// The invisible-panel primitive (DrawPortalPolyInternal @0x0059bc90 /// — punch far-Z / seal own-depth) is a GPU submission and belongs to FW2's /// ordered stream; here it surfaces as the /// punch callback so FW1 conformance can observe when retail would draw it. /// public static class WalkBuildingPortals { /// What the portal passes report outward: punches (pass 1) and /// look-in cell floods (pass 2, the oracle's DC ov=… events). public interface IWalkPortalPassSink { /// Pass 1 drew the portal polygon as a far-Z punch /// (DrawPortalPolyInternal(poly, 1)). void OnPunch(WalkPolygon polygon); /// Pass 2 completed a look-in flood and retail called /// PView::DrawCells — the DC event with the flood's list. void OnDrawCells(WalkPView pview); } /// /// BSPTREE::build_draw_portals_only @0x00539860 + the node/portal /// walkers: dispatch the root, then walk plane-side ordered — the child /// OPPOSITE the viewer first, so portals emit far-to-near. PORT nodes /// emit every in_portal on the POSITIVE and NEGATIVE arms; the IN_PLANE /// arm (|d| ≤ ε) visits the positive child and emits NOTHING. /// public static void BuildDrawPortalsOnly( WalkBspNode? root, int pass, Vector3 viewpointInBuilding, Action emitPortal) { if (root is null || root.IsFail) return; Walk(root, pass, viewpointInBuilding, emitPortal); } private static void Walk( WalkBspNode node, int pass, Vector3 viewpoint, Action emitPortal) { while (true) { float d = Vector3.Dot(node.SplittingPlane.Normal, viewpoint) + node.SplittingPlane.D; int side = d > WalkVisibilityMath.Epsilon ? 0 : d < -WalkVisibilityMath.Epsilon ? 1 : 2; WalkBspNode? next; if (node.IsPortal) { if (side == 0) { Visit(node.NegNode, pass, viewpoint, emitPortal); foreach (WalkPortalRef portal in node.InPortals!) emitPortal(portal, pass); next = node.PosNode; } else if (side == 1) { Visit(node.PosNode, pass, viewpoint, emitPortal); foreach (WalkPortalRef portal in node.InPortals!) emitPortal(portal, pass); next = node.NegNode; } else { Visit(node.PosNode, pass, viewpoint, emitPortal); next = node.NegNode; } } else { if (side == 0) { Visit(node.NegNode, pass, viewpoint, emitPortal); next = node.PosNode; } else { Visit(node.PosNode, pass, viewpoint, emitPortal); next = node.NegNode; } } if (next is null || next.IsFail) return; node = next; // retail's tail-continue } } private static void Visit( WalkBspNode? child, int pass, Vector3 viewpoint, Action emitPortal) { if (child is null || child.IsFail) return; Walk(child, pass, viewpoint, emitPortal); } /// /// PView::DrawPortal @0x005a5ab0 for one emitted portal polygon: /// resolve the CBldPortal, push view slots on its stab cells /// (add_views), run the CBldPortal ConstructView, on pass 2 /// success run the look-in DrawCells, then pop the stab views. The /// GPU-state backup/restore and the building-frame re-push are /// submission concerns (FW2); the CPU state here is complete. /// public static bool DrawPortal( WalkPView pview, WalkBuilding building, in WalkPortalRef portalRef, int pass, IWalkBuildingFrameContext ctx, IWalkPortalPassSink sink) { ref readonly WalkBldPortal bldPortal = ref building.Portals[portalRef.PortalIndex]; AddViews(bldPortal.StabList, ctx); bool ok = ConstructBuildingView( pview, building, in bldPortal, portalRef.Polygon, pass, ctx, sink); if (ok && pass != 1) sink.OnDrawCells(pview); RemoveViews(bldPortal.StabList, ctx); return ok; } /// /// The CBldPortal PView::ConstructView overload @0x005a59a0: /// the viewer's side of the portal plane must EQUAL the authored /// portal_side (IN_PLANE within ±ε fails both gates); the polygon must /// survive the active-view clip with ≥3 points; the destination cell /// must be Visible; the clipped view is appended to its top slot. Pass 1 /// punches the polygon; pass ≠ 1 recurses into the interior flood. /// public static bool ConstructBuildingView( WalkPView pview, WalkBuilding building, in WalkBldPortal bldPortal, WalkPolygon polygon, int pass, IWalkBuildingFrameContext ctx, IWalkPortalPassSink sink) { Vector3 viewpoint = ctx.ViewpointInBuilding(building); float d = Vector3.Dot(polygon.Plane.Normal, viewpoint) + polygon.Plane.D; int side = d > WalkVisibilityMath.Epsilon ? 0 : d < -WalkVisibilityMath.Epsilon ? 1 : 2; if (bldPortal.PortalSide != 0) { if (side != 1) return false; } else if (side != 0) { return false; } Span clipped = stackalloc WalkScreenPoint[64]; int n = ctx.ClipBuildingPolygon(building, polygon, side, clipped); if (n == 0) return false; WalkCell? cell = ctx.GetVisible(bldPortal.OtherCellId); if (cell is null) return false; if (!WalkCopyView.Append( cell.TopView, clipped[..n], ctx.Rays, ctx.WorldViewpoint)) return false; if (pass != 2) sink.OnPunch(polygon); // DrawPortalPolyInternal(poly, pass == 1) if (pass != 1) pview.ConstructView(cell, ToEntryIndex(bldPortal.OtherPortalId), ctx.CellContext); return true; } private static int ToEntryIndex(int otherPortalId) => otherPortalId < 0 ? 0xFFFF : otherPortalId; private static void AddViews(uint[] stabList, IWalkBuildingFrameContext ctx) { foreach (uint id in stabList) ctx.GetVisible(id)?.PushView(); } private static void RemoveViews(uint[] stabList, IWalkBuildingFrameContext ctx) { foreach (uint id in stabList) ctx.GetVisible(id)?.PopView(); } } /// The building half of the frame context: building-local /// viewpoint and projection (retail pushes the building's object frame /// before the portal pass), plus the shared cell context. public interface IWalkBuildingFrameContext { Vector3 ViewpointInBuilding(WalkBuilding building); /// The viewer's distance to the building /// (CPhysicsPart::UpdateViewerDistance's input) — selects the /// degrade level whose drawing BSP the portal pass walks. float ViewerDistanceTo(WalkBuilding building); /// Project + clip one building-local portal polygon against /// the ACTIVE view (retail: GetClip with do_clip=1 in the building /// frame). Returns the surviving count. int ClipBuildingPolygon( WalkBuilding building, WalkPolygon polygon, int side, Span output); WalkCell? GetVisible(uint cellId); IWalkRayCaster Rays { get; } Vector3 WorldViewpoint { get; } IWalkFrameContext CellContext { get; } }