diff --git a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs index 2f26f277..94213e52 100644 --- a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs +++ b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs @@ -136,6 +136,11 @@ public sealed class RetailFrameWalk { sink.Emit(WalkEvent.Building(building.PositionCellId)); if (!building.HasGeometry) return; + // Retail walks the CURRENT degrade level's drawing BSP + // (part->gfxobj[deg_level]); a degraded-out slot skips everything + // after publishing the portal list. + WalkBspNode? bsp = building.SelectDrawingBsp(ctx.ViewerDistanceTo(building)); + if (bsp is null) return; int viewCount = Math.Max(activeViews.ViewCount, 0); var passSink = new PortalPassSink(sink); @@ -144,11 +149,11 @@ public sealed class RetailFrameWalk { ctx.SetActiveView(activeViews, v); WalkBuildingPortals.BuildDrawPortalsOnly( - building.DrawingBsp, 1, viewpoint, + bsp, 1, viewpoint, (portalRef, pass) => WalkBuildingPortals.DrawPortal( _outdoorPView, building, portalRef, pass, ctx, passSink)); WalkBuildingPortals.BuildDrawPortalsOnly( - building.DrawingBsp, 2, viewpoint, + bsp, 2, viewpoint, (portalRef, pass) => WalkBuildingPortals.DrawPortal( _outdoorPView, building, portalRef, pass, ctx, passSink)); } diff --git a/src/AcDream.App/Rendering/Walk/WalkBuilding.cs b/src/AcDream.App/Rendering/Walk/WalkBuilding.cs index a43727bd..6a4d278b 100644 --- a/src/AcDream.App/Rendering/Walk/WalkBuilding.cs +++ b/src/AcDream.App/Rendering/Walk/WalkBuilding.cs @@ -37,6 +37,10 @@ public sealed class WalkBspNode 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 @@ -47,12 +51,35 @@ public sealed class WalkBuilding public WalkBldPortal[] Portals = []; - /// The drawing BSP of part 0's GfxObj (portal-only view). + /// 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 + } } /// @@ -246,6 +273,11 @@ 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. diff --git a/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs index 471dfd66..a969c34f 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/RetailFrameWalkTests.cs @@ -50,6 +50,7 @@ public sealed class RetailFrameWalkTests public float ViewportHeight => 480f; public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero; + public float ViewerDistanceTo(WalkBuilding building) => 0f; public IWalkFrameContext CellContext => this; // Permissive near plane: every column wholly inside. public WalkPlane CyPlane => new(new Vector3(0, 0, 1), 0f); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingPortalTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingPortalTests.cs index 631a5917..88ed24d0 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingPortalTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkBuildingPortalTests.cs @@ -48,6 +48,7 @@ public sealed class WalkBuildingPortalTests public float ViewportHeight => 480f; public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Zero; + public float ViewerDistanceTo(WalkBuilding building) => 0f; public IWalkFrameContext CellContext => this; public int ClipBuildingPolygon( diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs index 8f8aab54..f858150c 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkLandscapeDatBuilder.cs @@ -108,9 +108,31 @@ public static class WalkLandscapeDatBuilder int cellIndex = (int)(entry.Building.PositionCellId & 0xFFFFu) - 1; if (cellIndex >= 0 && cellIndex < 64) block.CellBuildings[cellIndex] = entry.Building; + + // Retail's loaded-interior rule (CLandBlock::init_buildings + // @0052fd80 → add_to_stablist → grab_visible_cells): a + // full-res block loads exactly its buildings' portal + // stab cells — NOT every interior. The flood halts at + // unloaded cells (GetVisible null), which is what + // limits retail's look-in punches to nearby doorways. + foreach (WalkBldPortal portal in entry.Building.Portals) + { + if (portal.OtherCellId != 0xFFFFFFFFu + && !cells.ContainsKey(portal.OtherCellId)) + { + WalkCell? c = WalkWorldDatAdapter.BuildCell( + dats, portal.OtherCellId, blockOffset); + if (c is not null) cells[c.CellId] = c; + } + foreach (uint stab in portal.StabList) + { + if (cells.ContainsKey(stab)) continue; + WalkCell? c = WalkWorldDatAdapter.BuildCell( + dats, stab, blockOffset); + if (c is not null) cells[c.CellId] = c; + } + } } - WalkWorldDatAdapter.BuildInteriorCells( - dats, landblockId, blockOffset, cells); } landscape.Blocks[gx * midWidth + gy] = block; } diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs index fa393784..6ca37bdc 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkLookInGateSweepTests.cs @@ -24,13 +24,7 @@ public sealed class WalkLookInGateSweepTests public void Emit(in WalkEvent walkEvent) => Events.Add(walkEvent); } - [Fact(Skip = "Adjudicated 2026-08-30: NO gate decode reproduces retail, and " - + "the join diagnostic proves the data is right — the missing mechanism " - + "is CEnvCell::GetVisible's LOADED-interior-cell gate (retail punched " - + "only the two buildings nearest the player; their interiors were " - + "loaded, farther ones were not; the replay loads everything). Port " - + "the interior load radius (landcell stab-list pull around the " - + "player) and re-run this sweep to pin the plane/side decode.")] + [Fact] public void Sweep_the_lookin_gate_decodes_against_the_street_fixture() { string? datDir = CornerFloodReplayTests.ResolveDatDir(); diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs index 63396e8b..a0821a2b 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceReplay.cs @@ -112,6 +112,10 @@ public sealed class WalkTraceReplayContext : IWalkFrameContext, IRetailFrameWalk public Vector3 ViewpointInBuilding(WalkBuilding building) => Vector3.Transform(WorldViewpoint, Buildings[building].InverseWorldTransform); + public float ViewerDistanceTo(WalkBuilding building) + => Vector3.Distance( + WorldViewpoint, Buildings[building].WorldTransform.Translation); + public int ClipBuildingPolygon( WalkBuilding building, WalkPolygon polygon, int side, Span output) { diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs index 907e31bc..fcd097e8 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkWorldDatAdapter.cs @@ -148,8 +148,26 @@ public static class WalkWorldDatAdapter } WalkBspNode? bsp = null; + var degradeLevels = new List(); if (dats.Get(buildingInfo.ModelId) is GfxObj gfxObj) + { bsp = ConvertDrawingBsp(gfxObj, gfxObj.DrawingBSP?.Root); + if (gfxObj.DIDDegrade != 0 + && dats.Get(gfxObj.DIDDegrade) + is GfxObjDegradeInfo degradeInfo) + { + foreach (GfxObjInfo level in degradeInfo.Degrades) + { + WalkBspNode? levelBsp = null; + if (level.Id != 0 + && dats.Get((uint)level.Id) is GfxObj levelGfx) + { + levelBsp = ConvertDrawingBsp(levelGfx, levelGfx.DrawingBSP?.Root); + } + degradeLevels.Add(new WalkBuildingDegradeLevel(level.MaxDist, levelBsp)); + } + } + } Matrix4x4 worldTransform = Matrix4x4.CreateFromQuaternion(buildingInfo.Frame.Orientation) @@ -161,6 +179,7 @@ public static class WalkWorldDatAdapter PositionCellId = positionCellId, Portals = portals, DrawingBsp = bsp, + DegradeLevels = degradeLevels.ToArray(), }, worldTransform, inverse));