refactor(ui): extract retained runtime composition
This commit is contained in:
parent
5d9e98c118
commit
ff06aa3107
7 changed files with 727 additions and 605 deletions
|
|
@ -111,6 +111,13 @@ reverse ownership order. `UiHost` removes Silk device subscriptions first, then
|
||||||
disposes the manager/controllers, then its GL renderer. `GameWindow.OnClosing`
|
disposes the manager/controllers, then its GL renderer. `GameWindow.OnClosing`
|
||||||
tears this runtime down before session and game-state sources.
|
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)
|
## Project Structure (current + target)
|
||||||
|
|
|
||||||
|
|
@ -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-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-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-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-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` |
|
| 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` |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,12 +293,19 @@ tests pass; full suite green: 4,549 passed, 5 skipped.
|
||||||
|
|
||||||
### 2.5 Extract the composition block
|
### 2.5 Extract the composition block
|
||||||
|
|
||||||
- [ ] Create `RetailUiRuntime` and focused binding records.
|
- [x] Create `RetailUiRuntime` and focused binding records.
|
||||||
- [ ] Move DAT import, window creation, controller construction, registry, and
|
- [x] Move DAT import, window creation, controller construction, registry, and
|
||||||
persistence wiring out of `GameWindow.OnLoad`.
|
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.
|
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
|
**Gate:** move/resize/toggle every window, restart at two resolutions, switch
|
||||||
characters, verify exact restore and global lock; chat maximize/minimize must resize
|
characters, verify exact restore and global lock; chat maximize/minimize must resize
|
||||||
chrome and content together; full suite and user visual gate.
|
chrome and content together; full suite and user visual gate.
|
||||||
|
|
|
||||||
|
|
@ -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
|
`GameWindow.OnClosing` disposes layout persistence and `UiHost` before live session
|
||||||
or game-state sources. This ordering is the recreate/relogin double-subscription
|
or game-state sources. This ordering is the recreate/relogin double-subscription
|
||||||
guard; do not reintroduce anonymous lifetime-bound event lambdas.
|
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`.
|
||||||
|
|
|
||||||
|
|
@ -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
|
// when no selection. Spec: docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md
|
||||||
private AcDream.App.UI.TargetIndicatorPanel? _targetIndicator;
|
private AcDream.App.UI.TargetIndicatorPanel? _targetIndicator;
|
||||||
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
|
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.UiHost? _uiHost;
|
||||||
private AcDream.App.UI.Testing.RetailUiAutomationScriptRunner? _uiAutomationRunner;
|
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
|
||||||
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.ItemInteractionController? _itemInteractionController;
|
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
|
// 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).
|
// 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.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer;
|
||||||
private AcDream.App.UI.UiViewport? _paperdollViewportWidget;
|
|
||||||
private AcDream.App.UI.UiNineSlicePanel? _inventoryFrame;
|
|
||||||
private bool _paperdollDollDirty = true;
|
private bool _paperdollDollDirty = true;
|
||||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||||
|
|
@ -2016,13 +2000,13 @@ public sealed class GameWindow : IDisposable
|
||||||
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
sendWield: (item, mask) => _liveSession?.SendGetAndWieldItem(item, mask),
|
||||||
sendDrop: item => _liveSession?.SendDropItem(item),
|
sendDrop: item => _liveSession?.SendDropItem(item),
|
||||||
toast: text => _debugVm?.AddToast(text));
|
toast: text => _debugVm?.AddToast(text));
|
||||||
_cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
|
var cursorFeedbackController = new AcDream.App.UI.CursorFeedbackController(
|
||||||
_itemInteractionController,
|
_itemInteractionController,
|
||||||
// Retail UpdateCursorState (0x00564630) keys target-mode
|
// Retail UpdateCursorState (0x00564630) keys target-mode
|
||||||
// valid/invalid off the SmartBox found object — the world
|
// valid/invalid off the SmartBox found object — the world
|
||||||
// entity under the cursor, self included.
|
// entity under the cursor, self included.
|
||||||
worldTargetProvider: () => PickWorldGuidAtCursor(includeSelf: true) ?? 0u);
|
worldTargetProvider: () => PickWorldGuidAtCursor(includeSelf: true) ?? 0u);
|
||||||
_retailCursorManager = new RetailCursorManager(_dats!, _datLock);
|
var retailCursorManager = new RetailCursorManager(_dats!, _datLock);
|
||||||
_characterSheetProvider = new AcDream.App.UI.Layout.CharacterSheetProvider(
|
_characterSheetProvider = new AcDream.App.UI.Layout.CharacterSheetProvider(
|
||||||
Objects, LocalPlayer,
|
Objects, LocalPlayer,
|
||||||
playerGuid: () => _playerServerGuid,
|
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 loaded for numeric overlay."
|
||||||
: "[D.2b] vitals dat-font 0x40000000 unavailable — falling back to debug font.");
|
: "[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
|
_uiHost.Root.Width = _window!.Size.X;
|
||||||
// (0x2100006C) via the LayoutImporter. The former hand-authored vitals.xml
|
_uiHost.Root.Height = _window.Size.Y;
|
||||||
// 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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase D.6 — retail gmRadarUI. The importer owns the exact static
|
var radarSnapshotProvider = new AcDream.App.UI.Layout.RadarSnapshotProvider(
|
||||||
// LayoutDesc 0x21000074 shell; RadarController owns retained behavior;
|
Objects,
|
||||||
// RadarSnapshotProvider is the thin live-world adapter.
|
_entitiesByServerGuid,
|
||||||
AcDream.App.UI.Layout.ImportedLayout? radarLayout;
|
_lastSpawnByGuid,
|
||||||
lock (_datLock)
|
playerGuid: () => _playerServerGuid,
|
||||||
radarLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
|
playerYawRadians: () => _playerController?.Yaw ?? 0f,
|
||||||
_dats!, AcDream.App.UI.Layout.RadarController.LayoutId,
|
playerCellId: () => _playerController?.CellId ?? 0u,
|
||||||
ResolveChrome, vitalsDatFont, ResolveDatFont);
|
selectedGuid: () => _selectedGuid,
|
||||||
if (radarLayout is not null && radarLayout.Root is AcDream.App.UI.UiRadar radarRoot)
|
coordinatesOnRadar: () => _persistedGameplay.CoordinatesOnRadar,
|
||||||
{
|
uiLocked: () => _persistedGameplay.LockUI);
|
||||||
_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 retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
|
var retailChatVm = new AcDream.UI.Abstractions.Panels.Chat.ChatVM(Chat, displayLimit: 200);
|
||||||
AcDream.App.UI.Layout.ElementInfo? chatRootInfo;
|
AcDream.App.UI.RetailUiPersistenceBindings? persistence = _settingsStore is null
|
||||||
AcDream.App.UI.Layout.ImportedLayout? chatLayout;
|
? null
|
||||||
lock (_datLock)
|
: new AcDream.App.UI.RetailUiPersistenceBindings(
|
||||||
{
|
|
||||||
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<DatReaderWriter.DBObjs.LayoutDesc>(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<AcDream.App.UI.UiElement>();
|
|
||||||
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,
|
|
||||||
_settingsStore,
|
_settingsStore,
|
||||||
characterKey: () => _activeToonKey,
|
CharacterKey: () => _activeToonKey,
|
||||||
screenSize: () => (_window!.Size.X, _window.Size.Y));
|
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
|
// 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
|
// 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
|
// 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).
|
// 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(
|
_paperdollViewportRenderer = new AcDream.App.Rendering.PaperdollViewportRenderer(
|
||||||
_gl, _wbDrawDispatcher, _sceneLightingUbo);
|
_gl, _wbDrawDispatcher, _sceneLightingUbo);
|
||||||
_paperdollViewportWidget.Renderer = _paperdollViewportRenderer;
|
paperdollViewport.Renderer = _paperdollViewportRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag})
|
// Phase A8: EnvCellRenderer init. Shares _meshShader (mesh_modern.{vert,frag})
|
||||||
|
|
@ -2910,7 +2427,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_liveSession.EnterWorld(user, characterIndex: 0);
|
_liveSession.EnterWorld(user, characterIndex: 0);
|
||||||
|
|
||||||
_activeToonKey = chosen.Name;
|
_activeToonKey = chosen.Name;
|
||||||
_windowLayoutPersistence?.RestoreAll();
|
_retailUiRuntime?.RestoreLayout();
|
||||||
SyncToolbarWindowButtons();
|
SyncToolbarWindowButtons();
|
||||||
if (_settingsStore is not null && _settingsVm is not null)
|
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
|
// Phase D.5.3a — advance the selected-object overlay flash (0.25s green pulse
|
||||||
// on selection, then revert). No-op when nothing is flashing.
|
// 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
|
// Phase K.2 — re-evaluate WantCaptureMouse for the MMB
|
||||||
// mouse-look state machine. Detect rising/falling edges so the
|
// 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).
|
// 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.
|
// 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
|
if (_options.RetailUi && _paperdollViewportRenderer is not null
|
||||||
&& _paperdollViewportWidget is { Visible: true } dollWidget
|
&& _retailUiRuntime?.PaperdollViewportWidget is { Visible: true } dollWidget
|
||||||
&& _inventoryFrame is { Visible: true })
|
&& _retailUiRuntime.InventoryFrame is { Visible: true })
|
||||||
{
|
{
|
||||||
if (_paperdollDollDirty && RefreshPaperdollDoll())
|
if (_paperdollDollDirty && RefreshPaperdollDoll())
|
||||||
_paperdollDollDirty = false;
|
_paperdollDollDirty = false;
|
||||||
|
|
@ -10255,12 +9772,12 @@ public sealed class GameWindow : IDisposable
|
||||||
// Self-contained 2D pass: UiHost.Draw → TextRenderer.Flush sets its own
|
// Self-contained 2D pass: UiHost.Draw → TextRenderer.Flush sets its own
|
||||||
// blend/depth state and restores. Drawn before ImGui so the devtools
|
// blend/depth state and restores. Drawn before ImGui so the devtools
|
||||||
// overlay composites on top during development.
|
// overlay composites on top during development.
|
||||||
if (_options.RetailUi && _uiHost is not null)
|
if (_options.RetailUi && _retailUiRuntime is not null)
|
||||||
{
|
{
|
||||||
_uiHost.Tick(deltaSeconds);
|
_retailUiRuntime.Tick(deltaSeconds);
|
||||||
_uiAutomationRunner?.Tick(deltaSeconds);
|
if (_input is not null)
|
||||||
UpdateRetailCursorFeedback();
|
_retailUiRuntime.UpdateCursor(_input.Mice);
|
||||||
_uiHost.Draw(new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y));
|
_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
|
// 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
|
// "default" and gets swapped to the actual character name on the
|
||||||
// first EnterWorld.
|
// first EnterWorld.
|
||||||
private AcDream.UI.Abstractions.Panels.Settings.SettingsStore? _settingsStore;
|
private AcDream.UI.Abstractions.Panels.Settings.SettingsStore? _settingsStore;
|
||||||
private AcDream.App.UI.RetailWindowLayoutPersistence? _windowLayoutPersistence;
|
|
||||||
private string _activeToonKey = "default";
|
private string _activeToonKey = "default";
|
||||||
|
|
||||||
private bool ToggleRetailWindow(string name)
|
private bool ToggleRetailWindow(string name)
|
||||||
{
|
{
|
||||||
bool visible = _uiHost?.ToggleWindow(name) ?? false;
|
return _retailUiRuntime?.ToggleWindow(name) ?? false;
|
||||||
SyncToolbarWindowButtons();
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseRetailWindow(string name)
|
|
||||||
{
|
|
||||||
_uiHost?.HideWindow(name);
|
|
||||||
SyncToolbarWindowButtons();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncToolbarWindowButtons()
|
private void SyncToolbarWindowButtons()
|
||||||
{
|
{
|
||||||
if (_toolbarController is null || _uiHost is null) return;
|
_retailUiRuntime?.SyncToolbarWindowButtons();
|
||||||
_toolbarController.SetInventoryOpen(_uiHost.IsWindowVisible(AcDream.App.UI.WindowNames.Inventory));
|
|
||||||
_toolbarController.SetCharacterOpen(_uiHost.IsWindowVisible(AcDream.App.UI.WindowNames.Character));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetRetailUiLocked(bool locked)
|
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)
|
private void OnUiDragReleasedOutside(object payload, int x, int y)
|
||||||
{
|
{
|
||||||
if (payload is AcDream.App.UI.ItemDragPayload itemPayload)
|
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.
|
// Retained controllers subscribe to session-owned object/selection/combat sources.
|
||||||
// Tear their window-manager ownership down first, while every source and GL-backed
|
// Tear their window-manager ownership down first, while every source and GL-backed
|
||||||
// UI resource is still alive. UiHost disposes controllers before its renderer.
|
// UI resource is still alive. UiHost disposes controllers before its renderer.
|
||||||
_windowLayoutPersistence?.Dispose();
|
_retailUiRuntime?.Dispose();
|
||||||
_windowLayoutPersistence = null;
|
_retailUiRuntime = null;
|
||||||
_uiHost?.Dispose();
|
|
||||||
_uiHost = null;
|
_uiHost = null;
|
||||||
|
|
||||||
// Phase A.1: join the streamer worker thread before tearing down GL
|
// Phase A.1: join the streamer worker thread before tearing down GL
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,10 @@ synthesis + six deep-dive docs under
|
||||||
- `RetailWindowLayoutPersistence.cs` / `IRetainedWindowStateController.cs` —
|
- `RetailWindowLayoutPersistence.cs` / `IRetainedWindowStateController.cs` —
|
||||||
per-character/per-resolution bounds, visibility, collapse/maximize, legacy
|
per-character/per-resolution bounds, visibility, collapse/maximize, legacy
|
||||||
migration, and safe resolution-clamped restore.
|
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
|
- `Layout/RetailWindowFrame.cs` — the single production/Studio mount
|
||||||
contract for imported-chrome and shared-wrapper windows.
|
contract for imported-chrome and shared-wrapper windows.
|
||||||
- `UiHost.cs` — one-shot wrapper: owns the `UiRoot`, a `TextRenderer`,
|
- `UiHost.cs` — one-shot wrapper: owns the `UiRoot`, a `TextRenderer`,
|
||||||
|
|
|
||||||
597
src/AcDream.App/UI/RetailUiRuntime.cs
Normal file
597
src/AcDream.App/UI/RetailUiRuntime.cs
Normal file
|
|
@ -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<uint, (uint Texture, int Width, int Height)> ResolveSprite,
|
||||||
|
Func<uint, UiDatFont?> ResolveFont,
|
||||||
|
UiDatFont? DefaultFont,
|
||||||
|
BitmapFont? DebugFont,
|
||||||
|
ControlsIni Controls,
|
||||||
|
IconComposer Icons);
|
||||||
|
|
||||||
|
public sealed record VitalsRuntimeBindings(VitalsVM ViewModel);
|
||||||
|
|
||||||
|
public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func<ICommandBus> CommandBus);
|
||||||
|
|
||||||
|
public sealed record RadarRuntimeBindings(
|
||||||
|
Func<UiRadarSnapshot> Snapshot,
|
||||||
|
Action<uint> SelectObject,
|
||||||
|
Action<bool> SetUiLocked);
|
||||||
|
|
||||||
|
public sealed record ToolbarRuntimeBindings(
|
||||||
|
ClientObjectTable Objects,
|
||||||
|
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> Shortcuts,
|
||||||
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||||
|
Action<uint> UseItem,
|
||||||
|
CombatState Combat,
|
||||||
|
ItemInteractionController ItemInteraction,
|
||||||
|
Action<uint, uint>? SendAddShortcut,
|
||||||
|
Action<uint>? SendRemoveShortcut,
|
||||||
|
Action<Action<uint?>> SubscribeSelectionChanged,
|
||||||
|
Action<Action<uint?>> UnsubscribeSelectionChanged,
|
||||||
|
Action<Action<uint, float>> SubscribeHealthChanged,
|
||||||
|
Action<Action<uint, float>> UnsubscribeHealthChanged,
|
||||||
|
Func<uint, bool> IsHealthTarget,
|
||||||
|
Func<uint, string?> ResolveName,
|
||||||
|
Func<uint, float> HealthPercent,
|
||||||
|
Func<uint, bool> HasHealth,
|
||||||
|
Func<uint, uint> StackSize,
|
||||||
|
Action<uint> SendQueryHealth);
|
||||||
|
|
||||||
|
public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
||||||
|
|
||||||
|
public sealed record InventoryRuntimeBindings(
|
||||||
|
ClientObjectTable Objects,
|
||||||
|
Func<uint> PlayerGuid,
|
||||||
|
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||||
|
Func<int?> Strength,
|
||||||
|
Action<uint>? SendUse,
|
||||||
|
Action<uint>? SendNoLongerViewing,
|
||||||
|
Action<uint, uint, int>? SendPutItemInContainer,
|
||||||
|
Action<uint, uint>? SendWield,
|
||||||
|
ItemInteractionController ItemInteraction);
|
||||||
|
|
||||||
|
public sealed record RetailUiPersistenceBindings(
|
||||||
|
SettingsStore Store,
|
||||||
|
Func<string> CharacterKey,
|
||||||
|
Func<(int Width, int Height)> ScreenSize);
|
||||||
|
|
||||||
|
public sealed record RetailUiProbeBindings(
|
||||||
|
bool Enabled,
|
||||||
|
string? ScriptPath,
|
||||||
|
bool DumpOnStart,
|
||||||
|
Action<string> 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);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<IMouse> 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<DatReaderWriter.DBObjs.LayoutDesc>(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<uint, DatReaderWriter.Types.BaseProperty> 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<UiElement>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue