diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index 6452fd24..07790044 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -111,6 +111,13 @@ reverse ownership order. `UiHost` removes Silk device subscriptions first, then disposes the manager/controllers, then its GL renderer. `GameWindow.OnClosing` tears this runtime down before session and game-state sources. +`RetailUiRuntime` is the production composition boundary. `GameWindow` creates +the GL/DAT resolvers and supplies focused state/action binding records in one +mount call; the runtime owns all LayoutDesc imports, controller construction, +window registration, plugin mounts, cursor feedback, layout persistence, and the +retained tick/draw/restore/dispose paths. Panel-specific construction must not +move back into `GameWindow.OnLoad`. + --- ## Project Structure (current + target) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index d916c42e..90e9144d 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -54,7 +54,7 @@ accepted-divergence entries (#96, #49, #50). | IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms) | `src/AcDream.App/UI/README.md:3` (window mgr: `UiRoot.cs` RegisterWindow/Show/Hide/Toggle/BringToFront — F12 inventory toggle IS retail-faithful) | keystone.dll has no PDB/decomp; semantics reconstructed from the six `docs/research/retail-ui/` deep-dives, keeping retail's event-type constants so panel switch-cases transplant cleanly | Edge-case input semantics the research under-specified (drag threshold, tooltip timing, focus hand-off, capture corners) differ silently with no oracle to diff against | keystone.dll Device DAT_00837ff4; docs/research/retail-ui/04-input-events.md | | IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) | | IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` | -| IA-15 | D.2b gameplay UI is our own `UiHost`/`UiRoot` retained tree, not a byte-port of Keystone. `RetailWindowManager`/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, and schema-v2 per-character/per-resolution layout persistence; `RetailWindowFrame` is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals `0x2100006C`, chat `0x21000006`, toolbar `0x21000016`, character `0x2100002E`, inventory `0x21000023` plus mounted `0x21000024/22/21`, and radar `0x21000074`. | `src/AcDream.App/UI/RetailWindowManager.cs`; `src/AcDream.App/UI/RetainedPanelControllerGroup.cs`; `src/AcDream.App/UI/UiHost.cs`; `src/AcDream.App/UI/RetailWindowLayoutPersistence.cs`; `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/Layout/LayoutImporter.cs`; production mounts in `GameWindow.cs` | Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle. | Persistence is behaviorally reconstructed from `saveui/loadui` semantics rather than Keystone internals; lifecycle edge cases remain constrained by conformance tests instead of a byte-port | Production LayoutDesc objects; `docs/research/2026-06-15-layoutdesc-format.md`; Keystone behavior notes in `docs/research/retail-ui/` | +| IA-15 | D.2b gameplay UI is our own `UiHost`/`UiRoot` retained tree, not a byte-port of Keystone. `RetailUiRuntime` owns the production import/mount graph; `RetailWindowManager`/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, and schema-v2 per-character/per-resolution layout persistence; `RetailWindowFrame` is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals `0x2100006C`, chat `0x21000006`, toolbar `0x21000016`, character `0x2100002E`, inventory `0x21000023` plus mounted `0x21000024/22/21`, and radar `0x21000074`. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/RetailWindowManager.cs`; `src/AcDream.App/UI/RetainedPanelControllerGroup.cs`; `src/AcDream.App/UI/UiHost.cs`; `src/AcDream.App/UI/RetailWindowLayoutPersistence.cs`; `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/Layout/LayoutImporter.cs`; binding supply in `GameWindow.cs` | Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle. | Persistence is behaviorally reconstructed from `saveui/loadui` semantics rather than Keystone internals; lifecycle edge cases remain constrained by conformance tests instead of a byte-port | Production LayoutDesc objects; `docs/research/2026-06-15-layoutdesc-format.md`; Keystone behavior notes in `docs/research/retail-ui/` | | IA-17 | Toolbar chrome is toolkit-supplied through the central `RetailWindowFrame` mount (`UiCollapsibleFrame` 8-piece bevel) because LayoutDesc `0x21000016` carries no baked frame. It also supports a toolkit-defined collapse-to-one-row (bottom-edge resize snapping between a row-1-only and a two-row height, row-2 visibility tied to the stop) — retail's real collapse is keystone.dll (no decomp) and the DAT stacks both rows always. | `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/UiCollapsibleFrame.cs`; toolbar policy in `GameWindow.cs`; spec: `docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md` | The central mount now owns wrapper geometry/registration uniformly; border-over-content prevents the row-2 right cap from poking through | The collapse stops remain a toolkit reconstruction rather than a byte-port of Keystone behavior | gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump) | | IA-18 | Effect overlay tile (enum 0x10000005) is a `ReplaceColor` SURFACE SOURCE — pure-white pixels in the composited drag icon are replaced PER-PIXEL with the same (x,y) pixel of the effect tile (the SURFACE overload `SurfaceWindow::ReplaceColor` 0x004415b0), preserving the tile's texture/gradient; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior. **Anti-regression: do NOT re-implement this as a blit layer NOR as a flat-color replace (it is a per-pixel surface copy).** | `src/AcDream.App/UI/IconComposer.cs` (`ReplaceWhiteFromSurface`) | Faithful port of `IconData::RenderIcons` @407614 → the SURFACE overload `ReplaceColor` 0x004415b0 (`dst[x,y]=src[x,y]` where `dst==white`); confirmed via clean Ghidra decompile + named decomp + visual (the Energy Crystal's blue is a gradient, 2026-06-17). | A blit-layer or flat-color re-implementation would show the wrong effect look (no gradient) — the visual-verification regression that retired the mean-color approximation | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407524; `ReplaceColor` SURFACE overload 0x004415b0:71656; `docs/research/2026-06-17-stateful-icon-RESOLVED.md` | diff --git a/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md b/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md index 8b081e96..eeba6b46 100644 --- a/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md +++ b/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md @@ -293,12 +293,19 @@ tests pass; full suite green: 4,549 passed, 5 skipped. ### 2.5 Extract the composition block -- [ ] Create `RetailUiRuntime` and focused binding records. -- [ ] Move DAT import, window creation, controller construction, registry, and +- [x] Create `RetailUiRuntime` and focused binding records. +- [x] Move DAT import, window creation, controller construction, registry, and persistence wiring out of `GameWindow.OnLoad`. -- [ ] Keep GL resolver construction and delegate supply in `GameWindow`; target a +- [x] Keep GL resolver construction and delegate supply in `GameWindow`; target a short, declarative mount call and one tick/draw/dispose path. +**Acceptance (2026-07-11):** the Debug live-world gate passed after two graceful +restart cycles. Vitals, radar, chat, toolbar, character, inventory/paperdoll, +saved layouts, cursor/input, and window controls remained responsive with no +double dispatch. `GameWindow` now supplies one typed mount graph and delegates +retained tick/draw/restore/dispose to `RetailUiRuntime`. +Full suite green: 4,549 passed, 5 skipped. + **Gate:** move/resize/toggle every window, restart at two resolutions, switch characters, verify exact restore and global lock; chat maximize/minimize must resize chrome and content together; full suite and user visual gate. diff --git a/memory/project_ui_architecture.md b/memory/project_ui_architecture.md index 79f7cecd..2aaa8635 100644 --- a/memory/project_ui_architecture.md +++ b/memory/project_ui_architecture.md @@ -109,3 +109,14 @@ subscriptions, then disposes the manager/controllers, then the GL text renderer. `GameWindow.OnClosing` disposes layout persistence and `UiHost` before live session or game-state sources. This ordering is the recreate/relogin double-subscription guard; do not reintroduce anonymous lifetime-bound event lambdas. + +## 2026-07-11 composition boundary + +`RetailUiRuntime` now owns production retained composition. Its focused binding +records receive state/action delegates plus DAT/font/sprite resolvers; +`GameWindow.OnLoad` retains only resolver/service creation and one declarative +`RetailUiRuntime.Mount` call. All LayoutDesc imports, controller construction, +window mounts, toolbar digit and inventory-cell asset lookup, plugin mounts, +cursor feedback, persistence, automation, and retained tick/draw/restore/dispose +paths live behind the runtime. The live Debug gate passed across two graceful +restart cycles. Do not add panel mount bodies back to `GameWindow`. diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 284743ff..2e0795b4 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -750,29 +750,13 @@ public sealed class GameWindow : IDisposable // when no selection. Spec: docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md private AcDream.App.UI.TargetIndicatorPanel? _targetIndicator; private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm; - // Phase D.2b — retail-look UI tree (dormant UiHost wired here). Null unless ACDREAM_RETAIL_UI=1. + // Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1. private AcDream.App.UI.UiHost? _uiHost; - private AcDream.App.UI.Testing.RetailUiAutomationScriptRunner? _uiAutomationRunner; - private AcDream.App.UI.CursorFeedbackController? _cursorFeedbackController; - private RetailCursorManager? _retailCursorManager; - // Phase D.5.1 — toolbar controller (kept for lifetime clarity; mirrors _chatWindowController pattern). - private AcDream.App.UI.Layout.ToolbarController? _toolbarController; + private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime; private AcDream.App.UI.ItemInteractionController? _itemInteractionController; - // Phase D.5.3a — selected-object strip controller (name, overlay state, health meter). - private AcDream.App.UI.Layout.SelectedObjectController? _selectedObjectController; - // Phase D.6 — retail gmRadarUI (LayoutDesc 0x21000074). Behavior lives in - // Core + the retained controller; GameWindow supplies live state only. - private AcDream.App.UI.Layout.RadarController? _radarController; - private AcDream.App.UI.Layout.RadarSnapshotProvider? _radarSnapshotProvider; - // Phase D.2b-B — inventory controller (backpack grid + pack-selector + burden meter). - private AcDream.App.UI.Layout.InventoryController? _inventoryController; - // Phase D.2b Sub-phase C — paperdoll equip-slot controller (wield drag handler). - private AcDream.App.UI.Layout.PaperdollController? _paperdollController; // Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport // widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625). private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer; - private AcDream.App.UI.UiViewport? _paperdollViewportWidget; - private AcDream.App.UI.UiNineSlicePanel? _inventoryFrame; private bool _paperdollDollDirty = true; // Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad. private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry; @@ -2016,13 +2000,13 @@ public sealed class GameWindow : IDisposable sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask), sendDrop: item => _liveSession?.SendDropItem(item), toast: text => _debugVm?.AddToast(text)); - _cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController( + var cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController( _itemInteractionController, // Retail UpdateCursorState (0x00564630) keys target-mode // valid/invalid off the SmartBox found object — the world // entity under the cursor, self included. worldTargetProvider: () => PickWorldGuidAtCursor(includeSelf: true) ?? 0u); - _retailCursorManager = new RetailCursorManager(_dats!, _datLock); + var retailCursorManager = new RetailCursorManager(_dats!, _datLock); _characterSheetProvider = new AcDream.App.UI.Layout.CharacterSheetProvider( Objects, LocalPlayer, playerGuid: () => _playerServerGuid, @@ -2080,555 +2064,87 @@ public sealed class GameWindow : IDisposable ? "[D.2b] vitals dat-font 0x40000000 loaded for numeric overlay." : "[D.2b] vitals dat-font 0x40000000 unavailable — falling back to debug font."); - // Phase D.2b — the vitals window is data-driven from the dat LayoutDesc - // (0x2100006C) via the LayoutImporter. The former hand-authored vitals.xml - // markup path was retired after the importer proved pixel-identical at the - // 2026-06-15 A/B gate. MarkupDocument stays for plugin/custom panels. - AcDream.App.UI.Layout.ImportedLayout? imported; - lock (_datLock) - imported = AcDream.App.UI.Layout.LayoutImporter.Import( - _dats!, 0x2100006Cu, ResolveChrome, vitalsDatFont, ResolveDatFont); - if (imported is not null) - { - AcDream.App.UI.Layout.VitalsController.Bind(imported, - healthPct: () => _vitalsVm!.HealthPercent, - staminaPct: () => _vitalsVm!.StaminaPercent ?? 0f, - manaPct: () => _vitalsVm!.ManaPercent ?? 0f, - healthText: () => (_vitalsVm!.HealthCurrent, _vitalsVm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "", - staminaText: () => (_vitalsVm!.StaminaCurrent, _vitalsVm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "", - manaText: () => (_vitalsVm!.ManaCurrent, _vitalsVm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : ""); - var vitalsRoot = imported.Root; - AcDream.App.UI.Layout.RetailWindowFrame.Mount( - _uiHost.Root, vitalsRoot, ResolveChrome, - new AcDream.App.UI.Layout.RetailWindowFrame.Options - { - WindowName = AcDream.App.UI.WindowNames.Vitals, - Chrome = AcDream.App.UI.Layout.RetailWindowChrome.Imported, - Left = 10f, - Top = 30f, - ResizeX = true, - ResizeY = false, - MinWidth = 40f, - ContentClickThrough = false, - }); - Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); - } - else - { - Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); - } + _uiHost.Root.Width = _window!.Size.X; + _uiHost.Root.Height = _window.Size.Y; - // Phase D.6 — retail gmRadarUI. The importer owns the exact static - // LayoutDesc 0x21000074 shell; RadarController owns retained behavior; - // RadarSnapshotProvider is the thin live-world adapter. - AcDream.App.UI.Layout.ImportedLayout? radarLayout; - lock (_datLock) - radarLayout = AcDream.App.UI.Layout.LayoutImporter.Import( - _dats!, AcDream.App.UI.Layout.RadarController.LayoutId, - ResolveChrome, vitalsDatFont, ResolveDatFont); - if (radarLayout is not null && radarLayout.Root is AcDream.App.UI.UiRadar radarRoot) - { - _radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider( - Objects, - _entitiesByServerGuid, - _lastSpawnByGuid, - playerGuid: () => _playerServerGuid, - playerYawRadians: () => _playerController?.Yaw ?? 0f, - playerCellId: () => _playerController?.CellId ?? 0u, - selectedGuid: () => _selectedGuid, - coordinatesOnRadar: () => _persistedGameplay.CoordinatesOnRadar, - uiLocked: () => _persistedGameplay.LockUI); - _radarController = AcDream.App.UI.Layout.RadarController.Bind( - radarLayout, - snapshotProvider: _radarSnapshotProvider.BuildSnapshot, - selectObject: guid => SelectedGuid = guid, - setUiLocked: SetRetailUiLocked, - datFont: vitalsDatFont); - - AcDream.App.UI.Layout.RetailWindowFrame.Mount( - _uiHost.Root, radarRoot, ResolveChrome, - new AcDream.App.UI.Layout.RetailWindowFrame.Options - { - WindowName = AcDream.App.UI.WindowNames.Radar, - Chrome = AcDream.App.UI.Layout.RetailWindowChrome.Imported, - Left = System.Math.Max(0f, _window!.Size.X - radarRoot.Width - 10f), - Top = 10f, - Resizable = false, - ResizeX = false, - ResizeY = false, - ConstrainDragToParent = true, - ContentClickThrough = false, - Controller = _radarController, - }); - Console.WriteLine("[D.6] retail radar/compass from LayoutDesc 0x21000074."); - } - else - { - Console.WriteLine("[D.6] radar: LayoutDesc 0x21000074 not found or root class mismatch."); - } - - // Retail chat window — data-driven from LayoutDesc 0x21000006 (gmMainChatUI), - // the same importer path as vitals. ChatWindowController binds the transcript, - // input, scrollbar and channel menu and routes through ChatVM + ChatCommandRouter. + var radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider( + Objects, + _entitiesByServerGuid, + _lastSpawnByGuid, + playerGuid: () => _playerServerGuid, + playerYawRadians: () => _playerController?.Yaw ?? 0f, + playerCellId: () => _playerController?.CellId ?? 0u, + selectedGuid: () => _selectedGuid, + coordinatesOnRadar: () => _persistedGameplay.CoordinatesOnRadar, + uiLocked: () => _persistedGameplay.LockUI); var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200); - AcDream.App.UI.Layout.ElementInfo? chatRootInfo; - AcDream.App.UI.Layout.ImportedLayout? chatLayout; - lock (_datLock) - { - chatRootInfo = AcDream.App.UI.Layout.LayoutImporter.ImportInfos( - _dats!, AcDream.App.UI.Layout.ChatWindowController.LayoutId); - chatLayout = chatRootInfo is null ? null - : AcDream.App.UI.Layout.LayoutImporter.Build(chatRootInfo, ResolveChrome, vitalsDatFont, ResolveDatFont); - } - if (chatRootInfo is not null && chatLayout is not null) - { - var chatController = AcDream.App.UI.Layout.ChatWindowController.Bind( - chatRootInfo, chatLayout, retailChatVm, - () => _commandBus ?? (AcDream.UI.Abstractions.ICommandBus)AcDream.UI.Abstractions.NullCommandBus.Instance, - vitalsDatFont, _debugFont, ResolveChrome); - if (chatController is not null) - { - // Ctrl+C / Ctrl+A on the transcript + Ctrl+C/X/V/A on the input need the - // keyboard for clipboard + modifier (Ctrl/Shift) state. _uiHost.Keyboard - // is set by WireKeyboard above — it is non-null here. - chatController.Transcript.Keyboard = _uiHost.Keyboard; - chatController.Input.Keyboard = _uiHost.Keyboard; - // Wrap the dat content in the universal 8-piece beveled window chrome — - // the SAME UiNineSlicePanel the vitals window uses. The chat's own dat - // layout only carries flat background sprites, so without this the window - // has no retail-style border (the user asked for the vitals border). The - // nine-slice IS the movable/resizable window; the dat content fills its - // interior, inset by the border. The gmMainChatUI content is authored 490 - // wide (its transcript/input panels) — KEEP that width + the dat-authored - // HEIGHT so the content's child anchors (input-bar-at-bottom, transcript- - // fills) capture correct margins on first layout; resizing the frame reflows - // them correctly from there. - var chatRoot = chatController.Root; - var chatHandle = AcDream.App.UI.Layout.RetailWindowFrame.Mount( - _uiHost.Root, chatRoot, ResolveChrome, - new AcDream.App.UI.Layout.RetailWindowFrame.Options - { - WindowName = AcDream.App.UI.WindowNames.Chat, - Chrome = AcDream.App.UI.Layout.RetailWindowChrome.NineSlice, - Left = 10f, - Top = 440f, - ContentWidth = 490f, - ContentHeight = chatRoot.Height, - RebaseContentLayout = true, - DatConstraintSource = chatController.DatWindowInfo, - MinWidth = 200f, - ResizeX = true, - ResizeY = true, - Opacity = 0.75f, - Controller = chatController, - StateController = chatController, - }); - chatController.AttachWindow(chatHandle); - // The LayoutDesc root is 800px wide because it shares the display - // coordinate space with auxiliary/floaty chat elements. The docked - // gmMainChatUI content is the left 490px. Rebase its immediate children - // after that intentional crop so subsequent frame resizing grows the - // transcript/input across the whole mounted interior. - // Tab / Enter enters "write mode" by focusing this input (retail's chat - // activation); a focused input suppresses character movement (see the - // WantsKeyboard gate in the movement poll). - _uiHost.Root.DefaultTextInput = chatController.Input; - Console.WriteLine("[D.2b] retail chat window from LayoutDesc importer (0x21000006)."); - } - else Console.WriteLine("[D.2b] chat: required role elements missing in 0x21000006."); - } - else Console.WriteLine("[D.2b] chat: LayoutDesc 0x21000006 not found."); - - // Phase D.5.1 — toolbar window, data-driven from LayoutDesc 0x21000016 - // (gmToolbarUI). Mirrors the vitals/chat import+bind+mount pattern above. - - // Read the shortcut-slot digit sprite DID arrays from LayoutDesc 0x21000037 - // (the UIItem cell template): element 0x1000034A under composite 0x10000346. - // Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465); - // gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps per stance. - // Occupancy branch (decomp 229481): - // occupied → StateDesc.Properties[0x10000042] (peace) / [0x10000043] (war) - // empty → StateDesc.Properties[0x1000005e] (background digit, stance-independent) - uint[]? toolbarPeaceDigits = null; - uint[]? toolbarWarDigits = null; - uint[]? toolbarEmptyDigits = null; - lock (_datLock) - { - var uiItemLd = _dats!.Get(0x21000037u); - if (uiItemLd is not null - && uiItemLd.Elements.TryGetValue(0x10000346u, out var composite) - && composite.Children.TryGetValue(0x1000034Au, out var shortcutNumElem) - && shortcutNumElem.StateDesc is { } sd - && sd.Properties is { } props) - { - // Mirror LayoutImporter.ReadState: Properties[key] is ArrayBaseProperty - // containing DataIdBaseProperty entries. Each DataIdBaseProperty.Value is - // the RenderSurface DID for that digit. - // Peace: property 0x10000042; War: property 0x10000043. - if (props.TryGetValue(0x10000042u, out var rawPeace) - && rawPeace is DatReaderWriter.Types.ArrayBaseProperty arrPeace) - { - toolbarPeaceDigits = new uint[arrPeace.Value.Count]; - for (int i = 0; i < arrPeace.Value.Count; i++) - if (arrPeace.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d) - toolbarPeaceDigits[i] = d.Value; - } - if (props.TryGetValue(0x10000043u, out var rawWar) - && rawWar is DatReaderWriter.Types.ArrayBaseProperty arrWar) - { - toolbarWarDigits = new uint[arrWar.Value.Count]; - for (int i = 0; i < arrWar.Value.Count; i++) - if (arrWar.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d) - toolbarWarDigits[i] = d.Value; - } - } - else - { - Console.WriteLine("[D.5.1] digit arrays: element 0x1000034A/0x10000346 not found in LayoutDesc 0x21000037 — falling back to cited constants."); - } - - // Empty-slot BACKGROUND digit lives under a DIFFERENT cell composite: - // composite 0x10000341 (the UIElement_UIItem-typed variant) carries property - // 0x1000005e (plainer digits 0x060010FA..0x06001102 for 1-9, 0x060074CF for the - // bottom row); composite 0x10000346 (peace/war, read above) does NOT carry it. - // Confirmed by a live dat property dump. Retail: UIElement_UIItem::SetShortcutNum - // (decomp 229481/229493) — empty branch (m_elem_Icon->m_state == 0x1000001c) reads - // 0x1000005e, stance-independent. No fallback constants (safe: no digit if absent). - if (uiItemLd is not null - && uiItemLd.Elements.TryGetValue(0x10000341u, out var emptyComposite) - && emptyComposite.Children.TryGetValue(0x1000034Au, out var emptyScn) - && emptyScn.StateDesc is { } emptySd - && emptySd.Properties is { } emptyProps - && emptyProps.TryGetValue(0x1000005Eu, out var rawEmpty) - && rawEmpty is DatReaderWriter.Types.ArrayBaseProperty arrEmpty) - { - toolbarEmptyDigits = new uint[arrEmpty.Value.Count]; - for (int i = 0; i < arrEmpty.Value.Count; i++) - if (arrEmpty.Value[i] is DatReaderWriter.Types.DataIdBaseProperty d) - toolbarEmptyDigits[i] = d.Value; - } - Console.WriteLine($"[D.5.1] empty digit array (0x10000341/0x1000005e): {toolbarEmptyDigits?.Length ?? 0} entries."); - } - - // Cited-constant fallback (UIElement_UIItem::SetShortcutNum, decomp 229465 + dat probe). - // Used when the dat navigation above fails (e.g. missing LayoutDesc in older dat). - if (toolbarPeaceDigits is null) - toolbarPeaceDigits = new uint[] - { 0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u, - 0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u }; - if (toolbarWarDigits is null) - toolbarWarDigits = new uint[] - { 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u, - 0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u }; - // Report the arrays actually used (after any fallback substitution). - Console.WriteLine($"[D.5.1] toolbar digit arrays ready: peace={toolbarPeaceDigits.Length}, war={toolbarWarDigits.Length}, empty={toolbarEmptyDigits?.Length ?? 0} entries."); - - AcDream.App.UI.Layout.ImportedLayout? toolbarLayout; - lock (_datLock) - toolbarLayout = AcDream.App.UI.Layout.LayoutImporter.Import( - _dats!, 0x21000016u, ResolveChrome, vitalsDatFont, ResolveDatFont); - if (toolbarLayout is not null) - { - _toolbarController = AcDream.App.UI.Layout.ToolbarController.Bind( - toolbarLayout, Objects, - () => Shortcuts, - iconIds: (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects), - useItem: guid => UseItemByGuid(guid), - combatState: Combat, - peaceDigits: toolbarPeaceDigits, - warDigits: toolbarWarDigits, - emptyDigits: toolbarEmptyDigits, - itemInteraction: _itemInteractionController, - sendAddShortcut: (i, g) => _liveSession?.SendAddShortcut(i, g), - sendRemoveShortcut: i => _liveSession?.SendRemoveShortcut(i)); - - // Phase D.5.3a — selected-object strip (name, overlay state, health meter). - // Analogue of retail gmToolbarUI::HandleSelectionChanged - // (acclient_2013_pseudo_c.txt:198635). - _selectedObjectController = AcDream.App.UI.Layout.SelectedObjectController.Bind( - toolbarLayout, - subscribeSelectionChanged: h => SelectionChanged += h, - unsubscribeSelectionChanged: h => SelectionChanged -= h, - subscribeHealthChanged: h => Combat.HealthChanged += h, - unsubscribeHealthChanged: h => Combat.HealthChanged -= h, - isHealthTarget: IsHealthBarTarget, - name: g => Objects.Get(g)?.Name, - healthPercent: g => Combat.GetHealthPercent(g), - hasHealth: g => Combat.HasHealth(g), - stackSize: g => (uint)(Objects.Get(g)?.StackSize ?? 0), - sendQueryHealth: g => _liveSession?.SendQueryHealth(g), - datFont: vitalsDatFont); - - var toolbarRoot = toolbarLayout.Root; - // Wrap the dat content in the universal 8-piece beveled window chrome — - // the SAME UiNineSlicePanel used by the vitals and chat windows. The - // toolbar LayoutDesc (0x21000016) is 300×122; the frame adds one border - // thickness on every side, giving an outer window of 310×132. - const int toolbarBorder = AcDream.App.UI.RetailChromeSprites.Border; - float toolbarContentW = 300f, toolbarContentH = toolbarRoot.Height; - var toolbarHandle = AcDream.App.UI.Layout.RetailWindowFrame.Mount( - _uiHost.Root, toolbarRoot, ResolveChrome, - new AcDream.App.UI.Layout.RetailWindowFrame.Options - { - WindowName = AcDream.App.UI.WindowNames.Toolbar, - Chrome = AcDream.App.UI.Layout.RetailWindowChrome.CollapsibleNineSlice, - Left = 10f, - Top = 300f, - ContentWidth = toolbarContentW, - ContentHeight = toolbarContentH, - Resizable = false, - ResizeX = false, - ResizeY = true, - ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom, - ContentAnchors = AcDream.App.UI.AnchorEdges.Left - | AcDream.App.UI.AnchorEdges.Top - | AcDream.App.UI.AnchorEdges.Right, - ContentClickThrough = true, - Controller = new AcDream.App.UI.RetainedPanelControllerGroup( - _toolbarController, - _selectedObjectController), - }); - var toolbarFrame = (AcDream.App.UI.UiCollapsibleFrame)toolbarHandle.OuterFrame; - - // Collapse-to-one-row: the frame's bottom edge snaps between a one-row (row 2 hidden) - // and two-row height. CollapsedHeight is computed from the layout (just above row 2), - // so there's no magic constant. Bottom-edge only; default expanded. - // The full row-2 band (all at content-y 90, toolbar dump 0x21000016): a 6px left - // edge-piece (0x100006B6) + the 9 slots (0x100006B7..BF) + an 8px right edge-piece - // (0x100006C0). Hide ALL 11 when collapsed — hiding only the 9 slots leaves the two - // edge-pieces drawing as black pillars below the bar. - uint[] row2Ids = - { - 0x100006B6u, - 0x100006B7u, 0x100006B8u, 0x100006B9u, 0x100006BAu, 0x100006BBu, - 0x100006BCu, 0x100006BDu, 0x100006BEu, 0x100006BFu, - 0x100006C0u, - }; - var toolbarRow2 = new System.Collections.Generic.List(); - float minRow2Top = float.MaxValue; - foreach (var id in row2Ids) - if (toolbarLayout.FindElement(id) is { } e2) - { - toolbarRow2.Add(e2); - if (e2.Top < minRow2Top) minRow2Top = e2.Top; - } - if (toolbarRow2.Count > 0) - { - float expandedH = toolbarContentH + 2 * toolbarBorder; // today's full height - float collapsedH = minRow2Top + 2 * toolbarBorder; // just above row 2 - toolbarFrame.CollapsedHeight = collapsedH; - toolbarFrame.ExpandedHeight = expandedH; - toolbarFrame.SecondRow = toolbarRow2; - toolbarFrame.Resizable = true; - toolbarFrame.ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom; // bottom edge only - toolbarFrame.MinHeight = collapsedH; - toolbarFrame.MaxHeight = expandedH; - // Height stays expandedH (already set at construction) = default expanded. - Console.WriteLine($"[D.2b] toolbar collapse: collapsed={collapsedH:0} expanded={expandedH:0} (row2Top={minRow2Top:0})."); - } - - Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016)."); - } - else Console.WriteLine("[D.5.1] toolbar: LayoutDesc 0x21000016 not found."); - - // Phase D.2b-C: live Character window (Attributes / Skills / Titles) from - // LayoutDesc 0x2100002E. Starts hidden and is toggled by the status-bar - // character button; stats use the same retail-bound sheet shape as the studio - // until the live character stat feed is wired. - AcDream.App.UI.Layout.ImportedLayout? characterLayout; - lock (_datLock) - characterLayout = AcDream.App.UI.Layout.LayoutImporter.Import( - _dats!, 0x2100002Eu, ResolveChrome, vitalsDatFont, ResolveDatFont); - if (characterLayout is not null) - { - AcDream.App.UI.Layout.CharacterStatController.Bind( - characterLayout, - data: _characterSheetProvider!.BuildSheet, - datFont: vitalsDatFont, - rowDatFont: ResolveDatFont(0x40000001u) ?? vitalsDatFont, - spriteResolve: ResolveChrome, - onRaiseRequest: _characterSheetProvider.HandleRaiseRequest, - onClose: () => CloseRetailWindow(AcDream.App.UI.WindowNames.Character)); - - // Retail-authored content is 380px tall but the skill list wants to - // grow: bottom-edge-only resize up to 760px, X locked to the dat width. - AcDream.App.UI.Layout.RetailWindowFrame.Mount( - _uiHost.Root, characterLayout.Root, ResolveChrome, - new AcDream.App.UI.Layout.RetailWindowFrame.Options - { - WindowName = AcDream.App.UI.WindowNames.Character, - Chrome = AcDream.App.UI.Layout.RetailWindowChrome.NineSlice, - Left = 540f, - Top = 18f, - MaxHeight = 760f, - ResizeX = false, - ResizeY = true, - ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom, - Visible = false, - ContentAnchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top - | AcDream.App.UI.AnchorEdges.Bottom, - ContentClickThrough = false, - }); - - Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E)."); - } - else Console.WriteLine("[D.2b-C] character: LayoutDesc 0x2100002E not found."); - - // Drain plugin-registered markup panels (buffered before the GL - // window opened) into the same UiRoot tree. A faulty plugin markup - // file is isolated — logged + skipped, never crashes the client. - if (_uiRegistry is not null) - { - foreach (var p in _uiRegistry.Drain()) - { - try - { - string pluginXml = System.IO.File.ReadAllText(p.MarkupPath); - var pluginPanel = AcDream.App.UI.MarkupDocument.Build( - pluginXml, p.Binding, ResolveChrome, controls); - _uiHost.Root.AddChild(pluginPanel); - Console.WriteLine($"[D.2b] plugin UI panel loaded: {p.MarkupPath}"); - } - catch (Exception ex) - { - Console.WriteLine($"[D.2b] plugin UI panel '{p.MarkupPath}' failed to load: {ex.Message}"); - } - } - } - - // Phase D.2b-B — the real inventory window from LayoutDesc 0x21000023 (gmInventoryUI), - // via the LayoutImporter. The sub-window mount pulls in the nested paperdoll/backpack/ - // 3D-items panels (Type-0 leaves inheriting BaseLayoutId 0x21000024/22/21). Starts - // HIDDEN; F12 toggles it via the window manager. Position is the dat's own (X=500,Y=138). - AcDream.App.UI.Layout.ImportedLayout? invLayout; - lock (_datLock) - invLayout = AcDream.App.UI.Layout.LayoutImporter.Import( - _dats!, 0x21000023u, ResolveChrome, vitalsDatFont, ResolveDatFont); - if (invLayout is not null) - { - var inventoryRoot = invLayout.Root; - // Wrap the dat content in the universal 8-piece beveled window chrome — the - // SAME UiNineSlicePanel the vitals/chat/toolbar windows use. The gmInventoryUI - // LayoutDesc (0x21000023) is 300×362; the frame adds one border thickness on - // every side. The frame is the draggable window; the dat content sits inside it. - float invContentW = inventoryRoot.Width, invContentH = inventoryRoot.Height; - var inventoryHandle = AcDream.App.UI.Layout.RetailWindowFrame.Mount( - _uiHost.Root, inventoryRoot, ResolveChrome, - new AcDream.App.UI.Layout.RetailWindowFrame.Options - { - WindowName = AcDream.App.UI.WindowNames.Inventory, - Chrome = AcDream.App.UI.Layout.RetailWindowChrome.NineSlice, - Left = inventoryRoot.Left, - Top = inventoryRoot.Top, - ContentWidth = invContentW, - ContentHeight = invContentH, - Visible = false, - ResizeX = false, - ResizeY = true, - ResizableEdges = AcDream.App.UI.ResizeEdges.Bottom, - MaxHeight = 560f, - ContentAnchors = AcDream.App.UI.AnchorEdges.Left - | AcDream.App.UI.AnchorEdges.Top - | AcDream.App.UI.AnchorEdges.Bottom, - }); - var inventoryFrame = (AcDream.App.UI.UiNineSlicePanel)inventoryHandle.OuterFrame; - - // InventoryController owns descendant resize/reflow policy and captures - // its hidden-window baselines when it binds below. - - // Phase D.2b-B — populate the inventory from ClientObjectTable: the - // "Contents of Backpack" grid + the pack-selector strip + the burden meter + - // captions. Analogue of gmInventoryUI/gmBackpackUI/gm3DItemsUI::PostInit. - - // Resolve each inventory list's empty-slot art from the dat cell template - // (retail UIElement_ItemList::InternalCreateItem 0x004e3570: attr 0x1000000e -> - // catalog 0x21000037 prototype's ItemSlot_Empty). The contents grid lives in - // gm3DItemsUI (0x21000021); the side-bag + main-pack in gmBackpackUI (0x21000022). - uint contentsEmpty, sideBagEmpty, mainPackEmpty; - lock (_datLock) - { - contentsEmpty = AcDream.App.UI.Layout.ItemListCellTemplate.ResolveEmptySprite(_dats!, 0x21000021u, 0x100001C6u); - sideBagEmpty = AcDream.App.UI.Layout.ItemListCellTemplate.ResolveEmptySprite(_dats!, 0x21000022u, 0x100001CAu); - mainPackEmpty = AcDream.App.UI.Layout.ItemListCellTemplate.ResolveEmptySprite(_dats!, 0x21000022u, 0x100001C9u); - } - Console.WriteLine($"[D.2b empty-slot] contents=0x{contentsEmpty:X8} sideBag=0x{sideBagEmpty:X8} mainPack=0x{mainPackEmpty:X8}"); - - _inventoryController = AcDream.App.UI.Layout.InventoryController.Bind( - invLayout, Objects, - playerGuid: () => _playerServerGuid, - iconIds: (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects), - strength: () => LocalPlayer.GetAttribute( - AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength) is { } sa - ? (int?)sa.Current : null, - datFont: vitalsDatFont, - ownerName: _characterSheetProvider!.CharacterName, - contentsEmptySprite: contentsEmpty, - sideBagEmptySprite: sideBagEmpty, - mainPackEmptySprite: mainPackEmpty, - sendUse: g => _liveSession?.SendUse(g), - sendNoLongerViewing: g => _liveSession?.SendNoLongerViewingContents(g), - sendPutItemInContainer: (item, container, placement) => - _liveSession?.SendPutItemInContainer(item, container, placement), - itemInteraction: _itemInteractionController, - onClose: () => CloseRetailWindow(AcDream.App.UI.WindowNames.Inventory)); - - // Slice 1: bind the paperdoll equip slots (same imported subtree) — show equipped gear + - // wield-on-drop. Unwield is handled by InventoryController (drag an equipped item to the grid). - _paperdollController = AcDream.App.UI.Layout.PaperdollController.Bind( - invLayout, Objects, - playerGuid: () => _playerServerGuid, - iconIds: (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects), - sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask), - // Empty equip slots show a visible frame (same square as the inventory grid) so every - // slot position is seen + usable; the live 3D character (the doll) is Slice 2. - emptySlotSprite: contentsEmpty, - datFont: vitalsDatFont, - itemInteraction: _itemInteractionController); // Slice 2: caption the "Slots" toggle button - - _uiHost.WindowManager.AttachController( - AcDream.App.UI.WindowNames.Inventory, - new AcDream.App.UI.RetainedPanelControllerGroup( - _inventoryController, - _paperdollController)); - - // Slice 2: capture the doll viewport widget (Type 0xD, element 0x100001D5) + the inventory - // frame so the per-frame pre-UI hook can render the 3-D doll into the widget's texture only - // when the window is open and in doll-view. The renderer itself is built after the - // WbDrawDispatcher exists (further down this method). - _paperdollViewportWidget = invLayout.FindElement(0x100001D5u) as AcDream.App.UI.UiViewport; - _inventoryFrame = inventoryFrame; - - Console.WriteLine("[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023)."); - } - else Console.WriteLine("[D.2b-B] inventory: LayoutDesc 0x21000023 not found."); - - _toolbarController?.BindWindowToggles( - toggleInventory: () => ToggleRetailWindow(AcDream.App.UI.WindowNames.Inventory), - toggleCharacter: () => ToggleRetailWindow(AcDream.App.UI.WindowNames.Character)); - SyncToolbarWindowButtons(); - - if (_settingsStore is not null) - { - _windowLayoutPersistence = new AcDream.App.UI.RetailWindowLayoutPersistence( - _uiHost.WindowManager, + AcDream.App.UI.RetailUiPersistenceBindings? persistence = _settingsStore is null + ? null + : new AcDream.App.UI.RetailUiPersistenceBindings( _settingsStore, - characterKey: () => _activeToonKey, - screenSize: () => (_window!.Size.X, _window.Size.Y)); - } + CharacterKey: () => _activeToonKey, + ScreenSize: () => (_window.Size.X, _window.Size.Y)); + void UiProbeLog(string message) => Console.WriteLine("[UI-PROBE] " + message); + + _retailUiRuntime = AcDream.App.UI.RetailUiRuntime.Mount( + new AcDream.App.UI.RetailUiRuntimeBindings( + Host: _uiHost, + Assets: new AcDream.App.UI.RetailUiAssets( + _dats!, _datLock, ResolveChrome, ResolveDatFont, + vitalsDatFont, _debugFont, controls, iconComposer), + Vitals: new AcDream.App.UI.VitalsRuntimeBindings(_vitalsVm), + Chat: new AcDream.App.UI.ChatRuntimeBindings( + retailChatVm, + () => _commandBus ?? (AcDream.UI.Abstractions.ICommandBus) + AcDream.UI.Abstractions.NullCommandBus.Instance), + Radar: new AcDream.App.UI.RadarRuntimeBindings( + radarSnapshotProvider.BuildSnapshot, + guid => SelectedGuid = guid, + SetRetailUiLocked), + Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings( + Objects, + () => Shortcuts, + (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects), + UseItemByGuid, + Combat, + _itemInteractionController, + (index, guid) => _liveSession?.SendAddShortcut(index, guid), + index => _liveSession?.SendRemoveShortcut(index), + handler => SelectionChanged += handler, + handler => SelectionChanged -= handler, + handler => Combat.HealthChanged += handler, + handler => Combat.HealthChanged -= handler, + IsHealthBarTarget, + guid => Objects.Get(guid)?.Name, + Combat.GetHealthPercent, + Combat.HasHealth, + guid => (uint)(Objects.Get(guid)?.StackSize ?? 0), + guid => _liveSession?.SendQueryHealth(guid)), + Character: new AcDream.App.UI.CharacterRuntimeBindings(_characterSheetProvider), + Inventory: new AcDream.App.UI.InventoryRuntimeBindings( + Objects, + () => _playerServerGuid, + (type, icon, under, over, effects) => iconComposer.GetIcon(type, icon, under, over, effects), + () => LocalPlayer.GetAttribute( + AcDream.Core.Player.LocalPlayerState.AttributeKind.Strength) + is { } strength ? (int?)strength.Current : null, + guid => _liveSession?.SendUse(guid), + guid => _liveSession?.SendNoLongerViewingContents(guid), + (item, container, placement) => + _liveSession?.SendPutItemInContainer(item, container, placement), + (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask), + _itemInteractionController), + Cursor: new AcDream.App.UI.RetailUiCursorBindings( + cursorFeedbackController, + retailCursorManager), + Plugins: _uiRegistry, + Persistence: persistence, + Probe: new AcDream.App.UI.RetailUiProbeBindings( + _options.UiProbeEnabled, + _options.UiProbeScript, + _options.UiProbeDump, + UiProbeLog))); - if (_options.UiProbeEnabled) - { - void ProbeLog(string message) => Console.WriteLine("[UI-PROBE] " + message); - var probe = new AcDream.App.UI.Testing.RetailUiAutomationProbe( - _uiHost.Root, - Objects, - ProbeLog); - _uiAutomationRunner = new AcDream.App.UI.Testing.RetailUiAutomationScriptRunner( - probe, - _options.UiProbeScript, - _options.UiProbeDump, - ProbeLog); - } } // Phase N.4+N.5 — WB rendering pipeline foundation. The modern path is @@ -2738,11 +2254,12 @@ public sealed class GameWindow : IDisposable // Slice 2: now that the dispatcher exists, build the paperdoll doll RTT renderer and hand it to // the viewport widget. Reuses the dispatcher + the scene-lighting UBO + _gl. The widget only // blits the texture; the pre-UI hook drives the 3-D pass (see OnRender). - if (_paperdollViewportWidget is not null && _sceneLightingUbo is not null) + if (_retailUiRuntime?.PaperdollViewportWidget is { } paperdollViewport + && _sceneLightingUbo is not null) { _paperdollViewportRenderer = new AcDream.App.Rendering.PaperdollViewportRenderer( _gl, _wbDrawDispatcher, _sceneLightingUbo); - _paperdollViewportWidget.Renderer = _paperdollViewportRenderer; + paperdollViewport.Renderer = _paperdollViewportRenderer; } // Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag}) @@ -2910,7 +2427,7 @@ public sealed class GameWindow : IDisposable _liveSession.EnterWorld(user, characterIndex: 0); _activeToonKey = chosen.Name; - _windowLayoutPersistence?.RestoreAll(); + _retailUiRuntime?.RestoreLayout(); SyncToolbarWindowButtons(); if (_settingsStore is not null && _settingsVm is not null) { @@ -8670,7 +8187,7 @@ public sealed class GameWindow : IDisposable // Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse // on selection, then revert). No-op when nothing is flashing. - _selectedObjectController?.Tick(dt); + // Retained panel-local timers advance through RetailUiRuntime.Tick on the draw seam. // Phase K.2 — re-evaluate WantCaptureMouse for the MMB // mouse-look state machine. Detect rising/falling edges so the @@ -10242,8 +9759,8 @@ public sealed class GameWindow : IDisposable // is open AND the paperdoll is in doll-view (the widget's Visible is driven by the Slots toggle). // The 3-D pass is fully sealed in a GLStateScope so it doesn't disturb the world/UI state. if (_options.RetailUi && _paperdollViewportRenderer is not null - && _paperdollViewportWidget is { Visible: true } dollWidget - && _inventoryFrame is { Visible: true }) + && _retailUiRuntime?.PaperdollViewportWidget is { Visible: true } dollWidget + && _retailUiRuntime.InventoryFrame is { Visible: true }) { if (_paperdollDollDirty && RefreshPaperdollDoll()) _paperdollDollDirty = false; @@ -10255,12 +9772,12 @@ public sealed class GameWindow : IDisposable // Self-contained 2D pass: UiHost.Draw → TextRenderer.Flush sets its own // blend/depth state and restores. Drawn before ImGui so the devtools // overlay composites on top during development. - if (_options.RetailUi && _uiHost is not null) + if (_options.RetailUi && _retailUiRuntime is not null) { - _uiHost.Tick(deltaSeconds); - _uiAutomationRunner?.Tick(deltaSeconds); - UpdateRetailCursorFeedback(); - _uiHost.Draw(new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y)); + _retailUiRuntime.Tick(deltaSeconds); + if (_input is not null) + _retailUiRuntime.UpdateCursor(_input.Mice); + _retailUiRuntime.Draw(new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y)); } // Teleport fade cover (retail-teleport spec C). Drawn AFTER the world + retail UI so @@ -11800,27 +11317,16 @@ public sealed class GameWindow : IDisposable // "default" and gets swapped to the actual character name on the // first EnterWorld. private AcDream.UI.Abstractions.Panels.Settings.SettingsStore? _settingsStore; - private AcDream.App.UI.RetailWindowLayoutPersistence? _windowLayoutPersistence; private string _activeToonKey = "default"; private bool ToggleRetailWindow(string name) { - bool visible = _uiHost?.ToggleWindow(name) ?? false; - SyncToolbarWindowButtons(); - return visible; - } - - private void CloseRetailWindow(string name) - { - _uiHost?.HideWindow(name); - SyncToolbarWindowButtons(); + return _retailUiRuntime?.ToggleWindow(name) ?? false; } private void SyncToolbarWindowButtons() { - if (_toolbarController is null || _uiHost is null) return; - _toolbarController.SetInventoryOpen(_uiHost.IsWindowVisible(AcDream.App.UI.WindowNames.Inventory)); - _toolbarController.SetCharacterOpen(_uiHost.IsWindowVisible(AcDream.App.UI.WindowNames.Character)); + _retailUiRuntime?.SyncToolbarWindowButtons(); } private void SetRetailUiLocked(bool locked) @@ -11844,15 +11350,6 @@ public sealed class GameWindow : IDisposable } } - private void UpdateRetailCursorFeedback() - { - if (_uiHost is null || _cursorFeedbackController is null || _retailCursorManager is null || _input is null) - return; - - var feedback = _cursorFeedbackController.Update(_uiHost.Root); - _retailCursorManager.Apply(_input.Mice, feedback); - } - private void OnUiDragReleasedOutside(object payload, int x, int y) { if (payload is AcDream.App.UI.ItemDragPayload itemPayload) @@ -14040,9 +13537,8 @@ public sealed class GameWindow : IDisposable // Retained controllers subscribe to session-owned object/selection/combat sources. // Tear their window-manager ownership down first, while every source and GL-backed // UI resource is still alive. UiHost disposes controllers before its renderer. - _windowLayoutPersistence?.Dispose(); - _windowLayoutPersistence = null; - _uiHost?.Dispose(); + _retailUiRuntime?.Dispose(); + _retailUiRuntime = null; _uiHost = null; // Phase A.1: join the streamer worker thread before tearing down GL diff --git a/src/AcDream.App/UI/README.md b/src/AcDream.App/UI/README.md index adef5ebf..e7f94432 100644 --- a/src/AcDream.App/UI/README.md +++ b/src/AcDream.App/UI/README.md @@ -44,6 +44,10 @@ synthesis + six deep-dive docs under - `RetailWindowLayoutPersistence.cs` / `IRetainedWindowStateController.cs` — per-character/per-resolution bounds, visibility, collapse/maximize, legacy migration, and safe resolution-clamped restore. +- `RetailUiRuntime.cs` — production composition owner. Focused binding records + carry state/action delegates and DAT/GL resolvers in; the runtime imports and + mounts every gameplay window and owns controller, cursor, persistence, probe, + tick, draw, restore, and disposal paths. - `Layout/RetailWindowFrame.cs` — the single production/Studio mount contract for imported-chrome and shared-wrapper windows. - `UiHost.cs` — one-shot wrapper: owns the `UiRoot`, a `TextRenderer`, diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs new file mode 100644 index 00000000..520df898 --- /dev/null +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -0,0 +1,597 @@ +using System.Collections.Generic; +using AcDream.App.Plugins; +using AcDream.App.Rendering; +using AcDream.App.UI.Layout; +using AcDream.App.UI.Testing; +using AcDream.Core.Combat; +using AcDream.Core.Items; +using AcDream.Core.Net.Messages; +using AcDream.UI.Abstractions; +using AcDream.UI.Abstractions.Panels.Chat; +using AcDream.UI.Abstractions.Panels.Settings; +using AcDream.UI.Abstractions.Panels.Vitals; +using DatReaderWriter; +using Silk.NET.Input; + +namespace AcDream.App.UI; + +public sealed record RetailUiAssets( + DatCollection Dats, + object DatLock, + Func ResolveSprite, + Func ResolveFont, + UiDatFont? DefaultFont, + BitmapFont? DebugFont, + ControlsIni Controls, + IconComposer Icons); + +public sealed record VitalsRuntimeBindings(VitalsVM ViewModel); + +public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func CommandBus); + +public sealed record RadarRuntimeBindings( + Func Snapshot, + Action SelectObject, + Action SetUiLocked); + +public sealed record ToolbarRuntimeBindings( + ClientObjectTable Objects, + Func> Shortcuts, + Func ResolveIcon, + Action UseItem, + CombatState Combat, + ItemInteractionController ItemInteraction, + Action? SendAddShortcut, + Action? SendRemoveShortcut, + Action> SubscribeSelectionChanged, + Action> UnsubscribeSelectionChanged, + Action> SubscribeHealthChanged, + Action> UnsubscribeHealthChanged, + Func IsHealthTarget, + Func ResolveName, + Func HealthPercent, + Func HasHealth, + Func StackSize, + Action SendQueryHealth); + +public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider); + +public sealed record InventoryRuntimeBindings( + ClientObjectTable Objects, + Func PlayerGuid, + Func ResolveIcon, + Func Strength, + Action? SendUse, + Action? SendNoLongerViewing, + Action? SendPutItemInContainer, + Action? SendWield, + ItemInteractionController ItemInteraction); + +public sealed record RetailUiPersistenceBindings( + SettingsStore Store, + Func CharacterKey, + Func<(int Width, int Height)> ScreenSize); + +public sealed record RetailUiProbeBindings( + bool Enabled, + string? ScriptPath, + bool DumpOnStart, + Action Log); + +public sealed record RetailUiCursorBindings( + CursorFeedbackController Feedback, + RetailCursorManager Manager); + +public sealed record RetailUiRuntimeBindings( + UiHost Host, + RetailUiAssets Assets, + VitalsRuntimeBindings Vitals, + ChatRuntimeBindings Chat, + RadarRuntimeBindings Radar, + ToolbarRuntimeBindings Toolbar, + CharacterRuntimeBindings Character, + InventoryRuntimeBindings Inventory, + RetailUiCursorBindings Cursor, + BufferedUiRegistry? Plugins, + RetailUiPersistenceBindings? Persistence, + RetailUiProbeBindings Probe); + +/// +/// Composition owner for the production retained gameplay UI. GameWindow supplies +/// state/action delegates and GL/DAT resolvers; this runtime imports layouts, binds +/// controllers, mounts windows, owns persistence, and presents one tick/draw/dispose seam. +/// +public sealed class RetailUiRuntime : IDisposable +{ + private readonly RetailUiRuntimeBindings _bindings; + private readonly RetailWindowLayoutPersistence? _persistence; + private readonly RetailUiAutomationScriptRunner? _automation; + private bool _disposed; + + private RetailUiRuntime(RetailUiRuntimeBindings bindings) + { + _bindings = bindings; + MountVitals(); + MountRadar(); + MountChat(); + MountToolbar(); + MountCharacter(); + MountPlugins(); + MountInventory(); + SyncToolbarWindowButtons(); + + if (bindings.Persistence is { } persistence) + { + _persistence = new RetailWindowLayoutPersistence( + Host.WindowManager, + persistence.Store, + persistence.CharacterKey, + persistence.ScreenSize); + } + + if (bindings.Probe.Enabled) + { + var probe = new RetailUiAutomationProbe( + Host.Root, + bindings.Inventory.Objects, + bindings.Probe.Log); + _automation = new RetailUiAutomationScriptRunner( + probe, + bindings.Probe.ScriptPath, + bindings.Probe.DumpOnStart, + bindings.Probe.Log); + } + } + + public UiHost Host => _bindings.Host; + public ItemInteractionController ItemInteraction => _bindings.Inventory.ItemInteraction; + public CharacterSheetProvider CharacterSheetProvider => _bindings.Character.Provider; + public ToolbarController? ToolbarController { get; private set; } + public SelectedObjectController? SelectedObjectController { get; private set; } + public UiViewport? PaperdollViewportWidget { get; private set; } + public UiNineSlicePanel? InventoryFrame { get; private set; } + + public static RetailUiRuntime Mount(RetailUiRuntimeBindings bindings) + { + ArgumentNullException.ThrowIfNull(bindings); + try + { + return new RetailUiRuntime(bindings); + } + catch + { + bindings.Host.Dispose(); + throw; + } + } + + public void Tick(double deltaSeconds) + { + SelectedObjectController?.Tick(deltaSeconds); + Host.Tick(deltaSeconds); + _automation?.Tick(deltaSeconds); + } + + public void Draw(System.Numerics.Vector2 screenSize) => Host.Draw(screenSize); + + public void UpdateCursor(IEnumerable mice) + { + CursorFeedback feedback = _bindings.Cursor.Feedback.Update(Host.Root); + _bindings.Cursor.Manager.Apply(mice, feedback); + } + + public void RestoreLayout() => _persistence?.RestoreAll(); + + public bool ToggleWindow(string name) + { + bool visible = Host.ToggleWindow(name); + SyncToolbarWindowButtons(); + return visible; + } + + public void CloseWindow(string name) + { + Host.HideWindow(name); + SyncToolbarWindowButtons(); + } + + public void SyncToolbarWindowButtons() + { + ToolbarController?.SetInventoryOpen(Host.IsWindowVisible(WindowNames.Inventory)); + ToolbarController?.SetCharacterOpen(Host.IsWindowVisible(WindowNames.Character)); + } + + private ImportedLayout? Import(uint layoutId) + { + lock (_bindings.Assets.DatLock) + return LayoutImporter.Import( + _bindings.Assets.Dats, + layoutId, + _bindings.Assets.ResolveSprite, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont); + } + + private void MountVitals() + { + ImportedLayout? layout = Import(0x2100006Cu); + if (layout is null) + { + Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable."); + return; + } + + VitalsVM vm = _bindings.Vitals.ViewModel; + VitalsController.Bind(layout, + () => vm.HealthPercent, + () => vm.StaminaPercent ?? 0f, + () => vm.ManaPercent ?? 0f, + () => (vm.HealthCurrent, vm.HealthMax) is (uint c, uint m) ? $"{c}/{m}" : "", + () => (vm.StaminaCurrent, vm.StaminaMax) is (uint c, uint m) ? $"{c}/{m}" : "", + () => (vm.ManaCurrent, vm.ManaMax) is (uint c, uint m) ? $"{c}/{m}" : ""); + RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Vitals, + Chrome = RetailWindowChrome.Imported, + Left = 10f, + Top = 30f, + ResizeX = true, + ResizeY = false, + MinWidth = 40f, + ContentClickThrough = false, + }); + Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C)."); + } + + private void MountRadar() + { + ImportedLayout? layout = Import(RadarController.LayoutId); + if (layout is null || layout.Root is not UiRadar radarRoot) + { + Console.WriteLine("[D.6] radar: LayoutDesc 0x21000074 not found or root class mismatch."); + return; + } + + RadarController controller = RadarController.Bind( + layout, + _bindings.Radar.Snapshot, + _bindings.Radar.SelectObject, + setUiLocked: _bindings.Radar.SetUiLocked, + datFont: _bindings.Assets.DefaultFont); + RetailWindowFrame.Mount(Host.Root, radarRoot, _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Radar, + Chrome = RetailWindowChrome.Imported, + Left = Math.Max(0f, Host.Root.Width - radarRoot.Width - 10f), + Top = 10f, + Resizable = false, + ResizeX = false, + ResizeY = false, + ConstrainDragToParent = true, + ContentClickThrough = false, + Controller = controller, + }); + Console.WriteLine("[D.6] retail radar/compass from LayoutDesc 0x21000074."); + } + + private void MountChat() + { + ElementInfo? info; + ImportedLayout? layout; + lock (_bindings.Assets.DatLock) + { + info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, ChatWindowController.LayoutId); + layout = info is null ? null : LayoutImporter.Build( + info, + _bindings.Assets.ResolveSprite, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont); + } + if (info is null || layout is null) + { + Console.WriteLine("[D.2b] chat: LayoutDesc 0x21000006 not found."); + return; + } + + ChatWindowController? controller = ChatWindowController.Bind( + info, + layout, + _bindings.Chat.ViewModel, + _bindings.Chat.CommandBus, + _bindings.Assets.DefaultFont, + _bindings.Assets.DebugFont, + _bindings.Assets.ResolveSprite); + if (controller is null) + { + Console.WriteLine("[D.2b] chat: required role elements missing in 0x21000006."); + return; + } + + controller.Transcript.Keyboard = Host.Keyboard; + controller.Input.Keyboard = Host.Keyboard; + UiElement root = controller.Root; + RetailWindowHandle handle = RetailWindowFrame.Mount( + Host.Root, + root, + _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Chat, + Chrome = RetailWindowChrome.NineSlice, + Left = 10f, + Top = 440f, + ContentWidth = 490f, + ContentHeight = root.Height, + RebaseContentLayout = true, + DatConstraintSource = controller.DatWindowInfo, + MinWidth = 200f, + ResizeX = true, + ResizeY = true, + Opacity = 0.75f, + Controller = controller, + StateController = controller, + }); + controller.AttachWindow(handle); + Host.Root.DefaultTextInput = controller.Input; + Console.WriteLine("[D.2b] retail chat window from LayoutDesc importer (0x21000006)."); + } + + private void MountToolbar() + { + ImportedLayout? layout = Import(0x21000016u); + if (layout is null) + { + Console.WriteLine("[D.5.1] toolbar: LayoutDesc 0x21000016 not found."); + return; + } + + var (peace, war, empty) = LoadToolbarDigits(); + ToolbarRuntimeBindings b = _bindings.Toolbar; + ToolbarController = Layout.ToolbarController.Bind( + layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat, + peace, war, empty, b.ItemInteraction, b.SendAddShortcut, b.SendRemoveShortcut); + SelectedObjectController = Layout.SelectedObjectController.Bind( + layout, + b.SubscribeSelectionChanged, + b.UnsubscribeSelectionChanged, + b.SubscribeHealthChanged, + b.UnsubscribeHealthChanged, + b.IsHealthTarget, + b.ResolveName, + b.HealthPercent, + b.HasHealth, + b.StackSize, + b.SendQueryHealth, + _bindings.Assets.DefaultFont); + + UiElement root = layout.Root; + RetailWindowHandle handle = RetailWindowFrame.Mount( + Host.Root, root, _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Toolbar, + Chrome = RetailWindowChrome.CollapsibleNineSlice, + Left = 10f, + Top = 300f, + ContentWidth = 300f, + ContentHeight = root.Height, + Resizable = false, + ResizeX = false, + ResizeY = true, + ResizableEdges = ResizeEdges.Bottom, + ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right, + ContentClickThrough = true, + Controller = new RetainedPanelControllerGroup( + ToolbarController, + SelectedObjectController), + }); + ConfigureToolbarCollapse(layout, (UiCollapsibleFrame)handle.OuterFrame, root.Height); + ToolbarController.BindWindowToggles( + () => ToggleWindow(WindowNames.Inventory), + () => ToggleWindow(WindowNames.Character)); + Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016)."); + } + + private (uint[] Peace, uint[] War, uint[]? Empty) LoadToolbarDigits() + { + uint[]? peace = null, war = null, empty = null; + lock (_bindings.Assets.DatLock) + { + var layout = _bindings.Assets.Dats.Get(0x21000037u); + if (layout is not null + && layout.Elements.TryGetValue(0x10000346u, out var composite) + && composite.Children.TryGetValue(0x1000034Au, out var number) + && number.StateDesc?.Properties is { } props) + { + peace = ReadDataIds(props, 0x10000042u); + war = ReadDataIds(props, 0x10000043u); + } + if (layout is not null + && layout.Elements.TryGetValue(0x10000341u, out var emptyComposite) + && emptyComposite.Children.TryGetValue(0x1000034Au, out var emptyNumber) + && emptyNumber.StateDesc?.Properties is { } emptyProps) + empty = ReadDataIds(emptyProps, 0x1000005Eu); + } + + peace ??= + [ + 0x0600109Eu, 0x0600109Fu, 0x060010A0u, 0x060010A1u, 0x060010A2u, + 0x060010A3u, 0x060010A4u, 0x060010A5u, 0x060010A6u, + ]; + war ??= + [ + 0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u, + 0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u, + ]; + Console.WriteLine($"[D.5.1] toolbar digit arrays ready: peace={peace.Length}, war={war.Length}, empty={empty?.Length ?? 0} entries."); + return (peace, war, empty); + } + + private static uint[]? ReadDataIds( + IReadOnlyDictionary properties, + uint key) + { + if (!properties.TryGetValue(key, out var raw) + || raw is not DatReaderWriter.Types.ArrayBaseProperty array) + return null; + var result = new uint[array.Value.Count]; + for (int i = 0; i < array.Value.Count; i++) + if (array.Value[i] is DatReaderWriter.Types.DataIdBaseProperty dataId) + result[i] = dataId.Value; + return result; + } + + private static void ConfigureToolbarCollapse( + ImportedLayout layout, + UiCollapsibleFrame frame, + float contentHeight) + { + uint[] ids = + [ + 0x100006B6u, 0x100006B7u, 0x100006B8u, 0x100006B9u, + 0x100006BAu, 0x100006BBu, 0x100006BCu, 0x100006BDu, + 0x100006BEu, 0x100006BFu, 0x100006C0u, + ]; + var row = new List(); + float top = float.MaxValue; + foreach (uint id in ids) + if (layout.FindElement(id) is { } element) + { + row.Add(element); + top = Math.Min(top, element.Top); + } + if (row.Count == 0) return; + + const int border = RetailChromeSprites.Border; + frame.CollapsedHeight = top + 2 * border; + frame.ExpandedHeight = contentHeight + 2 * border; + frame.SecondRow = row; + frame.Resizable = true; + frame.ResizableEdges = ResizeEdges.Bottom; + frame.MinHeight = frame.CollapsedHeight; + frame.MaxHeight = frame.ExpandedHeight; + } + + private void MountCharacter() + { + ImportedLayout? layout = Import(0x2100002Eu); + if (layout is null) + { + Console.WriteLine("[D.2b-C] character: LayoutDesc 0x2100002E not found."); + return; + } + CharacterSheetProvider provider = _bindings.Character.Provider; + CharacterStatController.Bind( + layout, + provider.BuildSheet, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont(0x40000001u) ?? _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveSprite, + provider.HandleRaiseRequest, + () => CloseWindow(WindowNames.Character)); + RetailWindowFrame.Mount(Host.Root, layout.Root, _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Character, + Chrome = RetailWindowChrome.NineSlice, + Left = 540f, + Top = 18f, + MaxHeight = 760f, + ResizeX = false, + ResizeY = true, + ResizableEdges = ResizeEdges.Bottom, + Visible = false, + ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, + ContentClickThrough = false, + }); + Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E)."); + } + + private void MountPlugins() + { + if (_bindings.Plugins is null) return; + foreach (var panel in _bindings.Plugins.Drain()) + { + try + { + string xml = File.ReadAllText(panel.MarkupPath); + UiElement element = MarkupDocument.Build( + xml, + panel.Binding, + _bindings.Assets.ResolveSprite, + _bindings.Assets.Controls); + Host.Root.AddChild(element); + Console.WriteLine($"[D.2b] plugin UI panel loaded: {panel.MarkupPath}"); + } + catch (Exception ex) + { + Console.WriteLine($"[D.2b] plugin UI panel '{panel.MarkupPath}' failed to load: {ex.Message}"); + } + } + } + + private void MountInventory() + { + ImportedLayout? layout = Import(0x21000023u); + if (layout is null) + { + Console.WriteLine("[D.2b-B] inventory: LayoutDesc 0x21000023 not found."); + return; + } + + UiElement root = layout.Root; + RetailWindowHandle handle = RetailWindowFrame.Mount( + Host.Root, root, _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Inventory, + Chrome = RetailWindowChrome.NineSlice, + Left = root.Left, + Top = root.Top, + ContentWidth = root.Width, + ContentHeight = root.Height, + Visible = false, + ResizeX = false, + ResizeY = true, + ResizableEdges = ResizeEdges.Bottom, + MaxHeight = 560f, + ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, + }); + + uint contents, sideBag, mainPack; + lock (_bindings.Assets.DatLock) + { + contents = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000021u, 0x100001C6u); + sideBag = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001CAu); + mainPack = ItemListCellTemplate.ResolveEmptySprite(_bindings.Assets.Dats, 0x21000022u, 0x100001C9u); + } + + InventoryRuntimeBindings b = _bindings.Inventory; + InventoryController inventory = InventoryController.Bind( + layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.Strength, + _bindings.Assets.DefaultFont, _bindings.Character.Provider.CharacterName, + contents, sideBag, mainPack, b.SendUse, b.SendNoLongerViewing, + b.SendPutItemInContainer, b.ItemInteraction, + () => CloseWindow(WindowNames.Inventory)); + PaperdollController paperdoll = PaperdollController.Bind( + layout, b.Objects, b.PlayerGuid, b.ResolveIcon, b.SendWield, + contents, _bindings.Assets.DefaultFont, b.ItemInteraction); + Host.WindowManager.AttachController( + WindowNames.Inventory, + new RetainedPanelControllerGroup(inventory, paperdoll)); + + PaperdollViewportWidget = layout.FindElement(0x100001D5u) as UiViewport; + InventoryFrame = (UiNineSlicePanel)handle.OuterFrame; + Console.WriteLine("[D.2b-B] retail inventory window from LayoutDesc importer (0x21000023)."); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _persistence?.Dispose(); + Host.Dispose(); + } +}