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
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>
The final CC4 re-review closed R1-R4 (the fixed-canvas arbiter, headless
create-gate proof, #402 filing) and left one residual: the arbiter's
mismatch-throw makes 'both char-select screens author 800x600' a crash
premise on the exact user-gate path (ACDREAM_OPEN_CHARGEN=1 ->
char-management declares -> chargen declares on top), and only
char-management's extent was DAT-pinned. The chargen live-DAT probe now
pins the root at 800x600 the same way — measured against the installed
DAT (passes 7/7), not inferred. Ledger row flipped to REVIEW-CLOSED with
real shas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EOF
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>
The CC3 narrow re-review returned CLOSED (both lenses PASS, merge
recommended) with residual R1: the post-create wire-slot assignment read
the cached wire CharacterList count, which ACE never refreshes after a
create — correct for the first create, off by one for a second create in
the same session (reachable via create Ok -> server-rejected guid enter
-> ReturnToSelection -> create again), the same wire-contract failure
class F2 fixed. Root fix now rather than carried: a
creates-since-CharacterList counter (the equivalent of retail's own
CharacterSet growing via AddIdentity per create), reset on every fresh
wire CharacterList apply and at generation reset, applied only to the
cached-wire branch since the display-roster fallback already contains
prior appends. Regression test drives the full
create->Ok->rejected-enter->create-again flow and pins wire slots
0/1/2/3.
Docs: CC3 ledger row flipped to REVIEW-CLOSED with real shas (re-review
R2); CC7 risk item 8 downgraded to LATENT with measured installed-DAT
data (user-prompted): every heritage's single cost override is Arcane
Lore at NormalCost=0/PrimaryCost=2 vs global 4/6, so ACE's over-deduction
(= NormalCost = 0) cannot fire with end-of-retail data — the earlier
"may be rejected" claim was inferred from code without measuring.
Runtime 1707/0 Release.
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>
Both narrow re-reviews returned CLOSED. This closeout takes the two cheap
re-review residuals before CC3 takes references to the shared model:
R2: every array handed into the typed chargen model is now wrapped in
Array.AsReadOnly at the projection seam — a T[] behind IReadOnlyList<T>
was still downcast-mutable, and ChargenOptions is a process-shared
singleton graph.
R3: the no-Chorizite-leak guard now also walks public fields; every
current type uses properties, but a public field would have slipped
through the property-only walk.
Ledger: CC1 fix-round sha corrected to cb4703e8 (the cell previously
cited the pre-amend 459a87f2), CC1/CC2 rows flipped to REVIEW-CLOSED
with the re-review outcomes, R1 (retail refunds +1 credit on a
both-tier cost miss; port charges 0 — unreachable via retail's own
listbox, noted for CC3) and the Olthoi-locked-to-template-0 decomp fact
recorded for CC3/CC4.
Core.Tests 4736/1 skip, Content.Tests 145/0, Release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements all six Opus review findings against 04450041 (Campaign CC
CC1 chargen data layer):
- F1 (HIGH, blocking): ChargenTemplate's doc claimed "Custom" has no
ChargenTemplate entry and cited two nonexistent addresses. Verified
against the named retail decomp: gmCGProfessionPage::UpdateProfession
@ 0x004821b0 resolves BOTH the highlighted button and the description
string from CharGenState.template_ 0..6, and case 0 is button
0x100003d9 / ID_CharGen_CustomText. Custom IS template index 0 (the
"Adventurer" row CC1 already found sitting at the attribute floor).
CharGenState::SetTemplate @ 0x005C5A60 confirms every button (including
Custom) calls CharGenState::ApplyTemplate @ 0x005C5080 when committing,
so selecting Custom resets the sliders/skills to that row rather than
leaving them untouched.
- F2 (MEDIUM): retail's skill-cost lookup is two-tiered
(ACCharGenData::GetSkillTrainedCost/GetSkillSpecializedCost @
0x005C26D0/0x005C27D0 fall through to the global SkillTable,
portal.dat 0x0E000004, on a heritage-list miss — confirmed against
ACE's identical PlayerFactory.cs precedence). ChargenTableReader now
also projects the global SkillTable into
ChargenOptions.GlobalSkillCostsBySkillId, and
ChargenSkillCreditMath.ComputeSpent/RemainingCredits check the
heritage list first and the global list on a miss. Added an
installed-DAT completeness assertion recording reality: the global
table prices 38/54 advancement skill ids, every one of the 13
installed heritages ships exactly one heritage-specific override
(always also priced globally), and 16 ids are genuinely uncostable in
both tiers. Also filed a CC7 risk-item note: ACE's own heritage-
override branch over-deducts on Specialize (PlayerFactory.cs:184-211)
— a retail-legal build may be rejected by local ACE at the CC7
connected gate; that is an ACE bug, not an acdream defect.
- F3 (MEDIUM): every collection ChargenTableReader hands into the
record model is now frozen at projection (ToFrozenDictionary/ToArray,
matching MagicCatalog's house pattern), including both
ChargenOptions.Empty dictionaries.
- F4 (LOW): added a reflection guard test
(ChargenNoChoriziteLeakTests) that walks every public
AcDream.Core.CharGen member (property/indexer/constructor/method
types, recursively through generic arguments) and fails if any
resolves to the DatReaderWriter or a Chorizite* assembly.
- F5 (LOW): ChargenGenderOptions.HasAnyAppearanceOptions's doc now
states precisely what the installed-DAT gate proves (an OR across
eight lists, for at least one gender per heritage) rather than the
stronger claim it previously made, and explicitly calls out the three
omitted color lists. Added a second installed-DAT gate that records
per-list reality across every gender of every heritage — found
complete, no empty lists anywhere in the installed DAT today.
- F6 (LOW): ChargenOptions.TryGetHeritage/TryGetStarterArea now use
[MaybeNullWhen(false)] instead of null! suppression, matching the
house pattern already used elsewhere in the test suite. Fixed every
call site this surfaced (more than the five originally estimated,
since Content.Tests has TreatWarningsAsErrors).
Core.Tests: 4737 passed / 1 skip (pre-existing, unrelated).
Content.Tests: 145 passed / 0 skip.
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>
Adds the CC1 data layer for Campaign CC (retail character creation):
a reader for portal.dat's CharGen table (0x0E000002) plus a
presentation-free, Chorizite-free typed options model, and the pure
attribute/skill credit math the later CC3 Runtime owner needs.
Retail oracle (docs/research/named-retail/acclient_2013_pseudo_c.txt):
- ACCharGenData::Serialize @ 0x005C36D0 (table shape: StartingAreas +
HeritageGroups)
- HeritageGroup_CG::Serialize @ 0x005C2100
- Sex_CG::Serialize @ 0x005C1600
- Template_CG::Serialize @ 0x005C0450
- CharGenState::SetHeritageGroup @ 0x005C67A0 and the six attribute-slider
setters (~0x005C46CE..0x005C494E): remainingAtrbCredits = totalAtrbCredits
- (str+end+coord+quick+focus+self) — a heritage's AttributeCredits is the
budget the six RAW attribute values must fit, not points above the floor.
- CharGenState::Reset @ 0x005C68A0: atrbMin=10, atrbMax=100.
- gmCharGenMainUI::DoFinish @ 0x004E9170: Finish refuses only when
remainingAtrbCredits > 0 (attributes only — skill credits are never
gated to zero, confirmed by reading the function body).
- CharGenState::UpdateRemainingSkillCredits @ 0x005C37C0: exactly one of
NormalCost/PrimaryCost is charged per Trained/Specialized skill.
- gmCGAppearancePage::Update @ 0x0047E8F0: the mHeritageGroup==0xc/0xd
(Olthoi/OlthoiAcid) camera-offset branch CC6 will need.
Cross-checked against ACE's ACE.DatLoader.FileTypes.CharGen and
ACE.DatLoader.Entity.HeritageGroupCG/SexCG/TemplateCG/SkillCG loaders
(same field order, different byte format) and ACE.Entity.Enum.HeritageGroup
/ SkillAdvancementClass for the two small stable enums the model exposes.
src/AcDream.Core/CharGen/: ChargenOptions (root: StarterAreas +
HeritagesById), ChargenHeritageOptions, ChargenGenderOptions (BaseObjDesc
+ every appearance-option list: hair styles/colors, eye colors, eye/nose/
mouth strips, headgear/shirt/pants/footwear, clothing colors),
ChargenTemplate, ChargenObjDesc (palette/subpalette/texture/anim-part-swap
shape, mirrors PaletteOverride's presentation-free pattern), and the pure
math: ChargenAttributeMath (RemainingCredits/IsFullySpent/range checks) and
ChargenSkillCreditMath (retail's Trained-xor-Specialized cost sum) plus
ChargenSkillAdvancementSet, a structurally-fixed 55-slot type (reserved
slot 0 + SkillId 1..54) so CC2's future wire builder cannot send anything
but exactly 55 entries.
src/AcDream.Content/CharGen/ChargenTableReader.cs projects the Chorizite
DBObj graph into the Core model (MagicCatalog.Load's shape) — no Chorizite
type crosses into ChargenOptions.
Tests: hand-built-fixture unit tests for the pure math (Core.Tests) and the
Content projector (Content.Tests), plus six installed-DAT gate tests
(ContentConformanceDats pattern) against the real portal.dat: 13 heritage
groups (11 standard + 2 Olthoi), the four named heritages with retail
display names incl. "Gharu'ndim", every heritage has a gender with
non-empty appearance option lists, every template's attributes stay in
10..100 and never exceed its heritage's budget (discovered live: NOT every
template fully spends it — each human heritage's "Adventurer" template
sits at the floor as retail's real-DAT-backed "Custom" starting point),
start-area indices resolve into the shared list, and skill costs key to
valid 1..54 wire ids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire (Core.Net):
- CharacterCreate.cs: outbound 0xF656 builder, byte-exact port of
Proto_UI::SendCharGenResult@0x00546a70 -> ACCharGenResult::Pack@0x005c7570
-> CG_Pack@0x005c7200. Account String16L first (packed outside CG_Pack),
then the constant-1 u32, heritage/gender, 14 appearance strip/style/color
u32s, 6 f64 shades (skin/hair/headgear/shirt/trousers/footwear, retail
order), template, 6 attributes, slot, classId, numSkills + exactly 55
u32 skill-advancement classes (ReadOnlySpan validated ==55, throws
ArgumentException otherwise — ACE terminates the session on any other
count via PlayerFactory.CreateResult.ClientServerSkillsMismatch), name
String16L, startArea, isAdmin, isEnvoy, and a trailing checksum whose
exact 19-term accumulation set (heritage+gender+3 strips+hairColor+
eyeColor+hairStyle+headgearStyle+shirtStyle+trousersStyle+footwearStyle+
template+6 attributes) is read byte-for-byte off CG_Pack's decompiled
accumulator (0x005c7213-0x005c74c3) — headgearColor/shirtColor/
trousersColor/footwearColor/shades/slot/classId are deliberately absent
from the sum despite sitting adjacent on the wire. Cross-checked against
ACE's CharacterCreateInfo.Unpack/Appearance.Unpack and holtburger's
CharacterCreateRequestData (types.rs:236-369), which agree on every
field and order. Retail routes via SendToLogon — the same queue
CharacterDelete already uses.
- CharGenVerificationResponse.cs (new): promotes the shared 0xF643 parse
out of CharacterRestore — full Code enum (Undef..AdminPrivilegeDenied=7,
ACE's CharacterGenerationVerificationResponse) plus the conditional
Ok-only identity payload (guid/String16L name/u32 secondsGreyedOut).
CharacterRestore.Parse now delegates to it; CharacterRestore's public
Parsed shape, Parse signature, and every existing test expectation are
UNCHANGED.
- PacketWriter.WriteDouble: f64 little-endian helper for the shade fields.
WorldSession dispatch (Core.Net):
- Added an awaiting-request latch (None/Restore/Create), armed by
SendRestoreCharacter/the new SendCharacterCreation immediately before
each send (SendCharacterCreation builds the body first so a skill-count
throw never arms the latch for a request that was never sent), cleared
the instant a matching 0xF643 is dispatched (success OR parse failure —
a malformed reply must never wedge the latch open) and on Dispose.
0xF643 now routes to CharacterRestoreReceived or the new
CharacterCreateResponseReceived (Action<CharGenVerificationResponse.Parsed>)
by that latch; an unexpected 0xF643 with nothing outstanding logs once
and is dropped, never misattributed. Fixed
WorldSessionCharacterSelectionTests' restore-dispatch test, which
previously fed a bare CharacterRestore response with no preceding
SendRestoreCharacter — that shape is now the "no outstanding request"
drop path by design.
Status events (Runtime + Launcher.Core, contract first):
- Amended docs/plans/2026-08-14-launcher-campaign.md §LA1's pinned status
vocabulary to add characterCreated{guid,name} (Ok reply identity, named
to mirror CharGenVerificationResponse's own fields and to read distinct
from enteredWorld — retail logs a freshly created character straight in
without a fresh characterList) and creationFailed{code,name} (raw Code
value + its enum member name).
- SessionStatusWriter.CharacterCreated/CreationFailed implement that
contract.
- Launcher.Core: CharacterCreatedStatusEvent/CreationFailedStatusEvent +
StatusEventParser cases, in lockstep.
Tests: CharacterCreateTests (byte-exact layout incl. checksum term-set,
55-slot fixture, wrong-count throws), CharGenVerificationResponseTests
(every Code value), WorldSessionCharacterCreationTests (create-then-
response routes correctly, restore unaffected, no-outstanding drop,
second-response-after-consumed drop, Dispose clears the latch, a builder
throw never arms it), SessionStatusWriterTests + Launcher.Core
StatusEventParserTests/StatusFileTailerTests (pinned shape + tailer
round-trip) for the two new events.
Verified: dotnet build AcDream.slnx -c Release — 0 errors. Full solution
test run green (Core.Net.Tests 993/993, Runtime.Tests 1667/1667,
Launcher.Core.Tests 323/323, plus every other project in the solution).
WSL Ubuntu: Core.Net.Tests 993/993, Runtime.Tests 1667/1667.
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>
Two live-integration gaps the ef96c554 unit tests could not see:
1. World box stayed empty against ACE: ServerName (0xF7E1) arrives in the
SAME connect batch as CharacterList, so ServerNameReceived fires during
the handshake pump BEFORE the controller binding subscribes - the
event-only wiring proved the state and controller but never the live
ordering. StartCore now reads the durable WorldSession.ServerInfo after
connect exactly like the roster (ILiveSessionOperations.GetServerInfo,
default interface method so no fake breaks); the event remains for
post-connect updates. Pinned by a Start-level test.
2. The exit confirmation rendered far right of the screen: all three
retail dialog views centered against the raw window size while the
active screen lays out in the fixed 800x600 canvas - center-of-1920
is canvas-760, which the stretch pushes off-center. Views now center
against UiRoot.EffectiveCanvasSize (canvas while a pre-world screen is
active, window otherwise). Pinned by growing the window over the fixed
canvas in the exit-dialog test and asserting the scrim spans the canvas
with the popup centered at 400.
Runtime 1666, App 5100+6 skips, 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>
Root cause: the LA8 character-select screen's root background RenderSurface
(0x06007576, LayoutDesc 0x21000004 element 0x1000039A) is PFID_CUSTOM_RAW_JPEG
— a complete JFIF byte stream (confirmed live: 414,230 bytes, FFD8...FFD9,
Width=0/Height=0 on disk) that SurfaceDecoder.DecodeRenderSurface had no case
for, so it fell through the switch's `_ => DecodedTexture.Magenta` default arm
with nothing logged. Retail's RenderSurface::CreateFromSourceData
(named-retail decomp @0x004440a0) hands this exact byte stream to the Intel
JPEG Library (`_ijlInit`/`_ijlRead`/`_ijlFree`) at runtime and reads the real
pixel dimensions from the JPEG's own SOF header rather than this
RenderSurface's Width/Height fields, which are legitimately 0 for this
format — the same reason the decoder's generic non-positive-Width/Height
guard was also wrong to apply here.
A per-id media sweep of the installed DAT (new EveryDeclaredMediaId_
ResolvesToADecodableTexture test) showed this was the ONLY unresolved id
among the screen's 25 distinct media ids — the listbox (0x1000039D) and every
button face resolve fine. The listbox interior and the ENTER button's
circular fill are both transparent regions layered on top of the root, so
the one broken root background bled through everywhere nothing opaque
covered it, producing all three symptoms (full-screen background, listbox
interior, ENTER circle) from one cause.
Fix: SurfaceDecoder now special-cases PFID_CUSTOM_RAW_JPEG before the
Width/Height guard and decodes it with StbImageSharp (dual Unlicense/MIT,
pure managed, no native dependency — works on the Linux headless/graphical
targets Slice K/L commit to). JPEG is ITU T.81-standardized, so any
conforming decoder reproduces the pixels IJL would; round-tripped a
synthetic fixture through the real decode path to confirm. Verified against
the live DAT: 0x06007576 now decodes to 800x600, exactly the screen's
LayoutDesc-authored size.
Guard: per claude-memory/feedback_ui_resolve_zero_magenta.md, an unresolved
id reaching the draw path should be loud. That memory's existing guard
("guard on the id, not the handle") only covers a DIFFERENT trap — a
zero/absent id — and could not have caught this one, which has a real,
non-zero, DAT-resolved id. No guard existed for "id resolves but can't
decode" or "id doesn't exist in either dat" before this change, so both were
silent. SurfaceDecoder now logs once per surface id on every magenta-return
path (null data, JPEG decode failure, unsupported format, no-palette
paletted format, decode exception); TextureCache.GetOrUploadRenderSurface
logs once per id when a RenderSurface isn't found in Portal or HighRes at
all.
Tests: CharacterManagementLiveDatTests.EveryDeclaredMediaId_
ResolvesToADecodableTexture (installed-DAT gate, ACDREAM_PROBE_LIVE_MOUNT=1)
sweeps every StateMedia id in the char-select root + listbox row template
and asserts none decode to the magenta placeholder — this class of gap now
fails the gate instead of shipping silently. SurfaceDecoderTests adds
PFID_CUSTOM_RAW_JPEG coverage (real decode via a synthetic from-scratch
JPEG fixture — not retail art, generated with StbImageWriteSharp and
round-tripped before being pasted in as a literal; corrupt-data and
null-SourceData magenta paths) plus PFID_P8/PFID_INDEX16 no-palette cases
that now flow through the same logged path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A launcher-spawned client showed the world with NO interface at all -
character screen included. RetailUi rode ACDREAM_RETAIL_UI (the dev-era
opt-in), FromSessionConfig inherited the env parse, and the launcher
strips ACDREAM_* from children by design, so every product launch got
the dev default. A session-config launch IS a product launch: RetailUi
is now forced true on that path; the env flag remains the dev-launch
opt-in. Pinned by the session-config options test with a null env.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
F1: the crash reporter comment claimed the launcher never holds a password
in any field - false (ProfileEditorDialogViewModel, AccountProfile.Password,
StartRequest.Password). Reworded to the true, narrower invariant (no throw
site interpolates a credential VALUE into an exception message) and pinned
it with CrashReportNeverContainsAStoredPassword: a real STJ failure over a
profiles document containing a known password, corrupted after the
credential, must yield a crash file with the stack and without the value.
F2: the co-deploy Inputs covered only Bake own sources; a Content edit
never refreshed the 83 MB exe. Now the full reference closure. Fixing it
surfaced two more incrementality traps, both fixed and comment-documented:
SkipUnchangedFiles left the output older than the triggering input (target
re-ran forever - added an explicit Touch), and %(Item.Metadata) in a plain
Include does not batch (the literal percent-text became a permanently
out-of-date phantom input - globs are now spelled per project). Verified:
Core edit retriggers, then two consecutive clean incremental builds.
F3: RID publishes ran BOTH co-deploy paths (two self-contained bake
publishes). Build-time target now guarded on _IsPublishing; verified a
real win-x64 publish runs zero build-target co-deploys and still ships
both exes.
F4: comment misattributed PublishBakeTool=false to CI lanes; it is
target-local recursion guarding. F5: the x:Name reflection sweep now walks
the markup as XML and tolerates template-scoped names (no generated field
exists for those). F6: dead using removed. Hardening: the crash reporter
positional --data-dir fallback requires a fully-qualified path so a
relative or flag-shaped value cannot create ./crash-reports at an
arbitrary CWD.
Launcher 67/67, Launcher.Core 317/317.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#398 was a crash on every modal open/close caused by MainWindow's
constructor calling AvaloniaXamlLoader.Load(this) instead of the
generated InitializeComponent() — only InitializeComponent assigns the
x:Name backing fields, so every named control was null and the first
Dispatcher.UIThread.Post callback in OnViewModelPropertyChanged threw
NullReferenceException, killing the process. It reached the user gate
because no test in tests/AcDream.Launcher.Tests (ViewModel-only) ever
constructed a MainWindow. #399 is the process gap that let that class of
defect through 14,012 green tests.
Adds Avalonia.Headless.XUnit 12.1.1 to the launcher test project. Its
net10.0 dependency group targets xunit v3, so the project migrates
xunit 2.9.3 -> xunit.v3 3.2.2 (drop-in: all 54 pre-existing tests compile
and pass unchanged under dotnet test via xunit.runner.visualstudio 3.1.4,
which already supported v1/v2/v3; two call sites needed
TestContext.Current.CancellationToken per the new xUnit1051 analyzer).
TestAppBuilder.cs wires [assembly: AvaloniaTestApplication] to a headless
AppBuilder.Configure<App>() so the real App.axaml FluentTheme is live in
tests.
MainWindowViewTests.cs adds 12 [AvaloniaFact]/[AvaloniaTheory] tests:
- an explicit non-null + type check of every x:Name field the
code-behind dereferences (ProfilesTree, ServerNameTextBox,
AccountNameTextBox, CharacterNameTextBox, EditorSubmitButton,
FirstRunDatDirectoryTextBox, FirstRunCloseButton, UpdateCloseButton)
- a reflection sweep over every x:Name found in MainWindow.axaml, so a
future named control without a matching non-null field fails loudly
- one open+close round trip per ProfileEditorKind (all seven, including
Remove), plus the first-run wizard and the update prompt, each pumping
Dispatcher.UIThread.RunJobs() so the queued focus callback actually
executes instead of just being asserted vacuously
- a dedicated test for the _focusBeforeModal-restore branch (not just
the ProfilesTree.Focus() fallback), anchored on a real focusable
button since ProfilesTree (TreeView) has Focusable="False" under
FluentTheme — its own tab stops are TreeViewItem rows, so the
close-path assertions check "no exception escaped the dispatcher"
rather than "focus landed on ProfilesTree"
Falsification (required evidence): reverting MainWindow's constructor to
AvaloniaXamlLoader.Load(this) and rerunning gives 12 failed / 0 passed —
10 tests throw NullReferenceException at MainWindow.FocusActiveModal,
propagating cleanly out of Dispatcher.UIThread.RunJobs() (confirming
dispatcher exceptions are not silently swallowed), and the 2 reflection
tests fail on an explicit "x:Name 'ProfilesTree' was null after
construction" message. Restoring InitializeComponent() gives 12 passed /
0 failed. Full launcher suite: 66 passed / 0 failed, reproduced on both
Windows and native Ubuntu (WSL, no display/Xvfb — Avalonia.Headless needs
none). AcDream.Launcher.Core.Tests: 317/317 unaffected.
No CI workflow change needed: .github/workflows/headless-portability.yml's
portable-launcher job already runs dotnet test on the launcher test
project on both windows-latest and ubuntu-latest with no display setup,
which is sufficient for Avalonia.Headless.
Closes#399.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>