fix(ui): Campaign AS gate fixes AS-GF1 — extras-list overflow ruled OUT as a code defect; paperdoll regression not isolated, probe added instead

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>
This commit is contained in:
Erik 2026-08-25 12:58:29 +02:00
parent 87e9839561
commit 65f6f5848a
5 changed files with 302 additions and 7 deletions

View file

@ -24,6 +24,91 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #443 — Examination window: player-target paperdoll viewport renders nothing (AS-GF1)
**Status:** OPEN — probe added, root cause NOT isolated.
**Component:** examination window / creature-appraisal private viewport.
**Filed:** 2026-08-25, AS-GF1 gate-fix session.
Owner report at the Campaign AS connected gate: the animated 3-D paperdoll
in the examination window (LayoutDesc `0x2100006B` element `0x10000148`)
worked correctly at baseline `974fe88a` (praised the same session) and was
gone by `87e98395` (ten commits later, AS2-AS5). The same gate also
reported "a reserved black rectangle at the window's bottom" and mid-row
clipping in the character extras list at the window's default (minimum
310x400) size, fixed by dragging the window taller.
**Exhaustive investigation (this session) found NO code bug in the
Campaign AS diff for either symptom:**
- Every file the AS2-AS5 window touches
(`AppraisalUiController.cs`, `RetailUiRuntime.cs`,
`CreatureAppraisalRows.cs`, `AllegianceRankTitleTable.cs` (new),
`CharacterIdentityText.cs`, `CharacterSheetProvider.cs`,
`InteractionRetainedUiComposition.cs`, plus two unrelated
mechanical `PublicWeenieFlags`-literal refactors) was read in full
against the pre-Campaign-AS baseline.
- A new hermetic regression test,
`AppraisalUiControllerTests.CharacterResponse_WorstCaseExtrasCombination_DoesNotThrowAndViewportGateStaysOpen`,
applies EVERY AS3+AS4 extras-list addition at once (armor-level trio,
society, allegiance cascade, ratings, all seven configurable extras) —
the combination none of the individual AS3/AS4 tests exercise together —
through the REAL DAT-derived examination layout and REAL row templates.
It proves `Apply`/`ApplyCreature`/`RebuildCreatureStats`/`BuildExtra`
never throw and always leave `ActiveView == Character`,
`CurrentObjectId != 0`, and the viewport's full ancestor-visibility
chain (`creaturePanel` → root) `Visible == true`, even in this worst
case. `RetailCreatureAppraisalFrameView.TryGetVisibleTarget`'s first
three gates (ActiveView, windowFrame visible, viewport visible) are
therefore unaffected.
- The SAME test also proves the extras list's clip-then-wheel-scroll
behavior is correct and unaffected: `UiItemList.OnEvent`'s
`UiEventType.Scroll` handler (pre-existing, unmodified) moves the
shared `Scroll` offset, and the next `LayoutCells()` pass reveals every
row, including the very last one of the 20-row / 400px worst case
against the DAT-authored 87px region. Retail's own LayoutDesc authors
NO scrollbar for this listbox either (`ScrollbarElementId == 0`,
verified against BOTH the committed fixture `tests/AcDream.App.Tests/
UI/Layout/fixtures/examine_2100006B_100005F2.json` and a fresh
`tools/LayoutDump` read of the live installed DAT — no drift). A
scrollbar-less, wheel-scrollable list clipped to its authored region
until the user scrolls or resizes IS retail's actual, already-correctly-
ported mechanism — not a regression.
- `src/AcDream.App/UI/UiViewport.cs:52` draws NOTHING (not black) when its
`TextureSlot` is unassigned (`if (!Visible || !TextureSlot.IsAssigned)
return;`). The creaturePanel's own full-panel backdrop
(`0x10000141`, DID `0x06004CC2`, ZLevel 100 — the furthest-back layer,
spanning the panel's whole `300x365` rect) is what shows through
wherever nothing else paints over it. This is the most likely explanation
for BOTH the missing paperdoll AND the "black rectangle": if the
viewport's `TextureSlot` never gets assigned, this backdrop is what the
owner is actually seeing, and it is genuinely the SAME defect wearing
two descriptions, not two.
- `CreatureAppraisalPresentation.cs` and `LivePresentationComposition.cs`
(the entire render-time viewport pipeline: `TryGetVisibleTarget`'s
fourth gate `CurrentObjectId`, `TrySynchronize`'s live-entity/mesh
lookup, and the dispatcher/composition gate that constructs the
presenter at all) are byte-for-byte UNCHANGED across the whole
`974fe88a..87e98395` window (`git log -p` for both files is empty).
**Conclusion:** the trigger is one of `TryGetVisibleTarget`'s
`CurrentObjectId` check or `TrySynchronize`'s `LiveEntityRuntime.
TryGetWorldEntity`/`MeshRefs.Count` check, in code nothing in Campaign AS
touches — meaning either a pre-existing, previously-latent condition this
gate round happened to trigger, or a live/timing condition a hermetic test
cannot reproduce (no live entity, no live wire exchange).
**Probe added this session** (temporary — delete with the real fix):
`ACDREAM_PROBE_CREATURE_APPRAISAL_VIEWPORT=1`
`CreatureAppraisalViewportDiagnostics` in `CreatureAppraisalPresentation.cs`
logs `[AS-GF1-PROBE] creature-appraisal viewport: <reason>` on every REASON
TRANSITION (not every frame) from both `TryGetVisibleTarget` and
`RetailCreatureAppraisalCloneFactory.TrySynchronize`. Next step: relaunch
with the flag set, examine a player, and read which one of the five
possible reasons (`no ActiveView`, `windowFrame hidden`, `viewport hidden`,
`no CurrentObjectId`, `entity not found`, `no MeshRefs`) fires — that
pinpoints the real fix.
## #442 — Flake: DirectionalShadowCasterFrameTests.WarmDenseChangedFrames_AllocateZeroAndReadNoSceneRecords fails intermittently under full parallel suite load
**Status:** OPEN.