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
|
|
@ -1673,6 +1673,36 @@ public sealed class CharacterCreationLiveDatTests
|
|||
Assert.Equal(116f, valueChild.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-1 (re-test 3): the raw authored value-child X (116, pinned above)
|
||||
/// is NOT where the value actually draws — <c>UiLayoutPolicy</c>'s
|
||||
/// raw-edge reflow (the value child's own Right-tracking edge modes
|
||||
/// against its base-inherited 150px <c>OriginalParentWidth</c> vs the
|
||||
/// Skills-credits button's actual 231px width) shifts it to X=197,
|
||||
/// landing right after the caption's own measured 193px span instead
|
||||
/// of colliding mid-caption ("Available Skill0Credits"). Health's own
|
||||
/// value child shares the SAME 150px OriginalParentWidth as its OWN
|
||||
/// actual 150px-wide button (no divergence), so it reflows to its
|
||||
/// byte-identical raw rect — proving the fix is additive, not a
|
||||
/// blanket shift.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SkillsCreditsButton_ValueBoxReflowsPastCaption_HealthValueBoxUnchanged()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiButton skillsCredits = Assert.IsType<UiButton>(screen.FindElement(0x100003F9u));
|
||||
Assert.Equal((197f, 0f, 34f, 28f), skillsCredits.ValueBox);
|
||||
Assert.Equal(UiButton.LabelAlignment.Right, skillsCredits.ValueAlign);
|
||||
|
||||
UiButton health = Assert.IsType<UiButton>(screen.FindElement(0x100003E3u));
|
||||
Assert.Equal((116f, 0f, 34f, 28f), health.ValueBox);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-3 (re-test 2): the info-box title (<c>0x100003fb</c>) and
|
||||
/// description (<c>0x100003fc</c>) panes' own AUTHORED boxes overlap —
|
||||
|
|
@ -1714,6 +1744,41 @@ public sealed class CharacterCreationLiveDatTests
|
|||
Assert.True(description.Y > title.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-3 (re-test 3): the description pane's own raw box (Y=460,
|
||||
/// H=100 -> bottom Y=560) extends PAST the bottom of the gold
|
||||
/// decorative frame that visually contains both info panes
|
||||
/// (<c>0x100003fa</c>, Y=430 H=110 -> bottom Y=540 — the SAME
|
||||
/// corner/edge sprite family GF-12 already renders,
|
||||
/// <c>0x100002de-e3</c>/<c>0x100000e8</c>/<c>0xea</c>). Pins the
|
||||
/// geometric mismatch itself (so a future DAT re-extract that removes
|
||||
/// it is visible) — <c>CharacterCreationSkillsPageTests</c>' own fixture
|
||||
/// covers the constructor's Height-clamp behavior against this exact
|
||||
/// shape.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SkillsInfoBoxFrame_ShorterThanDescriptionPane()
|
||||
{
|
||||
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 frame = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003FAu));
|
||||
ElementInfo description = Assert.IsType<ElementInfo>(FindInfo(rootInfo, 0x100003FCu));
|
||||
|
||||
float frameBottom = frame.Y + frame.Height;
|
||||
float paneBottom = description.Y + description.Height;
|
||||
Assert.True(
|
||||
frameBottom < paneBottom,
|
||||
$"expected the frame's own bottom (Y={frame.Y} H={frame.Height}, bottom={frameBottom}) to sit "
|
||||
+ $"ABOVE the description pane's own raw bottom (Y={description.Y} H={description.Height}, "
|
||||
+ $"bottom={paneBottom}) — if it no longer does, CharacterCreationSkillsPage's Height clamp "
|
||||
+ "may no longer be needed");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-4/R3-7 (re-test 2): retail authors TWO distinct
|
||||
/// <c>UIElement_Scrollbar</c> thumb shapes. Chat's own scrollbar
|
||||
|
|
|
|||
|
|
@ -457,6 +457,37 @@ public sealed class CharacterCreationUiControllerTests
|
|||
Assert.Equal(VJustify.Top, environment.SkillInfoText().VerticalJustify);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-3 (Campaign CC gate round 1 re-test 3): the description pane's
|
||||
/// own raw box (<c>0x100003fc</c>) is TALLER than the surrounding gold
|
||||
/// decorative frame that visually contains it (<c>0x100003fa</c> —
|
||||
/// live-DAT-measured, see <see cref="CharacterCreationSkillsPage"/>'s
|
||||
/// own R4-3 comment for the full geometry + decomp citation: retail's
|
||||
/// <c>ShowSkillsText</c> has no code relationship between the text
|
||||
/// panes and this frame, so the frame's own authored bottom edge is
|
||||
/// the only ground truth for "the visible box"). Before this fix, a
|
||||
/// long skill's formula line could draw into blank page space below
|
||||
/// the frame's own border — <c>BuildSkillsPage</c>'s fixture frame
|
||||
/// (Y=0 H=50) is deliberately shorter than <c>TextInfo</c>'s own
|
||||
/// default pane Height (60), so this pins the constructor clamping the
|
||||
/// live-mounted pane's own Height down to the frame's bottom edge
|
||||
/// (50) instead of leaving it at its own larger raw 60.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SkillsPage_InfoBoxDescriptionPane_HeightClampedToFrameBottom()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
|
||||
Assert.Equal(50f, environment.SkillInfoText().Height, 3f);
|
||||
// The title pane sits OUTSIDE the frame's own child range in this
|
||||
// fixture (a sibling, not touched by the clamp) — confirms the fix
|
||||
// is scoped to the description pane only, matching the constructor.
|
||||
Assert.Equal(60f, environment.SkillInfoTitle().Height, 3f);
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
|
@ -1229,6 +1260,34 @@ public sealed class CharacterCreationUiControllerTests
|
|||
environment.Button(CharacterCreationAppearancePage.RotateClockwiseId).OnClick!();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-4 (Campaign CC gate round 1 re-test 3): the framed help/
|
||||
/// instructions box (<c>0x100003ab</c>) — before this fix, this
|
||||
/// element was never touched by <see cref="CharacterCreationAppearancePage"/>'s
|
||||
/// constructor at all, so it kept <see cref="UiText"/>'s own chat-style
|
||||
/// default (<c>PreserveEndOnLayout=true</c>) and its own nested
|
||||
/// authored scrollbar (live-DAT-confirmed a direct Type-11 child,
|
||||
/// property <c>0x72</c>) was never wired to
|
||||
/// <see cref="UiText.Scroll"/>. Pins both halves of the fix: the box is
|
||||
/// no longer chat-style bottom-pinned, and the scrollbar's
|
||||
/// <see cref="UiScrollbar.Model"/> now points at the SAME
|
||||
/// <see cref="UiScrollable"/> the text itself scrolls.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AppearancePage_HelpText_TopOriented_AndOwnScrollbarIsWired()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
|
||||
UiText helpText = Assert.IsType<UiText>(
|
||||
environment.Screen.FindElement(CharacterCreationAppearancePage.HelpTextId));
|
||||
Assert.False(helpText.PreserveEndOnLayout);
|
||||
|
||||
UiScrollbar helpScroll = Assert.IsType<UiScrollbar>(
|
||||
UiElement.FindDescendant(helpText, 0x100002E7u));
|
||||
Assert.Same(helpText.Scroll, helpScroll.Model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-10 (Campaign CC gate round 1 Batch B): ports
|
||||
/// <c>gmCGAppearancePage::ZoomIn @0x0047CF00</c>
|
||||
|
|
@ -2813,6 +2872,21 @@ public sealed class CharacterCreationUiControllerTests
|
|||
page.Children.Add(ScrollbarInfo(0x100003F8u));
|
||||
page.Children.Add(ButtonInfo(0x100003F9u)); // credits badge
|
||||
page.Children.Add(TextInfo(0x100003FBu));
|
||||
// R4-3 (re-test 3): the description pane's own decorative frame
|
||||
// (0x100003fa, live-DAT-measured SHORTER than the pane it visually
|
||||
// contains — see CharacterCreationSkillsPage's own R4-3 comment).
|
||||
// Y=0/Height=50 here is deliberately shorter than TextInfo's own
|
||||
// default Height=60 so CharacterCreationSkillsPageTests can pin the
|
||||
// constructor's Height clamp without needing the real installed
|
||||
// DAT's exact pixel geometry.
|
||||
page.Children.Add(new ElementInfo
|
||||
{
|
||||
Id = 0x100003FAu,
|
||||
Type = 12u,
|
||||
Y = 0f,
|
||||
Width = 200f,
|
||||
Height = 50f,
|
||||
});
|
||||
page.Children.Add(TextInfo(0x100003FCu));
|
||||
return page;
|
||||
}
|
||||
|
|
@ -2912,6 +2986,22 @@ public sealed class CharacterCreationUiControllerTests
|
|||
page.Children.Add(ZoomButtonInfo(CharacterCreationAppearancePage.ZoomInId));
|
||||
page.Children.Add(ZoomButtonInfo(CharacterCreationAppearancePage.ZoomOutId));
|
||||
|
||||
// R4-4 (re-test 3): the framed instructions box, with its OWN
|
||||
// nested authored scrollbar child — live-DAT-confirmed shape (a
|
||||
// direct Type-11 child of the Type-12 text box, the SAME nesting
|
||||
// CharacterCreationSummaryPage's HowToScrollRelativeId already
|
||||
// uses). Deliberately taller than one view's worth so the long
|
||||
// static help paragraph genuinely overflows in the test below.
|
||||
var helpText = TextInfo(CharacterCreationAppearancePage.HelpTextId);
|
||||
var helpScrollInfo = new ElementInfo { Id = 0x100002E7u, Type = 11u, Width = 12f, Height = 40f };
|
||||
// UiText/UiField's own dat-children carve-out (LayoutImporter.BuildWidget)
|
||||
// only builds a child that carries its own authored StateMedia — the
|
||||
// SAME "genuinely renderable chrome, not swallowed prototype data"
|
||||
// gate the real scrollbar's own DirectState track sprite satisfies.
|
||||
helpScrollInfo.StateMedia[""] = (0x06001919u, 1);
|
||||
helpText.Children.Add(helpScrollInfo);
|
||||
page.Children.Add(helpText);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -499,6 +499,106 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal("42", button.ValueLabel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-1 (Campaign CC gate round 1 re-test 3): the value child
|
||||
/// (<c>0x100002f3</c>) is BASE-INHERITED across four sibling buttons of
|
||||
/// DIFFERING widths — Health/Stamina/Mana at 150px share the same
|
||||
/// child id/rect (local X=116) as the wider, 231px Skills credits
|
||||
/// button — so the child's own authored <c>OriginalParentWidth</c>
|
||||
/// (baked in at whichever button FIRST resolved it, 150) diverges from
|
||||
/// the ACTUAL containing button's current width (231) for exactly the
|
||||
/// wider button. Before this fix, <c>ValueBox</c> was the child's raw
|
||||
/// (un-reflowed) rect regardless — this fixture reproduces that exact
|
||||
/// shape (a 150px "design" width baked into the child, hosted under a
|
||||
/// 231px-wide button, with the child's own Right-tracking edge modes
|
||||
/// 2/1) and proves <c>UiLayoutPolicy</c>'s raw-edge reflow now shifts
|
||||
/// the value box by the SAME 81px the button grew (116 -> 197),
|
||||
/// landing clear of "Available Skill Credits"'s own measured span
|
||||
/// instead of colliding mid-caption ("Available Skill0Credits").
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildButton_ValueChildBaseInheritedNarrowerParent_ReflowsToWiderButton()
|
||||
{
|
||||
uint captionStringId = 333u;
|
||||
var info = new ElementInfo { Type = 1, Width = 231, Height = 28 };
|
||||
info.States[UiStateInfo.DirectStateId] = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
info.States[UiStateInfo.DirectStateId].Properties.Values[0x17u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.StringInfo,
|
||||
StringInfoValue = new UiStringInfoValue(0, captionStringId, 0, 0, 0, 0),
|
||||
};
|
||||
|
||||
var valueChild = new ElementInfo
|
||||
{
|
||||
Type = 12,
|
||||
X = 116,
|
||||
Y = 0,
|
||||
Width = 34,
|
||||
Height = 28,
|
||||
Left = 2,
|
||||
Top = 1,
|
||||
Right = 1,
|
||||
Bottom = 1,
|
||||
OriginalParentWidth = 150,
|
||||
OriginalParentHeight = 28,
|
||||
HasOriginalParentSize = true,
|
||||
HJustify = HJustify.Right,
|
||||
};
|
||||
info.Children.Add(valueChild);
|
||||
|
||||
var button = Assert.IsType<UiButton>(DatWidgetFactory.Create(
|
||||
info, NoTex, null,
|
||||
stringResolve: value => value.StringId == captionStringId ? "Available Skill Credits" : null));
|
||||
|
||||
Assert.Equal("Available Skill Credits", button.Label);
|
||||
// 116 + (231-150) = 197 -> width/height preserved (34/28).
|
||||
Assert.Equal((197f, 0f, 34f, 28f), button.ValueBox);
|
||||
Assert.Equal(UiButton.LabelAlignment.Right, button.ValueAlign);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Negative companion to the reflow test above: a value child whose
|
||||
/// OWN authored parent width already MATCHES the actual button (the
|
||||
/// Health/Stamina/Mana shape, un-widened) reflows to its byte-identical
|
||||
/// raw rect — delta is zero, so this is confirmed additive, not a
|
||||
/// blanket shift.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildButton_ValueChildOriginalParentMatchesActual_RectUnchanged()
|
||||
{
|
||||
uint captionStringId = 334u;
|
||||
var info = new ElementInfo { Type = 1, Width = 150, Height = 28 };
|
||||
info.States[UiStateInfo.DirectStateId] = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
info.States[UiStateInfo.DirectStateId].Properties.Values[0x17u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.StringInfo,
|
||||
StringInfoValue = new UiStringInfoValue(0, captionStringId, 0, 0, 0, 0),
|
||||
};
|
||||
|
||||
var valueChild = new ElementInfo
|
||||
{
|
||||
Type = 12,
|
||||
X = 116,
|
||||
Y = 0,
|
||||
Width = 34,
|
||||
Height = 28,
|
||||
Left = 2,
|
||||
Top = 1,
|
||||
Right = 1,
|
||||
Bottom = 1,
|
||||
OriginalParentWidth = 150,
|
||||
OriginalParentHeight = 28,
|
||||
HasOriginalParentSize = true,
|
||||
};
|
||||
info.Children.Add(valueChild);
|
||||
|
||||
var button = Assert.IsType<UiButton>(DatWidgetFactory.Create(
|
||||
info, NoTex, null,
|
||||
stringResolve: value => value.StringId == captionStringId ? "Health" : null));
|
||||
|
||||
Assert.Equal((116f, 0f, 34f, 28f), button.ValueBox);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Negative companion: a button whose caption was LIFTED from a
|
||||
/// distinct Type-12 child (the town-marker shape,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.App.UI;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -466,4 +471,56 @@ public class UiScrollbarTests
|
|||
Assert.Equal(expectedWidth, width, 3);
|
||||
}
|
||||
|
||||
private sealed class NullGpuFrameSource : ICurrentGpuFrameSource
|
||||
{
|
||||
public IGpuFrame? CurrentFrame => null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R4-2 (Campaign CC gate round 1 re-test 3): the re-test-2 single-
|
||||
/// sprite-thumb fallback (R3-4/R3-7, <see cref="UiScrollbar.OnDraw"/>'s
|
||||
/// no-cap-sprites branch) used to call the same UV-repeat
|
||||
/// <c>DrawTiled</c> the 3-slice middle tile uses — for a small fixed
|
||||
/// "diamond" marker sprite drawn into a MUCH taller track-proportional
|
||||
/// thumb rect, GL_REPEAT wrapping visibly tiled the marker several
|
||||
/// times down the track (Summary's overview bar ~9, Skills 2, per the
|
||||
/// live capture). Proves the fix draws exactly ONE quad for the thumb
|
||||
/// texture whose V range never exceeds native (1.0) — i.e. one
|
||||
/// unstretched, untiled sprite instance — even though the computed
|
||||
/// thumb rect (168px trackLen * ThumbRatio 0.75 = 126px, well past the
|
||||
/// sprite's native 16px) is far taller than the sprite.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SingleSpriteThumb_DrawsOneUntiledInstance_NotRepeatedDownTrack()
|
||||
{
|
||||
var device = new RecordingGpuDevice();
|
||||
var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused");
|
||||
renderer.Begin(new Vector2(800f, 600f));
|
||||
var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f));
|
||||
|
||||
const uint thumbTex = 42u;
|
||||
var model = new UiScrollable { ContentHeight = 200, ViewHeight = 150 };
|
||||
var bar = new UiScrollbar
|
||||
{
|
||||
Width = 16f,
|
||||
Height = 200f,
|
||||
SpriteResolve = id => id == thumbTex ? (thumbTex, 16, 16) : (0u, 0, 0),
|
||||
ThumbSprite = thumbTex,
|
||||
// ThumbTopSprite/ThumbBotSprite stay unset -> the R3-4/R3-7
|
||||
// single-sprite fallback shape (no 3-slice caps authored).
|
||||
Model = model,
|
||||
};
|
||||
|
||||
bar.DrawSelfAndChildren(ctx);
|
||||
|
||||
var thumbSegments = renderer.DebugSpriteSegmentVerts
|
||||
.Where(s => s.Texture == thumbTex)
|
||||
.ToArray();
|
||||
Assert.Single(thumbSegments);
|
||||
var verts = thumbSegments[0].Verts;
|
||||
// 8 floats/vertex (x,y,u,v,r,g,b,a), one quad = 6 vertices.
|
||||
Assert.Equal(6, verts.Count / 8);
|
||||
for (int i = 0; i < verts.Count; i += 8)
|
||||
Assert.True(verts[i + 3] <= 1.0001f, $"thumb sprite V={verts[i + 3]} exceeds native (tiled)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue