fix(chargen): Campaign CC gate round 1 re-test 2 — R3-3 skills info-box VerticalJustify

The info-box 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, so both
fall to this port's shared unauthored-VJustify default (currently Center).

Byte-traced retail's real ctor default (UIElement_Text::UIElement_Text
@0x004685ff, m_eVerticalJustification = 4) against UIElement_Text::
CalcJustification @0x00467260's actual enum semantics (1=Center, 3-or-5=
the far edge/Bottom, anything else INCLUDING the ctor's own default of 4
= the near edge/Top): the correct unauthored default is Top, not Center —
a genuine client-wide enum-mapping bug in this port. Under Top both panes
render near their own box's top edge (25px apart, no collision); under
Center both cluster toward the middle of their overlapping boxes.

Scoped fix: CharacterCreationSkillsPage force-sets VerticalJustify=Top on
both panes directly, rather than fixing the shared mapping/default — that
bug is client-wide and could regress already-shipped FROZEN surfaces
(vitals, chat, main game UI, Options) that may rely on the current Center
default. The shared fix is filed as ISSUES #410 / register AD-104 for its
own dedicated investigation + regression sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 17:20:08 +02:00
parent 7d6a7898f6
commit 7f6e93033f
4 changed files with 143 additions and 1 deletions

View file

@ -24,6 +24,87 @@ What does NOT go here:
- Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending.
- Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed.
## #410 — Client-wide VJustify (vertical text justification) enum mapping + unauthored default are wrong (retail default is Top, not Center)
**Status:** OPEN
**Severity:** MEDIUM (silently mispositions every DAT-imported `UiText` that
relies on the unauthored default, or that authors a raw vertical-
justification value other than 1 — currently invisible unless two
elements' boxes are close/overlapping the way the Skills info-box panes
are, but could affect vertical alignment anywhere client-wide)
Found during Campaign CC gate round 1 re-test 2's R3-3 investigation
(`docs/research/2026-08-16-campaign-cc-gate-round1-findings.md`). The
Skills page's info-box title (`0x100003fb`) and description (`0x100003fc`)
panes author NO dat property `0x15` (vertical justification) — live-DAT-
probe-confirmed absent on both — so both fall to whatever this port's
unauthored default resolves to, currently `VJustify.Center`
(`ElementReader.cs`'s `VJustify` field default and
`ElementReader.cs`/`DatWidgetFactory.cs`'s import-time mapping switches).
Byte-traced against retail:
- `UIElement_Text::UIElement_Text` (ctor) `@0x004685ff`: unconditionally
sets `this->m_eVerticalJustification = 4` (and
`m_eHorizontalJustification = 2` at `@0x004685f5`) BEFORE any dat
property is applied — i.e. retail's real unauthored default is the raw
value **4**, not whatever a "sensible default" might suggest.
- `UIElement_Text::CalcJustification` `@0x00467260`: the ACTUAL enum
semantics, shared by both the horizontal and vertical branches via one
`ecx_5` comparison — `ecx_5 == 1`**Center**; `ecx_5 == 3 || ecx_5 == 5`
→ the FAR edge (**Right** for horizontal, **Bottom** for vertical); any
OTHER value (0, 2, 4, ...) → `edi = 0`, the NEAR edge (**Left** for
horizontal, **Top** for vertical).
Cross-referencing: the ctor's own vertical default of 4 resolves via this
real semantic table to **Top**, not Center. This port's
`ElementReader.cs:507`'s import-time switch (`2u=>Top, 4u=>Bottom,
_=>Center`) and `DatWidgetFactory.cs:704`'s build-time switch are BOTH
wrong relative to the real table — only raw value `2` (coincidentally
falling into the correct "near edge" bucket) and `1` (Center, matching the
`_=>Center` catch-all by coincidence) currently resolve correctly; `0`,
`3`, `4`, and `5` all resolve to the wrong bucket. The `ElementInfo.VJustify`
field default (`VJustify.Center`) is ALSO wrong — it should be `Top` to
match the ctor's real resolved value.
**Why this is filed instead of fixed here:** the blast radius is
client-wide — every DAT-imported `UiText` that reaches the
`Centered`/`RightAligned`/`OneLine` static paths or the multi-line
honored-justification path (`_honorDatVerticalJustification`, set
unconditionally by `ConfigureDatState` for every DAT-imported text
element) is affected, including already-shipped, visually-verified,
FROZEN surfaces (vitals numbers, chat, main game UI, Options panel) that
may be relying on the CURRENT (wrong) Center default for their existing
correct-looking vertical alignment. Flipping the shared default/mapping
without a full client-wide regression sweep risks reintroducing
regressions in surfaces this session has no budget to re-verify. R3-3's
own fix (`CharacterCreationSkillsPage`'s constructor) scopes the
correction to ONLY the two Skills info-box panes via an explicit
`VerticalJustify = VJustify.Top` post-construction assignment — a
targeted, decomp-grounded correction that does not touch the shared
mapping.
**Fix direction when this issue is picked up:** (1) correct
`ElementReader.cs`'s import-time switch AND `DatWidgetFactory.cs`'s
build-time switch to the real table above (`1=>Center, 3 or 5=>Bottom,
else=>Top`) for BOTH horizontal and vertical justification (audit the
horizontal switch too — it currently special-cases `0u or 2u=>Left`
instead of "everything except 1/3/5"; likely benign today since 2 is the
only unauthored horizontal default in practice, but should be corrected
for the same reason); (2) flip `ElementInfo.VJustify`'s field default to
`Top`; (3) fix `ElementReader.cs:435`'s `Merge` sentinel
(`derived.VJustify != VJustify.Center ? derived : base_`) to use the NEW
default (`Top`) as the "unset" sentinel instead, or restructure to a
nullable/explicit-override tracking shape so the merge doesn't rely on a
magic default value at all; (4) a full client-wide live-DAT sweep of every
Type-12/Button element that authors OR omits property `0x15`/`0x14`,
cross-checked against a fresh full visual pass of chat, main game UI,
Options, and every chargen page (this port's own `CharacterCreationSkillsPage`
override from R3-3 should be REMOVED once the shared default is corrected,
since it would then be redundant); (5) the exact same audit for the
horizontal `HJustify` mapping while in this code, since it shares the
`CalcJustification` function and the same class of latent bug.
## #409 — Client-wide UI tooltip system is unshipped (GF-16, deferred out of Campaign CC gate round 1)
**Status:** OPEN

File diff suppressed because one or more lines are too long

View file

@ -305,6 +305,42 @@ internal sealed class CharacterCreationSkillsPage : IDisposable
_credits = UiElement.FindDescendant(pageRoot, 0x100003F9u) as UiButton;
_infoTitle = UiElement.FindDescendant(pageRoot, 0x100003FBu) as UiText;
_infoText = UiElement.FindDescendant(pageRoot, 0x100003FCu) as UiText;
// R3-3 (Campaign CC gate round 1 re-test 2): the title
// (0x100003fb, Y=435, Height=100) and description (0x100003fc,
// Y=460, Height=100) panes' own AUTHORED boxes overlap by 75px
// (live-DAT-measured) — retail relies on vertical JUSTIFICATION,
// not disjoint rects, to keep the two visually separate. Neither
// element authors dat property 0x15 (live-DAT-probe-confirmed
// absent on both), so both fall to whatever the unauthored default
// resolves to. Byte-traced against retail's own
// UIElement_Text::UIElement_Text ctor @0x004685ff
// (this->m_eVerticalJustification = 4) cross-referenced with
// UIElement_Text::CalcJustification @0x00467260 (the ACTUAL
// enum semantics: ecx_5==1 -> Center, ecx_5==3||5 -> the FAR edge
// (Bottom), any other value including the ctor's own default of 4
// -> edi=0, the NEAR edge, i.e. Top): the correct unauthored
// default is TOP, not Center. This port's shared
// ElementReader/DatWidgetFactory VJustify mapping and field
// default both currently resolve an absent 0x15 to Center — a
// client-wide mismatch with real retail semantics that is NOT
// fixed here (filed as ISSUES.md #410; the blast radius spans
// every already-shipped DAT-imported UiText that relies on the
// CURRENT Center default, so a global remap needs its own
// dedicated investigation + regression sweep, not a bundled
// fix inside this page). Scoped correction: force these two
// specific panes to the value retail's ctor actually resolves
// to. Under Top justification the title (OneLine, ~1 line) sits
// near its box's own top (global Y~435) and the description
// (multi-line, honoring the SAME justification via
// ConfigureDatState's _honorDatVerticalJustification) starts near
// ITS box's own top (global Y~460) — the two boxes' TOP edges are
// 25px apart, so short/typical content no longer collides even
// though the boxes' full 100px extents still overlap on paper.
if (_infoTitle is { } infoTitle)
infoTitle.VerticalJustify = VJustify.Top;
if (_infoText is { } infoText)
infoText.VerticalJustify = VJustify.Top;
}
internal void Refresh(

View file

@ -433,6 +433,30 @@ public sealed class CharacterCreationUiControllerTests
JoinedText(environment.SkillInfoText()));
}
/// <summary>
/// R3-3 (Campaign CC gate round 1 re-test 2): the info-box title and
/// description panes' own AUTHORED boxes overlap by 75px (live-DAT-
/// measured, see <see cref="CharacterCreationSkillsPage"/>'s
/// constructor comment for the full geometry + decomp citation) —
/// retail avoids the visual collision via vertical justification, not
/// disjoint rects. Pins that the page forces both panes to Top so the
/// title sits near ITS box's own top and the description sits near
/// its own, instead of both clustering toward the middle of their
/// overlapping boxes under the shared (currently wrong, ISSUES #410)
/// Center default.
/// </summary>
[Fact]
public void SkillsPage_InfoBoxPanes_ForceTopVerticalJustify_ToAvoidTitleDescriptionOverlap()
{
using var environment = new EnvironmentHarness();
environment.Controller.Open();
environment.Runtime.SelectHeritageDirect(AluvianId);
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
Assert.Equal(VJustify.Top, environment.SkillInfoTitle().VerticalJustify);
Assert.Equal(VJustify.Top, environment.SkillInfoText().VerticalJustify);
}
/// <summary>R2-4a: retail re-selects the row after an arrow click too
/// (<c>ListenToElementMessage @0x004814c0</c>'s own
/// <c>SetSelectedItem(...,1)</c> call following