Additive, nothing invokes it yet (the cutover flip is step 2):
- WalkProductionLeafRenderer + RetailPViewPassExecutor.WalkLeaf: the
walk driver's leaf turns over the SAME executor renderers the packed
path uses today - DrawWalkSky (the per-slice sky block looped under
one driver turn), DrawWalkTerrainSlice (the terrain block of
DrawLandscapeSlice), per-cell shells via EnvCellRenderer,
DrawWalkPunchFan (PortalDepthMaskRenderer far-Z, +ShellDrawLiftZ
matching today's DrawPortalDepthWrite until FW3.3 retires it), the
alpha barrier via FlushLandscapeAlphaFartherThan, and caller-supplied
clear/seal actions (the renderer owns the pass scope). The cutover
changes ORDER, never leaf mechanics.
- Punch fans now carry the ACTIVE VIEW INDEX end to end (retail pins
building_view = Render::portal_view_num @0x0059f3bf for the whole
two-pass walk; the fan clips by that view's slice planes):
PortalPassSink.ActiveViewIndex -> IWalkEventSink.OnPunchGeometry ->
IWalkFrameLeafRenderer.DrawPunchFan.
- The FW3.2b-2 rooting design is recorded in the plan (dual-compute
split, LookInObject route filtered to dynamics, consumer
re-pointing, gate list).
Suites: full Release build 0 warnings; Walk lane green; hermetic green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The oracle trace order DI|DC|LS is breakpoint-ENTRY order; retail's
actual DRAW order inside PView::DrawCells @0x005a4840 for an interior
root is LScape::draw FIRST (pc:432719, only when exit views survive),
then the depth clear (pc:432731-432732), the exit-portal seals
(pc:432785-432786), THEN the flood's own cells far-to-near. The
driver drew flood cells before the landscape - inverted.
RetailFrameWalk.DrawInside gains the additive
OnInteriorFloodDrawTurn(cells) hook firing after the conditional
landscape turn; the DC EVENT stays at its original point (conformance
untouched - 40/1 InstalledDat green). WalkFrameDriver records the
interior flood at the DC turn and draws it at the new turn:
flush -> ClearInteriorDepth -> flush -> DrawExitSeals -> per-cell
shell-then-contents. Building look-in floods still draw immediately at
their building turn (retail's reentrant DrawCells with no clear/seal).
Two new leaf members map to IWorldPassScope.ClearInteriorDepth and the
seal-fan machinery at FW3.2b-2. Reconciliation note recorded: the
driver clears unconditionally for interior roots while production
stages the clear on OutsideViewSlices>0 - observably equivalent at
ov=0, awaiting a firmer decomp read of the clear's gate.
Suites: full Release build 0 warnings; Walk lane 201/1 skip;
hermetic 6,753/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkFrameDriver executes one full static-content frame from the walk's
turns so GPU command-buffer order equals retail's walk order. One rule
does the interleaving: the accumulated OrderedDrawStream flushes
through SubmitOrderedStream immediately before EVERY non-stream draw
(sky, terrain slice, cell shell, punch fan, alpha barrier).
Turn script, all decomp-cited and two of them corrected in review:
- Interior flood: per cell IN FLOOD ORDER, shell first then contents
(PView::DrawCells @0x005a4840: DrawEnvCell @0x005a4abe precedes
DrawObjCellForDummies @0x005a4b0d).
- Landscape: sky once, terrain per active slice, then blocks
far-to-near; per cell the building turn precedes the cell's outdoor
statics (DrawSortCell @0x0059f140).
- Building (DrawBuilding @0x0059f2a0): the BLD probe event stays at
entry, but the ENTIRE body - alpha barrier, portal passes, shell -
sits inside retail's gfxobj[deg_level]!=0 gate @0x0059f2d3, and the
order is FlushAlphaList @0x0059f30b -> the two-pass punch/look-in
walk -> THEN the shell draw @0x0059f345. The driver review caught
both the missing gate and a shell-before-punch inversion; fixed
with the addresses cited.
Walk seam: three additive default-implemented IWalkEventSink hooks
(OnLandscapeCellTurn / OnBuildingTurn / OnBuildingShellTurn /
OnPunchGeometry) - every existing sink and all FW1 conformance
fixtures unchanged. WalkLandBlock gains LandblockId for the cell-id
encoding. Leaf draws go through IWalkFrameLeafRenderer so FW3.2b-2
wires the real renderers and the referee suite runs on fakes +
RecordingGpuDevice.
Flagged for FW3.2b-2/FW4 adjudication (documented in code):
FlushFartherThan(building distance) vs retail flush-all
FlushAlphaList(0f); terrain-before-statics within the landscape turn.
Suites: full Release build 0 warnings; Walk lane 200/1 skip;
InstalledDat Walk conformance 40/1 untouched; hermetic 6,752/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The piece that turns walk-visited static content into draws, with no
production frame wiring (FW3.2b roots the frame):
- TryClassifyBatch: ONE shared per-batch classify core (the #426
untextured gate, #188 opacity promotion, texture resolve, foliage
classification, in the exact original order) extracted from
ClassifyBatches; the classic and packed classifiers now call it -
behavior-identical, proven by the full hermetic + InstalledDat +
Core Wb suites.
- ClassifyEntityForWalk / WalkClassifiedBatch: the per-entity seam
yielding per-batch keys + instance data WITHOUT InstanceGroup
bucketing, plus the per-part selection data (picking stays alive on
the walk path - the survey's unlisted-consumer fix).
- WalkStaticStreamPopulator: per-entity walk-ordered opaque appends
(under depth Less, opaque order is pixel-relevant only for coplanar
surfaces, which retail resolves first-drawn-wins in ITS order -
never material-grouped), translucent instances to the SAME
RetailAlphaQueue via SubmitWalkAlphaInstance (identical viewer
distances; walk-order submission improves retail's tie fidelity),
selection parts published per entity.
- SubmitOrderedStream now owns _orderedDrawCullModes, retiring the
FW2-recorded alpha-scope interleaving constraint;
DrawIndirectRangeRhi takes an optional cull array (all existing
call sites unchanged). The referee test was verified to FAIL
against the old shared-scratch behavior.
- WalkDrawStage.OutdoorStatic added for the landscape turn.
Suites: full Release build 0 warnings; Walk lane 195/1 skip;
hermetic 6,747/0 (the two failures the implementation round reported
were transient - both pass in isolation and in the full run).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retail frame walk's world model now materializes from production
landblock-build owners through the legal IDatReaderWriter seam, with
zero frame wiring (FW3.2 roots the frame):
- WalkCellFactory: WalkCell built in the SAME pass as LoadedCell
(EnvCellLandblockBuild.BuildVisibilityCell) from the raw portal
Flags/polygons/planes/stab lists already parsed there; stored as
LoadedCell.Walk, committed atomically with the cell. The
fixture-pinned decodes (inverse-0x2 portal side, 0xFFFF->0xFFFFFFFF
exit widening) live here.
- WalkBuildingFactory + WalkBuildingRegistry: the production
WalkBuilding build (drawing BSP with PORT nodes, degrade ladder,
portal sides/stab lists, sort center, model frame) from the SAME
LandBlockInfo the streaming build already fetches, under the
factory's existing DAT lock - closing the gap where BuildingLoader
drops every walk field at load.
- WalkLandscapeAssembler: the retail 51x51 viewer-centred grid
(mid_radius 25) fed incrementally from landblock publish/retire;
per-block z-slab (heightTable[max]+200 / [min]-1) computed
worker-side in LandblockBuildFactory from the heights already in
hand. O(1) SetViewer on same-block frames.
- WalkProductionFrameContext: the walk's frame contexts over
CellVisibility + WalkBuildingRegistry with a generic
inverse-view-projection ray caster (rays feed cross products only -
scale-free) and the znear=0.1 CY plane.
- Publication: LandblockRenderPublisher owns both walk registries,
publishing in the same AdvanceCompleteOne step as BuildingRegistry
and retiring in RemoveBuildingRegistry - same commit, same
retirement, no new ticket stage.
Conformance: ALL TEN oracle fixtures replay identically through the
PRODUCTION builders (WalkProductionWorldConformanceTests) - same
signatures as the test adapter, first run. Known gap documented for
FW3.2: far-tier landblocks carry no EnvCell transaction, so their
z-slab never reaches the assembler.
Suites: full Release build 0 warnings; Walk lane 186/1 skip;
hermetic 6,738/0 (+24); RuntimeDatAccessArchitectureTests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The walk-order submission layer over the existing RHI (plan section FW2):
- OrderedDrawStream: append-only walk-ordered draw commands
(GroupKey + transform + per-instance data + WalkDrawStage + cell
provenance), struct-of-arrays with one lockstep Reset (#193 shape).
The PortalPunch stage exists but has no FW2 submission path - the
submitter throws on it; punch emission lands with FW3 wiring.
- WbDrawDispatcher.OrderedStream partial: per-instance-first emission
(the deferred-alpha shape - command i owns instance i, walk order
survives into the indirect array), each SSBO section written once,
then one DrawIndirectRangeRhi call per maximal merge run. Runs are
built by pure-CPU BuildOrderedMergeRuns and may never span a stage,
pipeline-bucket, or cull boundary; ValidateMergeRun re-checks every
emitted run and throws (the campaign fail-loud rule). Nothing is
sorted, reordered, or dropped: N commands in, N indirect commands
out, covered exactly once.
- WorldDepthContract: retail world depth verified verbatim from the
decomp - Render::zfuncVal @0x00820e1c = 0x2, SetDepthBufferMode
@0x005a2d10 writes the enum directly as D3DRS_ZFUNC so the value IS
D3DCMP_LESS, applied by the surface-state applier @0x0059c80a with
Z-write toggled by blend; the LESSEQUAL sites are GameSky::Draw-local.
Seven world pipeline sites now cite the named constant (no value
changes).
- Plan updated: FW1 status block + gate amendment (the ten pose-stamped
retail traces supersede re-expressing the old-builder replay
fixtures; those retire with the old builder at FW4 and their
scenario classes re-verify at the FW3/FW4 connected gates).
Known FW2 scope notes recorded in the code: the building-detail
overlay replay is production wiring (FW3); the _drawCullModes scratch
may not interleave with a mid-flight RetailAlphaQueue scope (FW3
sequencing constraint). The pixel A/B equivalence proof rides FW3's
cutover toggle where a walk-driven scene first exists.
Suites: full Release build 0 warnings; Walk lane 154/1 skip;
hermetic 6,714/0 (+27 new).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render::deg_mul is auto-tuned by frame load and SWUNG between oracle
captures: doorway-still ran right after the heavy terrace-edge capture
with the multiplier depressed (mul <= 0 puts thresholds at/below ideal,
so 001e/0026/002f select the portless level 1 - retail's zero look-in
floods, reproduced exactly at mul = 0), while every other fixture pins
~ +0.99 (thresholds at max). The recon session's "-0.99" live dump was
real - taken under the same cdb load. RetailFrameWalk now exposes the
multiplier; the doorway test pins 0, the rest use the default.
foundry-entry: frames 1-66 reproduce exactly; the F67-F79 standing
segment diverges ONLY in building 0036's intra-building DC order
(retail 116,118,11d vs replay 11d,116,118). The BSP-traversal microscope
pins the flip to 0036's ROOT plane (N=(0,0,-1) D=2.8): replay eye z
2.33 (d=+0.47, NEG-first) vs retail behaving as d<0 (POS-first) - a
structural ~0.5 m frame question (positionPush(2)/part-scale), only
adjudicable live. Turnkey probe:
tools/walk-oracle/fw1-f67-viewpoint-probe.cdb.
Fixture status: nine of ten fully conformant; foundry-entry exact
through F66 with the 13-frame order segment parked on the probe.
Suites: Walk 127/1 skip; hermetic 6,687/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The moving-fixture divergence was the building degrade ladder:
GfxObjDegradeInfo::get_degrade @0x0051e4b0 (Ghidra-verified - BN's
FPU-flag pseudo-C misread BOTH arm selection and one formula) slides
each level's threshold from ideal toward MAX as the multiplier
approaches 1, and the live client runs the positive arm at ~0.99, so
level 0's portal-bearing BSP survives to ~max_dist (48 for the Holtburg
cottages), not ideal (24). The recon note's "deg_mul = -0.99" was a
sign misread; the negative arm's threshold slides toward MIN and
contradicts the fixtures from both directions. With the two-arm port,
holtburg-walkout, holtburg-transitions, and holtburg-walkabout pass
every pairable frame - the walkout-F2 microscope's prediction
(103,100 | 100x3 | 124 through the cottage exit views) landed exactly.
Also this round, falsified and reverted: a BN-driven swap of the
portal walker's negative/in-plane arms (Ghidra shows side 1 = negative
EMITS, side 2 = in-plane does not - the original port was correct; the
swap broke four fixtures). The walker docs and unit tests now pin the
Ghidra-verified truth table.
Parked with findings: foundry-entry F67 (right flood set, one
plane-side classification at the +/-eps boundary orders 11d before
116/118) and doorway-still (retail shows zero floods at a pose one
meter from walkout-F2's flooding pose; multi-portal clip boundary).
Suites: Walk 124/3 skips; hermetic 6,687/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The moving driver gains adjacent-pose tolerance (the marker-lag capture
artifact) and stays parked: the four moving fixtures diverge at
punch-edge frames under BOTH adjacent poses. FALSIFIED this round and
reverted: the raw-decode + flipped-gates convention (it broke three
still fixtures - the inverted-decode convention stands, six still
fixtures frame-exact). Remaining instruments in the Skip note: znear,
per-view punch ordering inside DrawMesh, exit-view precision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three final pins complete the still-fixture set: (1) the CELL portal
side decode is the INVERSE of the 0x2 bit (uniform with the building
convention; the doorway-still flood proved it - ov=2 n=3 exact, and
foundry-deep stays green); (2) interior frames key the landscape order
off the OUTSIDE-projected landcell (get_outside_cell_id - derived from
the camera origin); (3) the outdoor pview has draw_landscape=FALSE so
look-in floods discard exit portals - the ov=0 pattern of every traced
look-in. CONFORMANT: foundry-deep (every frame), doorway-still,
street-outdoor, terrace-center, terrace-edge (the #456 acceptance
pose), cathedral-arrival - full frames identical to retail. The moving
four diverge only at punch-edge frames (walkabout F9, foundry-entry
F67) - pose-timing sensitivity parked in the driver Skip note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three pinned corrections close the outdoor frame: (1) the camera basis
RE-pinned to +Y-forward - the terrace-edge fixture EXTERNAL ground
truth (the ledge faces the F518 vista, east) overrules the walkabout
motion sweep, whose camera was mouse-turned off the run line; (2)
alwaysDrawObjects (.data default 1 @0x00820ed4) drawn into the
DrawSortCell gate - cell contents draw for every cell of an in-view
block; (3) the building portal side decode data-pinned as the INVERSE
of the cell 0x2 bit (the sweep winning arm). Street result: all 13
buildings in retail exact order (a9b3003c in, aab50002 correctly
absent), punches at exactly 001a and 0022 opening the exact first
cells 16e/164. SOLE remaining delta: look-in flood depth (mine 1 cell,
retail 3-4) - the doorway-still fixture adjudicates the interior flood
in isolation next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-portal dump for the street fixture (archived beside the fixtures):
retail punched exactly the buildings whose portal polygons MY camera
model clips to NOTHING (001a/0022: clipN=0, doorways ~75 deg off my
north-facing axis) and skipped the ones mine puts on-screen (001e/0026:
clipN=4-6) - a clean inversion isolating the final look-in delta to
either the camera FACING decode (re-check the terrace-edge fixture
against its known vista direction; the motion sweep's 20-degree
residual is now suspect) or a portal-pass clip mechanism that does not
gate on the active view (cdb ConstructView(CBldPortal) trace next
retail session). All layers above are retail-pinned.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
UpdateViewerDistance @0x0050e030 + get_degrade @0x0051e4b0 ported with
live-pinned globals: distance measured to the part SCALED SORT CENTER,
effective = max(0, dist - s_rDegradeDistance [live 100]), level = first
with effective < IdealDist (the live auto_update_deg_mul<=0 arm), else
the last level. WalkBuildingDegradeLevel carries the full authored
bands; the adapter fills sort centers; the replay context measures to
the transformed sort center. Sweep state after the rule: the level-0
building set is now correct; the residual divergence is the per-portal
side/clip gate (my arms punch a near-complement of retail two) - the
sweep driver carries the next instrument in its Skip note.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The offline degrade probe proved the mechanism: every Holtburg building
carries PORT nodes ONLY in its level-0 GfxObj (out to ~24-48 m); every
degraded level has zero. Retail walks the CURRENT degrade level BSP
(part->gfxobj[deg_level]) - that is what limits look-in punches to the
nearest buildings. WalkBuilding gains the degrade ladder +
SelectDrawingBsp (band pick; UpdateViewerDistance hysteresis is a port
TODO), the walk selects per viewer distance, the adapter builds
per-level BSPs, and the stab-list load rule (CLandBlock::init_buildings
@0052fd80: a full-res block loads exactly its buildings portal stab
cells) replaces load-everything in the landscape builder. The sweep now
shows clean rosters with all far-building punches gone; remaining
deltas: the near buildings 001a/0022 (50 m/28 m center distance vs the
48 m band edge - sphere-adjusted distance/hysteresis to port) and the
one ring-1 frustum boundary pair (aab50002/a9b3003c).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-building join diagnostic PASSES (001a/0022 portal tables lead
exactly to the traced a9b4016x punch cells; every BSP PortalRef indexes
validly), and the eight-arm gate-decode sweep proves NO plane-sign x
side-flag combination reproduces retail. Together they pin the missing
mechanism: CEnvCell::GetVisible gates punches by the LOADED interior
cell set around the player - retail punched only the two buildings
nearest the player cell; the replay loads every interior so geometry
alone over-punches. Next port piece: the interior load radius (the
landcell stab-list pull). Both diagnostics stay in the suite (the sweep
Skip-parked with the verdict).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The moving-fixture sweep pinned the pose decode (quat storage w,x,y,z;
facing = rotated +X; the consistent triple right=-rot(Y), fwd=rot(X),
up=rot(Z) - mean 20.8 deg vs motion, alternatives >=52). With it the
street-outdoor BLD roster and order match retail EXACTLY except one
ring-1 boundary pair (aab50002 extra / a9b3003c missing). Look-in
punches fire at the wrong buildings under BOTH GfxObj plane-winding
conventions (retail punched only 001a and 0022 into the a9b4016x
cells) - the next adjudication targets the PortalRef.PortalIndex join
and the BuildingPortal side-flag decode via a per-building dump. The
driving diff test carries the state in its Skip note; the flip toggle
stays for the next arm.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkLandscapeDatBuilder assembles the replay grid with the LIVE-dumped
retail landscape model (LScape mid_radius=25 -> 51x51; the resolution
pyramid observed on the capture client: side_cell_count 8 in the 3x3
core, 4 at ring 2, 2 at rings 3-4, 1 beyond; buildings attach only to
full-res blocks - matching the traces roster), retail z-slabs
(heightTable[max]+200 / [min]-1 per CLandBlock unpack @0052f297), and
camera-block-local coordinates. The replay context gains the building
half (placements, active-view install, building-polygon clip). The
street-outdoor conformance diff now reaches real adjudication: the walk
over-culls six traced buildings (jagged boundary - not a clean frustum
edge) and the look-in floods differ; the driving test is parked Skip
while the visibility map is instrumented.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-dumped from the capture client (Render::bw/bh 1024x720,
xinvscale/yinvscale 0.00025, tx 0.127875, ty 0.089875, vdst 0.1330767):
the replay context now uses retail unproject formula verbatim for the
ray caster and the exact frustum (fovY = 2*atan(ty/vdst), aspect =
tx/ty) for projection - no guessed camera constants remain.
foundry-deep conformance stays green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RuntimeDatAccessArchitectureTests correctly flagged WalkWorldDatAdapter
taking raw DatCollection in production. The adapter is the conformance
harness world builder, so it moves into the test assembly; the FW3
production adapter will consume the content-owner seam. Hermetic suite
back to 6,687/0; adapter + conformance 10/10.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The replay harness reconstructs the camera from a pose-stamped oracle
frame (Frame quaternion w,x,y,z storage order; +Y forward / +Z up;
landblock-local origin) and drives the ported walk over adapter-built
cells (cell transforms from EnvCell.Position now populated). The
foundry-deep fixture - the pure-interior frame shape - reproduces
retail EXACTLY on every complete frame: DI + DC(ov=0, [cell]) with no
landscape, 39/39. Conventions are now pinned by live retail output; the
remaining nine fixtures need the outdoor world build-out (landscape
blocks, terrain z-slabs, building transforms + active-view clip) and
join the same gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkOracleTrace decodes the P lines of the pose-stamped capture round
(raw IEEE-754 dwords from Render::viewer_pos @0x0081ef00: camera cell,
world origin, the four Frame quaternion components in storage order)
into WalkOraclePose on each frame. Poseless FW0 fixtures parse as
before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkWorldDatAdapter builds WalkCell/WalkBuilding from real DATs: cell
portals with the PortalSide flag bit and signed OtherPortalId, portal
polygons + planes via the production formula, stab lists, building
position cells (24 m landcell math), building portals with stab lists,
and the drawing-BSP -> WalkBspNode conversion incl. PORT-node PortalRef
polygons (the previously missing consumers the 2026-08-30 survey named).
Lane=InstalledDat conformance: every BLD id the FW0 oracle traces saw
retail draw (26 buildings over 7 landblocks) resolves in the adapter
roster at its EXACT position cell; the traced look-in cells build with
portals/polygons/stabs; the sanctuary BSP carries portal refs. 9/9.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RetailFrameWalk composes the ported machinery into retail frame shapes:
SmartBox::RenderNormalMode rooting (camera-cell low word < 0x100),
PView::DrawInside + the DrawCells event half (interior pview, the
traces pv=009d4a80), LScape::draw (per-view visibility, blocks
far-to-near, per-block cells far-to-near, buildings at their cell turn)
and RenderDeviceD3D::DrawBuilding (BLD at entry before the degrade
check; two-pass BSP portal walk per active view on the outdoor pview,
the traces pv=009d4b08). WalkLandscape ports the block grid +
draw_check_blocks/landcell_check visibility (192 m/24 m pitch,
viewer-relative, union across views, never downgrade). Six composition
tests: outdoor far-to-near emission, degrade-entry event, interior
ov=0/ov=1 shapes, rooting, complete view unwind.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkBuildingPortals transcribes the portal-only drawing-BSP walk
(BSPTREE/BSPNODE::build_draw_portals_only @0x00539860/@0x0053c100,
BSPPORTAL::portal_draw_portals_only @0x0053d870 - opposite-child-first
= far-to-near emission, IN_PLANE PORT arm emits nothing),
PView::DrawPortal @0x005a5ab0 (stab add/remove_views around the
look-in, DrawCells on pass-2 success), and the CBldPortal
PView::ConstructView overload @0x005a59a0 (side must EQUAL the authored
portal_side, clip survival, Visible destination, punch on pass 1, flood
recursion on pass 2). Punch surfaces via the pass sink for FW1
conformance; the depth-fan submission itself is FW2. Six tests: BSP
emission order both viewer sides, in-plane suppression, pass-1
punch-no-flood, pass-2 flood + DC event, sidedness rejection, unloaded
destination skip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkPView transcribes ConstructView @0x005a57b0, InitCell @0x005a4b70,
InsCellTodoList @0x005a4f50 (nearest-first pop), ClipPortals @0x005a5520,
OtherPortalClip @0x005a5400 (inverted ==0 sidedness), AddViewToPortals
@0x005a52d0 (update_count watermark, duplicate-reach arm), AddToCell
@0x005a4d90, SetOtherSeen @0x005a4e30, FixCellList/AdjustCellPlace/
AdjustDrawList/AdjustCellView @0x005a5250/0x005a5010/0x005a4e90/
0x005a5770, plus set_view and PView::GetClip @0x005a4320 over the
WalkScreenClip chain. Resolves and documents the portal-flag convention
(inflag=1 = the portal FACES the viewer and feeds max_indist; inflag=0 +
armed seen = an opening — the same side condition building look-ins
require), which the PDB names obscure. WalkWorld supplies the cell model
and frame-context seam. Six synthetic-world flood tests pass on first
run: traversal, facing rejection with the exact distance key, exit-view
raising, entry-portal no-ping-pong, chain ordering, unloaded skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkScreenClip ports PrimD3DRender::xformStart @0x0059b990 (homogeneous
viewport coords, y-flip, no divide) and ACRender::polyClipFinish
@0x006b6d00 (w>=cdstW plane then last-to-first edge passes, inside =
side<=0 homogeneous 2D cross, reverse-scan passes with original-winding
restore, <3 early-outs). WalkViews ports the view_type/portal_view_type
data model and Render::copy_view @0x0054dfc0 exactly: in-place divide,
the keep/last/stl/second pruning bookkeeping with all three closing
wrap checks, <3 reject leaving dest untouched, cap 31, pool-base reset
at view_count==0, retail fabs on copy, and edge planes
N=normalize(cross(ray[k+1],ray[k])), d=-dot(N,eye) behind an
IWalkRayCaster seam. Thirteen new tests; Walk namespace 85/85.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The second decomp round (flood bookkeeping/propagation, view-clip
support, landscape visibility) is archived Ghidra-arbitrated - it caught
a load-bearing BN inversion (InsCellTodoList pops NEAREST-first, which
is what makes the draw list far-to-near) and three more traps (the 192m
elided constant, the min/max double positional swap, the copy_view
cross order - the walk doc section 6 is corrected). WalkVisibilityMath
ports get_pt_limit @0x0054b840, get_clip_height @0x0054cff0,
corner/block_plane_check @0x0054b930/@0x0054d060, block_check
@0x0054dc50, and viewconeCheck @0x0054c250 with retail boundary
semantics (strict cull, inclusive partial, touch-out=Outside /
touch-in=EntirelyInside) under 23 focused tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LandWalkOrder ports LScape::get_block_order @0x00504c50 (near-to-far ring
list, walked backwards by LScape::draw), CLandBlock::calc_sq_draw_order
@0x0052f4a0 (cell array filled backwards from the closest cell),
LandDefs::get_dir @0x005a9aa0, and the 9-case closest-cell direction
switch. The shared 8-slot ring coefficient tables were byte-decoded from
the PDB-paired binary (block VA 0x0081cc6c.., cell VA 0x0081df88.. -
identical sets); the intra-ring visit order is preserved exactly because
it decides draw order between equidistant blocks and the oracle traces
pin it. Thirty focused tests cover coverage/ordering invariants, the
hand-expanded ring-1 pattern, and the direction/closest-cell contracts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkOracleTrace parses the FW0 retail traces (frames of LS/BLD/DI/DC
events, truncated detach frame dropped) and loads fixtures from the
research directory. Nineteen tests pin the load-bearing shapes: the far
building drawn every terrace-edge frame, the cathedral roster cull, the
stable doorway root, the one-frame walkout handover, camera-cell rooting
(porch-cam), the foundry landscape drop, and bit-identical stationary
frames. FW1 conformance builds on these.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail draws every cell's objects inside LScape::draw's far-to-near walk
(DrawSortCell @0x005A17C0) and drains the one delayed-alpha list at the
DrawCells boundary AFTER the finished walk (@0x005A4872). Our outdoor
frame drained at the landscape-stage end and then drew punches, interior
shells, cell objects, and ALL dynamics — every one of those opaque
passes overwrote the already-composited flames (the reopened#132
candle class: "the door draws over the candle", creatures at openings).
Depth and barrier A/Bs were no-ops because the eraser is opaque color
painted after the drain.
Two retail-cited ordering corrections, outdoor-node roots only:
1. The stage-boundary drain is skipped and FlushLandscapeAlpha() runs
after DrawDynamicsLast, where the frame's opaque world depth is
complete — the one far-to-near list composites over everything,
exactly like retail's boundary flush relative to its finished walk.
2. Before DrawExitPortalMasks, FlushLandscapeAlphaFartherThan(
ExitPortalMaskBarrierDistance(...)) drains everything at or beyond
the nearest cell whose exit-portal mask is about to write far-Z —
retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE its
portal-only pass, so in the far-to-near walk nothing already drained
can meet a punched aperture's falsified depth. Without this, the
first correction let exterior waterfalls z-pass across punched
apertures whose true landscape depth the punch erased (found live at
the cathedral gate). Nearer content stays queued and legitimately
composites in front of punched structures.
Interior roots keep the pre-clear stage-boundary drain unchanged.
User-gated live: Holtburg sign candle whole in front of the sign and
tower door at the aligned pose; cathedral waterfalls contained at every
camera zoom, inside and outside. Register row AP-236 retired (the
walk-order outcome reconstruction is complete; AP-34 remains the
umbrella for the CYpt-sort reconstruction itself). Filed #456 for the
separate occluded-distant-building/creature admission residual this
session diagnosed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The paperdoll was visible only in portal space. Root cause: the classic
WbDrawDispatcher.Draw path appended its transforms into the SHARED world
transform frame (WorldTransformFrameArena.Append) with a non-zero base
instance, but the default mesh shaders index every parallel per-instance
array - clip slots, light sets, indoor, OPACITY, selection lighting,
detail category - zero-based; only the packed world submission's shader
convention subtracts the shared-arena prefix. With a world frame active
the doll drew all instances at per-instance opacity 0 into a cleared
target: counted draws, blank pixels, deterministic. Portal space worked
because no world transform frame is active there, so the same code took
the ring path with base 0. The private viewports are the only production
consumers of the classic path, hiding the defect everywhere else.
Fix: WbDrawDispatcher.NextClassicDrawIsPrivatePass - the private
viewport renderer marks its draw and WriteWorldTransformSection routes
private passes onto the plain ring path unconditionally (self-contained
render state: the private pass owns its own camera, lighting, and
target, and must not depend on the world frame's pose address space).
Also landed, each independently justified:
- Per-GPU-flight-slot private targets (PrivateViewportFlightTargets),
restoring the pre-f6fe0f2a design: that revert's claim that frame
submission order protects the single target's write->sample transition
is not guaranteed across Vulkan command buffers. Per-slot completed
scenes fix the cleared-sibling-after-reveal wart the old attempt had.
- Paperdoll resource preparation moved to the frame resource phase
(IPrivateEntityViewportResourcePreparation) before world draws consume
the bounded composite-upload budget.
- The presenter redresses on every dirty edge (an appearance-equal clone
can pin retired readiness across generations; the renderer's two-phase
promote keeps the last completed image visible during replacement),
publishes only non-zero handles, and clears the viewport exactly once
at the explicit character-session boundary.
Verified live on the clean build: doll visible in the NORMAL world,
visible through portal space, and still visible after arrival - the
exact reported repro cycle. 26 paperdoll/private-viewport/preparation
tests plus 60 renderer-suite tests pass; owner visual gate pending.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two retail-parity corrections on top of 684380d4, user-gated together
(no regressions; door/creature-through-opening rendering verified live
at Holtburg; cathedral waterfall and look-north casts pass):
1. Drawn-once look-in dynamics. Retail marks every drawn non-player
part for the frame (DrawMeshInternal @0x0059F360 GetDrawnThisFrame), so
an object draws once, with its cell. acdream drew a look-in cell's
dynamics twice under an outdoor root - once correctly with the look-in,
then again in dynamics-last after the boundary alpha drain, where the
second draw overpainted nearer flames. Both the accepted path
(_lookInCellIds) and the frame product (BuildDynamicLastRoute) now
exclude dynamics whose parent cell drew as a look-in.
2. Pre/inter-building barriers drain only content FARTHER than the
building they precede (FlushLandscapeAlphaFartherThan +
RetailAlphaQueue.FlushFartherThan + conservative anchor-origin
threshold). Retail's far-to-near walk guarantees a building's
FlushAlphaList(0f) @0x0059F2A0 has only farther content queued; a
nearer emitter composites at a later flush. AP-236 documents the
remaining barrier-order divergence.
The #132 candle-before-door overdraw is NOT yet fixed by these steps
and stays open: the current suspect is that houses without a
constructed look-in still draw their interior-parented door in
dynamics-last after the outdoor candle's boundary drain (see the
2026-08-29 ledger for the retail flush-after-objects hypothesis).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Retail never clips a particle to a portal view: each emitter's polys
join the ONE alpha list during its owner cell's far-to-near walk turn
(LScape::draw @0x00506330 iterates block_draw_list reversed; DrawBlock
@0x005A17C0 walks cells; ShouldDrawParticles @0x0050FE60 gates by cell
and distance), and occlusion is the depth test at FlushAlphaList
@0x0059D2E0 (its float is a COUNT threshold - 0f = flush all). The
1d2f2f73 architecture instead re-submitted particles once per
OutsideView slice under that slice's hardware clip slot, which cut
effects at aperture boundaries and drew nothing when no outside slice
was in view (the cathedral look-north disappearance).
Now: unattached emitters submit once per frame by owner-cell kind
(outdoor landcells in the landscape stage, interior EnvCells in the
final world scope - new UnattachedEmitterCellScope filter); cell,
shell-route, barrier-static, and late-stage owners submit their
per-slice cone-cull UNION once with clipSlot 0; and particles emit in
the stage matching their PARENT CELL - an interior dynamic whose
sphere straddles an exit-portal plane keeps its mesh in both stages
(#118) but its particles move to the final pass, so the interior
stage can no longer repaint over them (the aperture-band star cut).
Also lands the inert Change-2 primitives for the AP-236 retirement
(candle-behind-door): RetailAlphaQueue.FlushFartherThan drains only
the far prefix without resetting sources, plus the executor
passthrough and the conservative look-in threshold helper - nothing
calls them yet.
User-gated 2026-08-29 round 2 at the Sanctuary Cathedral: spell and
recall stars cover the whole room at every camera direction including
north; waterfall containment holds on retail's depth/seal mechanism;
adjacent-room particles/lights, walls, Holtburg, recall unregressed
(paperdoll remains pre-existing intermittent #443). Register: AP-236
filed for the remaining barrier-order divergence.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Look-in rooms (cells reached through building portals) drew their
geometry but never entered the visible-cell set consumed by the
particle gate and the point-light candidacy scope, so their emitters
stayed frozen and their lanterns dark until the player entered or the
camera left the building. Retail has no look-in split:
ShouldDrawParticles @0x0050FE60 gates on cell->IsInView(), and a cell
installed by DrawBuilding @0x0059F2A0 -> PView::ConstructView
@0x005A57B0 is drawn by the same DrawCells traversal as any flooded
cell, so it is in view identically (per-cell in_view stamped by
PView::InitCell @0x005A4B70).
RetailPViewFrameResult now carries InViewCells (main flood + look-in
cells, the existing prepareCells union; the borrowed-scratch contract
is unchanged because RecycleLookInFrames runs at the start of the next
DrawInside). WorldSceneRenderer feeds InViewCells to
ParticleVisibilityController and the point-light scope. DrawableCells
is untouched for seals, the outside-stage predicate, diagnostics, and
the packed-product referee. The legacy 4-arg Reset defaults
InViewCells = DrawableCells for standalone callers.
User-gated 2026-08-29 at the Sanctuary Cathedral: adjacent look-in
rooms show particles and lantern lights from the root cell without
entry or an outside camera; recall/waterfall/Holtburg/paperdoll
unregressed. The pre-existing #132-family candle-behind-opening
report remains open and is unchanged by this fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
Two owner-reported defects at the Campaign AS connected gate on the
examination window (player targets): the animated paperdoll no longer
renders at all, and a "reserved black rectangle" appears at the window's
bottom with the character extras list clipped mid-row at default (310x400)
window size.
ROOT CAUSE — extras-list overflow (the "clipped mid-row" half of defect 2):
NOT a code bug. AS3 (armor-level trio) and AS4 (society/allegiance/
configurable extras) grew the extras list past its DAT-authored 87px region
(element 0x10000335) at the window's minimum size — a new hermetic
regression test proves the worst-case combination (every AS3+AS4 addition
at once) reaches 20 rows / 400px of content, a 4.6x overflow. But retail's
own LayoutDesc authors NO scrollbar for this listbox either
(ScrollbarElementId == 0, verified against both the committed fixture and a
fresh tools/LayoutDump read of the live installed DAT — no drift), and the
SAME test proves UiItemList's pre-existing, unmodified wheel-scroll handler
(OnEvent's UiEventType.Scroll branch) already reveals every row on the next
paint. A scrollbar-less, wheel-scrollable list clipped to its authored
region until the user scrolls or resizes IS retail's own already-correctly-
ported mechanism, not a regression — so no fix was made here.
ROOT CAUSE — paperdoll / "black rectangle" (defect 1): NOT ISOLATED despite
exhaustive investigation. Every file the Campaign AS diff touches
(AppraisalUiController.cs, RetailUiRuntime.cs, CreatureAppraisalRows.cs,
AllegianceRankTitleTable.cs, CharacterIdentityText.cs,
CharacterSheetProvider.cs, InteractionRetainedUiComposition.cs, plus two
unrelated mechanical PublicWeenieFlags-literal refactors) was reviewed in
full against the pre-Campaign-AS baseline. The same worst-case regression
test proves Apply/ApplyCreature/RebuildCreatureStats/BuildExtra never throw
and always leave ActiveView == Character, CurrentObjectId != 0, and the
viewport's full ancestor-visibility chain Visible == true — ruling out
RetailCreatureAppraisalFrameView.TryGetVisibleTarget's first three gates.
CreatureAppraisalPresentation.cs and LivePresentationComposition.cs (the
entire render-time viewport pipeline) are byte-for-byte unchanged across
the whole 974fe88a..87e98395 window. UiViewport.OnDraw draws NOTHING (not
black) when its TextureSlot is unassigned, and the creaturePanel's own
full-panel backdrop (0x10000141) is what would show through instead — the
most likely explanation tying both defects to ONE underlying condition, but
its exact trigger (TryGetVisibleTarget's CurrentObjectId check, or
TrySynchronize's live-entity/mesh-availability check) lies in code nothing
in Campaign AS touches, and could not be reproduced hermetically (needs a
live entity + a live examine exchange).
Filed #443 with the full investigation trail. Added a temporary,
state-change-gated diagnostic probe (ACDREAM_PROBE_CREATURE_APPRAISAL_
VIEWPORT=1, CreatureAppraisalViewportDiagnostics) at both
TryGetVisibleTarget and TrySynchronize so the next live repro pinpoints the
exact failing reason instead of another guess. Per CLAUDE.md's "no
workarounds without explicit approval" and the investigation mode's own
escape hatch ("if you cannot root-cause, say what runtime evidence you
need instead of shipping a guess"), no behavioral fix was shipped for
defect 1.
Tests: AcDream.App.Tests hermetic filter 6,337/0; full-solution hermetic
suite 15,612/0 (all 14 projects green, including the known #442 flake,
which did not trip this run).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AS5 review verified the port exhaustively (170/170 strings, 20
dispatch arms, 17 bounds tests, both call sites) and found one docs-class
defect: five code comments plus the plan slice text claimed AP-109 was
RETIRED while the register correctly keeps the row ACTIVE-narrowed (CT4
FormatXp GetNumberFormatA approximation sliver survives). Comment-only
edits; compile-checked; no behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the 17-function heritage×gender AllegianceSystem::GetTitle
@0x005B8DD0 dispatch and AllegianceData::GetFullName @0x005B6950 as
AllegianceRankTitleTable (src/AcDream.App/UI/Layout/), then wires both
retail call sites: the examination window's title bar
(AppraisalUiController.BuildCharacterTitleBarName, from ApplyCreature's
character branch) and the character panel's name line
(CharacterSheetProvider.BuildSheet).
Census confirmed directly against the decomp (matches AP-109's
2026-08-25 correction exactly): 11 heritages -> 17 functions. Gearknight
and Tumerok author only a MALE Get*Title function, reused for both
gender dispatch branches; Lugian authors only a FEMALE one, reused for
both. Heritage id 0xA (Penumbraen) aliases to the Shadowbound (5)
functions on both branches. Olthoi/OlthoiAcid (12/13) are excluded by
GetTitle's own unsigned range check (heritage-1) <= 0xa. Every one of
the 17 functions shares an identical unsigned rank bounds test
(rank-1) > 9 -> no title (valid range 1..10; there is no "clamp to
rank-10 title" behavior for an out-of-range rank). All 170 title
strings transcribed verbatim from the decomp, including several
PE-byte-recovered data-literal indirections in the Sho/Gearknight/
Tumerok tables ("Kou", "Ou", "Dux", "Ona", "Rea", "Tah") that match
published AC lore exactly.
GetFullName: title = GetTitle(rank, heritage, gender); when GetTitle
resolves nothing, the output is the plain name; when it resolves, the
output is "title" + a single ASCII space (PE-byte-recovered at
data_794098) + name.
Two call sites independently re-verified against the decomp for the
rank/heritage/gender property ids: CharExamineUI::SetAppraiseInfo's
local AllegianceData struct (proven by its ctor/dtor pair) never shows
an explicit field write for _rank/_hg/_gender in the decompile — a
Binary Ninja struct-flattening artifact, not a missing read — while
gmStatManagementUI::UpdateCharacterInfo shows the same three
CBaseQualities::InqInt(0x71/0xbc/0x1e) calls as plain, unambiguous
locals, confirming Gender=0x71/HeritageGroup=0xBC/AllegianceRank=0x1E
as the three inputs at both sites (ruling R8: read live off the
appraisal/qualities bundle, never RuntimeAllegianceState).
Register: AP-109 is NARROWED, not retired. Its stated risk (a ranked
character's Name line showing plain-name-only) is closed, but the same
CT4 narrowing also flagged FormatXp's non-byte-identical
GetNumberFormatA approximation as "this row's own residual sliver if
one is ever found" — that caveat is untouched by this slice and is now
the row's only surviving open item.
Tests: AllegianceRankTitleTableTests (per-function golden values,
gender-reuse rules, Penumbraen alias, Olthoi exclusion, rank/heritage/
gender bounds, GetFullName composition); AppraisalUiControllerTests
(title-bar prefix, plain-name fallback, monster-path regression pin);
CharacterSheetProviderTests (name-line prefix + plain-name fallback).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports the last three retail extras-list row families from CharExamineUI::
SetAppraiseInfo @0x004B45F0 into CreatureAppraisalRows.BuildExtra, closing
the character-path extras list end to end (AS2 header + AS3 armor-level/
ratings/legend + AS4 here). All rows are CHARACTER-path only; the monster
path (CreatureExamineUI::SetAppraiseInfo @0x004B3FF0) is unaffected and
pinned by a controller-level regression test.
Society row (gap G6, @0x004b49a1-@0x004b4c24): gated on PropertyInt 281
(Faction1Bits) being PRESENT — a literal reading of InqInt's found/not-
found return, not the ground-truth doc's informal "!= 0" value test.
Bit-priority if/else-if chain (Celestial Hand 0x1 -> Eldrytch Web 0x2 ->
"???" when Radiant Blood's 0x4 bit is ALSO clear -> else Radiant Blood)
comes straight off the decompiled branches. Rank-band suffix boundaries
(1-100 Initiate / 101-300 Adept / 301-600 Knight / 601-1000 Lord /
1001-1500 Master, no suffix outside every band) read directly off the
decomp's inclusive range checks @0x004b4ab9-@0x004b4b92 and match the
ground-truth table exactly. Color rule: green when the LOCAL player
shares the target's selected bit (checked first, so extra local bits
don't override a match), red when local has a different bit but not the
target's, normal when local has no society bits; the unrecognized "???"
branch never gets a color (retail's ebx_3 stays at its zero initializer).
The local player's own Faction1Bits comes from a new pure `localFactionBits`
parameter on BuildExtra — the composer never reads state directly.
Monarch/Patron/Followers cascade (gap G7, @0x004b4d97-@0x004b4f54): gated
by the caller on AllegianceRank (Int 30) >= 1, the SAME InqInt read AS2's
header AllegianceName binding already consumes. Four arms in retail's
exact order: MonarchsTitle (Str 21) absent -> "Alleg. Monarch:" + clamped
"%d Follower"/"%d Followers" (Int 35, singular only at exactly 1); present
+ PatronsTitle (Str 35) absent -> "Monarch:" only; both present and
ordinally equal -> one "Monarch/Patron:" row; both present and different
-> "Monarch:" then "Patron:".
Configurable extras (gap G8, @0x004b58be-@0x004b5c4d): Fellowship
(Str 10), Arrived in Dereth (Str 43 DateOfBirth), Time in Dereth (Int 125
Age via the ALREADY-PORTED RetailDurationText.Format — the same
ClientUISystem::DeltaTimeToString @0x00565E10 the decomp calls at
@0x004b59e0, reused rather than re-ported), Chess Rank (Int 181), Fishing
Skill (Int 192), Deaths (Int 43 NumDeaths, <= 0 -> "Has never died" with
the SAME "Deaths:" label, verified in the decomp), Titles Earned
(Int 262). Each row gates independently on its own property being
PRESENT (server already strips these per the target's visibility options
per ground truth §3) — no client-side option or success gating added. No
spacers separate these seven rows, matching the decomp exactly.
Seam: AppraisalUiController takes a new `Func<int> _localFactionBits`
dependency (per-call, never captured once — the secure-trade deferred-Func
lesson), invoked only on the character path. AppraisalRuntimeBindings
gained `LocalFactionBits`, wired in InteractionRetainedUiComposition from
`d.Character.LocalPlayer.Properties.GetInt(281)` — the SAME LocalPlayerState
instance CharacterSheetProvider already reads from, no new state path.
AS3 NIT 11: CharacterResponse_CombatRefreshRetainsArmorLevelRows now
applies a SECOND response with different armor-level values (proving the
refresh re-renders from fresh data) then a THIRD with armorLevels: null
(proving it clears). AS3 NIT 12: BuildExtra's XML doc now documents the
full authored row order with the @0x004b5d7d legend anchor. The test
Parsed() helper now ORs IdentifyResponseFlags.ArmorLevels into Flags
whenever armorLevels is supplied (realism sub-nit), applying uniformly to
every existing armor-level test in the file.
Ground-truth doc imprecisions found while verifying against the decomp
directly: (1) the Society gate is presence-of-property, not value != 0 —
InqInt's return is a found/not-found bool, the summary's "!= 0" phrasing
describes the common case but not the literal branch condition; (2) the
Society color rule's bit-priority (same-bit match checked before the
other-bits check) wasn't spelled out in the summary table, only "same/
different/none" — confirmed exact by reading all three branches
(@0x004b49fd/@0x004b4a49/@0x004b4a8b). No other disagreements found.
Register: docs/architecture/retail-divergence-register.md row AP-110
retires the "exhaustive character detail regions" clause from its
still-lacks list with a dated 2026-08-25 narrowing note (AS2+AS3+AS4
together closed the character-path extras list); the row's other
residuals (item-object preview, effective shield projection, cooldown-
remaining, augmentation-cost StringInfo, creature FontInfo-list
selection) are untouched.
Tests: 51 new rows-level tests in CreatureAppraisalRowsTests.cs (society
gate/bit-priority/band-boundaries/color-vs-local-faction, the full
allegiance cascade incl. follower singular/plural/clamp, each
configurable extra present/absent, "Has never died", monster-path
regression, one complete ordering-pin snapshot) plus 3 new controller-
level tests in AppraisalUiControllerTests.cs through the real LayoutDesc/
template binding (localFactionBits seam wired end to end, its default
fallback, and the strengthened monster-path regression). Hermetic
AcDream.App.Tests: 6253 passed. Full-solution hermetic run: 15,410
passed across all projects, 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Plumbs Parsed.ArmorLevels into the extras composer and ports the retail
armor-level trio + unenchantable legend for the player examination window's
extras list (0x10000335), closing gap G4 and the legend half of G8 from
docs/research/2026-08-25-campaign-as-ground-truth.md.
Decomp evidence (docs/research/named-retail/acclient_2013_pseudo_c.txt):
- CharExamineUI::SetAppraiseInfo @0x004B45F0: the armor-level trio
(@0x004B4FD1-@0x004B5410) gates on ANY of nine base_armor_* fields > 0,
emits one leading spacer, then three rows "Head/Chest/Groin" (Head,
Chest, Abdomen), "Bicep/Wrist/Hand" (UpperArm, LowerArm, Hand),
"Thigh/Shin/Foot" (UpperLeg, LowerLeg, Foot) formatted "AL: %s/%s/%s"
with each part "%d" below 0x270f (9999) or "*%d" with (value-9999) at/
above it (data_794344 vs data_7b110c). The trio precedes the ratings
block and has no trailing spacer of its own.
- The "* = Unenchantable" legend (@0x004B5D7D-@0x004B5DED) is added
UNCONDITIONALLY after the whole `if (InqCreature)` block closes —
confirming ruling R3's "unconditional" reading directly from the raw
decompile, not just the BN flattening theory.
- CreatureExamineUI::SetAppraiseInfo @0x004B3FF0 (monster path): reads the
same nine ratings properties with the same gating/spacer logic, but
never touches base_armor_* or the unenchantable literal. Confirmed the
monster (character:false) path gains neither the trio nor the legend —
CreatureAppraisalRows.BuildExtra is character-gated for both.
- Ruling R4 (spacer discipline): CharExamineUI's own ratings-block leading-
spacer flag (ebx_13) is a known BN-decompiler artifact loss (call-
argument mangling instead of a clean `= 1` assignment). Cross-checked
against CreatureExamineUI's clean version of the identical algorithm:
one leading spacer before the FIRST ratings-family row that fires, one
trailing spacer if ANY fired. The existing BuildExtra ratings logic
(per-row gates 307|313|314, 308|315|316, 350|351; single leading/
trailing spacer) already matched this exactly — no functional change to
the ratings section, only the signature/threading change to make room
for the trio and legend around it.
Changed:
- CreatureAppraisalRows.BuildExtra now takes (properties, armorLevels,
character) instead of (properties) alone. Character-gated trio + legend
wrap the unchanged ratings logic.
- AppraisalUiController.RebuildCreatureStats takes the character flag and
threads appraisal.ArmorLevels through; ApplyCreature passes its own
`character` parameter. No caching needed for the combat refresh to keep
the AL rows: AppraiseInfoParser always parses ArmorLevels into the fresh
Parsed value Apply receives, so a re-Apply of the refreshed response
renders the same rows for free.
- Test signature updates only (no behavior pins changed) plus new
coverage: ArmorLevelTrioUsesRetailGroupingLabelsAndFormatPrecedingRatings,
ArmorLevelPartRendersUnenchantableSentinelAtOrAbove9999 (theory: 9998/
9999/10123), ArmorLevelRowMixesStarredAndPlainPartsIndependently,
AllNineArmorLevelsZeroOrNegativeEmitsNoTrioAndNoSpacer,
ArmorLevelTrioAbsentWhenArmorLevelsIsNull, EachRatingRowGatesIndependently,
LegendIsAbsentOnMonsterPathEvenWithRatingsShown,
LegendIsAlwaysLastOnCharacterPathEvenWithNoOtherExtras (rows-level);
CharacterResponse_ArmorLevelTrioPopulatesExtraListThroughRealBinding,
CharacterResponse_CombatRefreshRetainsArmorLevelRows,
CreatureResponse_NeverGainsArmorLevelTrioOrLegend (controller-level,
through the real LayoutImporter/FixtureLoader binding seam).
No existing pin was corrected — the pre-AS3 ratings gating/spacer
behavior already matched the decomp; only the call signature changed.
Full hermetic suite: AcDream.App.Tests 6208/0 skips; full-solution
15,483/0 skips. Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>