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
|
|
@ -456,6 +456,88 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(36f, button.LabelOffsetX); // face.X(0) + face.Width(32) + 4
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-4a (Campaign CC gate round 1 Batch C): retail's chargen display
|
||||
/// buttons (live-DAT-measured shape) author their CAPTION directly as
|
||||
/// their own P0x17 AND carry one SEPARATE, media-less Type-12 child for
|
||||
/// the live value. <c>BuildButton</c> surfaces that child through
|
||||
/// <see cref="UiButton.ValueBox"/>/<see cref="UiButton.ValueLabel"/>
|
||||
/// instead of dropping it — coexisting with, not clobbering,
|
||||
/// <see cref="UiButton.Label"/>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildButton_OwnCaptionPlusMediaLessTextChild_SurfacesValueSlotWithoutClobberingLabel()
|
||||
{
|
||||
uint captionStringId = 111u;
|
||||
var info = new ElementInfo { Type = 1, Width = 80, Height = 20 };
|
||||
info.States[UiStateInfo.DirectStateId] = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
info.States[UiStateInfo.DirectStateId].Properties.Values[0x17u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.StringInfo,
|
||||
StringInfoValue = new UiStringInfoValue(0, captionStringId, 0, 0, 0, 0),
|
||||
};
|
||||
// The button carries its own background media (authoredFaces stays
|
||||
// empty — matches the real display buttons, which have their OWN
|
||||
// frame art, not a lifted face-segment child).
|
||||
info.StateMedia[""] = (0x06000001u, 1);
|
||||
|
||||
var valueChild = new ElementInfo { Type = 12, X = 5, Y = 2, Width = 60, Height = 16 };
|
||||
info.Children.Add(valueChild);
|
||||
|
||||
var button = Assert.IsType<UiButton>(DatWidgetFactory.Create(
|
||||
info, NoTex, null,
|
||||
stringResolve: value => value.StringId == captionStringId ? "Attribute Credits" : null));
|
||||
|
||||
Assert.Equal("Attribute Credits", button.Label);
|
||||
Assert.Null(button.ValueLabel); // nothing authored on the child itself
|
||||
Assert.Equal((5f, 2f, 60f, 16f), button.ValueBox);
|
||||
|
||||
// Writing the live value (as CharacterCreationProfessionPage.SetDisplay
|
||||
// does) must not touch the caption — the whole point of this fix.
|
||||
button.ValueLabel = "42";
|
||||
Assert.Equal("Attribute Credits", button.Label);
|
||||
Assert.Equal("42", button.ValueLabel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Negative companion: a button whose caption was LIFTED from a
|
||||
/// distinct Type-12 child (the town-marker shape,
|
||||
/// <c>!ReferenceEquals(labelInfo, info)</c>) must NOT pick up a
|
||||
/// ValueBox even if the button happens to have another Type-12 child —
|
||||
/// the gate is <c>ReferenceEquals(labelInfo, info)</c>, own-caption
|
||||
/// only.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildButton_LiftedCaption_NeverSurfacesValueSlot()
|
||||
{
|
||||
uint stringId = 222u;
|
||||
var info = new ElementInfo { Type = 1, Width = 106, Height = 80 };
|
||||
info.States[1u] = new UiStateInfo { Id = 1u, Name = "Normal" };
|
||||
info.States[6u] = new UiStateInfo { Id = 6u, Name = "Highlight" };
|
||||
|
||||
var caption = new ElementInfo { Type = 12, X = 0, Y = 4, Width = 100, Height = 37 };
|
||||
caption.States[UiStateInfo.DirectStateId] = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
caption.States[UiStateInfo.DirectStateId].Properties.Values[0x17u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.StringInfo,
|
||||
StringInfoValue = new UiStringInfoValue(0, stringId, 0, 0, 0, 0),
|
||||
};
|
||||
info.Children.Add(caption);
|
||||
|
||||
var marker = new ElementInfo { Type = 3, X = 36, Y = 36, Width = 38, Height = 38 };
|
||||
marker.StateMedia["Normal"] = (0x06004D60u, 1);
|
||||
marker.StateMedia["Highlight"] = (0x06004D61u, 1);
|
||||
info.Children.Add(marker);
|
||||
|
||||
var button = Assert.IsType<UiButton>(DatWidgetFactory.Create(
|
||||
info, NoTex, null,
|
||||
stringResolve: value => value.StringId == stringId ? "Holtburg" : null));
|
||||
|
||||
Assert.Equal("Holtburg", button.Label);
|
||||
Assert.Null(button.ValueBox);
|
||||
Assert.Null(button.ValueLabel);
|
||||
}
|
||||
|
||||
// ── Test 5b: Type 11 → UiScrollbar ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue