Commit graph

14 commits

Author SHA1 Message Date
Erik
7f6e93033f fix(chargen): Campaign CC gate round 1 re-test 2 — R3-3 skills info-box VerticalJustify
The info-box title (0x100003fb, Y=435 H=100) and description (0x100003fc,
Y=460 H=100) panes' own authored boxes overlap by 75px, live-DAT-measured
— retail relies on vertical justification, not disjoint rects, to keep
them visually separate. Neither pane authors dat property 0x15, so both
fall to this port's shared unauthored-VJustify default (currently Center).

Byte-traced retail's real ctor default (UIElement_Text::UIElement_Text
@0x004685ff, m_eVerticalJustification = 4) against UIElement_Text::
CalcJustification @0x00467260's actual enum semantics (1=Center, 3-or-5=
the far edge/Bottom, anything else INCLUDING the ctor's own default of 4
= the near edge/Top): the correct unauthored default is Top, not Center —
a genuine client-wide enum-mapping bug in this port. Under Top both panes
render near their own box's top edge (25px apart, no collision); under
Center both cluster toward the middle of their overlapping boxes.

Scoped fix: CharacterCreationSkillsPage force-sets VerticalJustify=Top on
both panes directly, rather than fixing the shared mapping/default — that
bug is client-wide and could regress already-shipped FROZEN surfaces
(vitals, chat, main game UI, Options) that may rely on the current Center
default. The shared fix is filed as ISSUES #410 / register AD-104 for its
own dedicated investigation + regression sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:20:08 +02:00
Erik
0fed5fdd91 fix(chargen): Campaign CC gate round 1 closeout — Group 2: Skills page four-bucket model
Ports the last remaining half of retail's Skills page: the four-bucket
sorted skill list (Specialized/Trained/UseableUntrained/UnuseableUntrained,
UpdateSkillEntry's own iMinlevel <= 1 test), plus the info box's
description + formula completion.

- ChargenSkillDetail/ChargenSkillFormula (Core) thread SkillBase.MinLevel/
  Description/Formula from the global SkillTable, exposed via a new
  ChargenOptions.TryGetSkillDetail (nullable-with-default parameter, so
  every pre-existing ChargenOptions call site compiles unchanged).
  ChargenTableReader.Project populates it from the same SkillTable loop
  that already builds GlobalSkillCostsBySkillId.
- CharacterCreationSkillsPage.RebuildRows now groups every costable skill
  into SkillBucket, sorts each bucket alphabetically by name
  (InsertEntrySorted's wcscmp, ported as string.CompareOrdinal), and
  builds one Templates[0] header row per bucket ahead of that bucket's
  Templates[1] skill rows — DoSkillRecords' own unconditional
  4-header-then-populate order. A level change re-buckets the row
  (detected per-refresh against each row's own cached bucket, then a
  full rebuild with the current selection explicitly preserved).
- RefreshInfoBox now composes description (word-wrapped via
  DatRichText.Compose) + the level-gated bonus line (an exact, unwrapped
  literal — NOT routed through word-wrap, which would have collapsed its
  authored double-space formatting) + ComposeFormula's "Formula : ..."
  line (MakeSkillFormula ported with high confidence for the prefix/
  per-attribute-term/divisor/bonus-suffix shape; the two-attribute
  connector text is a disclosed approximation, register AP-231, since
  the decompiled function's own connector literals could not be
  recovered byte-exact by this session's static-only tooling).

Register: AP-213 RETIRED (160 active rows). Live-DAT gate: the installed
SkillTable's MinLevel distribution matches the investigation's own
recorded finding exactly (38 entries, 23 useable-untrained / 15
trained-required). 3 new fixture tests + 1 new live-DAT test; 3
pre-existing integration tests fixed (they captured row widget
references before a bucket-changing click, which now rebuilds and
discards those references — a real, correct consequence of the new
model, not a bug).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 15:33:39 +02:00
Erik
8c30aa18ee fix(chargen): Campaign CC gate round 1 Batch F — Skills page buckets, selection, info box, cost text, arrow states
R2-4/review F1-F2 (gmCGSkillsPage): row click (and arrow click, matching
retail's own post-Increase/DecreaseSkillLevel re-select) now selects a
skill, highlights its row name, and writes the info panes' title (name +
score) and a level-gated bonus line; the description/formula halves stay
unported (SkillBase._description/_formula unreachable from this page's
current data surface, documented on RefreshInfoBox). The listbox's own
authored scrollbar link is wired to its Scroll model (live-DAT-confirmed
at 0x100003F8, matching the "+1 from the listbox" hypothesis). Cost text
now matches SetSkillText @0x00480600 exactly: Untrained's down-cost and
Specialized's up-cost are literal "0", unconditional, where the port
previously rendered blank; the 999-blank gate applies to the up-cost
only, never to a down-cost. Arrow Ghosted/Enabled state (0x1000001a/
0x1000001b) is now gated per branch, including bUntrainable/
bUnspecializable re-derived as "this row's own effective cost is
nonzero" — no new data needed since the page already resolves that cost.

R2-4b (the four-bucket sorted model) is NOT implemented — its Useable-
vs-Unuseable-Untrained split reads SkillBase.MinLevel, confirmed present
in the installed dat (SkillTable_MinLevelDistribution_NeverExceedsTrained)
but not threaded through ChargenOptions/ChargenHeritageOptions/
CharacterCreationRuntimeBindings. AP-213 row records the exact channel a
future fix needs. Also live-DAT-pinned: Templates[0]'s header-caption
child (0x100002f6) resolves as a UiButton, not UiText, in the real dat —
the same UIElement_Button-is-DynamicCast(0xc)-compatible-with-Text quirk
already ported for GF-4b's slider labels.

App suite (live-DAT env) 5321/3 -> 5328/3 (+7, zero regressions).
Runtime 1735/0 unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 14:18:13 +02:00
Erik
2349f8b4df fix(chargen): Campaign CC gate round 1 Batch C — Summary how-to + scrollbar linkage
Commit 3/3: Summary how-to text + Commit 2's owed scrollbar linkage +
bookkeeping sweep.

- Ports gmCGSummaryPage::SetHowToText @0x0047ae20 into the Summary
  page's how-to box (0x10000404, HowToTextId was declared and unused
  since CC5). Retail concatenates ID_CharGen_SummaryHowTo + a heritage/
  gender-specific name-suggestion list (heritages 1-4 — Aluvian/
  Gharundim/Sho/Viamontian — only; heritages 5-13's cases in the same
  switch decompile to a vtable-slot artifact, the same decompiler-
  mangled-symbol class the Heritage page's own BonusSkillsKeyByHeritage
  table already documents, so no name-suggestion string exists for them
  and none is invented) + ID_CharGen_SummaryHowToEnd, directly
  concatenated (no separator literal) into ONE plain SetText call — no
  per-run font/color argument, unlike Heritage's ...WithFont calls, so
  this routes through DatRichText as a single DefaultColor segment.

- Wires the description boxes' linked scrollbar to actual text
  scrolling — Commit 2 made the scrollbar child (0x100002e7) BUILD as a
  real UiScrollbar; this binds scrollbar.Model = text.Scroll, the exact
  pattern ChatWindowController already uses for the chat transcript.
  Live-DAT-measured: only Heritage's description (0x100003c4) and
  Summary's how-to box (0x10000404) actually author this child —
  Profession/Town's shorter description boxes do not (a genuine retail
  authoring fact, not something to "fix" further).

Register: AP-215/AP-216/AP-217 rewritten (Batch C's Commit 1 already
retired AP-218/AD-103) — no further changes needed this commit; ISSUES
#366 (chat's new-unseen-text indicator, 0x1000048C under the chat
transcript 0x10000011) NARROWED — its own pre-filed "fix shape"
recommendation (a UiText child carve-out mirroring UiMeter's) is
EXACTLY what Commit 2 shipped, confirmed by that commit's own
client-wide sweep; #366 stays open for the still-missing behavioral
half (no controller drives the indicator's visibility/click).

Findings doc updated: GF-2/GF-3/GF-4/GF-6/GF-11a/GF-12/GF-14's text
half all marked FIXED with their own root-cause notes; the two
remaining "suspected shared roots" (frames/labels, rich text) marked
CONFIRMED + CLOSED.

Full App suite (Debug and Release, live-DAT): 5307 passed / 0 failed /
3 skipped (up from 5304 after Commit 2). Runtime suite: 1735/0,
unaffected.

Campaign CC gate round 1 Batch C is CODE-COMPLETE across all three
commits — GF-2, GF-3, GF-4, GF-6, GF-11a, GF-12, and GF-14's text half
are fixed; AP-216/AP-217 partially closed (register-honest about what
shipped vs what needs a palette-to-RGB pipeline this batch didn't add).
Pending the user's visual gate, with chat + the main game UI flagged
for extra attention (Commit 2's client-wide blast radius).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 12:34:38 +02:00
Erik
0591b9a026 fix(chargen): Campaign CC gate round 1 Batch C — rich text + labels + backdrops
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>
2026-08-16 12:18:53 +02:00
Erik
7d09821fdc fix(chargen): Campaign CC gate round 1 Batch B — authored selection states, label state, zoom/swatch feedback
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>
2026-08-16 11:37:09 +02:00
Erik
1d9de5e095 fix(chargen): Campaign CC gate round 1 Batch A — GF-15 input, GF-5 skills rows, GF-13 GM toggles
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>
2026-08-16 10:54:41 +02:00
Erik
356545c530 fix(chargen): Campaign CC CC5 re-review residuals R1-R5 + nits — REVIEW-CLOSED
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>
2026-08-16 01:57:07 +02:00
Erik
0c8e1e7df1 fix(chargen): Campaign CC CC5 review fix round — F1-F14
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>
2026-08-16 01:13:52 +02:00
Erik
34e3a534be feat(chargen): Campaign CC slice CC5 — Summary page, Finish flow, RandomizeCharacter port
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>
2026-08-16 00:01:07 +02:00
Erik
d2a71152d2 fix(chargen): Campaign CC CC6b-MOUNT review fix round — F1-F13
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>
2026-08-15 22:19:02 +02:00
Erik
34c6fceab0 feat(chargen): Campaign CC slice CC6b-MOUNT — Appearance page + preview mount
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>
2026-08-15 21:00:10 +02:00
Erik
ec854db045 fix(app,runtime,headless): Campaign CC slice CC4 review-fix round — F1-F12
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>
2026-08-15 18:29:49 +02:00
Erik
0e71d3b829 feat(app): Campaign CC slice CC4 — chargen screen shell + Heritage/Profession/Skills/Town pages
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>
2026-08-15 17:45:51 +02:00