From 71bf24fb6f11bf537f8bcc464a2b3dd0a9f1c316 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 15 Aug 2026 09:42:53 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20Campaign=20LA=20gate=20round=202=20?= =?UTF-8?q?=E2=80=94=20character-select=20root=20background=20stretches,?= =?UTF-8?q?=20never=20tiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../retail-divergence-register.md | 3 +- .../Layout/CharacterManagementUiController.cs | 10 ++ src/AcDream.App/UI/Layout/UiDatElement.cs | 78 +++++++++++- .../Layout/CharacterManagementLiveDatTests.cs | 43 +++++++ .../CharacterManagementUiControllerTests.cs | 16 +++ .../UI/Layout/UiDatElementTests.cs | 111 ++++++++++++++++++ 6 files changed, 256 insertions(+), 5 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index f9b38e6b..981a496f 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -63,7 +63,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 73 active rows (AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) +## 2. Adaptation (AD) — 74 active rows (AD-98 filed 2026-08-15 at Campaign LA gate round 2 — the char-select root's own background stretches instead of tiling by resizing the mounted root element to the live viewport and marking its background quad UV 0..1, substituting for retail's fixed-800x600-canvas-stretched-at-presentation mechanism which acdream's live-resolution render pipeline has no analogue for; AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate visible-cell availability, full-catalog containment-root validation, and the @@ -189,6 +189,7 @@ readiness/requeue adaptation. See | AD-92 | **Filed 2026-08-13 at the #376/#388 review fix round (blast M6 / mechanism M4).** Two switcher adaptations with no retail counterpart: (1) the fullscreen refresh rate is the monitor's HIGHEST for the picked WxH — retail passed the device mode's own refresh as-is (`Device::ForceDisplayResolution`); (2) an invalid/unsupported fullscreen request is a logged refusal that leaves the window unchanged — retail attempted the switch and surfaced the device error. The persisted-flag divergence a refusal leaves behind is ISSUES #392. | `src/AcDream.App/Settings/DisplayModeSwitching.cs` (`TryFindRefreshRate`, the refusal paths); `src/AcDream.App/Settings/RuntimeSettingsTargets.cs` (`Apply`'s refused-mode logging) | Highest-refresh is strictly better on modern variable-refresh panels (retail predates them); refuse-and-log is #388's own no-crash requirement. | A capture comparing retail's exact chosen refresh for a mode will differ; a server/tooling flow expecting an error dialog on an invalid mode sees a console line instead. | `Device::ForceDisplayResolution @gmClient::Init 0x004047af`; docs/research/2026-08-13-376-388-{mechanism,blast}-review.md | | AD-94 | **Filed 2026-08-14 at the secure-trade feature.** Retail's `Event_AcceptTrade` payload (`Trade::Pack @0x005B9FF0`) appends two `PackableList` staged-item lists after the six fixed fields; acdream sends both as ZERO-COUNT lists. ACE parses and then discards the ENTIRE payload (`HandleActionAcceptTrade()` takes zero arguments — server trade state is fully self-derived; lane B §quirks), so the difference is unobservable against ACE; a byte-capture comparison against a real retail client would differ from offset 40. | `src/AcDream.Core.Net/Messages/TradeRequests.cs` (`BuildAcceptTrade`) | The `ContentProfile` pack layout was not byte-verified (ACE never reads it — no reader to check against), and guessing a wire struct violates the workflow; zero-count lists are well-formed `PackableList`s. | A future server that actually validates the accept echo would see empty item lists and could refuse or desync the accept. | `Trade::Pack @0x005B9FF0`; `GameActionAcceptTrade.cs:11-16`; `docs/research/2026-08-14-trade-laneB-wire.md` Table 1 | | AD-96 | **Filed 2026-08-14 at the OP8 re-gate fix round (key-name display).** Retail's `GetNameFromKey_Internal @0x00687800` falls back from the DAT string tables (key enum 4 → `0x2300000A`, meta enum 5 → `0x2300000B`) to the OS keyboard layout's own key name via DirectInput `IDirectInputDevice8::GetObjectInfo` (`tszName` — "SKIFT" on a Swedish layout). acdream reads the SAME layout-resident name data through Win32 `GetKeyNameTextW` instead (no DirectInput device exists in-process); on non-Windows hosts there is no OS lookup at all and the DIK-suffix spelling shows (un-localized English, e.g. "LSHIFT"). Mouse chords keep the pre-existing enum spelling — retail names them through the DirectInput mouse device. | `src/AcDream.App/Platform/PlatformKeyNameProvider.cs`; `src/AcDream.App/UI/Layout/RetailKeyNames.cs` (`Describe`, the mouse-device early-out) | GetKeyNameText and DirectInput's key names both come from the active keyboard-layout tables; adding a DirectInput device solely for name strings would be a heavyweight, dead-end dependency. Linux graphical work is parked at Slice L1. | A key whose GetKeyNameTextW name differs from DirectInput's `tszName` on some layout shows a slightly different caption than retail did; Linux graphical shows English DIK-suffix names where retail-on-Wine would localize; a mouse-chord caption reads as the Silk enum, not retail's device string. | `CInputManager_WIN32::GetNameFromKey_Internal @0x00687800`; `GetNameFromKey @0x00687F40`; `ControlSpecification::GetDIKName @0x0068ACB0`; `DBCache::GetDIDFromEnumStatic` category-4 probe 2026-08-14 (`KeyboardConfigLiveMountProbeTests.ProbeKeyboardFontsAndKeyNameStrings`) | +| AD-98 | **Filed 2026-08-15 at Campaign LA gate round 2 (character-select background tiling).** The LA8 root (0x1000039A) authors LeftEdge=TopEdge=RightEdge=BottomEdge=0 ("no anchor") in the installed DAT, so retail's own `UIElement::UpdateForParentSizeChange` (0x00462640) never resizes this element — it stays a fixed 800x600 rect in retail's own widget tree. Retail's generic 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 ~line 3135, and `MD_Data_Image::m_drawMode`/`DrawModeType` — both are COLOR-blend selectors, not tile-vs-stretch geometry modes). The only way retail's whole pre-world scene (background AND buttons AND listbox together) can still fill an arbitrary window resolution with no element ever resizing and a blitter that can only copy-or-tile is that these "flow" screens render into a fixed 800x600 target and the WHOLE FRAME is stretched once at presentation, outside the UI element/sprite system. 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, and `UiDatElement.StretchOwnBackgroundToFill` makes that resized root's own background draw as one UV-0..1 quad instead of tiling. | `src/AcDream.App/UI/Layout/UiDatElement.cs` (`StretchOwnBackgroundToFill`, `OnDraw`); `src/AcDream.App/UI/Layout/CharacterManagementUiController.cs` (constructor) | Reproducing retail's real mechanism (a fixed 800x600 UI render target scaled at presentation) would touch the render/swapchain pipeline (`GameWindow`, framebuffer setup) far beyond a background-draw fix; resizing the mounted root and stretching only ITS OWN background quad reaches the identical visual result (no tiling, non-uniform fill matching every resolution) confirmed against the installed DAT's zero edge-anchors and the decompiled blitter's copy-or-tile-only behavior. | If acdream ever gains a genuine fixed-resolution UI render target + present-time scale pass, this flag becomes redundant (every root would already present pre-stretched) and should be deleted along with the per-root resize in `CharacterManagementUiController`. Until then, any OTHER screen-level root mounted the same way (a future login/disconnected/datapatch screen) needs the same flag set explicitly — it is not automatic for arbitrary `UiDatElement`s. | `Graphic::Draw` 0x00693b20; `Graphic::PutImage` 0x00693a30; `UIElement::UpdateForParentSizeChange` 0x00462640; `BlitMode` acclient.h ~3135; `UIElementManager::CreateRootElement` 0x0045d020 (`UIElement::SetIsRootElement`); `CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf` | | AD-97 | **Filed 2026-08-14 at Campaign LA slice LA7a (character-restore request tail).** Retail's `CharacterRestore` request (`0xF7D9`) is ≥16 bytes: `CPlayerSystem::RestoreCharacter @0x0055d760` is, in the PDB-paired binary, `push 0x008173B4; push 0x008173B4; push guid; call Proto_UI::SendAdminRestoreCharacter @0x00546cf0`, and the callee packs BOTH constant `PStringBase*` arguments (`PStringBase::Pack @0x004fc6f0` emits ≥4 bytes even empty). Binary Ninja renders the two pushes as an uninitialized `edx` local plus `this` — a rendering artifact around constant `0x008173B4` (all 3 of its other pseudo-C appearances sit in provably-broken decompiles), but the arguments are real. acdream sends the 8-byte guid-only form. What the two constant strings contain is unresolved (a live cdb `db poi(0x008173b4)` would settle it). | `src/AcDream.Core.Net/Messages/CharacterRestore.cs` (`BuildRequestBody`) | ACE reads only `ReadUInt32()` and ignores any tail (`CharacterHandler.cs:331-385`), and holtburger ships guid-only from a real client command path against ACE successfully — the tail is unread by every server we can test against, and packing two strings whose CONTENT we cannot verify would be a guess. | A byte-capture comparison against a real retail client differs from offset 8; a future server that validates the full retail shape would reject our 8-byte request. | `CPlayerSystem::RestoreCharacter @0x0055d760` (binary bytes, not the BN rendering); `Proto_UI::SendAdminRestoreCharacter @0x00546cf0`; `PStringBase::Pack @0x004fc6f0`; ACE `CharacterHandler.cs:331-385`; holtburger `character_selection.rs:79-82`; LA7a Opus review F1 (2026-08-14) | | AD-93 | **Filed 2026-08-13 at social gate round 2, item 5 (the refused-drop notice port).** Two narrow gaps in the `ServerSaysAttemptFailed @0x0058EAE0` port: (1) **latched-guid preference** — retail's 0x00A0 dispatcher (`@0x0055B342`) PREFERS `prevRequestObjectID` over the wire guid when picking the item to name; acdream's `InventoryTransactionState.OnMoveFailed` instead REQUIRES the wire guid to match the latch (unobservable against ACE, which always sends the request's own guid on 0x00A0, and it protects a stale latch from mislabeling an unrelated failure — acdream has no retail-style latch timeout). (2) **unlatched request kinds** — retail latches `IR_MOVE`/`IR_WIELD` too; acdream's kind enum has no Move/Wield rows because wields ride `AutoWieldController` outside the single-request gate, so a refused wield/3D-move shows only the generic `HandleFailureEvent` leg, never "The X can't be wielded/moved". | `src/AcDream.Core/Items/InventoryTransactionState.cs` (`OnMoveFailed`); `src/AcDream.Core/Chat/InventoryFailureMessages.cs` (`Compose`'s absent Move/Wield rows); `src/AcDream.App/UI/ItemInteractionController.cs` (`OnInventoryRequestFailed`) | The match requirement is the compensating guard for the missing latch timeout; adding Wield/Move kinds means routing those sends through the single-request gate they deliberately bypass today — a behavior change beyond this gate item. | Only observable against a server that sends 0x00A0 with a guid that differs from the request's item (ACE never does), or on a refused wield/move, which shows no "can't be wielded/moved" verb line where retail would show one. | `ACCWeenieObject::ServerSaysAttemptFailed @0x0058EAE0`; the 0x00A0 dispatcher `@0x0055B342`; `ACCWeenieObject::RecordRequest @0x0058C220`; `docs/research/2026-08-13-confirm-and-weenie-error-display.md` §2 | diff --git a/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs b/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs index cd5aa341..ff5b34a7 100644 --- a/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs +++ b/src/AcDream.App/UI/Layout/CharacterManagementUiController.cs @@ -83,6 +83,16 @@ internal sealed class CharacterManagementUiController : IDisposable Root.ClickThrough = false; Root.Visible = false; + // Campaign LA gate round 2: this root is resized to the live viewport just + // above, which is bigger than its authored 800x600 canvas at almost every + // real resolution. Its own DirectState background (RenderSurface 0x06007576) + // must scale to fill that resized rect, not tile — see + // UiDatElement.StretchOwnBackgroundToFill's doc comment for the retail + // mechanism (a fixed-canvas screen stretched once at presentation) this + // substitutes. + if (Root is UiDatElement rootBackground) + rootBackground.StretchOwnBackgroundToFill = true; + // Create Character belongs to a future campaign. Keep retail's // authored control in place and visibly ghosted; do not hide it or // invent an action. diff --git a/src/AcDream.App/UI/Layout/UiDatElement.cs b/src/AcDream.App/UI/Layout/UiDatElement.cs index 584a4d5e..2277e087 100644 --- a/src/AcDream.App/UI/Layout/UiDatElement.cs +++ b/src/AcDream.App/UI/Layout/UiDatElement.cs @@ -205,6 +205,63 @@ public class UiDatElement : UiElement, IUiDatStateful /// public uint? RuntimeImageTexture { get; set; } + /// + /// When true, this element's OWN active-state background media draws as ONE quad + /// stretched to exactly fill / + /// (UV span 0,0 .. 1,1) instead of the native-pixel TILE formula every other + /// uses. Default false — every ordinary dat chrome/ + /// container element (corners, edges, drag bars, tab backdrops) keeps tiling. + /// + /// + /// Campaign LA gate round 2 (issue found in the live client: the LA8 + /// character-select background repeated across the window instead of scaling + /// with it). Retail's generic UI sprite blit — + /// Graphic::Draw (acclient 0x00693b20) dispatching to + /// Graphic::PutImage (0x00693a30) for an exact/undersized destination, or a + /// modulo-wrapped tile loop otherwise — has exactly two behaviors, copy or tile; + /// it can never scale a source image up to a larger destination. This is confirmed + /// against two candidate "draw-mode" fields that could have carried a stretch bit + /// and don't: BlitMode (acclient.h ~line 3135 — Blit_Normal/3Alpha/4Alpha/ + /// Colorize/Multiply/Screen/Grayscale/NOP are all COLOR-BLEND selectors) and + /// MD_Data_Image::m_drawMode/DrawModeType (Undefined/Normal/Overlay/ + /// Alphablend — also a blend selector; the "Normal → tile" reading in + /// docs/research/2026-06-15-layoutdesc-format.md §6 cited + /// ImgTex::TileCSI (0x0053e740), but that function is exclusively called from + /// TexMerge::CopyAndTile/ImgTex::CopyCSI for LAND-SURFACE terrain + /// texture compositing (TerrainTex) — never from the UI element system; the + /// citation was a coincidental name match, not the real call site). + /// + /// + /// + /// The LA8 root itself (0x1000039A) authors LeftEdge=TopEdge=RightEdge=BottomEdge=0 + /// ("no anchor" — confirmed against the installed DAT via + /// CharacterManagementLiveDatTests.RootAuthorsNoEdgeAnchors_RetailNeverResizesItSelf), + /// so retail's own UIElement::UpdateForParentSizeChange (0x00462640) never + /// touches this element's size at all — it stays a fixed 800x600 rect. The only way + /// retail's whole pre-world "flow" scene (background AND buttons AND listbox + /// together — "the background scales with the root") can still fill an arbitrary + /// window resolution edge-to-edge, with the generic sprite blit only ever able to + /// copy-or-tile, is that these screens render into a fixed, authored-size (800x600) + /// target and the WHOLE FRAME is stretched once at presentation — a step entirely + /// outside the UIRegion/Graphic::Draw sprite system. + /// + /// + /// + /// acdream has no offscreen fixed-resolution UI render target / present-time scale + /// pass — instead + /// resizes the MOUNTED ROOT ELEMENT itself to the live viewport (see its + /// constructor) so the screen still fills the window. This flag is the acknowledged + /// divergence for that substitution (register row: acdream resizes the element, + /// retail stretches the presented frame) — it makes the resized ROOT's own + /// background draw as one stretched quad so the VISUAL RESULT matches retail's + /// present-time stretch (no tiling) even though the MECHANISM differs. Set only on + /// a screen-level mounted root, never on an ordinary descendant/chrome element — + /// those keep the native tile formula, which IS what retail's own blit does for + /// content that lives inside the (in retail) fixed 800x600 canvas. + /// + /// + public bool StretchOwnBackgroundToFill { get; set; } + protected override void OnDraw(UiRenderContext ctx) { if (MediaVisible && RuntimeImageTexture is uint runtimeTexture) @@ -233,10 +290,23 @@ public class UiDatElement : UiElement, IUiDatStateful var (tex, tw, th) = _resolve(file); if (tex != 0 && tw != 0 && th != 0) { - // Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI - // texture), matching ImgTex::TileCSI. Overlay/Alphablend use the same blit (the - // sprite shader already alpha-blends). No Stretch mode exists in DrawModeType. - ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); + if (StretchOwnBackgroundToFill) + { + // One quad, UV 0..1 — see StretchOwnBackgroundToFill's doc comment + // for the retail mechanism this substitutes (a fixed-canvas screen + // stretched once at presentation). + ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, 1, 1, Vector4.One); + } + else + { + // Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped + // UI texture) — retail's Graphic::Draw/Graphic::PutImage (0x00693b20/ + // 0x00693a30) copy-or-tile blit; see StretchOwnBackgroundToFill's doc + // comment for the corrected citation (NOT ImgTex::TileCSI, which is + // land-surface-only). Overlay/Alphablend use the same blit (the sprite + // shader already alpha-blends). No Stretch mode exists in DrawModeType. + ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); + } } } diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementLiveDatTests.cs index 18266e64..4a0e911a 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementLiveDatTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementLiveDatTests.cs @@ -219,6 +219,49 @@ public sealed class CharacterManagementLiveDatTests + string.Join('\n', unresolved)); } + /// + /// 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 + /// UIElement::UpdateForParentSizeChange, 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 : + /// 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. + /// + [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( + 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; diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs index d9708e38..d7c77b57 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterManagementUiControllerTests.cs @@ -8,6 +8,22 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class CharacterManagementUiControllerTests { + /// + /// Campaign LA gate round 2: the constructor resizes Root to the host viewport + /// (see the constructor's Root.Width/Height block) — its own background must + /// therefore draw stretched, not tiled, or it visibly repeats at any resolution + /// bigger than the authored 800x600 canvas. See + /// . + /// + [Fact] + public void Constructor_MarksRootBackgroundToStretch_NotTile() + { + using var environment = new EnvironmentHarness(); + + var root = Assert.IsType(environment.Controller.Root); + Assert.True(root.StretchOwnBackgroundToFill); + } + [Fact] public void AuthoredChildContract_PreservesRuntimeOrderGreyTailHighlightAndButtonMatrix() { diff --git a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs index 5bf05158..9543e166 100644 --- a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs @@ -1,9 +1,120 @@ +using System.Numerics; +using AcDream.App.Rendering; +using AcDream.App.Rendering.Gpu; +using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.UI; using AcDream.App.UI.Layout; namespace AcDream.App.Tests.UI.Layout; public class UiDatElementTests { + private sealed class NullGpuFrameSource : ICurrentGpuFrameSource + { + public IGpuFrame? CurrentFrame => null; + } + + private static (TextRenderer renderer, UiRenderContext ctx) BuildRenderContext() + { + var device = new RecordingGpuDevice(); + var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); + renderer.Begin(new Vector2(1920f, 1080f)); + var ctx = new UiRenderContext(renderer, new Vector2(1920f, 1080f)); + return (renderer, ctx); + } + + /// + /// Campaign LA gate round 2: the char-select root's background (native 800x600, + /// resolved from a JPEG surface) was drawn with the ordinary UiDatElement TILE + /// UV formula (u1 = Width/tw) after CharacterManagementUiController resized the + /// root to the live viewport — at 1920x1080 that produces u1 = 2.4, v1 = 1.8, + /// which GL_REPEAT wraps into a visibly tiled background instead of one stretched + /// image. See 's doc comment + /// for the retail mechanism this substitutes. + /// + [Fact] + public void StretchOwnBackgroundToFill_False_TilesUvPastOne_WhenRectExceedsNativeSize() + { + var info = new ElementInfo { Width = 1920, Height = 1080 }; + info.StateMedia[""] = (0x06007576u, 1); + var e = new UiDatElement(info, _ => (7u, 800, 600)) + { + Left = 0, + Top = 0, + Width = 1920, + Height = 1080, + }; + + (TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext(); + e.DrawSelfAndChildren(ctx); + + var (texture, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts); + Assert.Equal(7u, texture); + // Vertex layout: x,y,u,v,r,g,b,a, 6 verts/quad. TextRenderer.AppendQuad emits + // vertex index 1 as (x+w, y+h, u1, v1) — the (u1,v1) far corner. + float uMax = verts[1 * 8 + 2]; + float vMax = verts[1 * 8 + 3]; + Assert.Equal(1920f / 800f, uMax, 3); + Assert.Equal(1080f / 600f, vMax, 3); + } + + /// + /// Campaign LA gate round 2 fix: with the flag set, the SAME oversized rect draws + /// as one quad spanning UV 0..1 — a single stretched image, matching retail's + /// observed (never-tiled) char-select background. + /// + [Fact] + public void StretchOwnBackgroundToFill_True_ClampsUvToOne_WhenRectExceedsNativeSize() + { + var info = new ElementInfo { Width = 1920, Height = 1080 }; + info.StateMedia[""] = (0x06007576u, 1); + var e = new UiDatElement(info, _ => (7u, 800, 600)) + { + Left = 0, + Top = 0, + Width = 1920, + Height = 1080, + StretchOwnBackgroundToFill = true, + }; + + (TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext(); + e.DrawSelfAndChildren(ctx); + + var (texture, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts); + Assert.Equal(7u, texture); + float uMax = verts[1 * 8 + 2]; + float vMax = verts[1 * 8 + 3]; + Assert.Equal(1f, uMax, 3); + Assert.Equal(1f, vMax, 3); + } + + /// + /// The flag must not change anything for an element whose rect already matches + /// its native texture size (every ordinary panel/window root today) — stretch + /// (UV 0..1) and tile (UV Width/tw) are numerically identical at that size, so + /// this only changes behavior for elements deliberately grown past their art. + /// + [Fact] + public void StretchOwnBackgroundToFill_True_MatchesTile_WhenRectEqualsNativeSize() + { + var info = new ElementInfo { Width = 800, Height = 600 }; + info.StateMedia[""] = (0x06007576u, 1); + var e = new UiDatElement(info, _ => (7u, 800, 600)) + { + Left = 0, + Top = 0, + Width = 800, + Height = 600, + StretchOwnBackgroundToFill = true, + }; + + (TextRenderer renderer, UiRenderContext ctx) = BuildRenderContext(); + e.DrawSelfAndChildren(ctx); + + var (_, verts) = Assert.Single(renderer.DebugSpriteSegmentVerts); + Assert.Equal(1f, verts[1 * 8 + 2], 3); + Assert.Equal(1f, verts[1 * 8 + 3], 3); + } + [Fact] public void ActiveMedia_PrefersNamedStateOverDirect() {