fix(chargen): Campaign CC gate round 1 Batch B — authored selection states, label state, zoom/swatch feedback
GF-1/GF-8: UiButton now recognizes retail's custom Unselected/Selected radio-pair (0x10000016/0x10000017), bypassing the standard Normal/ Highlight machine that never admitted those state names — .Selected now lights the heritage/template/gender/Face-Clothes rows it was always a no-op for. AP-222/GF-11b: per-state label color/outline (dat 0x1B/0x21) now applies off the REQUESTED retail state id, not the art-gated committed ActiveState — resolves the Appearance spins' current-part highlight (text recolors even though no Highlight art exists on either client) and the Town caption's Normal-to-white swap. GF-11c: UiButton.LabelBox lets a lifted caption with its own authored rect draw there instead of the face-relative offset that's only correct when the label is authored directly on the button (heritage/template family, unchanged). GF-9: wires the real nine companion overlay elements (SetColor's SetVisible mechanism) that swatch clicks were always meant to drive, retiring AP-215 item 1 (the swatch.Selected substitution was a permanent no-op — swatches author no Highlight media at all). GF-10: zoom buttons now set the retail-mirrored mutual-exclusive Highlight/Normal pair on click; InitializePage carries no initial SetState for either button, so both stay at "Normal" until first click. Register: AP-222 retired (mechanism identified and ported), AP-215 narrowed (item 1 retired, item 2 unrelated and unchanged), row count recount corrected 164 (was already one high before this batch). App suite 5282/3 (was 5266/3), Runtime 1735/0 unchanged. Fixture + live- DAT tests only — no graphical client launch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1d9de5e095
commit
7d09821fdc
11 changed files with 1043 additions and 42 deletions
|
|
@ -24,6 +24,22 @@ public static class RetailUiStateIds
|
|||
public const uint LockedUi = 0x10000063u;
|
||||
public const uint UnlockedUi = 0x10000064u;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC gate round 1 Batch B (GF-1/GF-8): retail's custom
|
||||
/// radio-selection state pair, live-DAT-probe-confirmed on the Heritage
|
||||
/// row (<c>0x100003BF</c>), Profession template (<c>0x100003D9</c>),
|
||||
/// Appearance Face/Clothes sub-tabs (<c>0x100003A9</c>/<c>0x100003AA</c>),
|
||||
/// and gender buttons (<c>0x100003A7</c>/<c>0x100003A8</c>). Named
|
||||
/// <c>UiStateInfo.Name</c> strings, not media file ids — the buttons
|
||||
/// author their state DESCRIPTORS under these two ids, with the actual
|
||||
/// per-state art living either directly on the button (gender) or on a
|
||||
/// single stateful face-segment child (heritage/template/sub-tabs).
|
||||
/// See <see cref="AcDream.App.UI.UiButton"/>'s custom-selection-pair
|
||||
/// bypass in <c>UpdateVisualState</c>.
|
||||
/// </summary>
|
||||
public const uint Unselected = 0x10000016u;
|
||||
public const uint Selected = 0x10000017u;
|
||||
|
||||
public static string StateName(uint stateId)
|
||||
=> stateId switch
|
||||
{
|
||||
|
|
@ -38,6 +54,8 @@ public static class RetailUiStateIds
|
|||
Minimized => "Minimized",
|
||||
LockedUi => "LockedUI",
|
||||
UnlockedUi => "UnlockedUI",
|
||||
Unselected => "Unselected",
|
||||
Selected => "Selected",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
|
|
@ -56,6 +74,8 @@ public static class RetailUiStateIds
|
|||
"Minimized" => Minimized,
|
||||
"LockedUI" => LockedUi,
|
||||
"UnlockedUI" => UnlockedUi,
|
||||
"Unselected" => Unselected,
|
||||
"Selected" => Selected,
|
||||
_ => 0u,
|
||||
};
|
||||
return stateId != 0;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,31 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
0x10000314u, 0x10000315u, 0x10000316u, 0x10000317u,
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// GF-9 (Campaign CC gate round 1 Batch B): the nine Type-3 companion
|
||||
/// "selected" overlay elements, one per <see cref="SwatchIds"/> entry at
|
||||
/// the SAME index — retail <c>gmCGAppearancePage::InitializePage</c>'s
|
||||
/// own id-pair table (<c>@0x004800ff-00480164</c>, the switch that fills
|
||||
/// <c>m_tColorWheel[i][0x10]</c>/<c>[0x14]</c>-ish offsets with the
|
||||
/// swatch/overlay id pair per index) resolves the SAME nine ids this
|
||||
/// array carries, in the SAME index order. <c>SetColor</c> (case
|
||||
/// <c>0x0047DD50</c>, <c>iCurColor</c> assignment) is retail's actual
|
||||
/// click-feedback mechanism — <c>SetVisible</c> on the overlay at
|
||||
/// <c>iCurColor</c>'s index, NOT a state swap on the swatch itself (the
|
||||
/// swatch buttons author only an unnamed DirectState sprite; measured
|
||||
/// against the installed dat, they have NO Normal/Highlight media at
|
||||
/// all, so <see cref="UiButton.Selected"/> was always a complete no-op
|
||||
/// here — see <see cref="RefreshColorAndShadeControls"/>). Live-DAT-
|
||||
/// probe-confirmed siblings of the swatches under the same color-wheel
|
||||
/// container (<c>0x100003B9</c>), each roughly centered on its paired
|
||||
/// swatch's own rect.
|
||||
/// </summary>
|
||||
internal static readonly uint[] SwatchOverlayIds =
|
||||
[
|
||||
0x10000318u, 0x10000319u, 0x1000031Au, 0x1000031Bu, 0x1000031Cu,
|
||||
0x1000031Du, 0x1000031Eu, 0x1000031Fu, 0x10000320u,
|
||||
];
|
||||
|
||||
/// <summary>Live-DAT-measured arrow geometry, uniform across all nine
|
||||
/// spins (every one is 200px wide): decrement child at local
|
||||
/// x=[80,127), increment child at x=[127,174). Anything outside both
|
||||
|
|
@ -144,6 +169,7 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
private readonly UiElement? _clothesChoices;
|
||||
private readonly Dictionary<Part, UiButton> _spins = [];
|
||||
private readonly UiButton?[] _swatches = new UiButton?[SwatchIds.Length];
|
||||
private readonly UiElement?[] _swatchOverlays = new UiElement?[SwatchOverlayIds.Length];
|
||||
private readonly UiScrollbar? _shadeScroll;
|
||||
private readonly UiButton? _rotateClockwise;
|
||||
private readonly UiButton? _rotateCounterClockwise;
|
||||
|
|
@ -209,6 +235,9 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
_swatches[i] = swatch;
|
||||
}
|
||||
|
||||
for (int i = 0; i < SwatchOverlayIds.Length; i++)
|
||||
_swatchOverlays[i] = Find<UiElement>(pageRoot, SwatchOverlayIds[i]);
|
||||
|
||||
_shadeScroll = Find<UiScrollbar>(pageRoot, ShadeScrollId);
|
||||
if (_shadeScroll is not null)
|
||||
_shadeScroll.ScalarChanged = SetShadeFromScalar;
|
||||
|
|
@ -223,10 +252,33 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
_rotateCounterClockwise.OnClick = () => PreviewControl?.RotateCounterClockwise();
|
||||
_zoomIn = Find<UiButton>(pageRoot, ZoomInId);
|
||||
if (_zoomIn is not null)
|
||||
_zoomIn.OnClick = () => PreviewControl?.ZoomIn();
|
||||
_zoomIn.OnClick = () =>
|
||||
{
|
||||
PreviewControl?.ZoomIn();
|
||||
// GF-10: gmCGAppearancePage::ZoomIn @0x0047CF00
|
||||
// (@0x0047d005/0x0047d00f) ends ZoomInButton->SetState(6)
|
||||
// (Highlight), ZoomOutButton->SetState(1) (Normal) — a
|
||||
// mutual-exclusive pair. Re-derived from InitializePage
|
||||
// @0x0047fdd0-0048032e (m_bZoomedIn = 0 at construction,
|
||||
// @0x004802c3): NO explicit initial SetState call exists
|
||||
// for either button, so both start at their DAT-authored
|
||||
// "Normal" default (live-DAT-probe-confirmed) until the
|
||||
// first real zoom click — this port does not force an
|
||||
// initial Highlight.
|
||||
_zoomIn.TrySetRetailState(UiButtonStateMachine.Highlight);
|
||||
_zoomOut?.TrySetRetailState(UiButtonStateMachine.Normal);
|
||||
};
|
||||
_zoomOut = Find<UiButton>(pageRoot, ZoomOutId);
|
||||
if (_zoomOut is not null)
|
||||
_zoomOut.OnClick = () => PreviewControl?.ZoomOut();
|
||||
_zoomOut.OnClick = () =>
|
||||
{
|
||||
PreviewControl?.ZoomOut();
|
||||
// GF-10: gmCGAppearancePage::ZoomOut @0x0047D050
|
||||
// (@0x0047d140/0x0047d14a) mirrors ZoomIn — ZoomOutButton
|
||||
// -> Highlight(6), ZoomInButton -> Normal(1).
|
||||
_zoomOut.TrySetRetailState(UiButtonStateMachine.Highlight);
|
||||
_zoomIn?.TrySetRetailState(UiButtonStateMachine.Normal);
|
||||
};
|
||||
|
||||
ApplyChoiceVisibility();
|
||||
}
|
||||
|
|
@ -608,12 +660,21 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
: UiButtonStateMachine.Normal);
|
||||
}
|
||||
|
||||
// GF-9 (Campaign CC gate round 1 Batch B): retail's ACTUAL swatch
|
||||
// click feedback is the companion overlay's visibility (SetColor
|
||||
// @0x0047DD50 -> m_tColorWheel[...][0x10][iCurColor*7]->SetVisible),
|
||||
// not a state swap on the swatch button — measured against the
|
||||
// installed dat, the nine swatches author only a DirectState sprite
|
||||
// with no Normal/Highlight media at all, so a prior
|
||||
// swatch.Selected assignment here was a permanent no-op (see
|
||||
// SwatchOverlayIds' own doc comment). Exactly one overlay is
|
||||
// visible: the one at the current part's own selected color index.
|
||||
ChargenAppearanceSlot? colorSlot = ColorSlotFor(_currentPart);
|
||||
uint currentColor = colorSlot is null ? Unset : ColorCurrent(_currentPart, snapshot.Appearance);
|
||||
for (int i = 0; i < _swatches.Length; i++)
|
||||
for (int i = 0; i < _swatchOverlays.Length; i++)
|
||||
{
|
||||
if (_swatches[i] is { } swatch)
|
||||
swatch.Selected = colorSlot is not null && currentColor == (uint)i;
|
||||
if (_swatchOverlays[i] is { } overlay)
|
||||
overlay.Visible = colorSlot is not null && currentColor == (uint)i;
|
||||
}
|
||||
|
||||
ChargenShadeSlot? shadeSlot = ShadeSlotFor(_currentPart);
|
||||
|
|
|
|||
|
|
@ -878,8 +878,31 @@ public static class DatWidgetFactory
|
|||
button.FaceTop = face.Y;
|
||||
button.FaceWidth = face.Width;
|
||||
button.FaceHeight = face.Height;
|
||||
button.LabelAlign = UiButton.LabelAlignment.Left;
|
||||
button.LabelOffsetX = face.X + face.Width + 4f;
|
||||
|
||||
if (!ReferenceEquals(labelInfo, info))
|
||||
{
|
||||
// GF-11c (Campaign CC gate round 1 Batch B): a DISTINCT
|
||||
// Type-12 caption was lifted (e.g. the Town page's per-
|
||||
// marker name label, 0x10000409 under each town button —
|
||||
// live-DAT-probe-confirmed authored rect + Center justify,
|
||||
// independent of the marker face's own geometry) — honor
|
||||
// ITS OWN authored rect/justify instead of the face-
|
||||
// relative offset below, which is only correct when the
|
||||
// label text is authored DIRECTLY on the button itself,
|
||||
// immediately beside a single-purpose face segment (the
|
||||
// heritage/template/Face-Clothes sub-tab row family —
|
||||
// still handled by the else-branch two lines down, since
|
||||
// ReferenceEquals(labelInfo, info) is true there).
|
||||
button.LabelBox = (labelInfo.X, labelInfo.Y, labelInfo.Width, labelInfo.Height);
|
||||
button.LabelAlign = labelInfo.HJustify == HJustify.Left
|
||||
? UiButton.LabelAlignment.Left
|
||||
: UiButton.LabelAlignment.Center;
|
||||
}
|
||||
else
|
||||
{
|
||||
button.LabelAlign = UiButton.LabelAlignment.Left;
|
||||
button.LabelOffsetX = face.X + face.Width + 4f;
|
||||
}
|
||||
}
|
||||
else if (labelInfo.HJustify == HJustify.Left)
|
||||
{
|
||||
|
|
@ -901,6 +924,15 @@ public static class DatWidgetFactory
|
|||
button.LabelOffsetX = labelInfo.X;
|
||||
}
|
||||
|
||||
// AP-222 / GF-11b (Campaign CC gate round 1 Batch B): per-state label
|
||||
// color/outline (dat properties 0x1B/0x21 authored PER STATE on the
|
||||
// label-bearing element — the Appearance spins' own states, or the
|
||||
// Town caption child's states) — additive, only non-null when the
|
||||
// authored dat genuinely carries more than one distinct value.
|
||||
button.SetPerStateLabelStyle(
|
||||
ElementReader.BuildPerStateColorMap(labelInfo, 0x1Bu),
|
||||
ElementReader.BuildPerStateBoolMap(labelInfo, 0x21u));
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
||||
|
|
@ -673,4 +674,65 @@ public static class ElementReader
|
|||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AP-222 / GF-11b (Campaign CC gate round 1 Batch B): resolves a color
|
||||
/// property (0x1B FontColor's Array-tolerant shape, same unwrap as
|
||||
/// <see cref="ReadEffectiveColorPalette"/>) for EVERY state <paramref
|
||||
/// name="info"/> itself authors, keyed by retail numeric state id.
|
||||
/// Returns null unless at least two states resolve to GENUINELY
|
||||
/// DIFFERENT colors — the overwhelming majority of elements author one
|
||||
/// color for every state (or none at all), and for those this returns
|
||||
/// null so the caller keeps its existing single-default-color behavior
|
||||
/// untouched. Only elements that really do recolor per state (the
|
||||
/// Appearance spins' Highlight brightening, the Town buttons' Normal-
|
||||
/// to-white caption swap) get a non-null map.
|
||||
/// </summary>
|
||||
internal static IReadOnlyDictionary<uint, Vector4>? BuildPerStateColorMap(
|
||||
ElementInfo info, uint propertyId)
|
||||
{
|
||||
Dictionary<uint, Vector4>? map = null;
|
||||
foreach (uint stateId in info.States.Keys)
|
||||
{
|
||||
if (!info.TryGetEffectiveProperty(propertyId, out UiPropertyValue value, stateId))
|
||||
continue;
|
||||
|
||||
UiPropertyValue? colorValue = value.Kind == UiPropertyKind.Color
|
||||
? value
|
||||
: value.Kind == UiPropertyKind.Array
|
||||
&& value.ArrayValue.Count > 0
|
||||
&& value.ArrayValue[0].Kind == UiPropertyKind.Color
|
||||
? value.ArrayValue[0]
|
||||
: null;
|
||||
if (colorValue is null)
|
||||
continue;
|
||||
|
||||
UiColorValue c = colorValue.ColorValue;
|
||||
float alpha = c.Alpha == 0 ? 1f : c.Alpha / 255f;
|
||||
(map ??= new Dictionary<uint, Vector4>())[stateId] =
|
||||
new Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, alpha);
|
||||
}
|
||||
|
||||
return map is { Count: > 1 } && map.Values.Distinct().Count() > 1 ? map : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AP-222 counterpart of <see cref="BuildPerStateColorMap"/> for a bool
|
||||
/// property (0x21 Outline) — same "null unless genuinely per-state"
|
||||
/// gating.
|
||||
/// </summary>
|
||||
internal static IReadOnlyDictionary<uint, bool>? BuildPerStateBoolMap(
|
||||
ElementInfo info, uint propertyId)
|
||||
{
|
||||
Dictionary<uint, bool>? map = null;
|
||||
foreach (uint stateId in info.States.Keys)
|
||||
{
|
||||
if (!info.TryGetEffectiveProperty(propertyId, out UiPropertyValue value, stateId)
|
||||
|| value.Kind != UiPropertyKind.Bool)
|
||||
continue;
|
||||
(map ??= new Dictionary<uint, bool>())[stateId] = value.BoolValue;
|
||||
}
|
||||
|
||||
return map is { Count: > 1 } && map.Values.Distinct().Count() > 1 ? map : null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,9 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
private readonly FaceSegment[] _faceSegments;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
||||
private readonly HashSet<uint> _availableStates = new();
|
||||
private readonly bool _hasCustomSelectionPair;
|
||||
private IReadOnlyDictionary<uint, Vector4>? _stateLabelColors;
|
||||
private IReadOnlyDictionary<uint, bool>? _stateLabelOutlines;
|
||||
private bool _pressed;
|
||||
private bool _pointerOver;
|
||||
private bool _selected;
|
||||
|
|
@ -157,6 +160,25 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
/// Left for the paperdoll "Slots" caption that sits at the left edge, before the slots.</summary>
|
||||
public LabelAlignment LabelAlign { get; set; } = LabelAlignment.Center;
|
||||
|
||||
/// <summary>
|
||||
/// GF-11c (Campaign CC gate round 1 Batch B): optional authored label
|
||||
/// rectangle, LOCAL to this button. When a caption is LIFTED from a
|
||||
/// DISTINCT Type-12 child that carries its own independent rect (e.g.
|
||||
/// the Town page's per-marker name label, positioned below/beside its
|
||||
/// marker rather than immediately right of it), <see cref="OnDraw"/>
|
||||
/// draws the label within THIS box using its own authored geometry
|
||||
/// instead of the FaceLeft-derived offset / full-button-width centering
|
||||
/// the ordinary case uses (label authored directly on the button, right
|
||||
/// beside a single-purpose face segment — the heritage/template/Face-
|
||||
/// Clothes row family, where the current face-relative math is already
|
||||
/// correct). Null (default, every pre-existing button) preserves the
|
||||
/// EXACT prior draw math — <see cref="LabelAlignment.Left"/> still adds
|
||||
/// <see cref="LabelOffsetX"/> to the button's own local origin, and
|
||||
/// <see cref="LabelAlignment.Center"/> still centers within the whole
|
||||
/// button width/height.
|
||||
/// </summary>
|
||||
public (float X, float Y, float Width, float Height)? LabelBox { get; set; }
|
||||
|
||||
/// <summary>Label horizontal alignment options.</summary>
|
||||
public enum LabelAlignment { Center, Left }
|
||||
|
||||
|
|
@ -330,6 +352,21 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
foreach (FaceSegment segment in _faceSegments)
|
||||
AddAvailableStates(segment.Info);
|
||||
|
||||
// Campaign CC gate round 1 Batch B (GF-1/GF-8): retail's custom
|
||||
// "Unselected"/"Selected" radio-selection state pair
|
||||
// (RetailUiStateIds.Unselected/Selected, 0x10000016/0x10000017) is
|
||||
// authored as STATE DESCRIPTORS whose names UiButtonStateMachine's
|
||||
// Normal/Highlight machine doesn't recognize — the standard
|
||||
// AddAvailableStates loop above never admits them, so the ordinary
|
||||
// RequestedState()-driven UpdateVisualState can never select them
|
||||
// (measured: Selected=true committed nothing against the installed
|
||||
// dat before this fix). HasStateMedia already checks the same media
|
||||
// presence (face-segment child OR the button's own StateMedia) used
|
||||
// everywhere else in this class, so this reuses that exact
|
||||
// detection rather than adding a new one.
|
||||
_hasCustomSelectionPair = HasStateMedia(RetailUiStateIds.StateName(RetailUiStateIds.Unselected))
|
||||
&& HasStateMedia(RetailUiStateIds.StateName(RetailUiStateIds.Selected));
|
||||
|
||||
ToggleBehavior = info.TryGetEffectiveBool(0x0Bu, out bool toggle) && toggle;
|
||||
RolloverEnabled = info.TryGetEffectiveBool(0x13u, out bool rollover) && rollover;
|
||||
HotClickEnabled = info.TryGetEffectiveBool(0x0Fu, out bool hotClick) && hotClick;
|
||||
|
|
@ -402,10 +439,17 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
|
||||
if (Label is { Length: > 0 } label && LabelFont is { } lf)
|
||||
{
|
||||
// GF-11c: LabelBox null (every pre-existing button) reduces boxX/
|
||||
// boxY to 0 and boxWidth/boxHeight to the button's own Width/
|
||||
// Height — byte-identical to the prior unconditional math.
|
||||
float boxX = LabelBox?.X ?? 0f;
|
||||
float boxY = LabelBox?.Y ?? 0f;
|
||||
float boxWidth = LabelBox?.Width ?? Width;
|
||||
float boxHeight = LabelBox?.Height ?? Height;
|
||||
float tx = LabelAlign == LabelAlignment.Left
|
||||
? LabelOffsetX
|
||||
: (Width - lf.MeasureWidth(label)) * 0.5f; // centered (default)
|
||||
float ty = (Height - lf.LineHeight) * 0.5f;
|
||||
? boxX + LabelOffsetX
|
||||
: boxX + (boxWidth - lf.MeasureWidth(label)) * 0.5f; // centered (default)
|
||||
float ty = boxY + (boxHeight - lf.LineHeight) * 0.5f;
|
||||
ctx.DrawStringDat(lf, label, tx, ty, LabelColor, Outline, OutlineColor);
|
||||
}
|
||||
|
||||
|
|
@ -638,13 +682,77 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
|
||||
private void UpdateVisualState()
|
||||
{
|
||||
uint requested = UiButtonStateMachine.RequestedState(new UiButtonVisualInput(
|
||||
Disabled: !Enabled,
|
||||
Selected: _selected,
|
||||
RolloverEnabled: RolloverEnabled,
|
||||
Pressed: _pressed,
|
||||
PointerOver: _pointerOver));
|
||||
if (_availableStates.Contains(requested))
|
||||
uint requested = ComputeRequestedStateId();
|
||||
if (_hasCustomSelectionPair)
|
||||
{
|
||||
// gmCGHeritagePage::Update @0x00483219-0x0048372D (and the
|
||||
// mirrored template/sub-tab/gender call sites): retail sets
|
||||
// this pair directly by SELECTION, not through the ordinary
|
||||
// Normal/Highlight/rollover/pressed machine — these buttons
|
||||
// never author rollover or pressed media for the pair, so
|
||||
// there is nothing faithful to compute beyond selected-or-not.
|
||||
ActiveState = RetailUiStateIds.StateName(requested);
|
||||
}
|
||||
else if (_availableStates.Contains(requested))
|
||||
{
|
||||
ActiveState = UiButtonStateMachine.StateName(requested);
|
||||
}
|
||||
|
||||
// AP-222: apply the per-state label style off the REQUESTED id, not
|
||||
// the (possibly art-gated) committed ActiveState — retail's own
|
||||
// SetState(6) commits the state's PROPERTIES (including text color)
|
||||
// unconditionally; only the SPRITE draw silently no-ops when a
|
||||
// state has no media (this class's own #382 comment on
|
||||
// TrySetRetailState documents the same distinction). The
|
||||
// Appearance spins' current-part highlight is exactly this case:
|
||||
// _availableStates never contains Highlight (their arrow face
|
||||
// segments carry no Highlight art), so ActiveState stays "Normal"
|
||||
// forever, but the spin's OWN label color must still swap.
|
||||
ApplyPerStateLabelStyle(requested);
|
||||
}
|
||||
|
||||
private uint ComputeRequestedStateId()
|
||||
=> _hasCustomSelectionPair
|
||||
? (_selected ? RetailUiStateIds.Selected : RetailUiStateIds.Unselected)
|
||||
: UiButtonStateMachine.RequestedState(new UiButtonVisualInput(
|
||||
Disabled: !Enabled,
|
||||
Selected: _selected,
|
||||
RolloverEnabled: RolloverEnabled,
|
||||
Pressed: _pressed,
|
||||
PointerOver: _pointerOver));
|
||||
|
||||
/// <summary>
|
||||
/// AP-222 / GF-11b (Campaign CC gate round 1 Batch B): optional per-
|
||||
/// RETAIL-STATE label color/outline override, additive over the single
|
||||
/// default <see cref="LabelColor"/>/<see cref="Outline"/> lifted once at
|
||||
/// construction. Set by <see cref="Layout.DatWidgetFactory"/> ONLY when
|
||||
/// the authored dat genuinely carries more than one distinct value
|
||||
/// across this button's (or its lifted caption child's) own states —
|
||||
/// e.g. the Appearance spins' Highlight-state gold brightening
|
||||
/// (dat properties <c>0x1B</c>/<c>0x21</c>, live-DAT-measured
|
||||
/// 218,167,85 -> 255,221,131 plus outline off -> on) or the Town
|
||||
/// buttons' Normal-to-white caption swap (218,167,85 -> 255,255,255).
|
||||
/// A button with a single authored color (the overwhelming majority)
|
||||
/// never calls this, so <see cref="LabelColor"/>/<see cref="Outline"/>
|
||||
/// keep behaving exactly as before — including every existing external
|
||||
/// post-construction assignment (e.g. <c>ChatWindowController</c>'s Send
|
||||
/// caption, <c>PaperdollController</c>'s Slots label), none of which
|
||||
/// author a second distinct per-state color.
|
||||
/// </summary>
|
||||
internal void SetPerStateLabelStyle(
|
||||
IReadOnlyDictionary<uint, Vector4>? colors,
|
||||
IReadOnlyDictionary<uint, bool>? outlines)
|
||||
{
|
||||
_stateLabelColors = colors;
|
||||
_stateLabelOutlines = outlines;
|
||||
ApplyPerStateLabelStyle(ComputeRequestedStateId());
|
||||
}
|
||||
|
||||
private void ApplyPerStateLabelStyle(uint requestedStateId)
|
||||
{
|
||||
if (_stateLabelColors is { } colors && colors.TryGetValue(requestedStateId, out Vector4 color))
|
||||
LabelColor = color;
|
||||
if (_stateLabelOutlines is { } outlines && outlines.TryGetValue(requestedStateId, out bool outline))
|
||||
Outline = outline;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue