fix(ui): Campaign LA gate round 2 — character-select root background stretches, never tiles

The LA8 char-select root (0x1000039A) authors LeftEdge=TopEdge=RightEdge=
BottomEdge=0 ("no anchor") in the installed DAT — confirmed via the new
CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf
gate — so retail's own UIElement::UpdateForParentSizeChange (0x00462640) never
resizes this element; it stays a fixed 800x600 rect in retail's own tree.
Retail's generic UI sprite blit, Graphic::Draw (0x00693b20) dispatching to
Graphic::PutImage (0x00693a30) for an exact/undersized destination or a
modulo-wrapped tile loop otherwise, has no third "stretch" mode — confirmed
against BlitMode (acclient.h ~3135) and MD_Data_Image::m_drawMode/DrawModeType,
both COLOR-blend selectors, not tile-vs-stretch geometry modes. The prior
"Normal -> tile, matching ImgTex::TileCSI" citation in UiDatElement was a
mis-attribution: ImgTex::TileCSI (0x0053e740) is called exclusively from
TexMerge::CopyAndTile/ImgTex::CopyCSI for LAND-SURFACE terrain texture
compositing, never from the UI element system.

Given the dat authors zero resize anchors and the blitter can only copy or
tile, the only way retail's whole pre-world scene (background + buttons +
listbox together) fills an arbitrary window resolution is that these
fixed-canvas "flow" screens render at 800x600 and the WHOLE FRAME is
stretched once at presentation — outside the UI sprite system entirely.
acdream has no offscreen fixed-resolution UI render target / present-time
scale pass; CharacterManagementUiController's constructor instead resizes
the MOUNTED ROOT element itself to the live viewport, which is why its own
background tiled (Width/tw > 1 at any resolution above 800x600, wrapped by
GL_REPEAT).

Fix: UiDatElement gains StretchOwnBackgroundToFill (default false, every
ordinary chrome/container element keeps tiling) — when set, the element's
own DirectState background draws as one UV-0..1 quad instead of the native
tile formula. CharacterManagementUiController sets it on Root right where
Root is resized to the host viewport, reaching the same visual result as
retail's present-time stretch (no tiling, no aspect-preserving letterbox)
through a different mechanism. Divergence register row AD-98 records the
substitution.

Tests: three new UiDatElementTests pin the UV-span mechanism generically
(tile past 1.0 when unset and rect exceeds native size; clamped to 1.0 when
set; byte-identical to the old tile formula when rect equals native size,
so every unaffected panel is untouched). CharacterManagementUiControllerTests
pins Root.StretchOwnBackgroundToFill == true post-construction. The live-DAT
gate confirms the root's zero edge-anchors and Type=3 against the installed
DAT. AcDream.App.Tests: 5084 passed / 3 skipped with ACDREAM_PROBE_LIVE_MOUNT=1
(5081/6 skipped without it — the 3 live-DAT-gated tests skip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-15 09:42:53 +02:00
parent 936077d576
commit 71bf24fb6f
6 changed files with 256 additions and 5 deletions

View file

@ -219,6 +219,49 @@ public sealed class CharacterManagementLiveDatTests
+ string.Join('\n', unresolved));
}
/// <summary>
/// Campaign LA gate round 2 (background-tiling investigation): the raw dat
/// authors NO edge anchors at all on the char-select root (0x1000039A) — every
/// one of LeftEdge/TopEdge/RightEdge/BottomEdge is 0 ("no anchor" per
/// <c>UIElement::UpdateForParentSizeChange</c>, acclient 0x00462640). Retail's
/// own edge-anchor resize mechanism therefore NEVER touches this element's size;
/// it stays a fixed 800x600 rect in retail's own widget tree. This is the pivot
/// fact behind <see cref="AcDream.App.UI.Layout.UiDatElement.StretchOwnBackgroundToFill"/>:
/// since the dat itself asks for no resize, whatever makes the char-select scene
/// fill an arbitrary window resolution in retail (background AND buttons AND
/// listbox together) cannot be a per-element anchor/draw-mode difference — it has
/// to be an out-of-band presentation-time scale of the whole fixed-size frame.
/// acdream instead resizes the MOUNTED root itself (CharacterManagementUiController's
/// constructor) to reach the same visual fill, which is why the background needs
/// its own explicit stretch flag rather than an authored draw-mode bit.
/// </summary>
[InstalledDatFact]
public void RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf()
{
string datDirectory = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
using var dats = new DatCollection(datDirectory, DatAccessType.Read);
uint layoutDid = RetailDataIdResolver.Resolve(
dats,
CharacterManagementUiController.RootEnum,
5u);
ElementInfo rootInfo = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(
dats,
layoutDid,
CharacterManagementUiController.RootElementId));
Assert.Equal(0u, rootInfo.Left);
Assert.Equal(0u, rootInfo.Top);
Assert.Equal(0u, rootInfo.Right);
Assert.Equal(0u, rootInfo.Bottom);
Assert.Equal(3u, rootInfo.Type); // UIElement_Field — generic container, not a custom gm*UI class id
}
private static ElementInfo? FindById(ElementInfo info, uint id)
{
if (info.Id == id) return info;