namespace AcDream.App.Rendering.Walk; /// /// Campaign FW1 — the frame walk's observable event stream. These events /// mirror the FW0 oracle-trace vocabulary one-to-one /// (docs/research/2026-08-30-fw-walk-oracle/README.md): the conformance /// contract is that RetailFrameWalk, fed the same world state and /// camera pose as a capture, emits the identical sequence. Richer draw /// commands (mesh/state/stage tuples for the FW2 ordered stream) layer on /// later; this vocabulary stays the conformance surface. /// public enum WalkEventKind { /// LScape::draw @0x00506330 ran (outdoor root, or drawn /// through an interior frame's exit views). Landscape, /// RenderDeviceD3D::DrawBuilding @0x0059f2a0 entered for /// one building (identified by its position cell id). Building, /// PView::DrawInside @0x005a5860 entered — the frame is /// rooted at this interior cell. DrawInside, /// PView::DrawCells @0x005a4840 entered with the flood's /// cell list (far-to-near iteration happens inside; the event carries the /// list in storage order, matching the oracle dump). DrawCells, } public readonly record struct WalkEvent( WalkEventKind Kind, uint CellId, int OutsideViewCount, IReadOnlyList Cells) { public static WalkEvent Landscape() => new(WalkEventKind.Landscape, 0, 0, Array.Empty()); public static WalkEvent Building(uint positionCellId) => new(WalkEventKind.Building, positionCellId, 0, Array.Empty()); public static WalkEvent DrawInside(uint cellId) => new(WalkEventKind.DrawInside, cellId, 0, Array.Empty()); public static WalkEvent DrawCells(int outsideViewCount, IReadOnlyList cells) => new(WalkEventKind.DrawCells, 0, outsideViewCount, cells); } /// /// The sink a walk emits into. FW1 conformance tests collect events; FW2's /// production sink will additionally receive the ordered draw stream. /// /// Campaign FW3.2b-1 additive seam: also /// calls the five richer, default-no-op members below at turns the /// vocabulary-only stream cannot express (a visited /// landscape cell with no building emits no at all; /// carries only a cell id, not the /// reference a driver needs to look up shell /// content, degrade state, or world transform; and punches are not part of /// the FW1 conformance vocabulary at all — the oracle traces do not log /// them). Every FW1/FW2 sink that only implements /// continues to compile and behave identically — these are C# default /// interface members, never called by any pre-FW3.2b-1 code path. /// public interface IWalkEventSink { void Emit(in WalkEvent walkEvent); /// /// Fires once per visited landscape cell, AFTER that cell's building /// turn (if any) — RenderDeviceD3D::DrawSortCell @0x0059f140 /// calls DrawBuilding(building) first, then /// DrawObjCell(cell) UNCONDITIONALLY (decomp-confirmed /// 2026-08-30). follows retail's outdoor cell /// encoding — (landblockId & 0xFFFF0000) | (cellIndex + 1) — /// the same convention /// and + /// already use. Default /// no-op. /// void OnLandscapeCellTurn(uint cellId) { } /// /// Fires at once retail's own /// gate has passed — RenderDeviceD3D::DrawBuilding @0x0059f2a0 /// wraps its ENTIRE body (the alpha barrier, the portal pass, the shell /// draw) in if (part->gfxobj[part->deg_level] != 0) /// @0x0059f2d3 — a degraded-out slot draws NOTHING beyond the /// unconditional /// call. and a non-null /// result together model that /// one gate, so this hook fires only after both have passed. This is the /// ALPHA BARRIER turn (D3DPolyRender::FlushAlphaList(0f) /// @0x0059f30b) — retail's own order runs it BEFORE the portal pass /// (CPhysicsPart::Draw(parts, 1)), which in turn runs BEFORE the /// shell draw (CPhysicsPart::Draw(parts, 0) — see /// , fired separately, after the portal /// pass completes). Default no-op. /// void OnBuildingTurn(WalkBuilding building) { } /// /// Fires at the END of , after /// its two-pass portal walk (punches + look-ins, across every active /// view) has fully completed — CPhysicsPart::Draw(parts, 0) /// @0x0059f331, retail's plain-mesh draw of the building's own shell, /// which runs strictly AFTER CPhysicsPart::Draw(parts, 1) (the /// portal flavor) per the decomp sequence at @0x0059f30b–0x0059f345. Only /// fires when also fired (same gate; see its /// doc comment) — a degraded-out slot reaches neither. Default no-op. /// void OnBuildingShellTurn(WalkBuilding building) { } /// /// Fires when the two-pass portal machinery /// () would draw a far-Z punch fan /// (DrawPortalPolyInternal @0x0059bc90, pass 1) — carries the /// owning building and the polygon in BUILDING-LOCAL space (as stored on /// the emitted ) so a driver can resolve the /// world transform itself. The FW1 oracle traces never log punches /// ('s internal PortalPassSink.OnPunch /// stayed a no-op through FW1/FW2 for exactly that reason). Default /// no-op. /// /// The building whose portal walk emitted the /// punch. /// The portal polygon, building-local. /// The active-view index the two-pass walk /// is pinned to (retail building_view @0x0059f3bf) — the punch /// fan clips against THIS view's slice planes in production. void OnPunchGeometry( WalkBuilding building, WalkPolygon polygon, int activeViewIndex) { } /// /// Fires once per interior root, at the point PView::DrawCells /// @0x005a4840 actually DRAWS the root flood's own cells — NOT where the /// call for the /// SAME flood fires (that one sits at breakpoint-ENTRY order, matching /// the FW0 oracle traces; it only RECORDS the flood list). Retail's own /// order inside DrawCells is: LScape::draw FIRST /// (pc:432719, only when exit views survived — see /// and /// ), then a full depth clear /// (pc:432731-432732), then the exit-portal seals (pc:432785-432786) — /// BOTH unconditional for an interior root's /// own flood, whether or not a landscape turn just ran — and ONLY THEN /// the flood's cells far-to-near (shell then contents per cell, same /// discipline as 's per-cell contents /// and a building's look-in). This hook fires at that later point, so /// this is where a driver should actually draw . /// Building look-in floods are UNAFFECTED — retail calls /// DrawCells re-entrantly there with ov==0 and no /// landscape/clear/seal step, so their /// call still fires at the actual draw point (a /// driver may keep drawing those immediately, as before). Default no-op. /// void OnInteriorFloodDrawTurn(IReadOnlyList cells) { } }