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>
This commit is contained in:
parent
7d09821fdc
commit
0591b9a026
13 changed files with 1018 additions and 66 deletions
98
src/AcDream.App/UI/Layout/DatRichText.cs
Normal file
98
src/AcDream.App/UI/Layout/DatRichText.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Shared multi-segment rich-text composer for the chargen description
|
||||
/// boxes (Campaign CC gate round 1 Batch C — GF-2/GF-3/GF-11a, and the
|
||||
/// Summary how-to text). Ports retail's
|
||||
/// <c>UIElement_Text::SetStringInfoWithFont</c> /
|
||||
/// <c>AppendStringInfoWithFont @ 0x00469D70</c> composition model: a text
|
||||
/// box is built from an ORDERED list of string segments, each carrying its
|
||||
/// OWN font-color palette index
|
||||
/// (<c>UIElement_Text::AppendStringInfoWithFont</c>'s
|
||||
/// <c>SetFontColorHelper</c> -> <c>InqProperty(0x1B)</c> array lookup —
|
||||
/// see <see cref="AcDream.App.UI.UiText.FontColorPalette"/>).
|
||||
///
|
||||
/// <para>
|
||||
/// The description pages used to bypass this entirely: they assigned a raw
|
||||
/// <c>LinesProvider</c> lambda returning ONE unwrapped <see cref="AcDream.App.UI.UiText.Line"/>
|
||||
/// per composed string, with no escape-normalize and no word-wrap. Two
|
||||
/// concrete symptoms this caused: literal two-character <c>"\n"</c>
|
||||
/// escapes rendered as backslash-n instead of a real line break (the DAT
|
||||
/// stores that literal escape — <c>DatWidgetFactory.BuildText</c>'s own
|
||||
/// authored-string path already normalizes it for single-element authored
|
||||
/// captions; this helper reproduces the SAME normalize for
|
||||
/// runtime-composed multi-segment text), and — for the Town page
|
||||
/// specifically — an unwrapped single line meant the town-specific SUFFIX
|
||||
/// of the composed string rendered far outside the box's clipped viewport,
|
||||
/// so switching towns looked like "the text never changes" even though the
|
||||
/// underlying string genuinely did (only its INVISIBLE tail differed).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class DatRichText
|
||||
{
|
||||
/// <summary>One composed segment: text plus the color it should render
|
||||
/// in. A null or empty <see cref="Text"/> is silently skipped (mirrors
|
||||
/// retail's own null-string-info no-op guards throughout this text
|
||||
/// composition family).</summary>
|
||||
public readonly record struct Segment(string? Text, Vector4 Color);
|
||||
|
||||
/// <summary>
|
||||
/// Escape-normalizes and word-wraps every segment (independently, so
|
||||
/// each segment's wrapped lines keep ITS OWN color), then concatenates
|
||||
/// the results in order. No separator is inserted between segments —
|
||||
/// retail's own composition calls concatenate directly
|
||||
/// (<c>AppendStringInfoWithFont</c>/<c>append_n_chars</c> with no
|
||||
/// interposed literal), so any blank-line spacing between sections
|
||||
/// comes from the authored DAT string content itself, not from code
|
||||
/// here.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<UiText.Line> Compose(
|
||||
UiText target,
|
||||
IReadOnlyList<Segment> segments)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
ArgumentNullException.ThrowIfNull(segments);
|
||||
|
||||
var lines = new List<UiText.Line>();
|
||||
float maximumWidth = MathF.Max(1f, target.Width - 2f * target.Padding);
|
||||
Func<string, float> measure = target.DatFont is { } font
|
||||
? font.MeasureWidth
|
||||
: static value => value.Length * 8f;
|
||||
|
||||
foreach (Segment segment in segments)
|
||||
{
|
||||
if (string.IsNullOrEmpty(segment.Text))
|
||||
continue;
|
||||
|
||||
// The installed DAT stores the LITERAL two-character escape
|
||||
// "\n" (0x5C 0x6E), not a real line break — same normalize
|
||||
// DatWidgetFactory.BuildText's authored-string path already
|
||||
// applies for single-element authored captions.
|
||||
string normalized = segment.Text
|
||||
.Replace("\\n", "\n")
|
||||
.Replace("\r", string.Empty);
|
||||
|
||||
foreach (string wrapped in UiText.WrapWords(normalized, measure, maximumWidth))
|
||||
lines.Add(new UiText.Line(wrapped, segment.Color));
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves <paramref name="target"/>'s own authored font-color
|
||||
/// palette (dat property <c>0x1B</c>) entry at <paramref name="index"/>,
|
||||
/// falling back to <paramref name="fallback"/> when the palette is
|
||||
/// absent or too short. Mirrors the same fallback shape
|
||||
/// <c>CharacterStatController.BuildSelectedTitleRuns</c> already uses
|
||||
/// for its own palette-indexed colors.
|
||||
/// </summary>
|
||||
public static Vector4 PaletteColor(UiText target, int index, Vector4 fallback) =>
|
||||
index >= 0 && index < target.FontColorPalette.Count
|
||||
? target.FontColorPalette[index]
|
||||
: fallback;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue