test(chargen): Campaign CC gate round 1 re-test 2 — R3-8 name-field exhaustive dump + closeout docs
R3-8: dumped EVERY property present on 0x10000402 (not just P0x17) across every state, cross-referenced against UIElement_Text::OnSetAttribute's complete case list (no UIElement_TextInput class exists in retail — the name field is a plain UIElement_Text/m_filter-bearing field). The full recognized-property space has no placeholder/prompt mechanism independent of P0x17. The BaseElement/prototype-inheritance hypothesis is also ruled out — the existing regression test already probes the fully-merged ElementInfo (post BaseElement resolution) and finds nothing. The only StringInfo-kind property present, 0x49, resolves to "Your name can be 32 characters long and cannot contain numbers or symbols." — but 0x49 is part of the same five-property tooltip family ISSUES #409/GF-16 already document client-wide (0x48's own DID, 0x21000041, is the EXACT tooltip popup LayoutDesc #409 cites) — a hover tooltip, not an in-field placeholder. No code change, per this batch's own "do not invent a placeholder" contract — third independent negative result on this question via three different mechanisms. The lead should request a live retail screenshot before any further investigation. Also carries the shared live-DAT regression suite for R3-1 through R3-7 (CharacterCreationLiveDatTests.cs holds tests spanning multiple findings in one file, so they land together) and the RE-TEST 2 findings-doc closeout writeup for all eight items. App suite live-DAT env 5358/3 -> 5372/3 (+14, zero regressions). Runtime 1735/0 unchanged (untouched this round). Full solution: 14578 tests / 4 skips / 0 failures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2886f79f37
commit
91f84dec9f
2 changed files with 562 additions and 1 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Campaign CC connected gate — round 1 findings (2026-08-16)
|
||||
|
||||
## RE-TEST 2 (build `1.0.2-cc.k`, post-E/F/G/closeout) — findings R3-1..R3-9
|
||||
## RE-TEST 2 (build `1.0.2-cc.k`, post-E/F/G/closeout) — findings R3-1..R3-8
|
||||
|
||||
Heritage PASSES. Remaining, with retail side-by-side screenshots:
|
||||
|
||||
|
|
@ -40,6 +40,250 @@ Heritage PASSES. Remaining, with retail side-by-side screenshots:
|
|||
is also empty, STOP and request a live retail screenshot of the field
|
||||
before any further work.
|
||||
|
||||
**RE-TEST 2 fix batch (2026-08-16, R3-1..R3-8) is CODE-COMPLETE, pending the
|
||||
user's visual gate.** All eight findings investigated and fixed except R3-8
|
||||
(genuinely no dat-authored placeholder mechanism exists — see its own
|
||||
disposition below). App suite live-DAT env 5358/3 → 5372/3 (+14, zero
|
||||
regressions); Runtime 1735/0 unchanged (untouched this round); full
|
||||
solution 14578 tests / 4 skips / 0 failures (0 Core/Content changes this
|
||||
round, so those suites are unaffected by construction, not merely by
|
||||
measurement). No client launches.
|
||||
|
||||
- **R3-1/R3-2 FIXED.** Root cause: Batch E's `UiButton.DrawBlockLabel`/
|
||||
`WrapBlockLines` auto-wrapped ANY caption that didn't fit its box width —
|
||||
live-DAT-probed, this is the WRONG mechanism. `UIElement_Text::
|
||||
CalcJustification @0x00467260` (shared by the horizontal/vertical
|
||||
branches) shows retail's real per-glyph break decision — BOTH the
|
||||
width-triggered wrap AND the explicit-newline break — sits behind ONE
|
||||
gate keyed on the `OneLine` flag passed into `GlyphList::Recalculate`;
|
||||
nothing in the decomp confines a caption's wrap width to a SIBLING
|
||||
element's rect (the ValueBox confinement Batch E added for the
|
||||
coexisting-value-label shape). Live-DAT evidence: the Coordination
|
||||
attribute-slider label (`0x100002ed`) authors `OneLine=true` (so it
|
||||
should never wrap, regardless of width — decomp-confirmed, not just
|
||||
measurement); the Skills credits button's "Available Skill Credits"
|
||||
caption measures 193px against its OWN full 231px button width (fits
|
||||
comfortably) — the 113px confined width Batch E fed into the wrap
|
||||
decision was never a real retail quantity. Fixed by making
|
||||
`UiButton.WrapBlockLines` split ONLY on the explicit (already-
|
||||
normalized) `\n` — never width-based — a strict superset of the
|
||||
pre-Batch-E single-line draw for every already-correct caption, and the
|
||||
exact "Attribute\n Credits" authored-break shape still works unchanged.
|
||||
The `ValueBox` confinement computation itself STAYS in
|
||||
`UiButton.OnDraw` (still feeds the Center-alignment tx formula and the
|
||||
now-unreachable-for-wrap clip rect for the rare multi-line+ValueBox
|
||||
case) — deliberately not deleted, since it's harmless now that nothing
|
||||
reads it for the wrap decision, and removing it would be unrelated
|
||||
scope. **Known residual risk, NOT re-solved here:** the original
|
||||
Batch E root-cause diagnosis for R2-2/R2-3 ("24dits"/"Credit0Credits")
|
||||
was that the caption's own unconfined single-line render visually
|
||||
overlapped the coexisting value label's rect (live-DAT-measured: the
|
||||
Skills credits caption's rendered span reaches x≈196, the value box
|
||||
starts at local x=116). Removing the WRAP does not reintroduce a
|
||||
confinement CLIP either (deliberately — see `DrawBlockLabel`'s own doc
|
||||
for why inventing a new clip boundary here would be exactly the
|
||||
guessing this project forbids), so this specific button's caption and
|
||||
value MAY visually overlap again in the live client. This was NOT
|
||||
something the current re-test (R3-2) flagged as broken — it only
|
||||
reported the wrap — so no action was taken beyond documenting the risk
|
||||
for the user's own re-check.
|
||||
Files: `src/AcDream.App/UI/UiButton.cs` (`DrawBlockLabel`,
|
||||
`WrapBlockLines`, `OnDraw`'s Label block). Tests: `UiButtonTests.cs`
|
||||
(`WrapBlockLines_LongSingleParagraph_NeverWordWraps` replaces the retired
|
||||
Batch E word-wrap expectation; new
|
||||
`WrapBlockLines_SingleWordNarrowerThanBoxButWiderThanOffsetAdjustedWidth_StaysOneLine`);
|
||||
2 new live-DAT pins in `CharacterCreationLiveDatTests.cs`
|
||||
(`CoordinationAttributeLabel_AuthorsOneLineTrue`,
|
||||
`SkillsCreditsButton_CaptionFitsFullWidth_ValueChildStartsAtMidpoint`).
|
||||
|
||||
- **R3-3 FIXED.** Root cause: the title (`0x100003fb`, Y=435 H=100) and
|
||||
description (`0x100003fc`, Y=460 H=100) panes' own AUTHORED boxes
|
||||
overlap by 75px, live-DAT-measured — retail relies on VERTICAL
|
||||
JUSTIFICATION, not disjoint rects, to keep them visually separate.
|
||||
Neither pane authors dat property `0x15` (live-DAT-confirmed absent on
|
||||
both), so both fall to this port's shared unauthored-VJustify default —
|
||||
currently `Center`. Byte-tracing retail's real ctor default
|
||||
(`UIElement_Text::UIElement_Text @0x004685ff`,
|
||||
`m_eVerticalJustification = 4`) against `UIElement_Text::
|
||||
CalcJustification @0x00467260`'s ACTUAL enum semantics (`ecx_5==1` →
|
||||
Center; `ecx_5==3||5` → the far edge/Bottom; anything else, INCLUDING
|
||||
the ctor's own default of 4 → the near edge/Top) shows the correct
|
||||
unauthored default is **Top, not Center** — a genuine, client-wide
|
||||
enum-mapping bug in this port (`ElementReader.cs`'s import-time switch,
|
||||
`DatWidgetFactory.cs`'s build-time switch, and `ElementInfo.VJustify`'s
|
||||
field default all currently resolve an absent `0x15` to Center). Under
|
||||
the CORRECT Top default both panes render near their own box's TOP edge
|
||||
(25px apart — no collision); under the current Center default both
|
||||
cluster toward the middle of their overlapping boxes (collision).
|
||||
**Scoped fix, not the systemic one:** `CharacterCreationSkillsPage`'s
|
||||
constructor now force-sets `VerticalJustify = VJustify.Top` on both
|
||||
panes directly, rather than fixing the shared mapping/default. The
|
||||
shared bug is CLIENT-WIDE (every DAT-imported `UiText` reaching the
|
||||
`Centered`/`RightAligned`/`OneLine` static paths or the multi-line
|
||||
honored-justification path) and could regress already-shipped, visually
|
||||
-verified, FROZEN surfaces (vitals numbers, chat, main game UI, Options
|
||||
panel) that may rely on the CURRENT Center default — fixing it properly
|
||||
needs its own dedicated investigation + full regression sweep, filed as
|
||||
**ISSUES.md #410** and register **AD-104**.
|
||||
Files: `src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs`
|
||||
(constructor). Tests: new
|
||||
`CharacterCreationUiControllerTests.SkillsPage_InfoBoxPanes_ForceTopVerticalJustify_ToAvoidTitleDescriptionOverlap`
|
||||
(fixture); new live-DAT
|
||||
`CharacterCreationLiveDatTests.SkillsInfoBoxTitleAndDescription_AuthoredBoxesOverlap`
|
||||
(pins the overlap premise itself, so a future DAT re-extract that makes
|
||||
the boxes genuinely disjoint is visible).
|
||||
|
||||
- **R3-4/R3-7 FIXED (single shared mechanism, confirmed).** Root cause:
|
||||
retail authors TWO DISTINCT `UIElement_Scrollbar` thumb shapes.
|
||||
`DatWidgetFactory.BuildScrollbar`'s existing vertical-thumb detection was
|
||||
built against chat's own scrollbar (`0x10000012`) — a 3-slice composite
|
||||
where the thumb CHILD carries no media of its own and three Type-3
|
||||
grandchildren supply the top-cap/middle/bottom-cap sprites. The chargen
|
||||
Skills listbox scrollbar (`0x100003f8`), Summary's OVERVIEW listbox
|
||||
scrollbar (`0x10000401`), the Summary how-to box's scrollbar
|
||||
(`0x100002e7`), AND the shade slider (`0x10000321`) all instead author a
|
||||
SIMPLE single-sprite thumb: the same structural child (Type 1, id 1, not
|
||||
the inc/dec button) carries its OWN direct Normal/Normal_rollover/
|
||||
Normal_pressed (or, for the shade slider, a single DirectState) media
|
||||
and has ZERO children — the 3-slice-only search found nothing for this
|
||||
shape, so every `Thumb*Sprite` stayed 0 regardless of overflow. Fixed by
|
||||
falling back to the thumb's own `DefaultImage` when the slice search
|
||||
finds nothing — additive; a thumb WITH real slice children (chat) is
|
||||
unaffected. This ONE fix covers R3-4's three listbox thumbs AND R3-7
|
||||
AND, as a natural consequence (same code path, same structural shape),
|
||||
the shade-slider indicator half of R3-5(c) — no separate fix was needed
|
||||
for the shade slider.
|
||||
**Process note:** the ORIGINAL live-DAT probe for this investigation
|
||||
mis-reported the shade slider's own thumb child (and, separately, the
|
||||
swatch/gradCircle elements investigated for R3-5/R3-6) as authoring
|
||||
"zero media" — a `string.Join(",", StateMedia.Keys)` display artifact
|
||||
(a single `""` DirectState key joins to an EMPTY STRING, indistinguishable
|
||||
from zero entries in a printed diagnostic — not a code defect, a
|
||||
diagnostic-only mistake caught and corrected mid-investigation by
|
||||
re-probing with an exact `.Count`/dictionary-content check instead of a
|
||||
joined string).
|
||||
Files: `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`
|
||||
(`BuildScrollbar`). Tests: new `DatWidgetFactoryTests.cs`
|
||||
(`Type11_VerticalScrollbar_SingleSpriteThumbWithNoSliceChildren_SetsThumbSprite`,
|
||||
`Type11_VerticalScrollbar_ThumbWithSliceChildren_StillUsesSliceMedia`
|
||||
negative companion); 3 new live-DAT pins in
|
||||
`CharacterCreationLiveDatTests.cs`
|
||||
(`SkillsListboxScrollbar_SingleSpriteThumbShape_BuildsWithNonZeroThumbSprite`,
|
||||
`ShadeSlider_ThumbAuthorsItsOwnDirectStateSprite_BuildsWithNonZeroThumbSprite`);
|
||||
existing `ChatFixture_ScrollbarImportsInheritedMediaRoles` (3-slice shape)
|
||||
re-verified unaffected.
|
||||
|
||||
- **R3-5/R3-6 CODE-COMPLETE.** Root cause, re-derived from
|
||||
`gmCGAppearancePage::DoColorSpots @0x0047d850` and `DoGradDisk
|
||||
@0x0047da90`: retail does NOT multiply-tint the swatch/grad-circle's
|
||||
authored sprite. It builds a FRESH composited surface once
|
||||
(`CreateLocalSurface` + `Blit`), then calls `SurfaceWindow::ReplaceColor`
|
||||
against old-color `RGBAColor(0,0,0,1)` (opaque black — the spot
|
||||
template's own placeholder fill, live-DAT-PIXEL-confirmed: the
|
||||
37x44 "spot" resource has a genuine solid-black CENTER region and a
|
||||
genuine non-black RING/border region) — swapping every EXACT opaque-black
|
||||
pixel for the swatch's real color while leaving the ring untouched. A
|
||||
multiply-tint (Batch G's mechanism) is architecturally wrong here: black
|
||||
multiplied by ANY color stays black (never recolors the center at all),
|
||||
and multiplying the ring's own non-black pixels shifts their hue,
|
||||
corrupting them — exactly the reported "we tint the ring" symptom.
|
||||
Beyond-count swatches (R3-5b) use a COMPLETELY DIFFERENT authored
|
||||
resource (enum `0x1000000f`, "blank" — live-DAT-pixel-confirmed almost
|
||||
no black pixels at all, i.e. genuinely different art, not "the spot with
|
||||
its center left un-recolored") shown UNTINTED — and retail's own
|
||||
`pColor->SetVisible(1)` is UNCONDITIONAL for all 9 swatches (never
|
||||
hidden, only the CONTENT differs). For Eyes (R3-6), `DoGradDisk`'s Eyes
|
||||
branch (`arg2=1`) blits the "grad plug" icon (enum `0x10000010`)
|
||||
UNTINTED (`Blit_Normal`, no color argument at all) — and, re-reading
|
||||
`SetSelection @0x0047e260`'s own Eyes/non-Eyes tail
|
||||
(`@0x0047e859-0047e878`), NEITHER branch ever calls
|
||||
`m_pGradCircle->SetVisible` — only `DoGradDisk` (swaps the source image)
|
||||
and `m_pShadeScroll->SetVisible` (a DIFFERENT element) are touched, so
|
||||
the disc itself is NEVER hidden for Eyes — a correction to this port's
|
||||
prior `_gradCircle.Visible = !isEyes` line (and its own now-renamed
|
||||
test).
|
||||
**Mechanism ported faithfully** via a new `ChargenColorSpotComposer`
|
||||
(CPU-side decode-once + per-color bake-and-cache-once, uploaded through
|
||||
the existing `TextureCache.UploadRgba8` seam — the SAME "decode,
|
||||
recolor by exact-match, upload, cache" shape `AcDream.App.UI.
|
||||
IconComposer.GetSpellComponentIcon` already established for item icons,
|
||||
just matching black instead of white) and a new opt-in
|
||||
`UiButton.ColorKeyFaceResolver`/reuse of the EXISTING
|
||||
`UiDatElement.RuntimeImageTexture` seam — both additive, zero behavior
|
||||
change for any element that doesn't set them. `Tint` itself is
|
||||
UNCHANGED in meaning (still the "this swatch's color is X" signal every
|
||||
existing test reads) for the swatches; the grad circle's own Tint STAYS
|
||||
a genuine multiply for the non-Eyes case, matching retail's OWN
|
||||
`Blit_Multiply` there (the ONE place a multiply tint is actually
|
||||
correct). Wired as a fourth late-bound composition seam
|
||||
(`SwatchTextureSource`), same pattern and same site as the existing
|
||||
three color-computation seams (`PalSetSource`/`ClothingTableSource`/
|
||||
`PaletteColorSource`), constructed in `LivePresentationComposition.cs`
|
||||
once `TextureCache` exists.
|
||||
**STOPPED item, same shape as Batch G's own two STOPPED items:** this
|
||||
is CODE-COMPLETE and unit/live-DAT-tested (including a real pixel-level
|
||||
proof that the spot template genuinely has a black center + non-black
|
||||
ring, and that the blank template genuinely doesn't), but has NOT been
|
||||
visually verified in the live client this round (no client launches per
|
||||
this batch's own constraint) — the user's connected gate is owed.
|
||||
Files: `src/AcDream.App/UI/Layout/ChargenColorSpotComposer.cs` (new),
|
||||
`src/AcDream.App/UI/UiButton.cs` (`ColorKeyFaceResolver`, `OnDraw`),
|
||||
`src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs`
|
||||
(`SwatchTextureSource`, `BuildSwatchTextureResolver`, the swatch/
|
||||
gradCircle refresh block), `src/AcDream.App/UI/Layout/
|
||||
CharacterCreationUiController.cs` + `src/AcDream.App/UI/
|
||||
RetailUiRuntime.cs` (pass-through seam), `src/AcDream.App/Composition/
|
||||
LivePresentationComposition.cs` (composition-root wiring). Tests: new
|
||||
`ChargenColorSpotComposerTests.cs` (3 pure byte-level tests for the
|
||||
exact-match recolor), 4 live-DAT tests in `CharacterCreationLiveDatTests.cs`
|
||||
(`ColorSpotAndGradDiskResources_ResolveToExpectedDimensions`,
|
||||
`SpotTemplate_HasBlackCenterAndNonBlackRing_BlankTemplateHasNeitherBlack`),
|
||||
8 `CharacterCreationAppearancePageSwatchColorTests.cs` assertions updated
|
||||
(`Visible` now always true; `EyesPart_GradientDiscStaysHiddenAndUntinted`
|
||||
renamed+rewritten to `EyesPart_GradientDiscStaysVisibleButUntinted_ShowsPlugIconInstead`).
|
||||
|
||||
- **R3-8 DISPOSITION: genuinely nothing authored — no code change, per
|
||||
this batch's own contract ("DO NOT invent a placeholder").** Dumped
|
||||
EVERY property present on `0x10000402` (not just P0x17) across every
|
||||
state, cross-referenced against `UIElement_Text::OnSetAttribute`'s
|
||||
COMPLETE case list (there is no `UIElement_TextInput` class in retail —
|
||||
the name field is a plain `UIElement_Text`/`m_filter`-bearing field,
|
||||
`DynamicCast(0xc)`-confirmed in `gmCGSummaryPage::InitializePage`; the
|
||||
task's own reference to that class name doesn't exist in the named
|
||||
decomp). The full recognized-property space (every id `0x14`-`0x29`
|
||||
plus the sparse high ids `0xC7`/`0xCB`/`0xCC` for
|
||||
TruncateTextToFit/LoseFocusOnEscape/LoseFocusOnAcceptInput) has NO
|
||||
mechanism for a placeholder/prompt string independent of the committed
|
||||
P0x17 caption. The BaseElement/prototype-inheritance hypothesis is ALSO
|
||||
ruled out — not by assumption, but because the EXISTING regression test
|
||||
(`SummaryNameField_AuthorsNoP0x17OnAnyState`) already probes the FULLY
|
||||
MERGED `ElementInfo` (post `LayoutImporter`'s BaseElement resolution,
|
||||
confirmed by reading `ElementReader.Merge`/`UiStateInfo.Merge`'s own
|
||||
"derived overrides, else inherit base" property-bag semantics) and finds
|
||||
no P0x17 anywhere. The live dump found exactly ONE StringInfo-kind
|
||||
property on the whole element: **`0x49`, resolving to "Your name can be
|
||||
32 characters long and cannot contain numbers or symbols."** — but
|
||||
`0x49` is part of the SAME five-property tooltip family ISSUES.md
|
||||
#409/GF-16 already documents client-wide (`0x47` tooltip behavior
|
||||
enum = `0x10000487`, `0x48` the tooltip popup LayoutDesc DID =
|
||||
`0x21000041` — the EXACT DID #409 cites, `0x49` the tooltip TEXT,
|
||||
`0x4B` tooltip-enabled = true) — a HOVER TOOLTIP describing naming
|
||||
rules, not an in-field placeholder, and #409's tooltip system is
|
||||
unshipped so this text is authored but never shown anywhere yet. The
|
||||
field's 8 children are the SAME gold-frame family (`0x100002DE-E3`/
|
||||
`0x100000E8`/`0xEA`) GF-12 already renders — reinforcing GF-15's
|
||||
existing hypothesis that the `[ Name` the user perceives is this frame's
|
||||
own bracket-style chrome around an empty box, not text content.
|
||||
**Batch A's closure and Batch E's re-check both stand; this is the
|
||||
THIRD independent negative result on the same question via three
|
||||
different mechanisms (retail code, DAT P0x17, now the complete property
|
||||
space + inheritance chain).** No further code-side avenue remains — the
|
||||
lead should request a live retail screenshot of the field per the
|
||||
batch's own contract before any further investigation.
|
||||
Test: `CharacterCreationLiveDatTests.SummaryNameField_AuthorsNoP0x17OnAnyState`
|
||||
extended with the exhaustive `0x49`-is-the-tooltip assertion (a
|
||||
regression pin, not just a probe finding).
|
||||
|
||||
## ROUND 1 RE-TEST (build `1.0.2-cc.i`, post-Batches B/C/D) — findings R2-1..R2-8
|
||||
|
||||
User's second visual pass with retail side-by-side screenshots (heritage
|
||||
|
|
|
|||
|
|
@ -944,6 +944,29 @@ public sealed class CharacterCreationLiveDatTests
|
|||
$"the name field's state 0x{stateId:X} ('{state.Name}') must not "
|
||||
+ "author a P0x17 caption either.");
|
||||
}
|
||||
|
||||
// R3-8 (re-test 2, third assertion): EXHAUSTIVE — every property
|
||||
// this element authors, cross-referenced against
|
||||
// UIElement_Text::OnSetAttribute's COMPLETE case list (every
|
||||
// property id that class recognizes at all: 0x14-0x29 plus the
|
||||
// sparse high ids 0xC7/0xCB/0xCC), not just P0x17. The ONLY
|
||||
// StringInfo-kind property present is 0x49 — part of the tooltip
|
||||
// family (0x47 TooltipBehavior/0x48 the tooltip popup LayoutDesc
|
||||
// DID 0x21000041/0x49 the tooltip TEXT/0x4B TooltipEnabled — the
|
||||
// SAME five-property family ISSUES.md #409/GF-16 already
|
||||
// documents client-wide) and resolves to "Your name can be 32
|
||||
// characters long and cannot contain numbers or symbols." — a
|
||||
// HOVER TOOLTIP, not an in-field placeholder; #409's tooltip
|
||||
// system is unshipped, so this text is authored but never shown
|
||||
// anywhere yet. No other property on this element (or its 8 gold-
|
||||
// frame children, the SAME 0x100002DE-E3/0x100000E8/0xEA family
|
||||
// GF-12 already renders) carries any string content.
|
||||
Assert.True(nameField.TryGetEffectiveProperty(0x49u, out var tooltip));
|
||||
Assert.Equal(UiPropertyKind.StringInfo, tooltip.Kind);
|
||||
var strings = new DatStringResolver(dats);
|
||||
Assert.Equal(
|
||||
"Your name can be 32 characters long and cannot contain numbers or symbols.",
|
||||
strings.Resolve(tooltip.StringInfoValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1583,6 +1606,272 @@ public sealed class CharacterCreationLiveDatTests
|
|||
+ "regardless of the R2-1 margin fix");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-1 (re-test 2): live-DAT pin for the wrap-mechanism fix's own
|
||||
/// premise — the Coordination attribute-slider label (<c>0x100002ed</c>
|
||||
/// under container <c>0x100003e8</c>) authors <c>OneLine=true</c> (dat
|
||||
/// property <c>0x20</c>), the SAME retail default a caption with no
|
||||
/// authored <c>0x20</c> resolves to for width-wrap purposes per
|
||||
/// <c>UIElement_Text::CalcJustification</c>'s per-glyph gate (see
|
||||
/// <see cref="UiButton.DrawBlockLabel"/>'s own doc) — so this element
|
||||
/// must never width-wrap regardless of font metrics.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void CoordinationAttributeLabel_AuthorsOneLineTrue()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
|
||||
LayoutImporter.ImportInfos(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId));
|
||||
|
||||
ElementInfo coordContainer = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003E8u));
|
||||
ElementInfo coordLabel = Assert.IsType<ElementInfo>(FindInfo(coordContainer, 0x100002EDu));
|
||||
|
||||
Assert.True(coordLabel.TryGetEffectiveBool(0x20u, out bool oneLine) && oneLine);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-2 (re-test 2): live-DAT pin for the wrap-mechanism fix's own
|
||||
/// premise — the Skills credits button's caption ("Available Skill
|
||||
/// Credits", <c>0x100003f9</c>) fits comfortably inside the button's
|
||||
/// own FULL authored width (never needing to wrap), and its value
|
||||
/// child (<c>0x100002f3</c>) sits at local X=116 — the confinement
|
||||
/// figure Batch E used to force a false wrap, no longer consulted for
|
||||
/// the wrap decision post-fix (see <see cref="UiButton.DrawBlockLabel"/>'s
|
||||
/// own doc).
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SkillsCreditsButton_CaptionFitsFullWidth_ValueChildStartsAtMidpoint()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
|
||||
LayoutImporter.ImportInfos(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId));
|
||||
var strings = new DatStringResolver(dats);
|
||||
|
||||
ElementInfo skillsCredits = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003F9u));
|
||||
Assert.True(skillsCredits.TryGetEffectiveProperty(0x17u, out var caption));
|
||||
string? captionText = strings.Resolve(caption.StringInfoValue);
|
||||
Assert.Equal("Available Skill Credits", captionText);
|
||||
|
||||
uint fontDid = skillsCredits.FontDid != 0 ? skillsCredits.FontDid : rootInfo.FontDid;
|
||||
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.Font>(fontDid, out var font) && font is not null);
|
||||
var glyphs = new Dictionary<char, DatReaderWriter.Types.FontCharDesc>(font!.CharDescs.Count);
|
||||
foreach (var cd in font.CharDescs) glyphs[(char)cd.Unicode] = cd;
|
||||
var datFont = new UiDatFont(0, 0, 0, 0, 0, 0, font.MaxCharHeight, font.BaselineOffset, glyphs);
|
||||
float measured = datFont.MeasureWidth(captionText!);
|
||||
|
||||
Assert.True(
|
||||
measured < skillsCredits.Width,
|
||||
$"caption measured {measured}px must fit the button's own full {skillsCredits.Width}px width");
|
||||
|
||||
ElementInfo valueChild = Assert.Single(skillsCredits.Children);
|
||||
Assert.Equal(116f, valueChild.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-3 (re-test 2): the info-box title (<c>0x100003fb</c>) and
|
||||
/// description (<c>0x100003fc</c>) panes' own AUTHORED boxes overlap —
|
||||
/// this is why <see cref="CharacterCreationSkillsPage"/>'s constructor
|
||||
/// forces both to <c>VJustify.Top</c> rather than relying on disjoint
|
||||
/// rects (see that constructor's own comment for the full decomp
|
||||
/// citation). Pinned so a future DAT re-extract that changes these
|
||||
/// boxes to genuinely disjoint rects is visible here, not silently
|
||||
/// contradicting the fix's own premise.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SkillsInfoBoxTitleAndDescription_AuthoredBoxesOverlap()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
|
||||
LayoutImporter.ImportInfos(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId));
|
||||
|
||||
ElementInfo title = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003FBu));
|
||||
ElementInfo description = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003FCu));
|
||||
|
||||
// Neither pane authors an explicit vertical-justify property — both
|
||||
// fall to this port's shared (currently Center) unauthored default,
|
||||
// ISSUES.md #410.
|
||||
Assert.False(title.TryGetEffectiveProperty(0x15u, out _));
|
||||
Assert.False(description.TryGetEffectiveProperty(0x15u, out _));
|
||||
|
||||
float titleBottom = title.Y + title.Height;
|
||||
float descriptionTop = description.Y;
|
||||
Assert.True(
|
||||
titleBottom > descriptionTop,
|
||||
$"expected the title's own box (Y={title.Y} H={title.Height}, bottom={titleBottom}) to "
|
||||
+ $"overlap the description's box (Y={description.Y}) — if it no longer does, the "
|
||||
+ "VerticalJustify.Top override in CharacterCreationSkillsPage may no longer be needed");
|
||||
// The two boxes' own TOP edges still leave enough of a gap for
|
||||
// Top-justified content not to collide — the fix's actual premise.
|
||||
Assert.True(description.Y > title.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-4/R3-7 (re-test 2): retail authors TWO distinct
|
||||
/// <c>UIElement_Scrollbar</c> thumb shapes. Chat's own scrollbar
|
||||
/// (<c>0x10000012</c> under LayoutDesc <c>0x2100006f</c>) is the
|
||||
/// 3-slice composite <see cref="DatWidgetFactory.BuildScrollbar"/>'s
|
||||
/// original thumb-detection was built against (the thumb child itself
|
||||
/// carries NO media; three Type-3 grandchildren supply the cap/middle/
|
||||
/// cap sprites). The chargen Skills listbox scrollbar (<c>0x100003f8</c>)
|
||||
/// instead authors a SIMPLE single-sprite thumb: the same structural
|
||||
/// child (Type 1, id 1, not the inc/dec button) carries its OWN direct
|
||||
/// Normal/Normal_rollover/Normal_pressed media and has ZERO children —
|
||||
/// before the fix, the 3-slice-only search found nothing and every
|
||||
/// Thumb*Sprite stayed 0. This test builds the REAL scrollbar end to
|
||||
/// end and asserts <see cref="UiScrollbar.ThumbSprite"/> now resolves.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SkillsListboxScrollbar_SingleSpriteThumbShape_BuildsWithNonZeroThumbSprite()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiScrollbar scrollbar = Assert.IsType<UiScrollbar>(screen.FindElement(0x100003F8u));
|
||||
Assert.False(scrollbar.Horizontal);
|
||||
Assert.NotEqual(0u, scrollbar.ThumbSprite);
|
||||
// The 3-slice caps stay unset for this shape — OnDraw's own
|
||||
// single-tile fallback (ThumbTopSprite/ThumbBotSprite both 0)
|
||||
// draws the whole thumb from ThumbSprite alone.
|
||||
Assert.Equal(0u, scrollbar.ThumbTopSprite);
|
||||
Assert.Equal(0u, scrollbar.ThumbBotSprite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-5/R3-6 (re-test 2): live-DAT pin for
|
||||
/// <see cref="ChargenColorSpotComposer"/>'s four hardcoded enum ids —
|
||||
/// resolves each through the SAME category-7 <c>RetailDataIdResolver</c>
|
||||
/// chain <c>gmCGAppearancePage::DoColorSpots</c>/<c>DoGradDisk</c> use,
|
||||
/// and pins the native dimensions those two decomp functions' own
|
||||
/// <c>CreateLocalSurface</c> calls size their composite surfaces to
|
||||
/// (spot/blank match the swatch buttons' own 37x44 authored rect;
|
||||
/// gradDisk/gradPlug match the grad circle's own 110x112 rect).
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void ColorSpotAndGradDiskResources_ResolveToExpectedDimensions()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
|
||||
(uint enumId, int width, int height)[] expected =
|
||||
[
|
||||
(0x1000000Du, 37, 44), // spot (active)
|
||||
(0x1000000Fu, 37, 44), // blank (blocked)
|
||||
(0x1000000Eu, 110, 112), // gradDisk
|
||||
(0x10000010u, 110, 112), // gradPlug
|
||||
];
|
||||
foreach ((uint enumId, int width, int height) in expected)
|
||||
{
|
||||
uint did = RetailDataIdResolver.Resolve(dats, enumId, 7u);
|
||||
Assert.NotEqual(0u, did);
|
||||
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.RenderSurface>(did, out var rs) && rs is not null);
|
||||
Assert.Equal(width, (int)rs!.Width);
|
||||
Assert.Equal(height, (int)rs.Height);
|
||||
}
|
||||
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
|
||||
LayoutImporter.ImportInfos(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId));
|
||||
|
||||
uint spotDid = RetailDataIdResolver.Resolve(dats, 0x1000000Du, 7u);
|
||||
uint gradDiskDid = RetailDataIdResolver.Resolve(dats, 0x1000000Eu, 7u);
|
||||
|
||||
// The nine pColor swatch elements (retail's DoColorSpots targets)
|
||||
// author exactly ONE DirectState sprite — and it resolves to the
|
||||
// SAME RenderSurface as the "spot" enum resource above, i.e. the
|
||||
// button's own static authored art already IS the un-recolored
|
||||
// (black-center) spot template. This is exactly why the OLD Tint-
|
||||
// multiply mechanism visibly tinted the ring: it was multiplying
|
||||
// this real authored sprite, not drawing over nothing. No Normal/
|
||||
// Highlight states exist (GF-9's own finding — the swatch's click
|
||||
// feedback is a SEPARATE overlay element, not a state swap here).
|
||||
ElementInfo spotElement = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x1000030Fu));
|
||||
Assert.Equal(37f, spotElement.Width);
|
||||
Assert.Equal(44f, spotElement.Height);
|
||||
var spotDirectState = Assert.Single(spotElement.StateMedia);
|
||||
Assert.Equal(string.Empty, spotDirectState.Key);
|
||||
Assert.Equal(spotDid, spotDirectState.Value.File);
|
||||
|
||||
// The grad circle likewise authors its own DirectState sprite —
|
||||
// resolving to the SAME RenderSurface as the "gradDisk" enum
|
||||
// resource — so the pre-fix Tint-multiply mechanism DID show
|
||||
// something for the disc too (the un-recolored gradient wheel,
|
||||
// multiplied); R3-6's actual gap was that Eyes needs a DIFFERENT
|
||||
// source image (the plug icon) which no per-state authored data
|
||||
// provides — SetSelection swaps it procedurally in retail, exactly
|
||||
// what RuntimeImageTexture now reproduces.
|
||||
ElementInfo gradCircleElement = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x1000030Eu));
|
||||
Assert.Equal(110f, gradCircleElement.Width);
|
||||
Assert.Equal(112f, gradCircleElement.Height);
|
||||
var gradDirectState = Assert.Single(gradCircleElement.StateMedia);
|
||||
Assert.Equal(string.Empty, gradDirectState.Key);
|
||||
Assert.Equal(gradDiskDid, gradDirectState.Value.File);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-5: pixel-level ground truth for
|
||||
/// <see cref="ChargenColorSpotComposer.ReplaceExactBlackWithColor"/>'s
|
||||
/// whole premise — the ACTIVE spot template (enum <c>0x1000000d</c>)
|
||||
/// has a genuinely near-black CENTER region and a genuinely non-black
|
||||
/// RING/border region, and the BLANK template (enum <c>0x1000000f</c>)
|
||||
/// has almost no black pixels at all (it is a DIFFERENT piece of art,
|
||||
/// not the spot with its center left un-recolored).
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SpotTemplate_HasBlackCenterAndNonBlackRing_BlankTemplateHasNeitherBlack()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint spotDid = RetailDataIdResolver.Resolve(dats, 0x1000000Du, 7u);
|
||||
uint blankDid = RetailDataIdResolver.Resolve(dats, 0x1000000Fu, 7u);
|
||||
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.RenderSurface>(spotDid, out var spotRs) && spotRs is not null);
|
||||
Assert.True(dats.TryGet<DatReaderWriter.DBObjs.RenderSurface>(blankDid, out var blankRs) && blankRs is not null);
|
||||
|
||||
var spot = AcDream.Core.Textures.SurfaceDecoder.DecodeRenderSurface(spotRs!);
|
||||
var blank = AcDream.Core.Textures.SurfaceDecoder.DecodeRenderSurface(blankRs!);
|
||||
|
||||
(int black, int nonBlackOpaque, int total) CountPixels(byte[] rgba)
|
||||
{
|
||||
int black = 0, nonBlackOpaque = 0, total = 0;
|
||||
for (int i = 0; i + 3 < rgba.Length; i += 4)
|
||||
{
|
||||
byte a = rgba[i + 3];
|
||||
if (a < 10) continue;
|
||||
total++;
|
||||
if (rgba[i] == 0 && rgba[i + 1] == 0 && rgba[i + 2] == 0) black++;
|
||||
else nonBlackOpaque++;
|
||||
}
|
||||
return (black, nonBlackOpaque, total);
|
||||
}
|
||||
|
||||
var spotCounts = CountPixels(spot.Rgba8);
|
||||
var blankCounts = CountPixels(blank.Rgba8);
|
||||
|
||||
// The spot genuinely has both a substantial black region (the
|
||||
// center, to recolor) AND a substantial non-black region (the
|
||||
// ring, to leave alone) — proves this isn't an all-black or
|
||||
// all-colored template.
|
||||
Assert.True(spotCounts.black > 100, $"expected a real black center, got {spotCounts.black} black pixels");
|
||||
Assert.True(spotCounts.nonBlackOpaque > 100, $"expected a real non-black ring, got {spotCounts.nonBlackOpaque}");
|
||||
|
||||
// The blank template is a DIFFERENT asset, not "spot with an
|
||||
// un-recolored center" — near-zero black pixels.
|
||||
Assert.True(
|
||||
blankCounts.black < spotCounts.black / 10,
|
||||
$"blank template has {blankCounts.black} black pixels, expected far fewer than the spot's {spotCounts.black}");
|
||||
}
|
||||
|
||||
private static void AssertButton(ImportedLayout layout, uint elementId) =>
|
||||
Assert.IsType<UiButton>(layout.FindElement(elementId));
|
||||
|
||||
|
|
@ -1603,4 +1892,32 @@ public sealed class CharacterCreationLiveDatTests
|
|||
null,
|
||||
new DatStringResolver(dats).Resolve);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-4/R3-5c (re-test 2): the chargen shade slider's own thumb child
|
||||
/// (<c>0x10000321</c> > structural id 1) authors ONE DirectState
|
||||
/// sprite of its own (<c>0x06004D50</c>) and ZERO children — the
|
||||
/// EARLIER (Batch F11-era) investigation misread this as "genuinely
|
||||
/// nothing authored here" from a <c>string.Join</c> display artifact (a
|
||||
/// single "" DirectState key joins to an empty string, indistinguishable
|
||||
/// from zero entries in a printed log — NOT a code defect, a
|
||||
/// diagnostic-only mistake). This is the EXACT SAME single-sprite-thumb
|
||||
/// shape <see cref="DatWidgetFactory.BuildScrollbar"/>'s R3-4/R3-7 fix
|
||||
/// already handles (<c>slices.Length == 0 -> ThumbSprite =
|
||||
/// DefaultImage(thumb)</c>) — no separate fix was needed for the shade
|
||||
/// slider; this test proves the SAME code path already resolves it.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void ShadeSlider_ThumbAuthorsItsOwnDirectStateSprite_BuildsWithNonZeroThumbSprite()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiScrollbar shadeSlider = Assert.IsType<UiScrollbar>(screen.FindElement(0x10000321u));
|
||||
Assert.False(shadeSlider.Horizontal);
|
||||
Assert.NotEqual(0u, shadeSlider.ThumbSprite);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue