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
|
|
@ -1,5 +1,6 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Content;
|
||||
|
|
@ -278,6 +279,239 @@ public sealed class CharacterCreationLiveDatTests
|
|||
UiElement.FindDescendant(townRoot, 0x10000409u));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-11b/GF-11c (Campaign CC gate round 1 Batch B). Live-DAT-measured:
|
||||
/// each town button's marker (<c>0x1000040D</c>'s own child
|
||||
/// <c>0x1000040C</c>) and its per-button name caption both carry the
|
||||
/// SAME numeric id <c>0x10000409</c> as the page-level description
|
||||
/// panel found by the sibling test above — a genuine id collision in
|
||||
/// the installed dat between two DIFFERENT elements in DIFFERENT
|
||||
/// subtrees (harmless for <c>DatWidgetFactory.BuildButton</c>'s lift,
|
||||
/// which walks the button's OWN <c>ElementInfo.Children</c> list rather
|
||||
/// than resolving by a global id lookup — but it means this test
|
||||
/// verifies the BUILT BUTTON's own <see cref="UiButton.Label"/>/
|
||||
/// <see cref="UiButton.LabelBox"/>/<see cref="UiButton.LabelColor"/>,
|
||||
/// not a second <c>FindDescendant</c> call, which would ambiguously
|
||||
/// return the unrelated page-level panel). Pins: the caption's own
|
||||
/// authored rect (0,4,100,37) survives instead of being overwritten by
|
||||
/// the marker-face-relative offset (GF-11c), and its color swaps
|
||||
/// Normal (218,167,85) -> Highlight/white (255,255,255) on selection
|
||||
/// (GF-11b).
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void TownPage_HoltburgButton_CaptionHonorsOwnRectAndRecolorsWhiteOnSelection()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement townRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.TownPageElementId));
|
||||
UiButton holtburg = AssertButton(townRoot, 0x1000040Du);
|
||||
|
||||
Assert.Equal("Holtburg", holtburg.Label);
|
||||
Assert.Equal(UiButton.LabelAlignment.Center, holtburg.LabelAlign);
|
||||
Assert.Equal((0f, 4f, 100f, 37f), holtburg.LabelBox);
|
||||
|
||||
holtburg.Selected = false;
|
||||
Assert.Equal(new Vector4(218f / 255f, 167f / 255f, 85f / 255f, 1f), holtburg.LabelColor);
|
||||
|
||||
holtburg.Selected = true;
|
||||
Assert.Equal(new Vector4(1f, 1f, 1f, 1f), holtburg.LabelColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-1 (Campaign CC gate round 1 Batch B). Live-DAT-measured: the
|
||||
/// Heritage row (<c>0x100003BFu</c>, Aluvian) authors retail's custom
|
||||
/// "Unselected"/"Selected" radio-pair state DESCRIPTORS directly on the
|
||||
/// row (property-only, no media), with the actual per-state art on its
|
||||
/// single stateful dot child (<c>0x100003C0</c>). Before this fix,
|
||||
/// <see cref="UiButton.Selected"/> committed nothing here.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void HeritagePage_Row_SelectedTogglesTheAuthoredRadioDotState()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement heritageRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.HeritagePageElementId));
|
||||
UiButton aluvian = AssertButton(heritageRoot, 0x100003BFu);
|
||||
|
||||
Assert.Equal("Unselected", aluvian.ActiveState);
|
||||
aluvian.Selected = true;
|
||||
Assert.Equal("Selected", aluvian.ActiveState);
|
||||
Assert.Equal(RetailUiStateIds.Selected, aluvian.ActiveRetailStateId);
|
||||
aluvian.Selected = false;
|
||||
Assert.Equal("Unselected", aluvian.ActiveState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-1 counterpart: the Profession template row (<c>0x100003D9u</c>,
|
||||
/// Custom/Adventurer) authors the identical custom radio-pair shape on
|
||||
/// its own icon child (<c>0x100002E9</c>).
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void ProfessionPage_TemplateButton_SelectedTogglesTheAuthoredIconState()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement professionRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.ProfessionPageElementId));
|
||||
UiButton template = AssertButton(professionRoot, 0x100003D9u);
|
||||
|
||||
Assert.Equal("Unselected", template.ActiveState);
|
||||
template.Selected = true;
|
||||
Assert.Equal("Selected", template.ActiveState);
|
||||
template.Selected = false;
|
||||
Assert.Equal("Unselected", template.ActiveState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-8: the Appearance page's Face/Clothes sub-tab buttons
|
||||
/// (<c>0x100003A9u</c>/<c>0x100003AAu</c>) author the SAME custom
|
||||
/// radio-pair shape on their own icon child (<c>0x100002E9</c>) — same
|
||||
/// mechanism as the heritage/template rows, different page.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void AppearancePage_FaceClothesSubTabButtons_SelectedTogglesTheAuthoredState()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
|
||||
|
||||
foreach (uint buttonId in new[]
|
||||
{
|
||||
CharacterCreationAppearancePage.FaceButtonId,
|
||||
CharacterCreationAppearancePage.ClothesButtonId,
|
||||
})
|
||||
{
|
||||
UiButton button = AssertButton(appearanceRoot, buttonId);
|
||||
Assert.Equal("Unselected", button.ActiveState);
|
||||
button.Selected = true;
|
||||
Assert.Equal("Selected", button.ActiveState);
|
||||
button.Selected = false;
|
||||
Assert.Equal("Unselected", button.ActiveState);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-1 family, gender-button shape: <c>0x100003A7u</c>/
|
||||
/// <c>0x100003A8u</c> author the custom Unselected/Selected media
|
||||
/// DIRECTLY on the button's own StateMedia (no separate face-segment
|
||||
/// child) — live-DAT-measured, distinct from the heritage/template/
|
||||
/// sub-tab family above. Exercises <see cref="UiButton"/>'s OTHER
|
||||
/// custom-selection-pair code path (media on the button itself, so
|
||||
/// <c>info.StateMedia.Count != 0</c> and no face child is ever
|
||||
/// computed).
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void AppearancePage_GenderButtons_SelectedTogglesTheAuthoredState()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
|
||||
|
||||
foreach (uint buttonId in new[]
|
||||
{
|
||||
CharacterCreationAppearancePage.FemaleButtonId,
|
||||
CharacterCreationAppearancePage.MaleButtonId,
|
||||
})
|
||||
{
|
||||
UiButton button = AssertButton(appearanceRoot, buttonId);
|
||||
Assert.Equal("Unselected", button.ActiveState);
|
||||
button.Selected = true;
|
||||
Assert.Equal("Selected", button.ActiveState);
|
||||
button.Selected = false;
|
||||
Assert.Equal("Unselected", button.ActiveState);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-9 (Campaign CC gate round 1 Batch B). Live-DAT-measured: all nine
|
||||
/// companion overlay elements (<c>SwatchOverlayIds</c>) resolve as
|
||||
/// siblings of the swatches under the color-wheel container — retail's
|
||||
/// ACTUAL click-feedback mechanism (<c>SetColor</c>'s
|
||||
/// <c>m_tColorWheel[...][0x10][iCurColor*7]->SetVisible</c>), not a
|
||||
/// state swap on the swatch buttons themselves (which author only an
|
||||
/// unnamed DirectState sprite — no Normal/Highlight media at all).
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void AppearancePage_SwatchOverlays_AllNinePresent()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
|
||||
|
||||
foreach (uint overlayId in CharacterCreationAppearancePage.SwatchOverlayIds)
|
||||
{
|
||||
UiElement overlay = Assert.IsAssignableFrom<UiElement>(
|
||||
UiElement.FindDescendant(appearanceRoot, overlayId));
|
||||
// Retail's overlay ring starts hidden — SetColor only shows the
|
||||
// one at the current color index; nothing is selected before
|
||||
// any color choice runs.
|
||||
Assert.True(overlay.Visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-10 (Campaign CC gate round 1 Batch B). Live-DAT-measured: both
|
||||
/// zoom buttons author a STANDARD Normal/Highlight(/rollover) pair —
|
||||
/// unlike the custom radio-pair family above, this is pure wiring
|
||||
/// (<see cref="CharacterCreationAppearancePage"/>'s click handlers), not
|
||||
/// a new UiButton mechanism.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void AppearancePage_ZoomButtons_AuthorStandardNormalHighlightPair()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement appearanceRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.AppearancePageElementId));
|
||||
|
||||
foreach (uint buttonId in new[]
|
||||
{
|
||||
CharacterCreationAppearancePage.ZoomInId,
|
||||
CharacterCreationAppearancePage.ZoomOutId,
|
||||
})
|
||||
{
|
||||
UiButton button = AssertButton(appearanceRoot, buttonId);
|
||||
Assert.Equal("Normal", button.ActiveState);
|
||||
Assert.True(button.TrySetRetailState(UiButtonStateMachine.Highlight));
|
||||
Assert.Equal("Highlight", button.ActiveState);
|
||||
Assert.True(button.TrySetRetailState(UiButtonStateMachine.Normal));
|
||||
Assert.Equal("Normal", button.ActiveState);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The exit-warning + all per-heritage/per-town DAT string
|
||||
/// keys this slice cites actually resolve in the installed table.
|
||||
/// </summary>
|
||||
|
|
@ -401,6 +635,25 @@ public sealed class CharacterCreationLiveDatTests
|
|||
spin.TrySetRetailState(UiButtonStateMachine.Highlight),
|
||||
$"spin 0x{spinId:X8} must accept a Highlight state request.");
|
||||
Assert.Equal("Normal", spin.ActiveState);
|
||||
|
||||
// AP-222 CORRECTED + RESOLVED (Campaign CC gate round 1 Batch B):
|
||||
// the art half of the "no-op" stays a genuine no-op (ActiveState
|
||||
// pinned to "Normal" above, unchanged) — retail's own spin art
|
||||
// authors no Highlight media either, matching acdream. But the
|
||||
// current-part highlight is NOT presentation-dead: retail's
|
||||
// SetState(6) also recolors the spin's caption text (dat
|
||||
// property 0x1B), live-DAT-measured 218,167,85 (Normal) ->
|
||||
// 255,221,131 (Highlight), plus outline off -> on (property
|
||||
// 0x21). DatWidgetFactory.BuildButton wires this per-state style
|
||||
// unconditionally, so it is already active on `spin` from
|
||||
// TrySetRetailState(Highlight) above, independent of whether the
|
||||
// page has assigned a Label string yet.
|
||||
Assert.Equal(new Vector4(255f / 255f, 221f / 255f, 131f / 255f, 1f), spin.LabelColor);
|
||||
Assert.True(spin.Outline, $"spin 0x{spinId:X8} must outline its label in the Highlight state.");
|
||||
|
||||
Assert.True(spin.TrySetRetailState(UiButtonStateMachine.Normal));
|
||||
Assert.Equal(new Vector4(218f / 255f, 167f / 255f, 85f / 255f, 1f), spin.LabelColor);
|
||||
Assert.False(spin.Outline, $"spin 0x{spinId:X8} must not outline its label in the Normal state.");
|
||||
}
|
||||
|
||||
// Every color-wheel-family id resolves through EXISTING
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue