fix(chargen): Campaign CC gate round 1 re-test 3 — R4-1..R4-4
Four visual residuals from the lead's own live-client captures of 1.0.2-cc.m, all root-caused via decomp + live-DAT evidence: - R4-1: Skills credits value overlapped mid-caption again. Root cause was a missing UiLayoutPolicy raw-edge reflow on UiButton's value-child rect (the child is base-inherited across four sibling buttons of differing widths, so its baked-in OriginalParentWidth diverges from the actual 231px-wide Skills credits button) plus an HJustify.Right value child mapped to Center instead of a real far-edge Right. - R4-2: the single-sprite scrollbar thumb tiled (GL_REPEAT) instead of drawing once — DrawTiled was reused for a small fixed marker graphic whose native size is far smaller than the track-proportional thumb rect. New DrawThumbMarker draws exactly one native-size instance. - R4-3: the skills info-box formula line clipped past the surrounding gold frame's own authored bottom edge (the pane's own raw box is 20px taller than the frame that visually contains it) — clamp the pane's Height to the frame's bottom (register AD-105, since retail's ShowSkillsText has no code relationship to the frame to cite). - R4-4: the Appearance help text started mid-sentence — the box was never touched by its page controller, so it kept UiText's chat-style PreserveEndOnLayout=true default; the scroll model's wasAtEnd check is vacuously true on its first-ever overflow transition, pinning the first render to the bottom. Set PreserveEndOnLayout=false (a static top-oriented report, not a transcript) and wired the box's own nested authored scrollbar, never wired before. App suite live-DAT env 5372/3 -> 5379/3 (+7, zero regressions). Runtime 1735/0 unchanged. Full solution 14585/4 skips/1 failure (the documented Core.Net NakEmission full-solution-only flake, confirmed standalone-pass). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
28704db4bf
commit
e6acb800cc
11 changed files with 678 additions and 12 deletions
|
|
@ -144,6 +144,26 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
internal const uint ShadeScrollId = 0x10000321u;
|
||||
internal const uint ViewportId = 0x100003BBu;
|
||||
|
||||
/// <summary>
|
||||
/// R4-4 (Campaign CC gate round 1 re-test 3): the framed instructions
|
||||
/// box (the SAME gold corner/edge sprite family the Skills info-box
|
||||
/// frame uses, <c>0x100002de-e3</c>/<c>0x100000e8</c>/<c>0xea</c> —
|
||||
/// live-DAT-confirmed identical children). Its own <c>P0x17</c> caption
|
||||
/// is the FULL static help paragraph (no <c>gmCGAppearancePage</c>
|
||||
/// runtime composition exists for it — unlike Town/Summary's
|
||||
/// <c>SetTownString</c>/<c>SetHowToText</c>, this text is purely
|
||||
/// DAT-authored, confirmed by the absence of any matching function in
|
||||
/// the named decomp).
|
||||
/// </summary>
|
||||
internal const uint HelpTextId = 0x100003ABu;
|
||||
|
||||
/// <summary>The help box's own NESTED scrollbar child (live-DAT-
|
||||
/// confirmed a direct child of <see cref="HelpTextId"/>, the SAME
|
||||
/// structural id/nesting shape as
|
||||
/// <c>CharacterCreationSummaryPage.HowToScrollRelativeId</c>'s own
|
||||
/// how-to box scrollbar).</summary>
|
||||
private const uint HelpScrollRelativeId = 0x100002E7u;
|
||||
|
||||
/// <summary>Retail's nine <c>SetColor(0..8)</c> swatch buttons, in
|
||||
/// index order — verbatim off <c>ListenToElementMessage</c>'s cases
|
||||
/// <c>5</c>-<c>0xd</c> (<c>elementId - 0x1000030a</c>).</summary>
|
||||
|
|
@ -202,6 +222,7 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
private readonly UiButton? _rotateCounterClockwise;
|
||||
private readonly UiButton? _zoomIn;
|
||||
private readonly UiButton? _zoomOut;
|
||||
private readonly UiText? _helpText;
|
||||
|
||||
/// <summary>The gradient disc (<c>0x1000030e</c>) — Type 3 in the
|
||||
/// authored dat, so <see cref="UiDatElement"/> (not the base
|
||||
|
|
@ -354,6 +375,39 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
_zoomIn?.TrySetRetailState(UiButtonStateMachine.Normal);
|
||||
};
|
||||
|
||||
// R4-4 (Campaign CC gate round 1 re-test 3): the help box's static
|
||||
// paragraph starts mid-sentence because this port never touched
|
||||
// this element at all — it built through the plain DatWidgetFactory
|
||||
// import path with UiText's own chat-style default
|
||||
// (PreserveEndOnLayout=true, "keep a view that is already at the
|
||||
// end pinned there" — see that property's own doc: "Chat uses the
|
||||
// default; top-oriented reports such as Character Information
|
||||
// disable it"). This box's content overflows its own view (a full
|
||||
// multi-paragraph instructions block in a 292px-tall frame), and
|
||||
// UiScrollable.SetExtents's own wasAtEnd check is vacuously true
|
||||
// the very first time a Scroll model transitions from its
|
||||
// zero-initialized state (ContentHeight=0/ViewHeight=0/ScrollY=0 ->
|
||||
// MaxScroll=0 -> AtEnd=(0>=0)=true) to real overflowing content —
|
||||
// with PreserveEndOnLayout still true, that spuriously pins the
|
||||
// FIRST-EVER render to the bottom, hiding the opening paragraph
|
||||
// exactly as reported ("right arrows next to the article of
|
||||
// clothing..." is mid-way through the third paragraph, not the
|
||||
// first). This is a static instructions box, not a chat transcript
|
||||
// — the SAME top-oriented-report shape PreserveEndOnLayout's own
|
||||
// doc already carves out. Also wires the box's own nested authored
|
||||
// scrollbar (property 0x72, live-DAT-confirmed a direct child) —
|
||||
// NEVER wired by this page before — so a user can still reach the
|
||||
// rest of the text if it doesn't fully fit, the SAME
|
||||
// scrollbar.Model = text.Scroll linkage
|
||||
// CharacterCreationSummaryPage's how-to box already uses.
|
||||
_helpText = Find<UiText>(pageRoot, HelpTextId);
|
||||
if (_helpText is not null)
|
||||
{
|
||||
_helpText.PreserveEndOnLayout = false;
|
||||
if (Find<UiScrollbar>(_helpText, HelpScrollRelativeId) is { } helpScroll)
|
||||
helpScroll.Model = _helpText.Scroll;
|
||||
}
|
||||
|
||||
ApplyChoiceVisibility();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -341,8 +341,46 @@ internal sealed class CharacterCreationSkillsPage : IDisposable
|
|||
infoTitle.VerticalJustify = VJustify.Top;
|
||||
if (_infoText is { } infoText)
|
||||
infoText.VerticalJustify = VJustify.Top;
|
||||
|
||||
// R4-3 (Campaign CC gate round 1 re-test 3): the description pane's
|
||||
// own raw box (0x100003fc, Y=460 H=100 -> bottom Y=560, live-DAT-
|
||||
// measured) extends 20px PAST the bottom of the gold decorative
|
||||
// frame that visually contains BOTH info panes (0x100003fa, Y=430
|
||||
// H=110 -> bottom Y=540, the SAME GF-12 corner/edge sprite family
|
||||
// Batch C un-consumed — 0x100002de-e3/0x100000e8/0xea). Retail's own
|
||||
// ShowSkillsText @0x00481250 has NO code relationship between the
|
||||
// text panes and this frame (SetText only; no clip/size handoff),
|
||||
// and the frame's 8 children carry no dat property linking them to
|
||||
// 0x100003fc either — so the frame's own geometry is the only
|
||||
// authored ground truth for "the visible box," and this port's
|
||||
// multi-line clip (UiText.DrawText's own PushClip(0,0,Width,Height))
|
||||
// was using the WRONG (larger, unbounded) Height, letting a long
|
||||
// skill's formula line draw into blank page space below the frame's
|
||||
// own border instead of being contained by it. Clamped to the
|
||||
// frame's own bottom edge (never grows it — additive, defensive if a
|
||||
// future dat re-extract makes the frame taller than the pane).
|
||||
// Scoped exactly like the VJustify.Top correction above: this is
|
||||
// NOT the client-wide "does a text pane's clip account for a
|
||||
// sibling decorative frame" mechanism (no evidence any other pane in
|
||||
// this codebase has the SAME independently-authored-taller-than-its-
|
||||
// frame shape), so a general import-time fix is unwarranted here.
|
||||
if (_infoText is { } clampedInfoText
|
||||
&& UiElement.FindDescendant(pageRoot, InfoBoxFrameElementId) is { } frame)
|
||||
{
|
||||
float frameBottom = frame.Top + frame.Height;
|
||||
float paneBottom = clampedInfoText.Top + clampedInfoText.Height;
|
||||
if (frameBottom < paneBottom)
|
||||
clampedInfoText.Height = frameBottom - clampedInfoText.Top;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The gold decorative frame (Type 12, 8 sprite children — the SAME
|
||||
/// GF-12 corner/edge family) that visually contains BOTH info panes
|
||||
/// (<c>0x100003fb</c>/<c>0x100003fc</c>) — see the R4-3 clamp above.
|
||||
/// </summary>
|
||||
private const uint InfoBoxFrameElementId = 0x100003FAu;
|
||||
|
||||
internal void Refresh(
|
||||
IRuntimeCharacterCreationView view,
|
||||
RuntimeCharacterCreationSnapshot snapshot)
|
||||
|
|
|
|||
|
|
@ -1009,14 +1009,29 @@ public static class DatWidgetFactory
|
|||
child => child.Type == 12u && child.StateMedia.Count == 0);
|
||||
if (valueChild is not null)
|
||||
{
|
||||
button.ValueBox = (valueChild.X, valueChild.Y, valueChild.Width, valueChild.Height);
|
||||
// R4-1 (Campaign CC gate round 1 re-test 3): reflow the value
|
||||
// child's authored rect through retail's own raw-edge policy
|
||||
// (UIElement::UpdateForParentSizeChange @0x00462640, ported
|
||||
// as UiLayoutPolicy) before it becomes ValueBox — see
|
||||
// ReflowValueChildRect's own doc for why this is needed and
|
||||
// decomp-cited.
|
||||
button.ValueBox = ReflowValueChildRect(valueChild, info);
|
||||
button.ValueFont = valueChild.FontDid != 0u && fontResolve is not null
|
||||
? fontResolve(valueChild.FontDid) ?? elementFont
|
||||
: elementFont;
|
||||
button.ValueColor = valueChild.FontColor ?? System.Numerics.Vector4.One;
|
||||
button.ValueAlign = valueChild.HJustify == HJustify.Left
|
||||
? UiButton.LabelAlignment.Left
|
||||
: UiButton.LabelAlignment.Center;
|
||||
button.ValueAlign = valueChild.HJustify switch
|
||||
{
|
||||
HJustify.Left => UiButton.LabelAlignment.Left,
|
||||
// R4-1: HJustify.Right (raw dat 3/5) previously fell into
|
||||
// this ternary's Center branch — CalcJustification's own
|
||||
// ecx_5==3||5 case is a DISTINCT far-edge formula (see
|
||||
// UiButton.LabelAlignment.Right's own doc), and every
|
||||
// value child in this family (0x100002f1/0x100002f3)
|
||||
// authors HJustify Right, live-DAT-confirmed.
|
||||
HJustify.Right => UiButton.LabelAlignment.Right,
|
||||
_ => UiButton.LabelAlignment.Center,
|
||||
};
|
||||
// Seed with whatever the child itself authors (typically
|
||||
// blank) so an unbound button doesn't draw stray leftover
|
||||
// text before a controller writes a real value.
|
||||
|
|
@ -1027,6 +1042,60 @@ public static class DatWidgetFactory
|
|||
return button;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-1 (Campaign CC gate round 1 re-test 3): the "Available Skill
|
||||
/// Credits" value overlapped mid-caption ("Available Skill0Credits")
|
||||
/// because <see cref="UiButton.ValueBox"/> was built from the value
|
||||
/// child's RAW authored rect, un-reflowed. Live-DAT probe: the value
|
||||
/// child (<c>0x100002f3</c>) is BASE-INHERITED across four sibling
|
||||
/// buttons of DIFFERING widths — Health/Stamina/Mana at 150px share the
|
||||
/// exact same child id/rect (local X=116) as the wider, 231px Skills
|
||||
/// credits button, and the child's own <c>OriginalParentWidth</c> (the
|
||||
/// design-time parent size baked in at whichever button FIRST resolved
|
||||
/// it — 150, matching Health's own actual width) diverges from Skills
|
||||
/// credits' actual current parent width (231) — exactly the shape
|
||||
/// <see cref="UiLayoutPolicy"/> (retail
|
||||
/// <c>UIElement::UpdateForParentSizeChange @0x00462640</c>, already the
|
||||
/// production raw-edge reflow for live mounted elements via
|
||||
/// <see cref="UiElement.ApplyAnchor"/>) exists to correct. The child's
|
||||
/// own edge modes (Left=2/Right=1, live-DAT-confirmed) are retail's
|
||||
/// "track the far edge as the parent grows" reflow: applying them moves
|
||||
/// the value box from local X=116 to X=197 for Skills credits — landing
|
||||
/// immediately after the caption's own measured end (~x=196,
|
||||
/// <c>SkillsCreditsButton_CaptionFitsFullWidth_ValueChildStartsAtMidpoint</c>)
|
||||
/// instead of colliding mid-caption. Health/Stamina/Mana and the
|
||||
/// Attribute/Credits value child (whose OWN OriginalParentWidth already
|
||||
/// matches their actual parent, or whose edge modes are all 0/fixed)
|
||||
/// reflow to their byte-identical raw rect (deltaX=0 or mode-0 passthrough)
|
||||
/// — this is additive for every already-correct button, not a per-button
|
||||
/// special case.
|
||||
/// </summary>
|
||||
private static (float X, float Y, float Width, float Height) ReflowValueChildRect(
|
||||
ElementInfo child, ElementInfo parent)
|
||||
{
|
||||
float originalParentWidth = child.HasOriginalParentSize ? child.OriginalParentWidth : parent.Width;
|
||||
float originalParentHeight = child.HasOriginalParentSize ? child.OriginalParentHeight : parent.Height;
|
||||
|
||||
var originalChild = UiPixelRect.FromPositionAndSize(
|
||||
(int)child.X, (int)child.Y, (int)child.Width, (int)child.Height);
|
||||
var originalParent = UiPixelRect.FromPositionAndSize(
|
||||
0, 0, (int)originalParentWidth, (int)originalParentHeight);
|
||||
var currentParent = UiPixelRect.FromPositionAndSize(
|
||||
0, 0, (int)parent.Width, (int)parent.Height);
|
||||
// Empty (Width=0/Height=0) "current child" so the static Apply's
|
||||
// currentChild-preservation branch never engages — every axis comes
|
||||
// from the Near/Far formula, matching mode 0's own "keep the raw
|
||||
// authored edge" default for the (frequent) no-anchor case.
|
||||
var noCurrentChild = new UiPixelRect(0, 0, -1, -1);
|
||||
|
||||
UiPixelRect reflowed = UiLayoutPolicy.Apply(
|
||||
child.Left, child.Top, child.Right, child.Bottom,
|
||||
originalChild, originalParent,
|
||||
noCurrentChild, currentParent);
|
||||
|
||||
return (reflowed.X0, reflowed.Y0, reflowed.Width, reflowed.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail UIOption_Checkbox is a UIElement_Button whose visible face is its
|
||||
/// authored indicator child. Its label lives on the option object rather than
|
||||
|
|
|
|||
|
|
@ -260,8 +260,18 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
/// <see cref="ValueBox"/> — the lifted child's own authored justify.</summary>
|
||||
public LabelAlignment ValueAlign { get; set; } = LabelAlignment.Center;
|
||||
|
||||
/// <summary>Label horizontal alignment options.</summary>
|
||||
public enum LabelAlignment { Center, Left }
|
||||
/// <summary>
|
||||
/// Label horizontal alignment options. <see cref="Right"/> (R4-1, Campaign
|
||||
/// CC gate round 1 re-test 3) is ValueLabel-only today — every value
|
||||
/// child on the chargen credit-display family (0x100002f1/0x100002f3)
|
||||
/// authors dat HJustify Right (raw 3/5), decomp-confirmed by
|
||||
/// <c>UIElement_Text::CalcJustification @0x00467260</c>'s
|
||||
/// <c>ecx_5==3||5</c> branch (<c>edi = availWidth - textWidth</c>, i.e.
|
||||
/// flush to the box's own far edge) — distinct from Center's halved
|
||||
/// offset. <see cref="LabelAlign"/> never authors Right today so no
|
||||
/// existing switch over it needs a new arm.
|
||||
/// </summary>
|
||||
public enum LabelAlignment { Center, Left, Right }
|
||||
|
||||
public bool ToggleBehavior { get; }
|
||||
public bool RolloverEnabled { get; }
|
||||
|
|
@ -568,9 +578,17 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
float boxY = ValueBox?.Y ?? 0f;
|
||||
float boxWidth = ValueBox?.Width ?? Width;
|
||||
float boxHeight = ValueBox?.Height ?? Height;
|
||||
float vx = ValueAlign == LabelAlignment.Left
|
||||
? boxX + LabelOffsetX
|
||||
: boxX + (boxWidth - vf.MeasureWidth(value)) * 0.5f;
|
||||
float valueWidth = vf.MeasureWidth(value);
|
||||
// R4-1: Right mirrors CalcJustification's own far-edge formula
|
||||
// (box's own right edge minus the measured text width, no
|
||||
// decorative inset — the decomp's Right branch adds none either,
|
||||
// and this box carries no threaded marginR of its own).
|
||||
float vx = ValueAlign switch
|
||||
{
|
||||
LabelAlignment.Left => boxX + LabelOffsetX,
|
||||
LabelAlignment.Right => boxX + boxWidth - valueWidth,
|
||||
_ => boxX + (boxWidth - valueWidth) * 0.5f,
|
||||
};
|
||||
float vy = boxY + (boxHeight - vf.LineHeight) * 0.5f;
|
||||
ctx.DrawStringDat(vf, value, vx, vy, ValueColor, Outline, OutlineColor);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,11 +286,60 @@ public sealed class UiScrollbar : UiElement
|
|||
}
|
||||
else
|
||||
{
|
||||
DrawTiled(ctx, resolve, ThumbSprite, 0f, ty, Width, th);
|
||||
// R4-2 (Campaign CC gate round 1 re-test 3): the single-
|
||||
// sprite thumb shape (no top/bottom caps — see this method's
|
||||
// own doc, the R3-4/R3-7 fallback family: Skills listbox
|
||||
// 0x100003f8, Summary overview 0x10000401, Summary how-to
|
||||
// 0x100002e7) is a small fixed "diamond" marker graphic, NOT
|
||||
// a stretchy bar — DrawTiled's UV-repeat was drawing it
|
||||
// MULTIPLE times to fill the track-proportional thumb rect
|
||||
// (~9 repeats on Summary's overview bar, ~2 on Skills, per
|
||||
// the live capture). DrawThumbMarker draws exactly ONE
|
||||
// instance at its own native size.
|
||||
DrawThumbMarker(ctx, resolve, ThumbSprite, 0f, ty, Width, th, vertical: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-2 (Campaign CC gate round 1 re-test 3): draws ONE instance of a
|
||||
/// single-sprite scrollbar thumb at its own native size, centered
|
||||
/// within the computed thumb rect (<see cref="ThumbRect"/>'s own
|
||||
/// decomp-cited <c>UIElement_Scrollbar::UpdateLayout @0x4710d0</c>
|
||||
/// track-proportional geometry stays unchanged — this only changes HOW
|
||||
/// the sprite fills that rect). Neither <see cref="DrawTiled"/> (UV-
|
||||
/// repeat — draws the small marker graphic several times to fill a
|
||||
/// large proportional thumb rect, R4-2's own "tiled diamonds" report)
|
||||
/// nor a naive 1:1 stretch across the full computed rect (would distort
|
||||
/// a small marker into an elongated bar) is correct for this shape —
|
||||
/// <paramref name="vertical"/> selects which
|
||||
/// axis is being filled/centered: a vertical scrollbar's thumb rect
|
||||
/// varies in height (X/Width stay the bar's own full width, matching
|
||||
/// every other draw call in this class), a horizontal one varies in
|
||||
/// width (Y/Height stay the bar's own full height).
|
||||
/// </summary>
|
||||
private void DrawThumbMarker(
|
||||
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint id, float rectX, float rectY, float rectW, float rectH, bool vertical)
|
||||
{
|
||||
if (id == 0 || rectW <= 0f || rectH <= 0f) return;
|
||||
var (tex, nativeW, nativeH) = resolve(id);
|
||||
if (tex == 0 || nativeW == 0 || nativeH == 0) return;
|
||||
|
||||
if (vertical)
|
||||
{
|
||||
float drawH = MathF.Min(nativeH, rectH);
|
||||
float y = rectY + (rectH - drawH) * 0.5f;
|
||||
ctx.DrawSprite(tex, rectX, y, rectW, drawH, 0f, 0f, rectW / nativeW, drawH / nativeH, Vector4.One);
|
||||
}
|
||||
else
|
||||
{
|
||||
float drawW = MathF.Min(nativeW, rectW);
|
||||
float x = rectX + (rectW - drawW) * 0.5f;
|
||||
ctx.DrawSprite(tex, x, rectY, drawW, rectH, 0f, 0f, drawW / nativeW, rectH / nativeH, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHorizontalModel(
|
||||
UiRenderContext ctx,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
|
|
@ -315,7 +364,8 @@ public sealed class UiScrollbar : UiElement
|
|||
}
|
||||
else
|
||||
{
|
||||
DrawTiled(ctx, resolve, ThumbSprite, tx, 0f, tw, Height);
|
||||
// R4-2: horizontal counterpart of the vertical fallback above.
|
||||
DrawThumbMarker(ctx, resolve, ThumbSprite, tx, 0f, tw, Height, vertical: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue