fix(chargen): Campaign CC gate round 1 Batch E — text origin, caption escapes, value rects, scrollbars, name prefill
R2-1/R2-6 (description-box text clipped left of the frame, regressed from
Batch C's frame un-consume): root cause was never the un-consume change
itself — the Heritage/Profession/Town/Summary description boxes
(0x100003C4/0x100003E0/0x10000409/0x10000404) all author retail's four
independent text-inset margins (dat properties 0x23-0x26,
UIElement_Text::OnSetAttribute cases 0xf-0x12: margL=9/margR=26/margU=15/
margD=15), which this codebase never read at all, before or after Batch C.
Un-consuming the gold-frame children just made the pre-existing missing-
margin bug visible for the first time (the frame's own left border now
draws around the same x=0 origin text always used). Fixed end to end:
ElementInfo.MarginLeft/Right/Top/Bottom (read in
ApplyCanonicalLegacyProjection, propagated in Merge), UiText.MarginLeft/
Right/Top/Bottom (additive with the pre-existing Padding), a new pure
UiText.ContentOffsetX static consumed by the multi-line draw path's
per-line placement, and matching wrap-width shrinkage in
DatRichText.Compose and BuildText's own authored-multiline path. Scoped to
the multi-line (non-OneLine) path only.
R2-2/R2-3 (Attribute\n Credits renders the literal backslash-n; the live
credit value overlaps mid-caption): two stacked gaps. (1) UiButton
captions never escape-normalized the DAT's literal "\n" — centralized the
normalize into DatWidgetFactory's ResolveAuthoredString (the one choke
point every P0x17 resolution already shares) plus a NormalizeEscapes
helper for the per-state caption loop, so every caller normalizes
identically. (2) UiButton.Label only ever drew one line — retail's
UIElement_Button IS a UIElement_Text with OneLine=false on these buttons,
so a caption should word-wrap/stack like any other Type-12 box. Added
UiButton.DrawBlockLabel + the pure, unit-tested WrapBlockLines. The
value-overlap itself: ValueBox was never wrong (live-DAT-measured correct
child rects) — the caption was drawing unconfined across the button's
full width ("Available Skill Credits" measures 193px in a 231px button
whose value box starts at x=116). Fixed by confining the caption's own
drawable width to stop before ValueBox.X whenever a ValueLabel coexists.
R2-7a (Summary overview listbox missing its scrollbar): pure wiring gap —
the listbox authors a linked scrollbar via dat property 0x72
(ScrollbarElementId=0x10000401) that CharacterCreationSummaryPage's
constructor never resolved, unlike every other UiTemplateListBox owner in
the codebase. Fixed with the same resolve-and-wire pattern.
R2-7b (how-to box scrollbar overlaps text, no thumb): traced to a
downstream symptom of R2-1, not an independent bug — UiScrollbar only
paints its thumb when the linked model has overflow, and the pre-fix wrap
width (un-inset) produced fewer/shorter lines than fit the view. Pinned
directly against the real installed strings/font (Aluvian's how-to text)
that the margin-correct width overflows. No UiScrollbar code changed.
R2-8 (name field should show "[ Name ]"): re-checked the one hypothesis
Batch A's GF-15 closure left open — an authored initial-text string on
the field's own P0x17. Confirmed absent on every state in the installed
DAT. No code change; Batch A's closure stands, now pinned as a live-DAT
regression test.
App suite 5334/3 (was 5321/3, +13, zero regressions). Runtime 1735/0
unchanged. Full solution Release build green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2ad805469d
commit
e24ec20882
11 changed files with 895 additions and 23 deletions
|
|
@ -245,6 +245,27 @@ public sealed class ElementInfo
|
|||
/// </summary>
|
||||
public bool Invisible;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC gate round 1 Batch E (R2-1): the four independent
|
||||
/// <c>UIElement_Text</c> text-inset margins, dat properties
|
||||
/// <c>0x23</c>/<c>0x24</c>/<c>0x25</c>/<c>0x26</c> (IntegerBaseProperty
|
||||
/// — <c>UIElement_Text::OnSetAttribute @0x0046a640</c> cases
|
||||
/// <c>0xf</c>/<c>0x10</c>/<c>0x11</c>/<c>0x12</c>, i.e.
|
||||
/// <c>BaseProperty::GetPropertyName(arg2) - 0x14</c>, writing
|
||||
/// <c>m_margL</c>/<c>m_margR</c>/<c>m_margU</c>/<c>m_margD</c>). Ctor
|
||||
/// default is 0 on all four (<c>UIElement_Text::UIElement_Text
|
||||
/// @0x004686d1-0046872d</c> clears them before any authored value
|
||||
/// applies). The chargen description boxes author <c>margL=9,
|
||||
/// margR=26, margU=15, margD=15</c> (live-DAT-probe-confirmed on
|
||||
/// <c>0x100003C4</c>/<c>0x100003E0</c>/<c>0x10000409</c>/
|
||||
/// <c>0x10000404</c>) — this codebase never read these four
|
||||
/// properties before this fix, so every DAT-imported <c>UiText</c>
|
||||
/// drew flush against its own outer rect (<c>Padding</c> alone,
|
||||
/// always 0 for DAT-built text) regardless of what the DAT actually
|
||||
/// authored.
|
||||
/// </summary>
|
||||
public int MarginLeft, MarginRight, MarginTop, MarginBottom;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a property for a state using retail's DirectState-as-base rule. A
|
||||
/// named state's key overrides DirectState by presence, including false/zero.
|
||||
|
|
@ -421,6 +442,15 @@ public static class ElementReader
|
|||
Outline = derived.Outline || base_.Outline,
|
||||
// OutlineColor: same "non-null derived wins" rule as FontColor.
|
||||
OutlineColor = derived.OutlineColor ?? base_.OutlineColor,
|
||||
// R2-1: margins follow the same "non-zero derived wins" convention as
|
||||
// FontDid/ZLevel above — a derived element that authors no margin
|
||||
// property (0 is ApplyCanonicalLegacyProjection's own unset default,
|
||||
// matching retail's ctor-cleared default too) inherits the base
|
||||
// prototype's margin instead of silently zeroing it out.
|
||||
MarginLeft = derived.MarginLeft != 0 ? derived.MarginLeft : base_.MarginLeft,
|
||||
MarginRight = derived.MarginRight != 0 ? derived.MarginRight : base_.MarginRight,
|
||||
MarginTop = derived.MarginTop != 0 ? derived.MarginTop : base_.MarginTop,
|
||||
MarginBottom = derived.MarginBottom != 0 ? derived.MarginBottom : base_.MarginBottom,
|
||||
// DefaultStateName: derived wins if set; otherwise inherit the base's default.
|
||||
DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName,
|
||||
// This helper merges one element snapshot only. LayoutImporter separately
|
||||
|
|
@ -526,6 +556,20 @@ public static class ElementReader
|
|||
}
|
||||
}
|
||||
|
||||
// R2-1 (Campaign CC gate round 1 Batch E): the four text-inset margins
|
||||
// (0x23 Left / 0x24 Right / 0x25 Up / 0x26 Down, IntegerBaseProperty —
|
||||
// see MarginLeft's own doc comment for the decomp anchor). Absent
|
||||
// properties leave the ElementInfo default of 0, matching retail's
|
||||
// ctor-cleared default.
|
||||
if (info.TryGetEffectiveInteger(0x23u, out int marginLeft))
|
||||
info.MarginLeft = marginLeft;
|
||||
if (info.TryGetEffectiveInteger(0x24u, out int marginRight))
|
||||
info.MarginRight = marginRight;
|
||||
if (info.TryGetEffectiveInteger(0x25u, out int marginTop))
|
||||
info.MarginTop = marginTop;
|
||||
if (info.TryGetEffectiveInteger(0x26u, out int marginBottom))
|
||||
info.MarginBottom = marginBottom;
|
||||
|
||||
// Tab table (0x2E): array of StructBaseProperty (MasterPropertyId 0x2F) — the
|
||||
// Type-8 tab control's authored {button element, page element, isDefault} rows
|
||||
// (docs/research/2026-08-10-options-panel-structure.md §1.3). Recomputed fresh
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue