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>
This commit is contained in:
Erik 2026-08-16 14:18:13 +02:00
parent 2ad805469d
commit 8c30aa18ee
5 changed files with 615 additions and 33 deletions

View file

@ -1045,6 +1045,66 @@ public sealed class CharacterCreationLiveDatTests
Assert.Equal(0x100002F4u, list.Templates[0].TemplateElementId);
}
/// <summary>
/// R2-4c (Campaign CC gate round 1 Batch F). Live-DAT probe: pins the
/// Skills listbox's own authored scrollbar link (dat property
/// <c>0x72</c>, <see cref="UiTemplateListBox.ScrollbarElementId"/>) —
/// CONFIRMED as <c>0x100003F8</c>, exactly the "+1 from the listbox"
/// hypothesis this batch's own investigation raised — and confirms a
/// real <see cref="UiScrollbar"/> resolves at that id under the Skills
/// page root, the exact fact
/// <see cref="CharacterCreationSkillsPage"/>'s constructor now wires
/// (<c>scrollbar.Model = list.Scroll</c>).
///
/// <para>
/// Also pins <c>Templates[0]</c>'s (<c>0x100002F4</c>) own header-
/// caption child id (<c>0x100002f6</c>, <c>DoSkillRecords @0x00481840</c>'s
/// own <c>GetChildRecursive</c> call) as prep evidence for whoever ports
/// the four-bucket sorted model (R2-4b, still open — see the AP-213
/// row): live-DAT-measured as a <see cref="UiButton"/>, NOT a
/// <see cref="UiText"/> — the SAME <c>UIElement_Button</c>-is-
/// <c>DynamicCast(0xc)</c>-compatible-with-<c>UIElement_Text</c> quirk
/// this campaign already ported for the six attribute-slider labels
/// (GF-4b) — retail's own <c>DynamicCast(0xc)</c> cast at
/// <c>0x00481855</c> would return null on a REAL Button object
/// otherwise, and the very next line unconditionally calls
/// <c>UIElement_Text::SetStringInfo</c> on it. A future port reads the
/// header caption through <c>UiButton.Label</c>, the same seam GF-4b
/// already established.
/// </para>
/// </summary>
[InstalledDatFact]
public void SkillsPage_Listbox_HasAScrollbarLink_AndHeaderTemplateHasACaptionChild()
{
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
uint layoutId = RetailDataIdResolver.Resolve(
dats, CharacterCreationUiController.RootEnum, 5u);
ImportedLayout screen = BuildSelected(
dats, layoutId, CharacterCreationUiController.RootElementId);
UiElement skillsRoot = Assert.IsAssignableFrom<UiElement>(
screen.FindElement(CharacterCreationUiController.SkillsPageElementId));
UiTemplateListBox list = Assert.IsType<UiTemplateListBox>(
UiElement.FindDescendant(skillsRoot, 0x100003F7u));
Console.WriteLine(
$"[CC-Batch-F-DAT] Skills listbox ScrollbarElementId=0x{list.ScrollbarElementId:X8}");
Assert.Equal(0x100003F8u, list.ScrollbarElementId);
Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(skillsRoot, list.ScrollbarElementId));
UiTemplateListEntry headerTemplate = list.Templates[0];
Assert.Equal(0x100002F4u, headerTemplate.TemplateElementId);
UiElement? headerRow = LayoutImporter.Import(
dats,
headerTemplate.TemplateLayoutId,
headerTemplate.TemplateElementId,
_ => (0u, 0, 0),
null)?.Root;
UiElement realHeaderRow = Assert.IsAssignableFrom<UiElement>(headerRow);
Assert.IsType<UiButton>(UiElement.FindDescendant(realHeaderRow, 0x100002F6u));
}
/// <summary>
/// GF-15 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed
/// during the investigation: the Message dialog catalog's popup