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>
This commit is contained in:
Erik 2026-08-16 12:34:38 +02:00
parent 5190e16915
commit 2349f8b4df
6 changed files with 391 additions and 45 deletions

View file

@ -135,6 +135,19 @@ internal sealed class CharacterCreationHeritagePage : IDisposable
_description = UiElement.FindDescendant(pageRoot, 0x100003C4u) as UiText;
_backdrop = UiElement.FindDescendant(pageRoot, 0x100003BEu);
// Commit 2/3 follow-up (Campaign CC gate round 1 Batch C): the
// description box's own linked scrollbar — live-DAT-measured
// present here (unlike Profession/Town's shorter description
// boxes, which author no scrollbar child at all) at the SAME
// relative id CharacterCreationSummaryPage's how-to box carries.
// ChatWindowController's own scrollbar.Model = transcript.Scroll
// pattern, scoped to this box's own descendant.
if (_description is not null
&& UiElement.FindDescendant(_description, 0x100002E7u) is UiScrollbar descriptionScroll)
{
descriptionScroll.Model = _description.Scroll;
}
}
internal void Refresh(

View file

@ -76,11 +76,48 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
/// </summary>
private const int MaxNameLength = 32;
/// <summary>
/// Commit 3 (Campaign CC gate round 1 Batch C): the how-to box's own
/// linked scrollbar, relative id — live-DAT-measured as the SAME
/// template child id the Heritage description box also carries
/// (<c>0x100002e7</c>), distinct from <see cref="ScrollId"/> (the
/// listbox's own scrollbar). Profession/Town's description boxes do
/// NOT author this child at all (shorter authored text, live-DAT-
/// confirmed) — only Heritage and Summary's how-to box do.
/// </summary>
private const uint HowToScrollRelativeId = 0x100002E7u;
/// <summary>
/// Heritage id -&gt; (male name-list key, female name-list key) per
/// <c>gmCGSummaryPage::SetHowToText @0x0047ae20</c>'s switch
/// (@0x0047aeb2-0x0047afda). ONLY heritages 1-4 (Aluvian/Gharundim/
/// Sho/Viamontian) resolve to real string literals
/// (<c>"ID_CharGen_&lt;Abbrev&gt;{Male,Female}Names"</c>, confirmed
/// present in the compiled string-constant table); every other
/// heritage's case in that same switch (5/0xa Shadowbound+Penumbraen
/// share one body, 6 Gearknight, 7 Tumerok, 8 Lugian, 9 Empyrean, 0xb
/// Undead, 0xc/0xd Olthoi/OlthoiAcid) decompiles to a vtable-slot
/// artifact instead of a string constant — the same decompiler-
/// mangled-symbol class the Heritage page's own
/// <c>BonusSkillsKeyByHeritage</c> table already documents — meaning
/// no real name-suggestion string exists for those heritages; this
/// port does not invent one.
/// </summary>
private static readonly IReadOnlyDictionary<uint, (string Male, string Female)> NameSuggestionKeysByHeritage =
new Dictionary<uint, (string, string)>
{
[(uint)ChargenHeritageGroup.Aluvian] = ("ID_CharGen_AluMaleNames", "ID_CharGen_AluFemaleNames"),
[(uint)ChargenHeritageGroup.Gharundim] = ("ID_CharGen_GharuMaleNames", "ID_CharGen_GharuFemaleNames"),
[(uint)ChargenHeritageGroup.Sho] = ("ID_CharGen_ShoMaleNames", "ID_CharGen_ShoFemaleNames"),
[(uint)ChargenHeritageGroup.Viamontian] = ("ID_CharGen_ViaMaleNames", "ID_CharGen_ViaFemaleNames"),
};
private readonly CharacterCreationRuntimeBindings _bindings;
private readonly RetailDialogFactory _dialogs;
private readonly string _nameTooLongMessage;
private readonly UiTemplateListBox? _list;
private readonly UiField? _nameField;
private readonly UiText? _howToText;
private string _lastCommittedName = string.Empty;
private uint _nameTooLongDialogContext;
private bool _disposed;
@ -145,6 +182,19 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
}
Viewport = UiElement.FindDescendant(pageRoot, ViewportId) as UiViewport;
// Commit 2/3 follow-up: the how-to box's linked scrollbar (Commit
// 2 made it BUILD as a real UiScrollbar; this wires it to actual
// scrolling) — ChatWindowController's own scrollbar.Model =
// transcript.Scroll pattern, scoped to THIS box's own descendant
// (the relative id recurs on Heritage's description box too, so a
// flat screen-wide lookup would be ambiguous).
_howToText = UiElement.FindDescendant(pageRoot, HowToTextId) as UiText;
if (_howToText is not null
&& UiElement.FindDescendant(_howToText, HowToScrollRelativeId) is UiScrollbar howToScroll)
{
howToScroll.Model = _howToText.Scroll;
}
}
internal void Refresh(
@ -176,6 +226,51 @@ internal sealed class CharacterCreationSummaryPage : IDisposable
RebuildListbox(view, snapshot);
RebuildPreview(view, snapshot);
RefreshHowToText(snapshot);
}
/// <summary>
/// Commit 3 (Campaign CC gate round 1 Batch C): ports
/// <c>gmCGSummaryPage::SetHowToText @0x0047ae20</c>. Retail
/// concatenates <c>ID_CharGen_SummaryHowTo</c> +
/// (heritage/gender-specific name-suggestion list, heritages 1-4
/// only) + <c>ID_CharGen_SummaryHowToEnd</c> directly
/// (<c>append_n_chars</c>, no separator literal) into ONE plain
/// <c>UIElement_Text::SetText</c> — no per-run font/color argument,
/// unlike Heritage's <c>...WithFont</c> calls, so this is a single
/// <see cref="UiText.DefaultColor"/> segment.
/// </summary>
private void RefreshHowToText(RuntimeCharacterCreationSnapshot snapshot)
{
if (_howToText is null)
return;
Func<string, string?>? resolveText = _bindings.ResolveText;
if (resolveText is null)
return;
var builder = new System.Text.StringBuilder();
if (resolveText("ID_CharGen_SummaryHowTo") is { } howTo)
builder.Append(howTo);
if (NameSuggestionKeysByHeritage.TryGetValue(snapshot.HeritageId, out (string Male, string Female) keys))
{
// gmCGSummaryPage::SetHowToText @0x0047af3f et al.: the raw
// "!= 2" comparison, no gender-unset special case — an unset
// gender (0) takes the male-key branch, matching retail's own
// literal comparison.
string key = snapshot.GenderKey == 2u ? keys.Female : keys.Male;
if (resolveText(key) is { } nameTokens)
builder.Append(nameTokens);
}
if (resolveText("ID_CharGen_SummaryHowToEnd") is { } howToEnd)
builder.Append(howToEnd);
if (builder.Length == 0)
return;
string composed = builder.ToString();
var segments = new[] { new DatRichText.Segment(composed, _howToText.DefaultColor) };
_howToText.LinesProvider = () => DatRichText.Compose(_howToText, segments);
}
// ── Name field (ListenToElementMessage @ 0x0047bf40) ────────────────