Commit 1/3: chargen-scoped, low blast-radius fixes.
- New DatRichText helper: escape-normalize + word-wrap + per-segment
palette color, porting UIElement_Text::SetStringInfoWithFont /
AppendStringInfoWithFont's composition model. Routes the Heritage
(GF-2), Town (GF-11a), and Profession (GF-3) description boxes
through it instead of a raw unwrapped single-Line LinesProvider.
Heritage headers use font-color palette index 1 (green), bodies
index 0 (white), matching AppendStringInfoWithFont's own font-index
argument. Town's diagnosed GF-11a root cause: a single un-wrapped
line meant the town-specific suffix rendered past the clipped
viewport, so switching towns looked like "text never changes" even
though the underlying composed string genuinely differed.
- GF-3: bind the Profession page's description textbox (0x100003e0,
gmCGProfessionPage::InitializePage @0x00483068) and compose its
per-template text (UpdateProfession @0x004821b0's CustomText/
BowText/SwashText/LifeText/WarText/WayText/SoldierText, plain
SetStringInfo — no palette).
- GF-4: UiButton gains a coexisting ValueLabel/ValueBox/ValueFont/
ValueColor slot alongside Label. Retail's chargen display buttons
(avail/health/stamina/mana credits, 0x100003e2-e5/0x100003f9)
author their caption directly on P0x17 AND carry a separate,
media-less Type-12 value child that UiButton.ConsumesDatChildren
used to drop entirely — pages substituted the button's own Label,
destroying the caption. DatWidgetFactory.BuildButton now surfaces
that child (gated on ReferenceEquals(labelInfo, info) — own-caption
buttons only) instead. The six Profession slider name labels
(0x100002ed, CharGenState::GetAttributeName @0x005C3A20's six
hardcoded literals) resolve as UiButton in this port (live-DAT-
measured Type 1 — retail's UIElement_Button is DynamicCast(0xc)-
compatible with UIElement_Text) and are written once at
construction, matching retail's own single InitializePage write.
- GF-6/AP-218: gmCGAppearancePage::Update writes a heritage-flavored
STATIC caption to the Hair/Eyes/Skin spins (plain / GearText_* /
OlthoiText_* variants) — never an index. Removed the prior 1-based-
ordinal/gear-name substitution entirely; the other six spins keep
their DAT-authored caption untouched, matching retail exactly.
- Root 1d: wire the Heritage (0x100003be, 13 states) and Profession
(0x100003d8, 7 states) backdrop SetState cascades
(gmCGHeritagePage::Update / gmCGProfessionPage::UpdateProfession).
- AP-216/AP-217 (partial, register updated honestly): swatches beyond
the current part's real color count now hide (DoColorSpots' blank-
blit half); the GradCircle now blanks for Eyes (DoGradDisk's blank-
plug half). The "paint with the actual represented/current color"
halves stay open — they need a PalSet/Palette-id -> RGB pipeline no
chargen page reads at runtime yet, judged disproportionate to add
alongside this batch's other ~10 fixes.
Register: AP-215 rewritten (item 2's "ordinal" framing is stale after
GF-6; restated as the icon-thumbnail gap), AP-216/AP-217 rewritten
(partially closed), AP-218 retired, AD-103 retired (the swallowed-
child Label substitution AD-103 tracked is replaced by ValueLabel's
own-geometry surfacing).
22 new tests (DatRichText unit tests, UiButton/DatWidgetFactory
ValueLabel tests, live-DAT structural pins, controller behavioral
tests) — all green. Full App suite (Release, live-DAT):
5300 passed / 1 pre-existing unrelated flake (PortalProjectionTests
allocation test, passes in isolation) / 3 skipped, up from the
baseline 5282/3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GF-1/GF-8: UiButton now recognizes retail's custom Unselected/Selected
radio-pair (0x10000016/0x10000017), bypassing the standard Normal/
Highlight machine that never admitted those state names — .Selected now
lights the heritage/template/gender/Face-Clothes rows it was always a
no-op for.
AP-222/GF-11b: per-state label color/outline (dat 0x1B/0x21) now applies
off the REQUESTED retail state id, not the art-gated committed
ActiveState — resolves the Appearance spins' current-part highlight
(text recolors even though no Highlight art exists on either client) and
the Town caption's Normal-to-white swap.
GF-11c: UiButton.LabelBox lets a lifted caption with its own authored
rect draw there instead of the face-relative offset that's only correct
when the label is authored directly on the button (heritage/template
family, unchanged).
GF-9: wires the real nine companion overlay elements (SetColor's
SetVisible mechanism) that swatch clicks were always meant to drive,
retiring AP-215 item 1 (the swatch.Selected substitution was a permanent
no-op — swatches author no Highlight media at all).
GF-10: zoom buttons now set the retail-mirrored mutual-exclusive
Highlight/Normal pair on click; InitializePage carries no initial
SetState for either button, so both stay at "Normal" until first click.
Register: AP-222 retired (mechanism identified and ported), AP-215
narrowed (item 1 retired, item 2 unrelated and unchanged), row count
recount corrected 164 (was already one high before this batch).
App suite 5282/3 (was 5266/3), Runtime 1735/0 unchanged. Fixture + live-
DAT tests only — no graphical client launch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GF-15 (the gate blocker): the Summary name field and Finish button were
NOT structurally broken — live repro over the project's own local ACE
test server showed clicks correctly focus the field and land characters.
The real bug only surfaces after the first dialog opens: pressing Finish
empty successfully creates the NoName RetailMessageDialogView (visible,
correct 400x95 geometry) but it renders nothing and silently absorbs
every click across the whole canvas. Root cause: CharacterCreationUiController.Tick
and CharacterManagementUiController.Tick both call UiRoot.BringToFront(Root)
unconditionally every frame (needed so chargen stays above the occluded
management screen, AP-229); a dialog root is a direct sibling under the
same UiRoot, and RetailWindowManager.BringToFront is "highest ZOrder among
siblings + 1" — whichever BringToFront runs last in a frame wins.
RetailDialogFactory.Tick never re-asserted its own dialogs' z-order, so
the next frame's screen Tick buried the dialog behind the screen's opaque
backdrop while it stayed the registered Modal with exclusive input
priority. Fixed by having RetailDialogFactory.Tick re-raise every open
dialog (in open-order) each tick, matching retail's always-on-top dialog
behavior. Live-verified the complete user sequence end to end: click
field, type, press Finish empty, dialog now visibly renders, OK dismisses
cleanly, field still typable afterward. The "[ Name" prefill question is
closed as a non-bug: neither CharGenState::RandomizeCharacter nor
gmCGSummaryPage::InitializePage write text into the field in the decomp;
retail's field is genuinely empty on open, matching acdream already.
GF-5: CharacterCreationSkillsPage.RebuildRows resolved the wrong listbox
template (Templates[0], retail's own 3-child bucket-header row) and
required the root to be a UiButton (it's a plain container). Byte-traced
gmCGSkillsPage::DoSkillRecords + tagSkillRecord's copy-ctor field order
to map every child id in the real row (Templates[1]): name, level/cost
text, and the two real per-row up/down arrow buttons. Wired the arrows to
retail's own plain-click dispatch, retiring (narrowing) AP-213's
click-to-advance/double-click-retreat single-button substitution.
GF-13: dat property 0x3B (Invisible) was never read by the importer.
Elements 0x10000403/0x10000494 ("Non-Admin"/"Non-Envoy") author it true.
A blast-radius sweep found 1,083 elements client-wide author the same
flag, so this fix stays chargen-scoped only (ElementInfo.Invisible /
UiElement.AuthoredInvisible are pure data additions; only
CharacterCreationUiController acts on them, by the authored flag, not a
hardcoded id list). General importer-wide honor filed as ISSUES.md #408;
register row AP-230 records the split.
Gates: solution build green; App 5266/3 skips/0 failed; Runtime 1735/0;
full-solution run 0 failures anywhere. Register: AP-230 filed, AP-213
narrowed. ISSUES: #408 filed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign CC gate round 1. The Config Resolution dropdown now offers
DisplayModeCatalog.WindowedResolutions — the curated hardware modes
UNIONed with the static modern-ladder sizes that fit the desktop —
because a windowed pick is a plain Size write needing no video mode,
and remote/RDP virtual displays advertise almost none (the live RDP
display exposed exactly 1920x1080 + the 2056x1290 desktop, leaving the
dropdown with nothing below 1920). The fullscreen apply still validates
against the hardware Resolutions list plus the switcher's
monitor-mode-list hard guard, so a fullscreen pick of a windowed-only
entry refuses safely (log-and-stay, #388/#392) — IA-22's
offered-implies-supported invariant narrows to the fullscreen half and
its register row carries the amendment. Three new pure-union tests
including the exact live RDP shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both dual-lens reviewers of `9cf6c522`+`ddcbf1fb` returned PASS-with-items.
This round closes all nine findings:
F1 files AP-229 for the screen-layering divergence (retail destroys/
reconstructs the current UI framework via UIFlow::UseNewMode; acdream
keeps both CharacterManagementUiController and CharacterCreationUiController
mounted for the whole lifetime and reveals/occludes) plus its narrow
residual risk (the shared RetailDialogFactory can hand UiRoot.Modal to a
dialog opened by the still-ticking, occluded management screen on an
inbound CharacterError) and what already matches retail (selection/
world-name persistence, click-through isolation, one coherent Modal
stack).
F2 rewrites the connected-gate script's roster-full step with the exact
`@modifylong max_chars_per_account` recipe and the pending-delete-counts
note. F3 adds AP-221's console-diagnostic lines to the known-gaps
paragraph. F4 adds an empty-name/AP-227 step. F9 notes that a uniform
Random pick over 13 heritages can repeat.
F5 adds an App-layer source-text pin
(GameWindowLiveSessionOwnershipTests.LiveSessionRuntimeFactoryBinds
CharacterCreatedAndCreationFailedToTheStatusWriter) for the delegate
wiring the reviewer proved was deletable without breaking any test — no
practical seam exists to construct LiveSessionRuntimeFactory without a
GameWindow, so this follows the file's own established source-text-pin
pattern; the payload shape is already pinned separately at
SessionStatusWriterTests.
F6 corrects the CC7 ledger's checksum-assertion wording (it is a
round-trip purity check, not an independent golden — the golden is
CharacterCreateTests.ComputeChecksum_ExactRetailAccumulationSet) and
cross-references it from the test's own doc comment.
F7 corrects the CC7 ledger's fixture-ordering claim (it had chargen
constructing first, backwards from RetailUiRuntime.Tick's real
management-then-chargen order) and reorders CharacterScreensFixedCanvas
ArbiterTests to match production, adding ClickThrough/ZOrder assertions
that pin the occlusion the reviewer previously verified only by hand.
F8 records a known flake (RuntimeCollisionReportingStateTests.
WarmedSteadyContactRefreshDoesNotAllocate, allocation-assertion load
sensitivity, pre-existing) seen under full-solution parallel load on
both reviewer runs.
Campaign status: all seven slices (CC1-CC7) are REVIEW-CLOSED; the
campaign is CODE-COMPLETE pending the user's own connected gate.
Runtime 1735/0 (unchanged), App 5257/3 skips (+1: the new F5 pin).
Full Release build: 0 warnings, 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Create button un-ghosts: retail's exact gate (gmCharacterManagementUI::
UpdateButtons @0x004ec240, roster count < allowed slot count) ported into
RuntimeCharacterSelectionButtons.CanCreate; the button's OnClick opens the
chargen screen through the same CharacterCreationUiController.Open() seam
the ACDREAM_OPEN_CHARGEN=1 dev path already used. Exit/Back confirm on
chargen needed no new return-path code — character-management is never
hidden while chargen is open on top of it — verified end-to-end by a new
cross-controller test rather than left as an inspection claim.
Full-flow test coverage: a new comprehensive test decodes every 0xF656
field (including the trailing checksum, recomputed via the production
CharacterCreate.ComputeChecksum) against a fully populated creation
(heritage/gender/all appearance slots/template/explicit skill command/
town/name); a new Theory drives the remaining six 0xF643 rejection codes
through the real wire decode path, closing the gap between the
already-covered isolated state-machine Theory and an actual WorldSession
round trip.
Launcher payload cycle: two new tests drive a real Runtime create/reject
through the real SessionStatusWriter (wired exactly as
LiveSessionRuntimeFactory/HeadlessSessionHost do in production) and read
the result back with the real Launcher.Core StatusFileTailer/
StatusEventParser — closing the one gap CC2's own per-layer tests never
reached. No gap was found in production wiring itself: GameWindow already
constructs a real, non-null SessionStatusWriter for both hosts.
Also fixes 4 pre-existing LiveSessionControllerTests assertions that
compared a full RuntimeCharacterSelectionButtons record and would have
failed once CanCreate started being computed; corrects register row
AP-211 to reflect that its own predicted resolution (the Create-button
gate landing) has now happened — both layers are intentionally kept as
retail-matching enforcement plus defense-in-depth, not one superseding
the other.
Adds docs/research/2026-08-16-campaign-cc-test-script.md, the user's
connected-gate script covering both the launcher and dev-shortcut launch
paths, the six-page create flow, every Finish outcome, and the known
cosmetic/behavioral divergences (AP-212/213/215/216/217/218/219/220/222/
224/226/228) so they aren't mistaken for new bugs during the gate.
Gates: full solution Release build green; Runtime 1735/0 (was 1726/0,
+9), App 5256/3 skips (was 5254/3, +2), Headless 166/0 (unchanged),
Launcher.Core 324/0, one full-solution pass across every project clean
(no known flakes reproduced this run).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The narrow re-review of fix commits 0c8e1e7d+2d4168f9 found every code fix
oracle-verified but returned NOT CLOSED on five test/doc residuals plus
three nits and a follow-up filing. All fixed:
- R1: the claimed "external-change->user-commit->SetName" regression test
for F1 (the deleted _suppressNextFieldEvent latch) never existed — the
Runtime-layer randomize test doesn't touch the page. Added
CharacterCreationUiControllerTests.SummaryNameField_RealCommitAfter
ExternalRefreshWhileUnfocused_StillReachesSetName: drives Refresh with a
revision bump + changed snapshot.Name while the field is unfocused (the
programmatic SetText path that used to arm the latch), THEN performs a
real user commit (field.SetText + field.Submit(), the actual event path),
asserting SetName receives the player's typed text.
- R2: RetailSkillFormula.CalculateChargenScore and ChargenSkillScoreResolver
had zero direct coverage (the F12(d) test substitutes skillId*10). Added
a Untrained/Trained(+5)/Specialized(+10) theory, the divisor-zero skip
path, and a six-way AttributeId theory (Str=1..Self=6) to
RetailSkillFormulaTests.cs.
- R3: RetailSkillFormula.cs's doc comment claimed "no retail-authored skill
sets MinLevel above Untrained=1" without ever reading the field — ACE's
own SkillBase.cs hedges the same field "// 1-2?". MEASURED (not assumed)
against the installed EoR dat's global SkillTable
(CharacterCreationLiveDatTests.SkillTable_MinLevelDistribution_
NeverExceedsTrained): 23 skills at MinLevel 1, 15 at MinLevel 2, zero
above 2, of 38 priced skills. ACE's hedge was right; the doc comment now
states the measured fact and leans on the structural argument (the gate
holds for Trained/Specialized under any MinLevel in {1,2}) as load-
bearing, not the unverified data claim.
- R4: filed AP-228 — the Summary/Skills skill-row KEY sources from
ItemAppraisalTextFormatter.SkillName's hardcoded English switch, where
retail's own key is DAT-sourced (SkillBase->_name via %hs,
0x0047b90f-0x0047b915) — same divergence class as AP-226 filed the same
round, reversed polarity, also present at CC4's Skills page. Softened
AP-224's "ported exactly, not simplified" claim: it only ever covered the
row's VALUE/template, never its KEY.
- R5: this commit corrects 0c8e1e7d's gate claim. "Release build zero
warnings" was false: a clean `dotnet build -c Release -t:Rebuild` shows
25 pre-existing warnings (18 in tests/AcDream.Core.Tests, 7 in
tests/AcDream.App.Tests — Composition/HostInputCameraCompositionTests.cs,
Composition/WorldRenderCompositionTests.cs,
UI/Layout/OptionsPanelLiveMountProbeTests.cs), none in any file this
campaign or its residual round touched. History is not amended; this is
the correction.
Nits: the ChargenPreviewController ctor doc now also cites
gmCGSummaryPage::Update @0x0047baa0 (the per-heritage re-derive site — 0xc
Olthoi/0xd OlthoiAcid/else — not just the one-shot InitializePage seed) as
the stronger justification for why Rebuild re-derives the zoomed-out eye
per heritage on every change. RuntimeCharacterCreationState's F2 comment
("Finish becoming a permanent no-op") reworded: the same unconditional
_verificationPending = false assignment ran pre-fix too, so Finish was
never blocked — only the response FEEDBACK vanished (no dialog, no created
character, nothing), not the request itself. Filed #404 for
ChargenSkillScoreResolver's own independent SkillTable read alongside
ChargenTableReader's (cleanup follow-up, out of this round's scope).
Ledger: CC5 flipped REVIEW-CLOSED in the campaign plan (dual-lens
architectural PASS-with-items / retail-fidelity FAIL -> F1-F14 fix round
0c8e1e7d -> narrow re-review: all code oracle-verified, residuals R1-R5
test/doc -> this commit; re-reviewer pre-authorized lead diff-check close).
This commit's own sha is recorded by a follow-up ledger-only commit,
matching 2d4168f9's own pattern.
Gates: Release build 0 errors (25 pre-existing warnings, unrelated to this
round — see R5 above); App suite 5257/3 skips (was 5242/3), 0 failed;
Runtime suite 1726/0 (unchanged); the three new/measured tests (R1, R2's
ten cases, R3) all pass individually.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opus dual-lens review of 34e3a534+a975efd1 returned architectural
PASS-with-items / retail-fidelity FAIL. Every finding fixed:
- F1 (BLOCKER): deleted CharacterCreationSummaryPage's dead
_suppressNextFieldEvent latch. UiField.SetText never raises
OnFocusLost/OnSubmit, so the latch never had anything genuine to
suppress — it stayed armed until the player's own next real commit
and silently ate their typed name.
- F2: byte-re-derived gmCharGenMainUI::RecvNotice_
CharGenVerificationResponse @0x004e9030's jump table — Pending is an
explicit switch case landing on the SAME NameDBDown label as
Corrupt/DatabaseDown, and Undef/out-of-range falls through the
function's own unsigned-underflow default arm to that identical
label. Retail's dispatch has NO silent branch. ApplyCreationResponse
now produces a real rejection for Pending/Undef instead of a silent
reset; ReconcileDialogs maps them to NameDBDown. Corrects the wrong
"retail swallows Pending" claim everywhere it was repeated (plan doc,
Core.Net doc comment, Runtime doc comments).
- F3: skill rows now use the key/value template with
CharGenState::GetSkillScore @0x005C4B50 as the value (ported via the
new RetailSkillFormula.CalculateChargenScore /
ChargenSkillScoreResolver, wired through a new GetSkillScore
binding), not template 0/name-only; bucket headers are unconditional.
Writing this fix's own regression test surfaced a second, more severe
bug: CharacterCreationSummaryPage never wired _list.TemplateResolver
at all, so RebuildListbox has been a silent no-op since CC5 shipped —
fixed by threading templateResolver through the page's constructor,
matching every sibling UiTemplateListBox owner.
- F4: added the missing _errorMessageDialogContext one-outstanding
guard to the 0xF643 rejection dialog, matching
MakeErrorMessageDialog's own guard @0x004e8cc4 and the other four
sibling dialogs' shape (registered in CloseAllDialogs, suppress-
callback checked).
- F5: the Summary preview camera now seeds/re-derives retail's
zoomed-OUT eye (byte-decoded (0,-2.5,0.95) at gmCGSummaryPage::
InitializePage ~0x0047bd14-0x0047bd44) instead of Appearance's
zoomed-in default, via a new ChargenPreviewController
useZoomedOutEye flag.
- F6: retired AP-225 outright — re-derived the ListenToElementMessage
length gate is NUL-inclusive, so MaxNameLength=32 was always
byte-correct, not merely internally consistent.
- F7: amended AP-221 to cover the Summary preview's duplicate
one-shot-composition binding gap (CC5 duplicated the pattern instead
of closing it).
- F8: byte-decoded GetRandomReal @0x00563940's fmul operand at
0x007cd650 — an 8-byte double, not a 4-byte float — is EXACTLY
1.0/32767.0, not 1/32768. Added RollShadeLocked
(_random.Next(32768) * (1.0/32767.0)) and switched all six shade
rolls onto it.
- F9: evaluated porting retail's exact empty-name-commit no-op
(NUL-inclusive length==1 skips SetName entirely) and rejected it —
it would fight the F1 field-sync model by spontaneously reverting an
emptied field on the next unrelated revision bump. Kept the clear,
documented the tradeoff, filed AP-227.
- F11: filed AP-226 documenting retail's static pcProfessions/pcGender/
pcHeritage/pcTown label tables versus acdream's DAT-sourced labels,
including the non-human-heritage-renders-bare-"Heritage:" retail
quirk.
- F12: added exclude-current determinism (count-2 lists), Random-
clears-name, repeat-identical-rejection-reshows, and RebuildListbox
content tests (the last one found F3's TemplateResolver bug).
- F13: threaded an optional Random through GameRuntimeDependencies ->
LiveSessionController -> RuntimeCharacterCreationState, matching the
existing TimeProvider injection shape, closing the Slice-K
determinism hazard on a bot-reachable Randomize* command family.
- F14: RandomizeCharacterLocked now assigns _heritageId unconditionally
before the TryGetHeritage gate, matching retail's SetHeritageGroup
@0x005C67A0 (mHeritageGroup written before the DAT lookup).
Gates: Runtime 1726/0 (was 1722/0), App 5242/3 skips (was 5240/3),
Headless 166/0, Core.Net 993/994 (the one failure, NakEmissionTests
LossSoak, is a known pre-existing flake — passes standalone), full
solution Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fills TS-82's Summary placeholder with a faithful port of gmCGSummaryPage
(name field with NameInputFilter + the retail commit-on-focus-lost/submit
dispatch + the >32-char ID_CharGen_NameTooLong reject-and-revert path, the
REAL three-row-template listbox confirmed against the installed EoR dat
before writing any page code, and Summary's own independent gmCG3DView
preview instance wired through a second ChargenPreviewController pair
mirroring the Appearance page's exact composition shape).
Ports CharGenState::RandomizeCharacter and its six sub-primitives into
RuntimeCharacterCreationState — not approximated: the RandInt/RollDice
semantics are independently confirmed from both the decompiled RNG bodies
and the CharGenStateVtbl union struct in acclient.h. Three consumers:
the chargen screen's open-roll (retiring AP-214's honest-blank deviation
and reproducing the Appearance page's gender-flip-on-init quirk), the
Summary page's Random button (behind the retail randomize-warning
confirm), and the Appearance page's Random button (narrowing AP-212 to
just Heritage/Profession/Town's still-approximated rolls and Skills'
still-unported RandomizeSkills).
Wires the Finish button (previously ghosted) with retail's NoName/
CreditWarning dialog pair, adds the F12 amendment's HeritageOrGenderUnset
local refusal to TryBeginFinish (register AP-223) as a defensive backstop
now that the screen-open roll normally makes it unreachable, and wires
the four ID_Character_Err_* rejection dialogs for the 0xF643 response
codes CC3 already parsed but nothing displayed.
Register: TS-82 retired, AP-214 retired, AP-212 narrowed, AP-223/224/225
filed (heritage/gender Finish refusal, Summary's two-bucket skill-list
narrowing, the 32-vs-33 name-length threshold reconciliation).
Runtime 1722/0 (was 1713), App 5240/3 skips (was 5223/3), Headless 166/0
unchanged, full solution Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
R1: LivePresentationComposition's chargen-preview diagnostic fired on every
ordinary launch without ACDREAM_RETAIL_UI set, since interaction.RetainedUi
is null in that configuration and there's no Appearance page to warn about.
Narrowed the else-if guard to require RetainedUi is not null too, so the
diagnostic only fires in the one configuration it actually diagnoses.
R2: filed AP-221 for the F8 one-shot-binding disposition the re-reviewer
accepted as scoped but which shipped without its own register row — the
chargen preview's GPU-side binding reads the retryable mount coordinator's
widget exactly once, so a slow-DAT frame permanently kills the preview for
the session with only R1's diagnostic as evidence.
R3: rewrote AP-217 after re-deriving from the decomp. The original row
claimed the GradCircle was an interactive click-to-hue picker with no
handler wired up. gmCGAppearancePage::ListenToElementMessage's dispatch
switch has no case for the GradCircle's offset at all — it isn't a click
target in retail either. DoGradDisk is a paint-only routine that blits the
gradient art tinted with the current color (or blanks it for Eyes)
whenever SetColor/SetSelection run. acdream's real gap is that it never
repaints the GradCircle — a cosmetic paint gap, not a dead control.
N1: tightened AP-220's "leaving Gearknight for something else" — the
decomp shows leaving Gearknight for Olthoi/OlthoiAcid takes a separate
branch that does not randomize; only leaving for a non-Olthoi heritage
does.
N2: added the requested media pin to the F2 spin-highlight live-DAT test,
then measured it against the installed EoR dat rather than assuming it
would pass. It doesn't: none of the nine spins author Highlight-state
media on either consumed arrow face segment, so
TrySetRetailState(Highlight) is a silent no-op for all of them today.
Pinned the test to the measured reality (ActiveState stays "Normal") and
filed AP-222 documenting the discovery — unresolved whether retail's own
spin art has the same gap.
Plan doc: CC6b-MOUNT ledger row updated to REVIEW-CLOSED with the full
commit chain and re-review disposition; OWED list corrected for AP-217/
AP-222.
Gates: dotnet build -c Release green. App suite 5223 passed / 3 skipped
(ACDREAM_PROBE_LIVE_MOUNT=1, ACDREAM_DAT_DIR set) — count held exactly at
baseline. Runtime suite 1713/0 — unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes every finding from the dual-lens review of 34c6fceab0 (architectural
PASS-with-items, retail-fidelity FAIL). Re-derived every decomp citation
against docs/research/named-retail/acclient_2013_pseudo_c.txt directly
rather than trusting the reviewer's transcription.
Wrap/normalize semantics (F1): CycleIndex's decrement-from-Unset landed on
0; the decomp's shared decrement tail (label_47f065/label_47f6d9, the same
switch the headgear ring was ported from) computes new=cur-1=-2 on the raw
signed int32, which wraps to count-1 — matching headgear's own ring shape.
Also ports the spin body-click normalize-and-write-back retail's cases
0xa5-0xae all share (NormalizeChoiceOnSelect), which acdream had dropped
entirely. Flips the one test that pinned the wrong expectation and adds
select-zone coverage no prior test isolated.
Heritage gate (F3): Update's Gearknight/Olthoi/OlthoiAcid branches reset
SetChoice(FACE)/SetSelection(HAIR) unconditionally, not only when Clothes
was showing — a conditional gate stranded Nose/Mouth as the current part
under a Face-tab session.
Doc corrections propagated everywhere they repeated (F4, F5, F6, plus the
plan doc's own CC6b-MOUNT ledger row for F1/F3): the gmBarberUI heading
citation conflated PostInit with InitializePage; Random's Appearance
disable was mislabeled a placeholder when it's really AP-212's unported
RandomizeAppearance/RandomizeClothing gap; the master-page doc still called
the Appearance page content-inert after this campaign made it real.
Visual substitutions widened (F2): AP-215 named only two of the Appearance
page's swatch/spin substitutions. Ports the two cheap ones directly —
current-part highlight via SetSelection's SetState(1)/SetState(6), routed
through the existing UiButtonStateMachine.Normal/Highlight ids and
IUiDatStateful.TrySetRetailState seam (installed-DAT-confirmed
ToggleBehavior=true on all nine spins); the shade scrollbar's SetVisible(0)
for Eyes vs acdream's Enabled=false. Files the other five (DoColorSpots,
the inert GradCircle, spin-caption/heritage-caption loss, the Skin-spin
MoveTo reposition, the Gearknight-boundary randomize calls) as new register
rows AP-216..AP-220 and corrects the plan doc's false claim that AP-215
already named the GradCircle.
Unlocked DAT read (F7, BLOCKER): ChargenPreviewController.Rebuild called
ChargenAppearanceFactory.TryCompose outside _datLock while the very next
line correctly locked TryBuildAnimated — CC6a's own F4 class of bug,
reintroduced at this catalog's first production call site. Wrapped in the
same lock; documented the invariant on ChargenAppearanceCatalog itself.
One-shot preview mount (F8): LivePresentationComposition reads
ChargenPreviewViewportWidget once, but its underlying mount
(CharacterCreationUiMountCoordinator) is explicitly retryable while this
GPU-resource composition pass is not — unlike PaperdollViewportWidget,
which IS eager/non-retryable, so the "mirrors Paperdoll" doc claim was
false. Retrofitting cross-frame retry here would mean restructuring this
composition's one-shot contract for every private viewport (paperdoll,
creature appraisal) and FrameRootComposition's fixed frame-group array —
out of this round's blast radius. Corrected the doc and made the failure
loud (a diagnostic log) instead of silent.
Dispose leak (F9): ChargenPreviewController.Dispose left the preview
WorldEntity referenced by the leased renderer until the renderer's own,
later disposal. Releases it on its own teardown now.
Test-quality items (F10, F11, F13): pinned the spin arrow widths
(47px, both arrows) the 174 zone boundary is derived from, plus a
controller test for the previously-uncovered select zone. Measured the
shade scrollbar's authored orientation instead of assuming it — it is
VERTICAL (33x85) — which is a real production bug: UiScrollbar only routed
scalar-mode mouse events when Horizontal was true, so the shade control
never fired in production. Added OnVerticalScalarEvent/DrawVerticalScalar
mirroring the existing horizontal scalar path. Converted
ChargenPreviewControllerTests from silent-pass [Fact] to the shared
InstalledDatFactAttribute skip-reporting pattern.
Adjudication (F12): AD-101's retirement leaves TryBeginFinish's four local
refusals (NoName/AttributeCreditsUnspent/AlreadyPending/RosterFull) with no
heritage/gender gate — currently latent since Finish stays hard-disabled
this round. Amended the campaign plan's CC5 slice scope to require BOTH a
heritage/gender refusal AND a real RandomizeCharacter port before the
connected user gate opens Finish; noted the interaction on AP-214's own
register row. No CC5 implementation in this commit.
Gates: dotnet build -c Release green across the full solution. App suite
(Release, ACDREAM_PROBE_LIVE_MOUNT=1) 5223/3 skips, Runtime suite
1713/0 — both clean across repeated runs. A full-solution run surfaced
three pre-existing, previously-documented flakes unrelated to this change
(Streaming.LandblockBuildFactoryTests/LandblockPresentationPipelineTests
#402, Core.Net.Tests.NakEmissionTests loss soak) — each confirmed passing
in isolation, consistent with their known full-suite-parallelism-timing
history; none touch any file this commit changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The page-mount half CC6b-PRE deferred: CharacterCreationAppearancePage
(gender buttons, Face/Clothes sub-tabs, nine spin controls with retail's
decrement/increment/select-as-current-part OnClickAt zones, nine color
swatches, shade scrollbar, zoom/rotate wiring) plus ChargenPreviewController,
which bridges the ChargenPreviewRenderer/ChargenPreviewZoomController
camera-injection gap CC6a/CC6b-PRE left open and mounts as the third private
creature viewport beside paperdoll/creature-appraisal.
Color-wheel scouting (campaign risk item 4): live-DAT probe found every
color-wheel-family id resolves through existing DatWidgetFactory mappings
(Button/Scrollbar/generic fallback) — no new widget type needed.
The @140355 gender-flip-on-init oddity (risk item 5): resolved via decomp
alone — gmCharGenMainUI's own ctor calls CharGenState::RandomizeCharacter
before any page constructs, so retail's chargen screen is never actually
blank on open; the Appearance page's gender-flip code always fires against
a real, randomly-rolled gender. Filed AP-214 (acdream doesn't port
RandomizeCharacter this round, so it opens honestly blank instead) and
AP-215 (two narrow visual substitutions: swatch .Selected highlight vs
retail's separate overlay, ordinal labels vs retail's icon-only spins).
AD-101 retired: the Heritage page's auto-gender-select interim default is
deleted now that the Appearance page's real gender buttons exist. TS-82
narrowed to Summary-only.
Scope addendum: ChargenPreviewRotationController's parameterless-constructor
default changes from 0f to a new RetailDefaultHeadingDegrees=180f constant
(retail's InitializePage override, not the ctor's raw 0) — every real
gmCG3DView owner converges on 180 before its first frame, so a controller
defaulting to 0 was a trap for future consumers.
Runtime 1713/0, Core 4786/1 skip, Content 147/0, App 5220/3 skips (Release,
ACDREAM_PROBE_LIVE_MOUNT=1) — zero failures across two clean full-solution
runs; the one Core.Net.Tests NakEmissionTests flake observed on a third run
is the same pre-existing, previously-documented timing flake (zero files
under src/AcDream.Core.Net/ touched, passes 100% in isolation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CC6a (index->ObjDesc factory + static-pose offscreen renderer) and
CC6b-PRE (idle loop, rotation, zoom, alternate-setup plumbing) both
closed through dual-lens review -> fix round -> narrow re-review. The
branch carries its own cross-branch renumbering (TS-84, ISSUES #403) so
this merge is number-clean against the CC4 rows.
Notable review outcomes carried in: the barber refutation (chargen has
NO alternate-setup checkbox — all five write sites are gmBarberUI), the
idle-by-default finding with its corrected InitializePage evidence, and
the 180-degree initial heading owed to the mount half.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
# docs/ISSUES.md
# docs/architecture/retail-divergence-register.md
R1: the unsound elided-ctor-byte argument survived at its canonical
citation site (ChargenPreviewEntityBuilder's class doc, which the two
corrected docs point at) and in the ledger row's Deliverables column,
which contradicted its own review-status column. Both now carry the real
evidence: InitializePage @0x0047FDD0 writes an explicit m_bZoomedIn = 0
at 0x004802C3.
R2: the verified 180-degree initial heading (m_fCurHeading = 180f at
0x00480235 + SetPlayerHeading at 0x0048023F, cross-confirmed at
gmBarberUI::PostInit and the summary page) now has a durable home in the
CC6b-mount OWED list — without it the mount half ships a character
facing away from the camera.
Merge prep: the branch-local TS-82 renumbered to TS-84 (the CC4 branch
independently allocated TS-82 and landed first) and the branch-local
ISSUES #402 renumbered to #403 (same collision, same rule), with the
Core doc reference updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F1 (BLOCKING, doc-only) — the idle-by-default rationale rested on an unsound
"uninitialized C++ member defaults to 0" argument (heap operator-new memory
is indeterminate, not zero). Verified and replaced with the real evidence:
gmCGAppearancePage::InitializePage @0x0047FDD0 writes an EXPLICIT
this->m_bZoomedIn = 0; at 0x004802C3, immediately after that same function
points the camera at the zoomed-IN per-heritage eye (0x00480286-0x0048029E).
Fixed in all three places: the register's TS-83 retirement clause,
ChargenPreviewAnimator's class doc, ChargenPreviewZoomController.IsZoomedIn's
doc. Recorded the retail quirk this implies: the character starts framed
close-up while not-zoomed-in, so the first Zoom In click (once mounted)
tweens close-eye->close-eye (visually null) while still freezing the
animation — the port reproduces this faithfully.
F2 — ChargenPreviewZoomController and ChargenPreviewAnimator kept
independent _zoomedIn bools synced only via a nullable animator parameter,
risking desync. Retail's m_bZoomedIn is a single field gating both camera
and animation, so the fix makes the animator the sole state owner:
ChargenPreviewZoomController now takes its ChargenPreviewAnimator as a
required constructor dependency, IsZoomedIn reads straight through to it,
and ZoomIn/ZoomOut no longer take a parameter at all — there is no second
bool left to disagree.
F3 — documented the DoRotation counter-clockwise branch's x87-stack
decompiler artifact (BN renders x87_r7_1 = x87_r6_3 at 0x0047CAEB, which
would store delta-degrees instead of the timestamp for CCW only); the port
already stores "now" in both branches, cited against
feedback_bn_decomp_field_names.md.
F4 — ChargenPreviewAnimator.ApplyIdleFrame now double-buffers two
List<MeshRef> instead of allocating fresh every 30fps tick.
F5 — filed docs/ISSUES.md #402 tracking the RetailAnimationCyclePlayback /
LiveEntityAnimationPresenter duplication as an owned post-CC follow-up,
referenced from the new type's own doc.
F6 — reworded the ChargenPreviewEntityBuilder.TryBuild "byte-identical"
claim to result-identical (TryBuildAnimated now also resolves the idle DID
and loads the idle Animation before the wrapper discards them).
F7 — added the missing clockwise >360 clamp test (readable decomp
polarity, unlike F3's CCW artifact).
ALSO — rewrote the CC6b ledger row's m_alternateSetupID MUST-COVER note per
the reviewer's F11 concession: all five write sites belong to gmBarberUI
(the post-creation barber shop), not gmCGAppearancePage, which has no
option-checkbox-equivalent field at all. Added the enclosing-function
citations and an explicit directive that CC6b-mount must NOT build a
crown/no-flame checkbox on the Appearance page.
Tests: ChargenPreviewRotationControllerTests +1 (10 total),
ChargenPreviewZoomControllerTests +2 and every case rewritten for the
required-animator constructor (9 total). Core.Tests 4786/1 skip (unchanged),
Content.Tests 147/0, App.Tests 5152/6 skips (+3) — zero failures in
isolation, full solution Release build green. Two pre-existing flakes
observed across repeated full-solution runs, neither caused by this round
and neither reproducing standalone: Core.Net.Tests' NakEmissionTests loss
soak, and Content.Tests' DecodedTextureCacheTests concurrency race.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Idle animation loop: decomp re-read of gmCGAppearancePage::Update's trailing
StartAnimation/StopAnimation gate (~0x0047EF01-0x0047EF12) plus the ctor
evidence that m_bZoomedIn is a decompiler-elided bool (never explicitly set
away from its zero default, unlike its two sibling bools) establishes that
retail's chargen preview defaults to the idle loop PLAYING, not the frozen
rest pose CC6a shipped as a deliberate simplification (TS-83) — the rest pose
only appears once Zoom In fires. New Core primitive
RetailAnimationCyclePlayback ports CPhysicsObj::set_sequence_animation's
advance-with-wrap + lerp/slerp effect (the same algorithm
LiveEntityAnimationPresenter's legacy NPC-idle branch already carries inline;
not consolidated this round — out of blast radius for a preview-only
feature, noted in the new type's own doc). New ChargenPreviewAnimator drives
the per-tick swap; ChargenPreviewEntityBuilder gained TryBuildAnimated
alongside the byte-behavior-unchanged TryBuild. Olthoi/OlthoiAcid use the
SAME enum key for idle and rest DIDs (decomp-confirmed quirk). TS-83 retired
in the register (§4 count 50->49).
Rotation controller: ChargenPreviewRotationController ports
Rotate/DoRotation (0x0047CB50/0x0047CA80) verbatim — toggle-to-stop,
deltaDegrees = ((now-last)/RotationSecondsPerRevolution)*360, single-pass
+-360 clamp (not a full modulo, matching retail's own tail), the -1.0
invalidation sentinel. Applies to the entity's heading via the existing
MoveToMath.SetHeading port, not the camera, confirming CC6a's own note.
Zoom tween: ChargenPreviewZoomController ports ZoomIn/ZoomOut/
DoZoomAnimation (0x0047CF00/0x0047D050/0x0047C960) — a LINEAR 0.6s tween
(no easing curve in the decomp) between the already-recorded camera eye
profiles, calling into the animator's zoom swap IMMEDIATELY at button-press
time, matching retail's call order exactly.
m_alternateSetupID (research correction): re-reading the decomp
function-by-function found all five m_alternateSetupID write sites —
including the two the CC6a review cited — belong to gmBarberUI (the
post-creation barber shop), not gmCGAppearancePage, which has no
m_pOption1Checkbox-equivalent field and never writes the field. For
character creation the field is always INVALID_DID in retail. TryCompose
still gained a real, decomp-cited alternateSetupIdOverride parameter
(default no-op) implementing gmCG3DView::Update's generic override
precedence, for a future non-chargen consumer.
RetailHeldPose extraction: shared ResolvePoseDid/ComposePartTransform
between RetailPaperdollPoseApplicator and ChargenPreviewEntityBuilder — a
clean mechanical extraction, behavior-identical on the paperdoll side.
Bookkeeping: CC6a ledger row now cites its real commit SHAs (55bfd9ca,
1774d8b2); new CC6b-PRE ledger row records scope done + the page-mount half
still owed.
Tests: RetailAnimationCyclePlaybackTests (10, Core), ChargenAppearanceFactoryTests
(+4), ChargenPreviewRotationControllerTests (9), ChargenPreviewZoomControllerTests
(7), ChargenPreviewAnimatorTests (7, hand-built fixtures), ChargenPreviewEntityBuilderTests
(+5, installed-DAT). Core.Tests 4786/1 skip, Content.Tests 147/0, App.Tests
5149/6 skips — zero failures, full solution Release build green. One
pre-existing, unrelated flake noted: Core.Net.Tests' NakEmissionTests loss
soak failed once in the full-suite run, passed 1/1 isolated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CC4 re-review returned NOT CLOSED: R1 (MEDIUM, blocking) is a new residual
the F1 fix itself introduced, plus three LOW riders (R2, R3, R4).
R1 — nulling UiRoot.FixedCanvasSize on chargen Close() stripped it from
character-management, which stays active underneath and only sets the
canvas on its own activation edge. Root cause (reviewer-named): two
controllers writing one host-global with no owner. Fixed with the
root-cause shape (reviewer's option (c)): UiRoot.DeclareFixedCanvas(owner,
size)/RevokeFixedCanvas(owner), an owner-scoped arbiter — every declarer
must agree on the canvas size (a mismatch throws instead of silently
last-writer-wins), and the canvas nulls only once EVERY declarer has
revoked. Both CharacterCreationUiController and CharacterManagementUi-
Controller now declare/revoke instead of writing FixedCanvasSize directly;
grepped for stragglers, none remain in production code (the raw setter
stays public only for UiRootFixedCanvasTests' isolated scale-math
coverage). New test (reviewer-specified):
CharacterScreensFixedCanvasArbiterTests — two controllers sharing one
UiRoot, proving the canvas stays set through chargen's Exit-confirm Close
while char-management is still active, nulling only once char-management
also deactivates, plus the original F1 defect's own covering case (both
revoke together at world entry).
R3 — HeadlessSessionHostTests.ContentLease_InstallsRealChargenOptions_
SelectHeritageIsAccepted proves F6's install actually opens the gate: a
content lease carrying a real hand-built DatCharGen heritage (not
ChargenOptions.Empty) is installed, and TrySelectHeritage for it succeeds.
R2 — filed docs/ISSUES.md #402 for the pre-existing
Streaming.LandblockBuildFactoryTests.Build_UsesTheSuppliedSharedReaderGate
full-suite flake (unrelated to Campaign CC).
R4 — fixed "unchached" -> "uncached" typo in
InteractionRetainedUiComposition.cs.
Runtime 1713/0, App 5127/13 skips (+2), Headless 166/0 (+1), full solution
Release build green. Live-DAT probes 7/7 under ACDREAM_PROBE_LIVE_MOUNT=1.
The known #402 flake did not fire across 3 consecutive full-suite runs
this session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dual-lens review of CC4 (0e71d3b8) returned architectural FAIL (F1, F6)
and retail-fidelity PASS-with-reservations (F2, F3, F4), plus LOW
findings F5, F7-F12. F13 (TS-82's merge collision with campaign-cc6a) is
merge mechanics for the orchestrator, not addressed here.
F1 (HIGH, blocking): CharacterCreationUiController never released
UiRoot.FixedCanvasSize, on a FALSE premise that CharacterManagementUi-
Controller does a per-tick set (it does not — it sets once on activation
and nulls on Deactivate/Dispose). Root cause: RuntimeCharacterCreation-
State had no CompleteEnter() analogue to RuntimeCharacterSelectionState's,
so the creation view reported IsActive=true for an entire in-world
session. Added CompleteEnter(), wired at both LiveSessionController
in-world edges (StartCore, EnterHighlightedCore); made Open/Close/
Deactivate/Dispose set/null the canvas symmetrically; corrected the false
comment and ledger claim; added FixedCanvasSize test coverage.
F2 (MEDIUM-HIGH, blocking): the attribute-slider scalar mapping was not
retail's. Fixed display to value/100f (UpdateAttributeValues @
0x0048251d) and the drag inverse to truncate+clamp-low-only, no rescale
(ListenToElementMessage @ 0x004829c0, independently re-verified against
the decomp). Added tests at scalar 0.5/0.0 plus a display-direction test.
F3 (MEDIUM, blocking): ported the unported heritage-button tab-restore
arm (ListenToElementMessage @ 0x004e9450) — SHOW/HIDE id sets independently
re-derived from the decomp, including the genuine Lugian (0x100005f1)
no-restore quirk, reproduced faithfully. Wired via a new HeritagePage
click callback; added restore + quirk tests.
F4 (MEDIUM): ported SetTown's (@ 0x0047c360) separate per-town page-root
state literal (Holtburg->0x10000034 etc.), independently re-derived from
the decomp's tail-merged branches; wired via the existing
IUiDatStateful.TrySetRetailState seam; added a test.
F5 (MEDIUM): softened AD-103's unmeasured pixel-equivalence claim.
F6 (MEDIUM, blocking): DECISION — install ChargenOptions in the headless
content path (chosen over marking headless creation out-of-scope).
HeadlessSessionHost now calls InstallOptions off the shared content
lease's Dats, beside the existing InstallSpellMetadata call.
F7: AP-213 already named the label format and click/double-click
substitution explicitly on inspection — no edit needed.
F8: AP-212 now names all six DoRandom primitives with a known landing site.
F9: AD-101 retirement corrected to precede CC5's Finish un-ghosting.
F10: merged ItemAppraisalTextFormatter's duplicate <summary> block.
F11: fixed TS-82's wrong AP-211 cross-reference.
F12: cached the chargen DatStringResolver once per composition instead of
per ResolveText call.
Runtime 1713/0, App 5125/13 skips (+8 new tests), Headless 165/0, full
solution Release build green. Live-DAT probes 7/7 under
ACDREAM_PROBE_LIVE_MOUNT=1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the CC6a dual-lens review (architectural PASS with reservations,
retail fidelity PASS with reservations, merge after F1/F2/F3).
F1 (BLOCKING) - AlternateSetup/setupId tested the wrong sentinel (0)
instead of retail's INVALID_DID (0xFFFFFFFF, CharGenState::GetSetupID
@0x005C5B22). A hair style storing that value would have been adopted as
a literal Setup id, nulling Get<Setup> and killing the whole preview.
Fixed both sites with a new InvalidDid constant; added two hand-built
tests plus an installed-DAT sweep of every hair style across all 26
heritage/gender combinations (869 selections, zero unresolved Setup ids).
F2 (BLOCKING) - TS-82's register row, ChargenClothingTable.cs's doc, and
the plan's ledger row all understated Undead's measured clothing-coverage
gap as "headgear/trousers/footwear" (3 slots) with a self-contradicting
"4 of 4 non-shirt slots" aside. Corrected everywhere to the true measured
ALL FOUR slots (headgear, trousers, shirt, footwear).
F3 (BLOCKING) - the palette-math "three independent sources" claim
overcounted: ACViewer's ClothingTableList.xaml.cs:97 computes a different
expression for a different problem, and its vendored PaletteSet.cs is
ACE's own file, not an independent implementation. Rewrote the evidence
paragraph in ChargenPalSetMath.cs to the two sources that actually hold
(decomp control flow + ACE's "Taken from acclient.c" port).
F4 (MEDIUM) - ChargenPreviewEntityBuilder.TryBuild did unlocked dat reads;
DatCollection is not thread-safe and every sibling dat-touching resolver
in this layer takes a shared datLock. Added a required datLock parameter;
every dat read now happens inside one lock, mirroring
RetailPaperdollPoseApplicator.Apply's shape.
F5 (LOW) - noted the pre-existing Streaming.LandblockBuildFactoryTests
timing flake in the ledger so a future session doesn't chase it.
F6 (LOW) - fixed ChargenPreviewCamera.cs's rotation doc, which cited a
nonexistent identifier in a dimensionally-wrong expression; corrected to
retail's actual DoRotation @0x0047CAC7 per-tick formula.
F7 (LOW-MEDIUM) - the TS-82 measurement was WriteLine-only; pinned with
real assertions (zero gaps for the 9 standard heritages, exactly the 4
measured Undead table ids on both genders). Kept the existing env-gated
skip pattern (confirmed house convention).
F8 (LOW) - the inner PalSet-miss loop recorded-and-continued past a miss;
retail's own loop returns immediately on a miss (~0x005A7B32), aborting
every remaining choice in that garment. Changed continue to break; added
a test proving a subsequent present PalSet is correctly not applied.
F9 (LOW) - fixed three dangling <see cref="...Compose"/> doc references
(the method is TryCompose).
F10 (LOW) - the packed (byte)(range/8) narrowing was unchecked; a real
NumColors of 2048 happened to wrap to the correct "whole palette" 0
sentinel by unchecked-cast accident. Replaced with explicit PackOffset/
PackNumColors helpers that document the 2048->0 equivalence deliberately
and throw on any other unrepresentable shape.
F11/F12 (LOW, CC6b scope) - noted in the plan's CC6b row: the second
m_alternateSetupID override source is unmodelled, and a shared
RetailHeldPose helper is worth extracting before a fourth consumer.
Test counts: Core.Tests 4772/1 skip (+5), Content.Tests 147/0 (+1),
App.Tests 5121/6 skips (unchanged; F5's named flake did not reproduce) -
zero failures, full solution Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mounts gmCharGenMainUI (enum 0x10000039, root 0x100003CC) via
CharacterCreationUiController/CharacterCreationUiMountCoordinator,
cloning CharacterManagementUiController's recipe. Master shell ports
SetProgressState @0x004e7a10 (Olthoi tab-hide + redirect) and
ListenToElementMessage @0x004e9450 (Back/Next/Finish/Help/Exit/Random
nav) verbatim, with free tab navigation over all six pages. Heritage,
Profession, Skills, and Town pages bind to CC3's
RuntimeCharacterCreationState commands; Appearance and Summary mount as
content-inert placeholders for CC6b/CC5.
Live-DAT probing (CharacterCreationLiveDatTests) found two widget-
mapping surprises the decomp's DynamicCast hints don't predict: the
Profession slider's value field imports as an editable UiField (wired
for direct numeric entry), and the avail/health/stamina/mana/credits
displays author as UIElement_Button hosts whose Type-12 value child is
swallowed by UiButton.ConsumesDatChildren — substituted with the
button's own Label. No new DatWidgetFactory widget types were needed.
Threads the installed DAT's real ChargenOptions into Runtime via the
new RuntimeCharacterCreationState.InstallOptions, called from
ContentEffectsAudioCompositionPhase.Compose (mirrors
InstallSpellMetadata's pattern); headless keeps ChargenOptions.Empty
unchanged. Wires CC3's F14 status-hook gap (ApplyCharacterCreated/
ApplyCreationFailed) to SessionStatusWriter for both graphical and
headless hosts, and adds the CharacterCreation view/command seam
through CurrentGameRuntimeAdapter and DeferredGameRuntimeStateCommands
alongside CharacterSelection's existing shape.
Register: AD-101/102/103, AP-212/213, TS-82 filed for the auto-gender-
select interim default, the omitted ToD-account gate, the button-Label
widget substitution, the Random-button approximation, the flat-listbox
Skills simplification, and the Appearance/Summary placeholders.
Runtime 1713/0 (was 1707), App 5117/13 skips (was 5101/6), Headless
165/0 unaffected, full solution Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Delivers the CC6a foundation half of the chargen 3D preview: the missing
index->ObjDesc appearance factory the campaign plan's acdream-seams
section named, plus a static-pose offscreen renderer following
PrivateEntityViewportRenderer's proven paperdoll/appraisal architecture.
Page mount, spin/color-wheel controls, and rotate/zoom behavior stay out
of scope per the CC4-parallel worktree contract (CC6b, after CC4 merges).
Core (src/AcDream.Core/CharGen/, pure, no Chorizite on public surfaces):
ChargenAppearanceFactory.TryCompose ports gmCG3DView::Update @0x004EE9D0's
ObjDesc rebuild in its exact decompiled order - base body, hair style,
clothing in retail's own Headgear/Trousers/Shirt/Footwear order (not the
UI tab order or the wire's field order, both of which differ), eyes
(bald-aware), nose, mouth, then the unconditional skin subpalette, hair
color, eye color. ChargenPalSetMath ports PalSet::GetPaletteID's
shade-to-index formula, cross-checked three ways (decomp control flow,
ACE's PaletteSet.GetPaletteID "Taken from acclient.c" citation, ACViewer's
identical slider math). ChargenPalSet/ChargenClothingTable are pure
projections behind IChargenPalSetSource/IChargenClothingTableSource so the
factory itself never touches a dat.
Content (src/AcDream.Content/CharGen/): ChargenAppearanceCatalog is the
cached dat-backed implementation of those two source interfaces, mirroring
ChargenTableReader's no-leak discipline.
App (src/AcDream.App/Rendering/): ChargenPreviewRenderer is a third facade
over PrivateEntityViewportRenderer beside PaperdollViewportRenderer and
CreatureAppraisalViewportRenderer - no existing rendering file touched.
ChargenPreviewCamera carries the four retail-verbatim per-heritage eye
profiles from gmCGAppearancePage::Update @0x0047E8F0 (cross-checked
against ZoomIn/ZoomOut's identical literals) plus the recovered rotation
(3.0 s/revolution) and zoom-tween (0.6 s, reconstructed from the
decompiler's garbled float literals - the plan's own "measure if it
matters" note is resolved, not garbled beyond recovery). Rotation applies
to the character model, not the camera, per gmCGAppearancePage::DoRotation.
ChargenPreviewEntityBuilder resolves Setup/GfxObj/Surface/Animation itself
(there is no live entity yet), reusing DatLiveEntityProjectionMaterializer's
surface-override algorithm and RetailPaperdollPoseApplicator's held-pose
technique, generalized to chargen's per-heritage rest-pose DID.
Two register rows filed: TS-83 (the plan-named CC6a static-pose-vs-retail-
idle-loop staging, CC6b to retire) and TS-82 (measured, not assumed - the
un-ported clothing Setup-substitution fallback chain costs nothing for the
9 standard heritages with clothing UI, but Undead's default gear choices
genuinely lack ClothingBaseEffects coverage for Undead's own body Setup).
Tests: ChargenPalSetMathTests, ChargenAppearanceFactoryTests (hand-built
fixtures), ChargenAppearanceCatalogInstalledDatTests (installed-DAT sweep,
all 26 heritage/gender combinations, zero missing PalSet/ClothingTable
ids), ChargenPreviewCameraTests, ChargenPreviewEntityBuilderTests
(installed-DAT-gated, proves a real 34-part Aluvian mesh resolves).
Core.Tests 4767/1 skip, Content.Tests 146/0, App.Tests 5121/6 skips - all
pre-existing skips, zero failures, full solution Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opus dual-lens review of CC3's RuntimeCharacterCreationState passed on
retail fidelity but failed the controller integration: the post-create
log-straight-in indexed the CACHED wire CharacterList, which ACE never
resends after a create (it only appends server-side and replies Ok) —
with zero pre-existing characters this throws, with N it can silently
enter the WRONG character. The same stale-index problem corrupted every
pre-existing character's delete slot on roster re-sort. Fixes all four
blocking findings plus a credit-gate correctness bug (retail warns and
lets the user confirm through unspent credits; it does not force a full
spend) and eight lower-severity findings from the same review round.
F1 (blocking): WorldSession gained a guid-based EnterWorld(uint,string,
TimeSpan?) overload sharing EnterWorldCore with the index-based one;
ILiveSessionOperations gained a default EnterWorldByGuid method.
LiveSessionController factored EnterSelectedCore/the new
EnterCreatedCharacterCore through a shared EnterHighlightedCore so the
post-create enter sends by the exact guid the 0xF643 Ok reply carried,
never by a roster index.
F2 (blocking): RuntimeCharacterSelectionState gained a real
AppendCreatedCharacter primitive that preserves every existing entry's
ActiveIndex (a wire contract — SendDeleteCharacter sends it as the
CharacterSet slot) and assigns the new entry's from the pre-create wire
roster count, instead of round-tripping the post-create roster through
ApplyRoster's name-sort-and-renumber.
F3 (blocking): retail's DoFinish(this, arg2) gate is
"arg2 != 0 && remainingAtrbCredits > 0" — the ordinary click warns and
refuses, but the warning dialog's own confirm re-invokes DoFinish(this,
0), which sends anyway with credits unspent (ACE accepts this).
TryBeginFinish/Finish gained a confirmedUnspentCredits parameter; the
plan doc's "retail FORCES full spend" line is corrected in the same
commit.
F4 (blocking): a stale out-of-range template index surviving a heritage
switch to a heritage with fewer templates now clears to TemplateUnset,
matching ConstrainAllByHeritage's clamp.
F5/F9/F10: three register-row/doc citation corrections (AP-207's real
FitTemplateToCharacter call sites — a fourth one the original filing
also missed; the Slot field's real retail assignment source; AP-209's
classID branch table for Olthoi/OlthoiAcid). F6: ApplyCreationResponse
no longer publishes from inside the owner lock. F7: two new tests pin
BalanceAttributes' persistent donor cursor (successive-overspend
advance, Self-to-Strength wrap). F8: ResetSkillLevels' doc corrected to
retail's real both-costs->=0 gate. F11: the integration test fixture
captures guid-based enter calls and uses two pre-existing characters
whose wire order differs from alphabetical order, so the roster
assertion actually exercises F2 instead of coinciding with it by
accident. F12: filed register row AP-211 for the client-side RosterFull
slot-cap refusal (no retail DoFinish-layer counterpart). F13: narrowed
Finish's bare catch to InvalidOperationException/SocketException and
bound _scope to a local. F15: RandomizeStartAreaLocked leaves the start
area unchanged on an empty list instead of forcing -1, matching retail.
Runtime 1706/0 (was 1701), Core.Net unchanged at 994/0, full solution
Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ports retail's CharGenState as the one Runtime-owned character-creation
state machine, mirroring RuntimeCharacterSelectionState's exact pattern
(snapshot/delta/event-stream, borrow-only view, generation-gated
commands, one mutable owner, no App types). Every command ports a named
retail function: SetHeritageGroup, SetGender, SetTemplate/ApplyTemplate
(Custom = template 0, Olthoi force-lock), the six attribute setters plus
GetAbsRemainingCredits/BalanceAttributes (retail's literal round-robin
order and fairness cursor), SetSkillLevel plus ResetSkillLevels' free-skill
baseline (reusing CC1's ChargenSkillCreditMath two-tier cost lookup
verbatim), RandomizeStartArea, and DoFinish's complete gate sequence
(empty name / unspent attribute credits / already-Pending / client-side
roster-vs-slotCount cap).
LiveSessionController gained a sibling IRuntimeCharacterCreationCommands
implementation, a CreateCharacter wire hook, and a response handler that
reuses existing machinery rather than inventing new paths: the Ok
identity is appended to the roster via RuntimeCharacterSelectionState's
own ApplyRoster, and the "log straight in" behavior reuses the private
EnterSelectedCore. ILiveSessionLifecycleHost gained two default-no-op
hooks (ApplyCharacterCreated/ApplyCreationFailed) so AcDream.App needs
zero changes to keep compiling; wiring them to the status stream is a
CC4 follow-up.
Filed four divergence-register rows for the corners deliberately not
ported: the FPU-unrecoverable FitTemplateToCharacter auto-detect (AP-207,
ACE only reads the field for title text), the per-style color-count
approximation (AP-208, CC1's model has no per-style palette data), the
classID DAT-DID placeholder (AP-209, ACE ignores the field), and
ApplyTemplate's atomic-vs-sequential attribute apply (AP-210).
34 new tests: full state-machine coverage (every Finish gate, every
rejection-code mapping, duplicate-NameInUse tolerance, Olthoi lock,
attribute balance/lock interaction, uncostable-skill rejection) plus a
LiveSessionController integration suite proving the wire send is exactly
55 skill slots (decoded from a real WorldSession + GameMessageCapture)
and the full Ok/rejection round trip through WorldSession.ProcessDatagram.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F1 (MEDIUM): the correlation-latch docs claimed replies are never
misattributed; in truth an overlapping send OVERWRITES the latch and the
first reply routes to the newest request's event. Narrowed all three doc
sites to the exact contract (single outstanding request; overlap refusal
is CC3's Runtime verification gate, retail's DoFinish UNDEF-state rule)
and pinned the overwrite behavior with
OverlappingSend_OverwritesTheLatch_ReplyRoutesToNewestRequest.
F2 (LOW): filed register AD-100 for the drop-unless-armed deviation —
retail's Handle_CharGenVerificationResponse@0x0055E8B0 has no armed gate
and processes whatever arrives against its persistent verification state.
F3 (LOW): doc note in CharacterCreate.cs — ACE double-sends NameInUse
(IsCharacterNameAvailable runs twice; the first callback's return exits
only the lambda), so the second reply hitting the drop path during a
connected gate is EXPECTED, not a defect.
F4 (LOW): creationFailed's enum-member key renamed name -> reason and the
ATTEMPTED character name added as name, before any consumer shipped —
one status vocabulary must not give the same key two meanings
(characterCreated.name is a character name). Contract, writer, tailer,
and shape-pinning tests updated in lockstep.
F5 (LOW): the thread-id probe-note pointer now cites
ProbeNetLogOutbound's doc comment, where the note actually lives.
Fidelity fold (reviewer's positive note): the latch is retail's OWN
discriminator one layer down — 0x0055E8B0 case 1 branches on
GetVerificationState()==PENDING (create) vs not (restore) — now cited in
both the latch doc and CharGenVerificationResponse.cs.
Core.Net 994, Runtime 1667, Launcher.Core 324, all green Release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F1 (MUST-FIX): RetailWaitDialogView was the ONE dialog view the 0a7dc7d6
EffectiveCanvasSize sweep missed - the Entering World wait dialog (fires
on ENTER, the char screen primary action) still centered against the raw
window and landed off the visible canvas. Same three-line fix as its three
siblings; the enter-wait test now grows the window over the fixed canvas
and asserts canvas-space centering.
F3/F4: two stale assertions about the DELETED first AD-98 substitution
(the register section-2 header line and the live-DAT oracle test doc) now
describe the completed FixedCanvasSize mechanism - the C4-closeout failure
mode, caught before it cost anything.
F5: RetailDialogData.Confirmation sets ElementAttribute40 itself (retail
MakeConfirmExitDialog writes 0x8E=1, 0xAC=1, 0xC5); the manual set in
GameplayConfirmationController is gone.
F6: MapWindowToCanvas truncates instead of rounding - rounding mapped the
window far edge one past the canvas last valid coordinate, a 1px dead
hit-test band; test updated to truncation semantics + far-edge case.
F7: AD-98 records that the no-letterbox aspect claim has no decomp
citation and is confirmed by the user live gate pass 2026-08-15.
F8: the durable world-name read in StartCore is IsCurrent-gated like every
neighbouring step.
F2 filed as #401 (invert RetailUi to opt-out - product-default decision,
not a gate fix).
App 5100+6 skips, Runtime 1666, green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Finding 1 (Exit button dead): retail's gmCharacterManagementUI Exit
button (element 0x100003A4, offset 7 from the listbox base in
ListenToElementMessage@0x004ed5a0) opens MakeConfirmExitDialog
(0x004ed250), whose exact ID_CharacterManagement_ConfirmExit text
(table 0x23000002) and m_confirmExitDialogContext re-entry guard are
now ported. On confirm (matching RecvNotice_CloseDialog@0x004ed760
case 1's ConfirmationResult check) the client exits through the
EXISTING graceful window-close seam (CharacterSelectionRuntimeBindings
.RequestExit -> d.Window.Close, the same delegate
GameplayInputCommandController's Escape fallback already uses) so
disconnected/exited status events still fire via GameWindow.OnClosing
-> CompleteShutdown. Retail's real post-confirm destination is
QueueUIMode(0x10000009) -> gmEpilogueUI, an epilogue screen this round
does not port — recorded as AD-99. Credits (element 0x100003A3,
QueueUIMode(0x10000005) -> gmCreditsUI) stays visibly ghosted like
Create, same treatment, out of scope this round.
Finding 2 (row names center-aligned, retail is left): the character
row template (LayoutDesc 0x21000004, element 0x100003A5, live-DAT
confirmed HJustify=Left with three stateful Type-3 highlight-art
children and no Type-12 caption child) authors its OWN justify
directly, with no separate text child to lift a label from.
DatWidgetFactory.BuildButton's Left-justify branch required
!ReferenceEquals(labelInfo, info) — true only when a label was LIFTED
from a distinct child — so a button's own direct HJustify=Left was
silently dropped to UiButton's Center default. Widened the branch to
also honor the direct case, preserving the existing lifted-child
LabelOffsetX behavior and leaving genuinely-centered buttons
(CREATE/ENTER/DELETE/RESTORE) untouched.
Finding 3 (World box empty): parsed ACE's GameMessageServerName
(opcode 0xF7E1, ACE.Server/Network/GameMessages/Messages/
GameMessageServerName.cs; retail CM_Login::DispatchUI_WorldInfo
@0x006ad860 -> ClientUISystem::Handle_Login__WorldInfo@0x005641a0 ->
ECM_Login::SendNotice_WorldName@0x00692b10, notice 0x186a2, consumed
by gmCharacterManagementUI::UpdateWorldName@0x004ec120 /
RecvNotice_WorldName@0x004ec360 onto element 0x1000039B) as
src/AcDream.Core.Net/Messages/ServerName.cs, cross-checked against
holtburger's ServerNameData. WorldSession.ServerNameReceived fires
alongside CharacterListReceived (ACE sends both in one
SendConnectResponse batch); RuntimeCharacterSelectionState.
ApplyWorldName is the new J-owner field (ungated by lifecycle, since
either message can arrive first); CharacterManagementUiController
binds it onto the WorldTextElementId UiText. Per the LA1 status
vocabulary, the characterList STATUS event's worldName field is
intentionally NOT added this round (kept bounded to the client-side
fix) — a follow-up if the launcher UI wants it.
Also corrects AD-44, discovered stale while filing AD-99: its opening
claim ("acdream has no retained character-management screen") was
false as of this session — LA7/LA8 shipped the screen in earlier
commits without updating this row.
Tests: exit-confirm open/cancel/confirm/re-entry-guard flow;
DatWidgetFactory own-HJustify-Left/Center regression tests plus the
live-DAT pinned row-justify assertion; ServerName parse round-trip
(byte-exact vs ACE's AceWireWriter fixture, truncation/wrong-opcode
cases); WorldSession dispatch test (roster+world in one wire batch);
RuntimeCharacterSelectionState.ApplyWorldName tests (order-independent
of ApplyRoster, unchanged-value no-op, Reset clears); controller test
binding the World text element to the live snapshot. Extended the
shared RetailDialogFactoryTests.BuildDialogLayout test fixture with a
Confirmation-type branch (Accept/Reject buttons) since this is its
first RetailDialogType.Confirmation consumer.
Suites: full solution Release build green; AcDream.App.Tests 5100/6
skips, AcDream.Core.Net.Tests 965/0, AcDream.Runtime.Tests 1665/0, all
Release, 0 failures; live-DAT probes (ACDREAM_PROBE_LIVE_MOUNT=1)
green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AD-98's fixed-canvas stretch (73041d70) scales every retained-UI quad at
TextRenderer.AppendQuad, but the live gate reported it JAGGED — text
especially. Cause: dat-font glyph atlases and IconComposer's composited
icons upload nearest (TextureCache.UploadUiTexture's UiNearestRepeat
sampler) — correct at the native 1:1 scale (pixel-exact retail art), but
aliased once magnified 2.4x1.8. Chrome/background art was already fine:
it uploads through GpuSamplerDescription.WorldRepeat (linear) by default.
Retail's own fixed-canvas presentation is a single bilinear-filtered
frame blit, never a per-texture stretch — this closes that gap one step
earlier, at the source texture, without adding RHI surface area.
- TextureCache.GetOrCreateLinearUiTwin: lazily registers a SECOND table
slot for a nearest handle's IGpuTexture, sampled WorldRepeat (linear)
instead of nearest — no re-decode, no re-upload, no extra memory-ledger
bytes. Returns the handle unchanged for anything never registered
nearest (chrome, UiTextureTableHandle.None), so it's a cheap
unconditional probe. Twin slots are released in Dispose without
double-disposing the shared texture.
- TextRenderer.LinearTwinResolver + the DrawSprite chokepoint: swaps a
sprite's texture handle through the resolver only while
CanvasScale != One. At CanvasScale == One the resolver is never even
called — zero overhead on the ordinary in-world/UI path.
- InteractionRetainedUiComposition wires the resolver to TextureCache
right after every UiHost acquisition (the lease can hand back a host
from a prior session against a fresh TextureCache).
- AD-98's register row gets one added sentence recording the fix.
Tests: TextRendererLinearTwinTests pins the renderer-side handle-swap
seam GPU-free (segment handle selection); TextureCacheLinearTwinTests
pins twin creation/reuse/dispose against RecordingGpuDevice. App suite
5097/3 skips (Release, ACDREAM_PROBE_LIVE_MOUNT=1 live-DAT probes
included). Full solution builds clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third iteration on the screen, completing AD-98. The previous substitution
stretched only the root BACKGROUND while the child widgets stayed at their
authored 800x600 pixel positions - and the background painting carries
visual anchors (the World/Characters captions are art), so the user gate
showed captions overlapping the listbox and every widget misaligned
against the stretched art.
Retail model (established at 71bf24fb): fixed-canvas pre-world screens
render at authored 800x600 and the whole composed frame stretches once at
presentation; the blitter has no stretch mode. Our equivalent now does the
same one stage earlier:
- UiRoot.FixedCanvasSize: while the char-select screen is active, the
retained tree lays out in its authored canvas and Draw scopes a uniform
scale onto TextRenderer.CanvasScale; the mouse entry points apply the
exact inverse so MouseX/MouseY and every hit test live in canvas space.
- TextRenderer.AppendQuad is the single emission chokepoint - sprites,
rects, AND glyphs scale together, including retail-authentic non-uniform
aspect distortion and stretched text. World-space HUD stays native (the
scale resets outside UiRoot.Draw).
- CharacterManagementUiController stops resizing Root to the viewport;
activate/deactivate/dispose set and clear the host canvas.
- UiDatElement returns to retail-pure copy-or-tile; the interim
StretchOwnBackgroundToFill flag is deleted.
- AD-98 updated to describe the completed substitution.
Tests: canvas-scale quad math, inverse input mapping (window click lands
on the canvas-space widget), degenerate-size guards, controller keeps
authored extent + sets/clears the canvas. App suite 5085/6 skips; live-DAT
char-select probes 3/3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The LA8 char-select root (0x1000039A) authors LeftEdge=TopEdge=RightEdge=
BottomEdge=0 ("no anchor") in the installed DAT — confirmed via the new
CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf
gate — so retail's own UIElement::UpdateForParentSizeChange (0x00462640) never
resizes this element; it stays a fixed 800x600 rect in retail's own tree.
Retail's generic UI sprite blit, Graphic::Draw (0x00693b20) dispatching to
Graphic::PutImage (0x00693a30) for an exact/undersized destination or a
modulo-wrapped tile loop otherwise, has no third "stretch" mode — confirmed
against BlitMode (acclient.h ~3135) and MD_Data_Image::m_drawMode/DrawModeType,
both COLOR-blend selectors, not tile-vs-stretch geometry modes. The prior
"Normal -> tile, matching ImgTex::TileCSI" citation in UiDatElement was a
mis-attribution: ImgTex::TileCSI (0x0053e740) is called exclusively from
TexMerge::CopyAndTile/ImgTex::CopyCSI for LAND-SURFACE terrain texture
compositing, never from the UI element system.
Given the dat authors zero resize anchors and the blitter can only copy or
tile, the only way retail's whole pre-world scene (background + buttons +
listbox together) fills an arbitrary window resolution is that these
fixed-canvas "flow" screens render at 800x600 and the WHOLE FRAME is
stretched once at presentation — outside the UI sprite system entirely.
acdream has no offscreen fixed-resolution UI render target / present-time
scale pass; CharacterManagementUiController's constructor instead resizes
the MOUNTED ROOT element itself to the live viewport, which is why its own
background tiled (Width/tw > 1 at any resolution above 800x600, wrapped by
GL_REPEAT).
Fix: UiDatElement gains StretchOwnBackgroundToFill (default false, every
ordinary chrome/container element keeps tiling) — when set, the element's
own DirectState background draws as one UV-0..1 quad instead of the native
tile formula. CharacterManagementUiController sets it on Root right where
Root is resized to the host viewport, reaching the same visual result as
retail's present-time stretch (no tiling, no aspect-preserving letterbox)
through a different mechanism. Divergence register row AD-98 records the
substitution.
Tests: three new UiDatElementTests pin the UV-span mechanism generically
(tile past 1.0 when unset and rect exceeds native size; clamped to 1.0 when
set; byte-identical to the old tile formula when rect equals native size,
so every unaffected panel is untouched). CharacterManagementUiControllerTests
pins Root.StretchOwnBackgroundToFill == true post-construction. The live-DAT
gate confirms the root's zero edge-anchors and Type=3 against the installed
DAT. AcDream.App.Tests: 5084 passed / 3 skipped with ACDREAM_PROBE_LIVE_MOUNT=1
(5081/6 skipped without it — the 3 live-DAT-gated tests skip).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Opus retail-lens review decoded the PDB-paired binary at
CPlayerSystem::RestoreCharacter@0x0055d760 and refuted the
uninitialized-edx justification: the two extra arguments are real
push imm32 of a constant PStringBase (BN mis-renders them, but they
pack to >=4 bytes each), so retail 0xF7D9 is >=16 bytes where ours
is 8. The guid-only CODE stands (ACE reads only the guid; holtburger
consensus) but it is an adaptation, not a corrected decompile — filed
as divergence register AD-97 and the doc comment now states the true
mechanism.
Also from the review: the 0xF643 conditional-parse doc now names BOTH
ACE flag-only failure branches (NameInUse + Corrupt); CharacterError
0x08 doc corrected (ACE misnames it ServerCrash2 — the port corrects
an ACE misnaming; ACE omits three values, not four); LA7b hazard notes
added (ACE silent no-reply on unknown restore guid; retail SendToLogon
vs SendToControl routing; NumErrors never rendered); two review-nit
tests (flag=0 Undef flag-only, non-Ok body with trailing bytes
ignored).
Core.Net suite: 953 passed / 0 failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The re-review closed all six findings and flagged one docs-only nit: the
Platform layer block described App as reaching Platform transitively when
the same commit made the reference direct, and spoke of the launcher in
the present tense. Both corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>