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
|
|
@ -1,4 +1,3 @@
|
|||
using System.Globalization;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.CharGen;
|
||||
using AcDream.Runtime;
|
||||
|
|
@ -175,6 +174,7 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
private readonly UiButton? _rotateCounterClockwise;
|
||||
private readonly UiButton? _zoomIn;
|
||||
private readonly UiButton? _zoomOut;
|
||||
private readonly UiElement? _gradCircle;
|
||||
|
||||
private Choice _currentChoice = Choice.Face;
|
||||
private Part _currentPart = Part.Hair;
|
||||
|
|
@ -242,6 +242,8 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
if (_shadeScroll is not null)
|
||||
_shadeScroll.ScalarChanged = SetShadeFromScalar;
|
||||
|
||||
_gradCircle = Find<UiElement>(pageRoot, GradCircleId);
|
||||
|
||||
Viewport = Find<UiViewport>(pageRoot, ViewportId);
|
||||
|
||||
_rotateClockwise = Find<UiButton>(pageRoot, RotateClockwiseId);
|
||||
|
|
@ -327,8 +329,8 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
}
|
||||
ApplyChoiceVisibility();
|
||||
|
||||
if (TryGetGender(view, snapshot, out ChargenGenderOptions? gender))
|
||||
RefreshSpins(gender, snapshot.Appearance);
|
||||
// GF-6: heritage-flavored, index-independent — no gender needed.
|
||||
RefreshSpinCaptions(snapshot.HeritageId);
|
||||
|
||||
RefreshColorAndShadeControls(view, snapshot);
|
||||
RebuildPreview(view, snapshot);
|
||||
|
|
@ -677,6 +679,37 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
overlay.Visible = colorSlot is not null && currentColor == (uint)i;
|
||||
}
|
||||
|
||||
// AP-216 (Campaign CC gate round 1 Batch C, PARTIAL): retail's
|
||||
// DoColorSpots @0x0047d850 blits ACTUAL-color art for each valid
|
||||
// swatch and BLANK art for any swatch beyond the current part's
|
||||
// real color count. Painting each swatch with its own represented
|
||||
// color needs a PalSet/Palette-id -> RGB resolution pipeline this
|
||||
// batch does not add (no chargen page currently reads DAT palette
|
||||
// pixels at runtime) — register AP-216 stays open for that half.
|
||||
// This ships the cheap, fully-evidenced half: hiding a swatch a
|
||||
// part's color list doesn't actually have (closest faithful
|
||||
// rendering the existing pipeline supports — Visible=false is the
|
||||
// acdream equivalent of "blit nothing").
|
||||
int colorCount = colorSlot is not null
|
||||
&& TryGetGender(view, snapshot, out ChargenGenderOptions? swatchGender)
|
||||
? ColorCount(_currentPart, swatchGender)
|
||||
: 0;
|
||||
for (int i = 0; i < _swatches.Length; i++)
|
||||
{
|
||||
if (_swatches[i] is { } swatch)
|
||||
swatch.Visible = colorSlot is not null && i < colorCount;
|
||||
}
|
||||
|
||||
// AP-217 (PARTIAL): gmCGAppearancePage::DoGradDisk @0x0047da90
|
||||
// blits the blank "grad plug" for Eyes (DoGradDisk(this, 1),
|
||||
// called from SetSelection @0x0047e85d) and a gradient graphic
|
||||
// TINTED with the current part's color otherwise — the tinted
|
||||
// repaint needs the same palette-to-RGB pipeline AP-216's open
|
||||
// half needs, so it stays open too. This ships the evidenced
|
||||
// Eyes-blank half only.
|
||||
if (_gradCircle is not null)
|
||||
_gradCircle.Visible = _currentPart != Part.Eyes;
|
||||
|
||||
ChargenShadeSlot? shadeSlot = ShadeSlotFor(_currentPart);
|
||||
if (_shadeScroll is null)
|
||||
return;
|
||||
|
|
@ -694,36 +727,57 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// ── Spin labels ──────────────────────────────────────────────────
|
||||
// ── Spin captions ────────────────────────────────────────────────
|
||||
|
||||
private void RefreshSpins(ChargenGenderOptions gender, RuntimeCharacterCreationAppearance a)
|
||||
/// <summary>
|
||||
/// GF-6/AP-218 (Campaign CC gate round 1 Batch C):
|
||||
/// <c>gmCGAppearancePage::Update @ 0x0047e8f0</c> writes the Hair/Eyes/
|
||||
/// Skin spins' caption to a heritage-flavored STATIC string via
|
||||
/// <c>UIElement_Text::SetStringInfoWithFont</c> — never an index or a
|
||||
/// style name. Normal heritage: <c>ID_CharGen_HairStyle</c>/
|
||||
/// <c>_Eyes</c>/<c>_Skin</c> (@0x0047ebad/0x0047ebe3/0x0047ec6a).
|
||||
/// Gearknight (heritage 6): <c>ID_CharGen_GearText_HairButton</c>/
|
||||
/// <c>_EyesButton</c>/<c>_SkinButton</c> (@0x0047e9ef/0x0047ea25/
|
||||
/// 0x0047eaa9). Olthoi/OlthoiAcid (heritage 0xc/0xd):
|
||||
/// <c>ID_CharGen_OlthoiText_HairButton</c>/<c>_EyesButton</c>/
|
||||
/// <c>_SkinButton</c> (@0x0047ed5b/0x0047ed91/0x0047ee15). The other
|
||||
/// six spins (Nose/Mouth/Headgear/Shirt/Trousers/Footwear) are NEVER
|
||||
/// touched by <c>Update</c> — their DAT-authored static caption
|
||||
/// (already resolved at build time by
|
||||
/// <c>DatWidgetFactory.BuildButton</c>'s own P0x17 lift) is left
|
||||
/// alone. Retail shows NO per-style index or name anywhere on this
|
||||
/// page — the live 3D preview is the player's only feedback for which
|
||||
/// style/gear is currently selected; acdream's own prior "1-based
|
||||
/// ordinal"/gear-name substitution here was never a retail behavior
|
||||
/// (register AP-218, retired by this fix; AP-215's own icon-thumbnail
|
||||
/// item stays open — a DIFFERENT gap, see that row's own text).
|
||||
/// </summary>
|
||||
private void RefreshSpinCaptions(uint heritageId)
|
||||
{
|
||||
SetStyleSpinLabel(Part.Hair, gender.HairStyles.Count, a.HairStyle);
|
||||
SetStyleSpinLabel(Part.Eyes, gender.EyeStrips.Count, a.EyesStrip);
|
||||
SetStyleSpinLabel(Part.Nose, gender.NoseStrips.Count, a.NoseStrip);
|
||||
SetStyleSpinLabel(Part.Mouth, gender.MouthStrips.Count, a.MouthStrip);
|
||||
SetGearSpinLabel(Part.Headgear, gender.Headgears, a.HeadgearStyle);
|
||||
SetGearSpinLabel(Part.Shirt, gender.Shirts, a.ShirtStyle);
|
||||
SetGearSpinLabel(Part.Trousers, gender.Pants, a.TrousersStyle);
|
||||
SetGearSpinLabel(Part.Footwear, gender.Footwear, a.FootwearStyle);
|
||||
(string hairKey, string eyesKey, string skinKey) = heritageId switch
|
||||
{
|
||||
(uint)ChargenHeritageGroup.Gearknight => (
|
||||
"ID_CharGen_GearText_HairButton",
|
||||
"ID_CharGen_GearText_EyesButton",
|
||||
"ID_CharGen_GearText_SkinButton"),
|
||||
(uint)ChargenHeritageGroup.Olthoi or (uint)ChargenHeritageGroup.OlthoiAcid => (
|
||||
"ID_CharGen_OlthoiText_HairButton",
|
||||
"ID_CharGen_OlthoiText_EyesButton",
|
||||
"ID_CharGen_OlthoiText_SkinButton"),
|
||||
_ => ("ID_CharGen_HairStyle", "ID_CharGen_Eyes", "ID_CharGen_Skin"),
|
||||
};
|
||||
|
||||
SetSpinCaption(Part.Hair, hairKey);
|
||||
SetSpinCaption(Part.Eyes, eyesKey);
|
||||
SetSpinCaption(Part.Skin, skinKey);
|
||||
}
|
||||
|
||||
private void SetStyleSpinLabel(Part part, int count, uint index)
|
||||
private void SetSpinCaption(Part part, string key)
|
||||
{
|
||||
if (!_spins.TryGetValue(part, out UiButton? spin))
|
||||
return;
|
||||
spin.Label = index != Unset && index < (uint)count
|
||||
? (index + 1).ToString(CultureInfo.InvariantCulture)
|
||||
: "-";
|
||||
}
|
||||
|
||||
private void SetGearSpinLabel(Part part, IReadOnlyList<ChargenGearOption> options, uint index)
|
||||
{
|
||||
if (!_spins.TryGetValue(part, out UiButton? spin))
|
||||
return;
|
||||
spin.Label = index != Unset && index < (uint)options.Count
|
||||
? options[(int)index].Name
|
||||
: "None";
|
||||
if (_bindings.ResolveText?.Invoke(key) is { } text)
|
||||
spin.Label = text;
|
||||
}
|
||||
|
||||
// ── Preview rebuild ──────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue