fix(chargen): Campaign CC gate round 1 Batch D — gmCG3DView environment backdrop

Retail's chargen 3D views (Appearance and Summary) are not black behind
the model: gmCG3DView::Update @0x004EE9D0 constructs a SECOND CPhysicsObj
from the current heritage's HeritageGroup_CG.environmentSetupID field
(acclient.h verbatim struct layout; the decompiler elides the actual field
read, but HeritageGroup_CG::GetSubDataIDs @0x005c05d0 explicitly walks
iconImage/setupID/environmentSetupID by name, confirming the identity) and
adds it to the SAME viewport's creature_mode_objects the player object
lives in, inserted BEFORE the player (whose own re-AddObject happens much
later, at ~0x004ef199, after the full clothing ObjDesc composes). The
backdrop gets no explicit position/orientation/scale — CPhysicsObj::
makeObject(eax_32, 0, 1) leaves it at the scene origin with identity
orientation, same as the player object's own placement. This id was
already parsed as ChargenHeritageOptions.EnvironmentSetupId
(ChargenTableReader.cs) but never consumed anywhere in production (GF-7/
GF-14).

Fixed by:
- ChargenPreviewEntityBuilder.TryBuildBackdrop: builds a plain, unposed
  Setup mesh from the heritage's EnvironmentSetupId, returning null for
  id 0/unset or an unresolvable Setup (retail's own INVALID_DID gate).
- PrivateEntityViewportRenderer: an optional second entity slot
  (SetBackdrop), reserved via a backdropRenderId constructor parameter so
  paperdoll and creature-appraisal — which never pass one — cannot
  acquire a second entity even by accident (SetBackdrop throws without a
  reserved slot). Per-entity mesh-reference/texture-owner lifetime is
  factored into a private EntitySlot helper shared by both the main and
  backdrop slots. Draw-entity assembly is a pure, directly-testable
  helper (BuildDrawEntities) that puts the backdrop first, matching
  retail's own AddObject insertion order.
- ChargenPreviewController.Rebuild: rebuilds the backdrop whenever the
  HERITAGE changes (narrower than the existing camera-eye-reset gate,
  since environmentSetupID is a pure function of heritage, never gender
  or appearance selection).

Both Appearance and Summary get the fix from the same ChargenPreviewRenderer
facade — confirmed both pages call the identical gmCG3DView::Update on
their own gmCG3DView instance, so no page-specific code was needed.
Lighting was independently re-verified against the same function's
SetLight call (DISTANT_LIGHT, intensity 2.0, direction (0.3, 1.9, 0.65),
default white color) and found to already match byte-for-byte what CC6a
shipped.

Also files docs/ISSUES.md #409 for GF-16 (client-wide UI tooltip system),
investigated in the same root-cause pass but explicitly out of this
batch's scope, and marks it DEFERRED in the findings doc.

Tests: 11 new/extended (ChargenPreviewEntityBuilderTests.TryBuildBackdrop_*,
ChargenPreviewControllerTests backdrop rebuild/swap/absent/no-op cases,
PrivateEntityViewportRendererDrawOrderTests pinning the paperdoll/creature-
appraisal single-entity invariant). Live-DAT measurement: all 13 retail
heritages' EnvironmentSetupId resolve to a real, drawable installed Setup.

App suite 5307/3 -> 5321/3 (+14, 0 regressions). Runtime 1735/0 unchanged.
Launcher.Core.Tests 337/0 and Launcher.Tests 67/0 unchanged (first build of
the merged tree carrying the #406 launcher merge). Full solution: 14508
total / 14504 passed / 4 skipped / 0 failed, dotnet test exit code 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 13:05:23 +02:00
parent 0b05b58514
commit 63bf64c934
9 changed files with 866 additions and 103 deletions

View file

@ -41,6 +41,23 @@ first time this batch — the user's own visual check of both is owed
before considering Commit 2 closed; automated coverage cannot catch a
purely visual placement regression.**
**Batch D (chargen 3D preview backdrop) is CODE-COMPLETE 2026-08-16, pending
the user's visual gate.** GF-7 and GF-14 are fixed — see each entry's own
FIXED note below. GF-16 (client-wide tooltips) was investigated in the same
root-cause pass but is explicitly out of this batch's scope — deferred as
`docs/ISSUES.md` #409 with its own decomp anchors. Fixture + live-DAT tests
only this round (no graphical client launch); App suite went from 5307/3 to
5321/3 (+14, zero regressions), Runtime 1735/0 unchanged. Both Launcher test
projects (this being the first build of the merged tree carrying the #406
launcher merge) pass at their own baselines: Launcher.Core.Tests 337/0,
Launcher.Tests 67/0. Blast radius: `PrivateEntityViewportRenderer` (shared
with paperdoll and creature-appraisal) gained an OPTIONAL second entity
slot reserved via a `backdropRenderId` constructor parameter — paperdoll
and creature-appraisal never pass one, so their draw stays single-entity
by construction (`SetBackdrop` throws if called without a reserved slot,
and the entity-list-assembly helper `BuildDrawEntities` degrades to
exactly the main entity whenever no backdrop is configured/set).
User ran the six-page chargen flow live (build `1.0.2-cc.e`, RDP session,
windowed). Screenshots: retail Heritage, acdream Heritage, retail
Profession. The user's side-by-side retail reports are AXIOMS
@ -236,10 +253,43 @@ ISSUES.md; this doc is the six-page batch.
AP-215's own icon-thumbnail item (the four icon-only spins still show no
per-choice icon art — a DIFFERENT, still-open gap) is rewritten, not
retired — see that row.
- **GF-7 Preview backdrop black** on Appearance (and Summary, GF-14);
retail's chargen 3D view shows a scenic backdrop. (The Heritage-page
preview area shows terrain in BOTH clients — establish from the decomp
what actually renders behind the model per page/view.)
- **GF-7 Preview backdrop black on Appearance (and Summary, GF-14) — FIXED
(Campaign CC gate round 1, Batch D).** Root cause: retail's
`gmCG3DView::Update @0x004EE9D0` (~0x004eecd3-0x004eed44) constructs a
SECOND `CPhysicsObj` from the current heritage's own
`HeritageGroup_CG.environmentSetupID` field (verbatim struct layout,
`acclient.h`) and adds it to the SAME viewport's `creature_mode_objects`
the player object lives in — this codebase already parsed that id as
`ChargenHeritageOptions.EnvironmentSetupId` (`ChargenTableReader.cs`)
but never consumed it anywhere. The decompiler elides the actual field
read (`var_b8`/`eax_32`, an unresolved-call artifact — see
`claude-memory/feedback_bn_decomp_field_names.md`); cross-referencing
`acclient.h`'s `HeritageGroup_CG` struct (environmentSetupID sits right
after setupID) confirmed what the elided value is. The backdrop object
gets NO explicit position/orientation/scale — `CPhysicsObj::makeObject`
(0x004eed2f) leaves it at the scene origin with identity orientation,
same as the player object's own default placement, and retail's own
`AddObject` insertion order puts the backdrop BEFORE the player (the
player's own re-`AddObject` happens later, at ~0x004ef199, after the
full clothing ObjDesc composes). Fixed by extending
`ChargenPreviewEntityBuilder` with `TryBuildBackdrop` (builds a plain,
unposed Setup mesh from the heritage's `EnvironmentSetupId`, returning
null for id 0/unset or an unresolvable Setup — matching retail's own
`if (eax_32 != INVALID_DID.id)` gate at 0x004eed29), giving
`PrivateEntityViewportRenderer` an optional second entity slot
(`SetBackdrop`, reserved via a `backdropRenderId` constructor param so
paperdoll/creature-appraisal — which never pass one — cannot acquire a
second entity even by accident), and wiring `ChargenPreviewController`
to rebuild the backdrop whenever the HERITAGE changes (narrower than the
existing camera-eye-reset gate, since `environmentSetupID` is a pure
function of heritage, never gender or appearance selection). Both
Appearance and Summary get the fix from the SAME `ChargenPreviewRenderer`
facade — no page-specific code needed, confirmed both pages call the
identical `gmCG3DView::Update` on their own separate `gmCG3DView`
instance. Lighting was independently re-verified against the same
function's `SetLight` call (`DISTANT_LIGHT, 2.0, (0.3, 1.9, 0.65)`
direction, default white color) and found to ALREADY match byte-for-byte
what CC6a shipped — no lighting change was needed.
- **GF-8 Appearance Face/Clothes sub-tab selection unmarked — FIXED
(Campaign CC gate round 1, Batch B).** Same root and same fix as GF-1:
the Face (`0x100003A9`)/Clothes (`0x100003AA`) sub-tab buttons author
@ -306,14 +356,22 @@ ISSUES.md; this doc is the six-page batch.
this EXACT carve-out). Full App suite (5304 tests): zero regressions.
**The user's own visual check of chat + the main game UI is still owed**
— automated coverage cannot catch a purely visual placement regression.
- **GF-14 Summary paperdoll backdrop black** (same family as GF-7,
UNCHANGED, out of this batch's scope — the 3-D preview backdrop, not a
text-widget gap). **Summary textbox wrapper + scrollbar — FIXED (Batch
C, Commit 2 for the frame/build half, Commit 3 for the scrollbar LINK
and the how-to text's own content — see the Suspected-shared-roots
entry and Commit 3's own composition of `gmCGSummaryPage::SetHowToText`
into `0x10000404`).**
- **GF-14 Summary paperdoll backdrop black — FIXED (Campaign CC gate round
1, Batch D, same fix as GF-7 above — both pages call the identical
`gmCG3DView::Update` on their own `gmCG3DView` instance).** **Summary
textbox wrapper + scrollbar — FIXED (Batch C, Commit 2 for the
frame/build half, Commit 3 for the scrollbar LINK and the how-to text's
own content — see the Suspected-shared-roots entry and Commit 3's own
composition of `gmCGSummaryPage::SetHowToText` into `0x10000404`).**
- **GF-16 Hover tooltips missing on all pages** (retail pops tooltips).
DEFERRED to its own gate round — filed as
[`docs/ISSUES.md` #409](../ISSUES.md) with the decomp anchors
(`UIElement::StartTooltipAtMouse @0x00460D70`,
`UIElementManager::StartTooltip @0x0045DE90` + `@0x00459700`, layout DID
`0x21000041`, properties P0x47-P0x4B, ~253 authored elements, prefs
`Misc_TooltipEnable`/`Misc_TooltipDelay`) the Batch D investigation
surfaced. Out of Batch D's scope: it is a CLIENT-WIDE mechanism, not the
chargen 3D preview backdrop Batch D actually fixed (GF-7/GF-14 above).
## Suspected shared roots (to be CONFIRMED by the investigation, not assumed)
@ -349,7 +407,13 @@ ISSUES.md; this doc is the six-page batch.
distinct from the art/media commit, and NOT gated by the same art-
availability check `ActiveState` is). See each GF's own FIXED entry
above and the retired AP-222 / narrowed AP-215 register rows.
4. Preview backdrop (GF-7/GF-14) — what gmCG3DView clears/draws.
4. ~~Preview backdrop (GF-7/GF-14) — what gmCG3DView clears/draws.~~
CONFIRMED, CLOSED at Batch D: `gmCG3DView::Update`'s own
`m_pbgObject`/`m_bgSetupID` pair, sourced from the heritage's
`HeritageGroup_CG.environmentSetupID` field — already parsed into this
codebase as `ChargenHeritageOptions.EnvironmentSetupId` but never
consumed before this fix. See GF-7's own FIXED entry above for the full
decomp citation.
5. ~~Input routing on Summary (GF-15) — focus/typing path on the stacked
chargen screen.~~ CLOSED: focus/typing routing was never broken (live-
verified); the real cause was `RetailDialogFactory` never re-asserting