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)
{
/// FW4 slice 1: the landscape turn carries the ACTIVE view
/// set's ViewCount (retail LScape::draw iterates the
/// views the walk itself installed — 1 for the outdoor root's
/// full-screen default view, the interior root's own surviving
/// exit-view count otherwise). The driver fans exactly this many
/// terrain slices; the old clip apparatus no longer supplies the
/// count. Reuses the record's existing
/// field; the conformance trace mapping compares the kind only
/// (LS), so oracle fixtures are unaffected.
public static WalkEvent Landscape(int activeViewCount)
=> new(WalkEventKind.Landscape, 0, activeViewCount, 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) { }
}