From d1d603105f2f1094c0490913671a5427bc1ada3d Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 17 Jul 2026 11:36:43 +0200 Subject: [PATCH] fix(ui): complete retail indicator detail panels Port the authored effect row template, remaining-time and selection details, synchronize the full gmPanelUI child geometry, and route the burden indicator to Character Information panel 3. Co-authored-by: OpenAI Codex --- docs/ISSUES.md | 14 +- .../retail-divergence-register.md | 2 +- docs/plans/2026-04-11-roadmap.md | 9 +- docs/plans/2026-05-12-milestones.md | 13 +- ...-retail-magic-ui-and-casting-pseudocode.md | 12 + ...6-07-17-retail-indicator-bar-pseudocode.md | 37 +- ...-17-retail-shared-main-panel-pseudocode.md | 57 +- .../UI/Layout/CharacterController.cs | 39 +- .../UI/Layout/EffectRowTemplateFactory.cs | 148 ++ .../UI/Layout/EffectsUiController.cs | 74 +- .../UI/Layout/IndicatorBarController.cs | 2 +- .../UI/Layout/RetailPanelUiController.cs | 127 +- src/AcDream.App/UI/RetailPanelCatalog.cs | 2 + src/AcDream.App/UI/RetailUiRuntime.cs | 73 +- src/AcDream.App/UI/UiText.cs | 9 +- src/AcDream.App/UI/WindowNames.cs | 1 + .../UI/Layout/CharacterControllerTests.cs | 31 +- .../UI/Layout/EffectsUiControllerTests.cs | 70 +- .../UI/Layout/FixtureLoader.cs | 10 + .../UI/Layout/IndicatorBarControllerTests.cs | 2 +- .../UI/Layout/RetailLayoutFixtureGenerator.cs | 24 + .../UI/Layout/RetailPanelUiControllerTests.cs | 19 +- .../character_info_2100006E_10000183.json | 2107 +++++++++++++++++ .../effects_row_2100001B_10000128.json | 961 ++++++++ 24 files changed, 3696 insertions(+), 147 deletions(-) create mode 100644 src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs create mode 100644 tests/AcDream.App.Tests/UI/Layout/fixtures/character_info_2100006E_10000183.json create mode 100644 tests/AcDream.App.Tests/UI/Layout/fixtures/effects_row_2100001B_10000128.json diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 93a426b6..d255eb9b 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -61,10 +61,14 @@ back to inert generic DAT elements. `gmUIElement_EffectsIndicator`. The retained controller consequently had no live bindings for session packet age, Vitae, or the player load ratio and did not dispatch Character Information or the shared logout confirmation path. -The strip and its actions are now ported. Link Status (panel 8), both effect -pages (4/5), Vitae (15), and the authored Mini Game shell (9) are registered as -children of the same retained main-panel owner as Inventory/Character/Magic, -so switching pages preserves one canonical position. Link ping RTT and Vitae +The strip and its actions are now ported. Character Information (panel 3), Link +Status (8), both effect pages (4/5), Vitae (15), and the authored Mini Game +shell (9) are registered as children of the same retained main-panel owner as +Inventory/Character/Magic, so switching pages preserves one canonical size and +position. Helpful/Harmful use retail's authored 300x32 icon/name/time rows, +selection states, `SELECT A SPELL` prompt, and selected spell description. +The burden icon routes to Character Information rather than Attributes/Skills. +Link ping RTT and Vitae recovery XP are live; transport packet-loss averaging and Mini Game gameplay remain the explicitly narrowed AP-110 residual. @@ -81,7 +85,7 @@ helpful, harmful, Vitae, and burden react to live state; the burden icon opens Character Information; effects open their matching panels; crossed swords use the shared confirmation dialog and close gracefully only after acceptance. Every non-ghosted detail icon opens its authored page in the shared main-panel -position; closing a restore-previous page returns to the deferred ordinary page. +geometry; closing a restore-previous page returns to the deferred ordinary page. --- diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 31fb7525..c2b7231e 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -50,7 +50,7 @@ accepted-divergence entries (#96, #49, #50). | IA-9 | One unified camera matrix for terrain — retail's separate `LScape::update_viewpoint` landscape viewpoint does not exist | `src/AcDream.App/Rendering/TerrainModernRenderer.cs:266` | Phase W T4.2: with one matrix everywhere, viewpoint-desync bugs are unrepresentable — the unification IS the correctness argument | Anything retail derives from the landcell-relative viewpoint (float precision at extreme coords, viewpoint-keyed state) has no analogue; a future port expecting it silently reads the camera | `LScape::update_viewpoint`; `LScape::draw` 0x00506330 | | IA-10 | Transparent groups sorted back-to-front per GROUP by first-instance position (no within-group sort) vs retail per-poly BSP-order draw | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:1364` (comparer :1662) | One MDI call per pass requires group-granularity ordering; per-poly sorting is incompatible with instanced multi-draw; works when group instances are spatially coherent | Spatially spread or interleaved transparent groups composite in the wrong order — popping / wrong see-through layering as the camera moves | retail per-poly BSP-order transparent draw (D3DPolyRender / PView::DrawCells) | | IA-11 | Tier-1 cross-frame batch-classification cache for static entities (retail re-walks part arrays every frame) | `src/AcDream.App/Rendering/Wb/EntityClassificationCache.cs:12` | Issue #53 perf tier; invariants documented (keys = EntityId + OWNING-landblock hint post-**#119** fix `2163308`; invalidation at despawn/LB-unload; mutation audit 2026-05-10) | Key collision or missed invalidation serves one entity another's batches — session-sticky wrong meshes (the #119 broken-stairs/water-barrel symptom) | retail per-frame part-array classification (no cache) | -| 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). Synthetic wrapper borders/whole-window drag regions use the exact DAT Type-2/Type-9 control cursors. `gmPanelUI` primary children are independently imported retained frames rather than one physical parent, but `RetailPanelUiController` owns their one canonical placement and exclusive child lifecycle | `src/AcDream.App/UI/README.md:3`; `src/AcDream.App/UI/CursorFeedbackController.cs`; `src/AcDream.App/UI/Layout/RetailPanelUiController.cs` | keystone.dll has no PDB/decomp; semantics are reconstructed from the retail UI deep-dives and named client methods plus production LayoutDesc media. Separate child wrappers preserve each LayoutDesc's content/resize policy while typed handle synchronization gives Inventory/Character/Magic the same persistent parent position | Edge-case input semantics can differ silently; if retail relies on parent-size anchoring across a panel switch, separate child extents may resize differently even though placement and visibility match | `UIElementManager::CheckCursor` 0x0045ABF0; `UIElement_Resizebar::StartMouseResizing` 0x0046B7E0; `UIElement_Dragbar::StartMouseMoving` 0x0046C760; `gmPanelUI::SetupChildren` 0x004BC9E0; 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). Synthetic wrapper borders/whole-window drag regions use the exact DAT Type-2/Type-9 control cursors. `gmPanelUI` children are independently imported retained frames rather than one physical parent, but `RetailPanelUiController` owns their one canonical geometry and exclusive child lifecycle | `src/AcDream.App/UI/README.md:3`; `src/AcDream.App/UI/CursorFeedbackController.cs`; `src/AcDream.App/UI/Layout/RetailPanelUiController.cs` | keystone.dll has no PDB/decomp; semantics are reconstructed from retail UI deep-dives, named client methods, and production LayoutDesc media. Separate child wrappers preserve each LayoutDesc's content tree while typed move/resize synchronization gives all registered toolbar/detail children the same persistent parent rectangle | Edge-case low-level input semantics can differ silently even though outer geometry, visibility, and restore-previous ownership match | `UIElementManager::CheckCursor` 0x0045ABF0; `UIElement_Resizebar::StartMouseResizing` 0x0046B7E0; `UIElement_Dragbar::StartMouseMoving` 0x0046C760; `gmPanelUI::SetupChildren` 0x004BC9E0; 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. `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, schema-v2 per-character/per-resolution automatic layouts, and portable named `saveui/loadui` profiles; `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`, dialog catalog `0x2100003C`, and radar `0x21000074`. The dialog context/queue/callback lifecycle is now a named-client port; only its retained rendering remains under this Keystone adaptation. | `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/RetailDialogFactory.cs`; `src/AcDream.App/UI/Layout/RetailConfirmationDialogView.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 and low-level widget rendering are behaviorally reconstructed from retail semantics rather than a Keystone byte-port; lifecycle edge cases remain constrained by conformance tests | Production LayoutDesc objects; `DialogFactory @ 0x004773C0..0x00478470`; `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`; Keystone behavior notes in `docs/research/retail-ui/` | diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index ca94d438..78e16501 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -1,6 +1,6 @@ # acdream — strategic roadmap -**Status:** Living document. Updated 2026-07-17. **M3 active; automated implementation complete, visual gates pending.** M2's connected demo loop is complete. M3 now has the retail cast-intent/component/target lifecycle, exact live enchantment wire state, scoped Magic input, retained spell bar, spellbook, component book, positive/negative effects panels, the complete authored seven-control gameplay indicator strip and its Link/Vitae detail pages, and shared `gmPanelUI` switching over the Step 9 projectile/PhysicsScript/particle/portal foundation. The remaining M3 work is the connected single-client magic-UI/casting gate and final two-client portal-out/materialization observer gate. +**Status:** Living document. Updated 2026-07-17. **M3 active; automated implementation complete, visual gates pending.** M2's connected demo loop is complete. M3 now has the retail cast-intent/component/target lifecycle, exact live enchantment wire state, scoped Magic input, retained spell bar, spellbook, component book, positive/negative effects panels with authored rows and selected-spell details, the complete seven-control gameplay indicator strip and its Character Information/Link/Vitae detail pages, and shared `gmPanelUI` geometry/switching over the Step 9 projectile/PhysicsScript/particle/portal foundation. The remaining M3 work is the connected single-client magic-UI/casting gate and final two-client portal-out/materialization observer gate. **Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file. --- @@ -23,8 +23,11 @@ active enchantment, then recall through the DAT-driven portal presentation. positive/negative active-enchantment panels, the seven-control link/effect/ Vitae/burden/mini-game/session indicator strip, and the retail one-active-main-panel lifecycle. Link Status, Helpful/Harmful, Vitae, and - the inactive Mini Game shell now occupy that same canonical main-panel - position; Link RTT and Vitae recovery XP are live. Connected-gate + Character Information, and the inactive Mini Game shell now occupy that same + canonical main-panel size and position; Link RTT and Vitae recovery XP are + live. Effects now use the exact icon/name/time row template, selection state, + and `SELECT A SPELL`/description field; burden opens Character Information. + Connected-gate correction now also pins type-12 spellbook tabs, retail zero text margins, and `PassToChildren` Closed/Open chrome through real-DAT fixtures. 3. **Pending user visual gate:** connected projectile/self-buff casts plus the diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index c89e2b3c..32a144cd 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -714,11 +714,14 @@ the burden/effect/logout actions use the shared panel/dialog owners. Main-panel visibility now flows through retail's one-active `gmPanelUI` owner. M3 remains active only for the connected single-client magic UI/casting visual gate and the final two-client portal observer gate. -The 2026-07-17 indicator follow-up also mounts the authored Link Status, Vitae, -and Mini Game detail roots as children of that same main-panel owner. Helpful, -Harmful, Link, and Vitae therefore replace Inventory/Character/Magic in one -canonical window position. Link uses retail's payload-free ping/RTT cadence; -Vitae uses PropertyInt 129/139 and the retail XP-pool threshold formula. +The 2026-07-17 indicator follow-up also mounts authored Character Information, +Link Status, Vitae, and Mini Game detail roots as children of that same +main-panel owner. Helpful, Harmful, Character Information, Link, and Vitae +therefore replace Inventory/Character/Magic in one canonical window rectangle. +Helpful/Harmful instantiate retail's DAT icon/name/time row and selected-spell +information presentation. The burden icon sends panel 3 (Character Information), +not panel 11 (Attributes/Skills). Link uses retail's payload-free ping/RTT +cadence; Vitae uses PropertyInt 129/139 and the retail XP-pool threshold formula. The first connected gate exposed and corrected two retained-import defects: spellbook tabs are authored stateful text controls (not buttons), and retail text starts with zero margins. Real-DAT fixtures now pin spellbook binding, diff --git a/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md index ce0a3aa4..53758c54 100644 --- a/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md +++ b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md @@ -569,6 +569,8 @@ negative root 0x10000121 (inherits 0x10000122) shared effects template 0x10000122 effect list 0x10000123 information text 0x10000126 +effect row template 0x10000128 (300 x 32) +row icon / name / duration 0x10000129 / 0x1000012A / 0x1000012B effect UI type property 0x1000000C ``` @@ -622,6 +624,16 @@ an active token stores its spell ID, so every layer of that spell highlights together and shows the shared name/description; clicking either layer again clears the selection. +`InfoRegion::InfoRegion @ 0x004F1450` creates each row with +`UIElement_ListBox::AddItemFromTemplateList(template 0)`. In installed retail +data the icon is x=0 width 32, name is x=37 width 188, and duration is x=225 +width 50. `EffectInfoRegion::Update @ 0x004F1C00` formats finite time as +`m:ss` or `h:mm:ss`; a negative/permanent duration leaves the field empty. +`gmEffectsUI::UpdateSelection @ 0x004B7F90` displays localized +`ID_Effects_Info_SelectASpell` while nothing is selected, otherwise spell name +plus two newlines plus description. Normal rows use state 1 and every row with +the selected spell ID uses Highlight state 6. + ## Portal presentation boundary Recall and portal presentation remains DAT-driven. `/ls` starts the retail diff --git a/docs/research/2026-07-17-retail-indicator-bar-pseudocode.md b/docs/research/2026-07-17-retail-indicator-bar-pseudocode.md index 72ac82e2..274bee2a 100644 --- a/docs/research/2026-07-17-retail-indicator-bar-pseudocode.md +++ b/docs/research/2026-07-17-retail-indicator-bar-pseudocode.md @@ -223,15 +223,44 @@ For every toolbar or indicator detail page: register page with gmPanelUI panel id when shown: hide the current child - move the requested child to the shared main-panel placement + apply the shared main-panel rectangle to the requested child show only the requested child when a page with bool property 0x10000049 closes: restore the deferred ordinary child ``` -Helpful/Harmful, Link Status, Vitae, and the Mini Game shell therefore share -the same retained host position as Inventory, Character, and Magic. They do -not remember independent positions. +Character Information, Helpful/Harmful, Link Status, Vitae, and the Mini Game +shell therefore share the same retained host size and position as Inventory, +Character/Skills, and Magic. They do not remember independent rectangles. + +The burden/backpack indicator sends panel id `3`, whose production child is +root `0x10000183` under LayoutDesc `0x2100006E`. Panel `11` is the separate +Attributes/Skills child and is not the burden indicator destination. + +### Helpful and harmful effect rows + +```text +gmEffectsUI::RebuildList @ 0x004B8350: + sort visible enchantment tokens alphabetically by spell name + AddItemFromTemplateList(template 0) + +EffectInfoRegion row template 0x10000128 (LayoutDesc 0x2100001B): + icon 0x10000129 at x=0, 32 x 32 + name 0x1000012A at x=37, 188 x 32 + time left 0x1000012B at x=225, 50 x 32 + +gmEffectsUI::UpdateSelection @ 0x004B7F90: + no selected spell -> display ID_Effects_Info_SelectASpell + selected spell -> display name + "\n\n" + description + matching rows use Highlight state 6; other rows use Normal state 1 + +gmEffectsUI::SetSelectedSpell @ 0x004B8290: + clicking the selected spell again clears selection +``` + +The installed local.dat resolves `ID_Effects_Info_SelectASpell` to +`SELECT A SPELL`. Remaining finite durations use `m:ss` below one hour and +`h:mm:ss` at one hour or more. ### End character session diff --git a/docs/research/2026-07-17-retail-shared-main-panel-pseudocode.md b/docs/research/2026-07-17-retail-shared-main-panel-pseudocode.md index d71b51f2..f3060e3a 100644 --- a/docs/research/2026-07-17-retail-shared-main-panel-pseudocode.md +++ b/docs/research/2026-07-17-retail-shared-main-panel-pseudocode.md @@ -1,4 +1,4 @@ -# Retail shared main-panel placement — pseudocode +# Retail shared main-panel geometry — pseudocode **Date:** 2026-07-17 **Symptom:** moving Inventory and then opening Character/Skills or Magic opens @@ -73,10 +73,11 @@ SetPanelVisibility(panelId, show): requested.child.SetVisible(false) ``` -The decisive placement fact is structural: Inventory, Character/Skills, and -Magic are children of one `gmPanelUI`. The switch changes child visibility; -there is no child move and no per-panel top-level position. Dragging the parent -therefore moves the one position subsequently used by every child. +The decisive geometry fact is structural: Inventory, Character Information, +Character/Skills, Magic, Effects, and the other detail pages are children of +one `gmPanelUI`. The switch changes child visibility; there is no child move, +resize, or per-panel top-level rectangle. Moving or resizing the parent +therefore changes the one rectangle subsequently used by every child. ## Resize follow-up @@ -86,9 +87,12 @@ The production end-of-retail LayoutDesc supplies the missing geometry oracle: LayoutDesc 0x2100006E gmPanelUI host 0x100005FE 310 x 372 content parent 0x10000180 300 x 362, anchored on all edges - Inventory child 0x1000018B 300 x 362, anchored on all edges - Character child 0x1000018E 300 x 362, anchored on all edges - Magic child 0x10000190 300 x 362, anchored on all edges + Character Information 0x10000183 panel 3, 300 x 362 + Helpful Effects 0x10000184 panel 4, 300 x 362 + Harmful Effects 0x10000185 panel 5, 300 x 362 + Inventory child 0x1000018B panel 7, 300 x 362 + Character/Skills child 0x1000018E panel 11, 300 x 362 + Magic child 0x10000190 panel 13, 300 x 362 top-center 0x1000065C Type 2 Dragbar, cursor 0x06006119 bottom-center 0x10000660 Type 9 Resizebar, cursor 0x06005E66 @@ -102,7 +106,8 @@ properties `0x3C..0x3F`. There is no retail 760-pixel Character limit and no maximum captured from the host's original screen position. acdream retains independent wrappers under IA-12, so the faithful adaptation -is to give each primary wrapper the same bottom-only vertical resize policy, +is to give each registered child wrapper the same bottom-only vertical resize +policy, leave its maximum unbounded by an authored substitute, and calculate the available desktop extent from its current position during the resize. The viewport remains the physical limit; an old static maximum must not become a @@ -113,31 +118,31 @@ smaller limit after the shared panel moves. acdream imports these child LayoutDesc roots independently, so each currently has its own retained wrapper. `RetailPanelUiController` already ports retail's one-active-child and restore-previous lifecycle. Complete that logical-parent -ownership by adding one canonical placement for the primary toolbar children -(Inventory `7`, Character `11`, Magic `13`): +ownership by adding one canonical rectangle for all registered main-panel +children, including Character Information `3`, Helpful/Harmful `4`/`5`, +Inventory `7`, Character/Skills `11`, and Magic `13`: ```text -on primary child frame moved: - canonicalPosition = moved frame position - move every other primary child frame to canonicalPosition +on registered child frame moved or resized: + canonicalGeometry = frame left, top, width, height + apply canonicalGeometry to every hidden sibling -before showing a primary child: - if canonicalPosition exists: - move requested child frame to canonicalPosition +before showing a registered child: + if canonicalGeometry exists: + apply canonicalGeometry to requested child frame else: - canonicalPosition = requested child frame position + canonicalGeometry = requested child frame geometry -before hiding the active primary child: - capture its current position as canonicalPosition +before hiding the active child: + capture its current geometry as canonicalGeometry ``` The controller owns this synchronization and subscribes to typed -`RetailWindowHandle.Moved` events. Toolbar commands, keyboard commands, -restore-previous effects, persistence restore, and direct window dragging all -therefore pass through the same owner. Panel-authored size/resize policy stays -with the active child, but the three primary wrappers apply the shared host's -same vertical resize contract; only placement is synchronized between the -independent frames. +`RetailWindowHandle.Moved` and `Resized` events. Toolbar, indicator, and +keyboard commands, restore-previous effects, persistence restore, and direct +window dragging all therefore pass through the same owner. Each wrapper +retains its own imported content tree, but its outer rectangle and bottom-only +resize contract mirror the shared retail host. ACE contains no client UI implementation, and the in-tree WorldBuilder reference contains no `gmPanelUI` port, so neither supplies a competing diff --git a/src/AcDream.App/UI/Layout/CharacterController.cs b/src/AcDream.App/UI/Layout/CharacterController.cs index 709c8262..b63b2708 100644 --- a/src/AcDream.App/UI/Layout/CharacterController.cs +++ b/src/AcDream.App/UI/Layout/CharacterController.cs @@ -6,7 +6,8 @@ using AcDream.App.UI; namespace AcDream.App.UI.Layout; /// -/// Per-window controller for the Character window (LayoutDesc 0x2100002E, gmCharacterInfoUI). +/// Per-window controller for the Character Information child in the production +/// gmPanelUI layout (LayoutDesc 0x2100006E, root 0x10000183). /// /// Retail fills a single scrollable UIElement_Text element (id 0x1000011d, /// m_pMainText) by calling a sequence of Update* methods that each @@ -32,9 +33,14 @@ namespace AcDream.App.UI.Layout; /// public static class CharacterController { + public const uint LayoutId = 0x2100006Eu; + public const uint RootId = 0x10000183u; + /// Dat element id for the main report text (m_pMainText). Confirmed in /// gmCharacterInfoUI::PostInit 0x004b86f0: GetChildRecursive(this, 0x1000011d). public const uint MainTextId = 0x1000011du; + public const uint ScrollbarId = 0x1000011Eu; + public const uint CloseId = 0x100000FCu; /// White body text — matches retail's default UIElement_Text foreground. private static readonly Vector4 BodyColor = new(1f, 1f, 1f, 1f); @@ -48,27 +54,27 @@ public static class CharacterController /// so the Studio or a live session can push a fresh /// without re-binding. /// - /// The element must resolve as a (Type 12 in the dat). - /// If 0x1000011d is absent from the layout the bind is a no-op — partial layouts - /// (e.g. test fakes) don't cause errors. + /// Production data resolves the element as a + /// (Type 12). A synthetic fallback is created only for deliberately reduced + /// test layouts. /// - /// Imported 0x2100002E layout tree. + /// Imported 0x2100006E / 0x10000183 layout tree. /// Provider returning the current . /// Retail dat font forwarded from the render stack. May be null /// (falls back to the BitmapFont debug path). public static void Bind( ImportedLayout layout, Func data, - UiDatFont? datFont = null) + UiDatFont? datFont = null, + Action? close = null) { - // Retail's gmCharacterInfoUI CREATES m_pMainText (0x1000011d) at RUNTIME — it is NOT a static - // element in any LayoutDesc (confirmed: acdream's dat-import of 0x2100002E AND 0x2100006E both - // lack it; only a runtime UI-tree capture has it). So replicate that: build the report text - // element ourselves and place it in the panel body, exactly as the gm*UI does at runtime. + // End-of-retail data authors m_pMainText under the Character Information + // child. The fallback exists only so focused controller tests can bind a + // deliberately reduced tree without fabricating production structure. var root = layout.Root; if (root is null) return; - var text = new UiText + UiText text = layout.FindElement(MainTextId) as UiText ?? new UiText { EventId = MainTextId, Name = "m_pMainText", @@ -81,8 +87,17 @@ public static class CharacterController DatFont = datFont, ClickThrough = false, }; + if (text.Parent is null) + root.AddChild(text); + else if (text.DatFont is null && datFont is not null) + text.DatFont = datFont; + text.PreserveEndOnLayout = false; + if (layout.FindElement(ScrollbarId) is UiScrollbar scrollbar) + scrollbar.Model = text.Scroll; text.LinesProvider = () => BuildReport(data()); - root.AddChild(text); + + if (layout.FindElement(CloseId) is UiButton closeButton) + closeButton.OnClick = close; } // ── Report builder ──────────────────────────────────────────────────────── diff --git a/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs new file mode 100644 index 00000000..d888cfd8 --- /dev/null +++ b/src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs @@ -0,0 +1,148 @@ +using DatReaderWriter; + +namespace AcDream.App.UI.Layout; + +/// +/// Instantiates retail's effect-token template 0x10000128. This is the +/// retained equivalent of InfoRegion::InfoRegion @ 0x004F1450 calling +/// UIElement_ListBox::AddItemFromTemplateList for +/// EffectInfoRegion. +/// +public sealed class EffectRowTemplateFactory +{ + private readonly ElementInfo _template; + private readonly Func _resolveSprite; + private readonly UiDatFont? _defaultFont; + private readonly IReadOnlyDictionary _fonts; + + public EffectRowTemplateFactory( + ElementInfo template, + Func resolveSprite, + UiDatFont? defaultFont, + IReadOnlyDictionary? fonts = null) + { + _template = template ?? throw new ArgumentNullException(nameof(template)); + _resolveSprite = resolveSprite ?? throw new ArgumentNullException(nameof(resolveSprite)); + _defaultFont = defaultFont; + _fonts = fonts ?? new Dictionary(); + } + + public float Width => _template.Width; + public float Height => _template.Height; + + public static EffectRowTemplateFactory? TryLoad( + DatCollection dats, + Func resolveSprite, + UiDatFont? defaultFont, + Func? resolveFont) + { + ElementInfo? template = LayoutImporter.ImportInfos( + dats, + EffectsUiController.LayoutId, + EffectsUiController.RowTemplateId); + if (template is null + || Find(template, EffectsUiController.RowIconId) is null + || Find(template, EffectsUiController.RowLabelId) is null + || Find(template, EffectsUiController.RowDurationId) is null + || template.Width <= 0f + || template.Height <= 0f) + return null; + + var fonts = new Dictionary(); + CaptureFonts(template, resolveFont, fonts); + return new EffectRowTemplateFactory( + template, resolveSprite, defaultFont, fonts); + } + + public EffectRow Create( + uint spellId, + uint iconTexture, + string name, + string remaining) + { + ImportedLayout content = LayoutImporter.Build( + _template, + _resolveSprite, + _defaultFont, + did => _fonts.TryGetValue(did, out UiDatFont? font) + ? font + : _defaultFont); + UiElement iconHost = Required(content, EffectsUiController.RowIconId); + UiText label = Required(content, EffectsUiController.RowLabelId); + UiText duration = Required(content, EffectsUiController.RowDurationId); + + iconHost.AddChild(new UiTextureElement + { + Width = iconHost.Width, + Height = iconHost.Height, + Anchors = AnchorEdges.Left | AnchorEdges.Top + | AnchorEdges.Right | AnchorEdges.Bottom, + Texture = iconTexture, + }); + label.LinesProvider = () => [new UiText.Line(name, label.DefaultColor)]; + + var slot = new UiTemplateListSlot( + content, + spellId, + UiButtonStateMachine.Normal, + UiButtonStateMachine.Highlight); + return new EffectRow(slot, duration, remaining); + } + + public sealed class EffectRow + { + private readonly UiText _duration; + private string _remaining; + + internal EffectRow( + UiTemplateListSlot slot, + UiText duration, + string remaining) + { + Slot = slot; + _duration = duration; + _remaining = remaining; + _duration.LinesProvider = () => + [new UiText.Line(_remaining, _duration.DefaultColor)]; + } + + public UiTemplateListSlot Slot { get; } + + public string Remaining + { + get => _remaining; + set => _remaining = value; + } + } + + private static T Required(ImportedLayout content, uint id) + where T : UiElement + => content.FindElement(id) as T + ?? throw new InvalidOperationException( + $"Retail effect template element 0x{id:X8} did not resolve to {typeof(T).Name}."); + + private static UiElement Required(ImportedLayout content, uint id) + => content.FindElement(id) + ?? throw new InvalidOperationException( + $"Retail effect template element 0x{id:X8} is missing."); + + private static ElementInfo? Find(ElementInfo root, uint id) + { + if (root.Id == id) return root; + foreach (ElementInfo child in root.Children) + if (Find(child, id) is { } found) + return found; + return null; + } + + private static void CaptureFonts( + ElementInfo info, + Func? resolveFont, + Dictionary fonts) + { + if (info.FontDid != 0u && !fonts.ContainsKey(info.FontDid)) + fonts[info.FontDid] = resolveFont?.Invoke(info.FontDid); + foreach (ElementInfo child in info.Children) + CaptureFonts(child, resolveFont, fonts); + } +} diff --git a/src/AcDream.App/UI/Layout/EffectsUiController.cs b/src/AcDream.App/UI/Layout/EffectsUiController.cs index cc04b290..772b2af2 100644 --- a/src/AcDream.App/UI/Layout/EffectsUiController.cs +++ b/src/AcDream.App/UI/Layout/EffectsUiController.cs @@ -16,16 +16,24 @@ public sealed class EffectsUiController : IRetainedPanelController public const uint NegativeRootId = 0x10000121u; public const uint CloseId = 0x100000FCu; public const uint ListId = 0x10000123u; + public const uint ScrollbarId = 0x10000124u; public const uint InfoTextId = 0x10000126u; + public const uint RowTemplateId = 0x10000128u; + public const uint RowIconId = 0x10000129u; + public const uint RowLabelId = 0x1000012Au; + public const uint RowDurationId = 0x1000012Bu; private readonly Spellbook _spellbook; private readonly bool _positive; private readonly Func _serverTime; private readonly Func _resolveSpellIcon; + private readonly EffectRowTemplateFactory _templates; + private readonly string _selectPrompt; private readonly UiItemList _list; private readonly UiText? _info; private readonly UiButton? _close; - private readonly Dictionary _rows = new(); + private readonly UiScrollbar? _scrollbar; + private readonly Dictionary _rows = new(); private uint? _selectedSpellId; private double _lastDurationUpdate = double.NaN; private bool _disposed; @@ -38,6 +46,8 @@ public sealed class EffectsUiController : IRetainedPanelController bool positive, Func serverTime, Func resolveSpellIcon, + EffectRowTemplateFactory templates, + string selectPrompt, UiItemList list, Action? close) { @@ -45,13 +55,18 @@ public sealed class EffectsUiController : IRetainedPanelController _positive = positive; _serverTime = serverTime; _resolveSpellIcon = resolveSpellIcon; + _templates = templates; + _selectPrompt = selectPrompt; _list = list; _info = layout.FindElement(InfoTextId) as UiText; _close = layout.FindElement(CloseId) as UiButton; + _scrollbar = layout.FindElement(ScrollbarId) as UiScrollbar; if (_close is not null) _close.OnClick = close; _list.Columns = 1; - _list.CellWidth = Math.Max(1f, _list.Width); - _list.CellHeight = 32f; + _list.CellWidth = templates.Width; + _list.CellHeight = templates.Height; + if (_scrollbar is not null) + _scrollbar.Model = _list.Scroll; ConfigureInfo(); _spellbook.EnchantmentsChanged += Rebuild; Rebuild(); @@ -64,6 +79,8 @@ public sealed class EffectsUiController : IRetainedPanelController Func serverTime, Func spriteResolve, Func resolveSpellIcon, + EffectRowTemplateFactory templates, + string selectPrompt, Action? close = null) { UiElement? host = layout.FindElement(ListId); @@ -82,7 +99,15 @@ public sealed class EffectsUiController : IRetainedPanelController host.AddChild(list); } return new EffectsUiController( - layout, spellbook, positive, serverTime, resolveSpellIcon, list, close); + layout, + spellbook, + positive, + serverTime, + resolveSpellIcon, + templates, + selectPrompt, + list, + close); } public void Tick() @@ -95,8 +120,10 @@ public sealed class EffectsUiController : IRetainedPanelController return; _lastDurationUpdate = now; foreach (ActiveEnchantmentRecord enchantment in VisibleEnchantments()) - if (_rows.TryGetValue(enchantment.Identity, out UiCatalogSlot? row)) - row.Detail = FormatRemaining(enchantment, now); + if (_rows.TryGetValue( + enchantment.Identity, + out EffectRowTemplateFactory.EffectRow? row)) + row.Remaining = FormatRemaining(enchantment, now); } private void Rebuild() @@ -111,22 +138,18 @@ public sealed class EffectsUiController : IRetainedPanelController { _spellbook.TryGetMetadata(enchantment.SpellId, out SpellMetadata? metadata); uint identity = enchantment.Identity; - var row = new UiCatalogSlot - { - EntryId = enchantment.SpellId, - CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId), - Label = metadata?.Name ?? $"Spell {enchantment.SpellId}", - Detail = FormatRemaining(enchantment, _serverTime()), - ShowLabel = true, - SpriteResolve = _list.SpriteResolve, - }; - row.Clicked = () => Select(enchantment.SpellId); + EffectRowTemplateFactory.EffectRow row = _templates.Create( + enchantment.SpellId, + metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId), + metadata?.Name ?? $"Spell {enchantment.SpellId}", + FormatRemaining(enchantment, _serverTime())); + row.Slot.Clicked = () => Select(enchantment.SpellId); _rows[identity] = row; - _list.AddItem(row); + _list.AddItem(row.Slot); } } if (_selectedSpellId is uint selected - && !_rows.Values.Any(row => row.EntryId == selected)) + && !_rows.Values.Any(row => row.Slot.EntryId == selected)) _selectedSpellId = null; SyncSelection(); } @@ -145,7 +168,9 @@ public sealed class EffectsUiController : IRetainedPanelController private static string FormatRemaining(ActiveEnchantmentRecord enchantment, double now) { - if (enchantment.Duration < 0) return "Permanent"; + // EffectInfoRegion::Update @ 0x004F1C00 starts from retail's empty + // PString and only formats it when the remaining duration is >= 0. + if (enchantment.Duration < 0) return string.Empty; double remaining = Math.Max(0, enchantment.StartTime + enchantment.Duration - now); if (!double.IsFinite(remaining)) return "--:--"; remaining = Math.Min(remaining, TimeSpan.MaxValue.TotalSeconds); @@ -165,25 +190,27 @@ public sealed class EffectsUiController : IRetainedPanelController private void SyncSelection() { - foreach (UiCatalogSlot row in _rows.Values) - row.Selected = row.EntryId == _selectedSpellId; + foreach (EffectRowTemplateFactory.EffectRow row in _rows.Values) + row.Slot.SetSelected(row.Slot.EntryId == _selectedSpellId); } private void ConfigureInfo() { if (_info is null) return; + _info.PreserveEndOnLayout = false; _info.LinesProvider = () => { if (_selectedSpellId is not uint spellId) - return Array.Empty(); + return [new UiText.Line(_selectPrompt, _info.DefaultColor)]; ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot .Cast() .FirstOrDefault(record => record?.SpellId == spellId); if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata)) - return Array.Empty(); + return [new UiText.Line(_selectPrompt, _info.DefaultColor)]; return [ new UiText.Line(metadata.Name, _info.DefaultColor), + new UiText.Line(string.Empty, _info.DefaultColor), new UiText.Line(metadata.Description, _info.DefaultColor), ]; }; @@ -197,5 +224,6 @@ public sealed class EffectsUiController : IRetainedPanelController _disposed = true; _spellbook.EnchantmentsChanged -= Rebuild; if (_close is not null) _close.OnClick = null; + if (_scrollbar is not null) _scrollbar.Model = null; } } diff --git a/src/AcDream.App/UI/Layout/IndicatorBarController.cs b/src/AcDream.App/UI/Layout/IndicatorBarController.cs index 713e87f5..1cf19182 100644 --- a/src/AcDream.App/UI/Layout/IndicatorBarController.cs +++ b/src/AcDream.App/UI/Layout/IndicatorBarController.cs @@ -87,7 +87,7 @@ public sealed class IndicatorBarController : IRetainedPanelController _harmful.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.NegativeEffects); _link.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.LinkStatus); _vitae.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.Vitae); - _burden.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.Character); + _burden.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.CharacterInformation); _miniGame.OnClick = () => bindings.TogglePanel(RetailPanelCatalog.MiniGame); _endCharacterSession.OnClick = bindings.RequestEndCharacterSession; diff --git a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs index 50e9e251..c5e9c8d5 100644 --- a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs +++ b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs @@ -21,8 +21,8 @@ public sealed class RetailPanelUiController : IDisposable private uint? _activePanel; private uint? _deferredPanel; private bool _applying; - private bool _synchronizingPlacement; - private PanelPlacement? _mainPanelPlacement; + private bool _synchronizingGeometry; + private PanelGeometry? _mainPanelGeometry; private bool _disposed; public RetailPanelUiController( @@ -38,11 +38,11 @@ public sealed class RetailPanelUiController : IDisposable public uint? ActivePanelId => _activePanel; /// - /// Registers one of retail gmPanelUI's primary toolbar children. - /// Inventory, Character, and Magic retain panel-specific content/resize - /// policy but share the one parent placement. A drag of any registered - /// child updates every hidden sibling through typed window handles, so - /// persistence observes the same canonical position too. + /// Registers one of retail gmPanelUI's toolbar or detail children. + /// All primary and detail children retain panel-specific content but share + /// one parent geometry. A move or resize of any registered child updates + /// every hidden sibling through typed window handles, so persistence sees + /// the same canonical rectangle too. /// public void RegisterMainPanel( uint panelId, @@ -61,7 +61,7 @@ public sealed class RetailPanelUiController : IDisposable windowName, restorePrevious, window, - sharesMainPanelPlacement: true); + sharesMainPanelGeometry: true); } /// @@ -75,14 +75,14 @@ public sealed class RetailPanelUiController : IDisposable windowName, restorePrevious, window: null, - sharesMainPanelPlacement: false); + sharesMainPanelGeometry: false); private void RegisterCore( uint panelId, string windowName, bool restorePrevious, RetailWindowHandle? window, - bool sharesMainPanelPlacement) + bool sharesMainPanelGeometry) { ObjectDisposedException.ThrowIf(_disposed, this); if (panelId == 0) throw new ArgumentOutOfRangeException(nameof(panelId)); @@ -95,14 +95,17 @@ public sealed class RetailPanelUiController : IDisposable windowName, restorePrevious, window, - sharesMainPanelPlacement); + sharesMainPanelGeometry); _byPanel.Add(panelId, entry); _byWindow.Add(windowName, panelId); if (window is not null) + { window.Moved += OnWindowMoved; + window.Resized += OnWindowResized; + } - if (sharesMainPanelPlacement && _mainPanelPlacement is { } placement) - MoveWindowTo(entry, placement); + if (sharesMainPanelGeometry && _mainPanelGeometry is { } geometry) + ApplyWindowGeometry(entry, geometry); if (_isVisible(windowName)) ObserveWindowVisibility(windowName, visible: true); } @@ -144,7 +147,7 @@ public sealed class RetailPanelUiController : IDisposable { if (_activePanel == panelId) { - PrepareMainPanelPlacement(requested); + PrepareMainPanelGeometry(requested); return _show(requested.WindowName); } @@ -156,7 +159,7 @@ public sealed class RetailPanelUiController : IDisposable : null; if (previous is { } previousEntry) - CaptureMainPanelPlacement(previousEntry, synchronizeSiblings: true); + CaptureMainPanelGeometry(previousEntry, synchronizeSiblings: true); _deferredPanel = requested.RestorePrevious && previous is { RestorePrevious: false } @@ -165,7 +168,7 @@ public sealed class RetailPanelUiController : IDisposable _activePanel = panelId; if (previous is not null) _hide(previous.Value.WindowName); - PrepareMainPanelPlacement(requested); + PrepareMainPanelGeometry(requested); return _show(requested.WindowName); } @@ -175,7 +178,7 @@ public sealed class RetailPanelUiController : IDisposable return _hide(requested.WindowName); } - CaptureMainPanelPlacement(requested, synchronizeSiblings: true); + CaptureMainPanelGeometry(requested, synchronizeSiblings: true); bool hidden = _hide(requested.WindowName); _activePanel = null; if (_deferredPanel is not uint deferredId @@ -184,7 +187,7 @@ public sealed class RetailPanelUiController : IDisposable _deferredPanel = null; _activePanel = deferredId; - PrepareMainPanelPlacement(deferred); + PrepareMainPanelGeometry(deferred); return _show(deferred.WindowName) || hidden; } finally @@ -205,76 +208,109 @@ public sealed class RetailPanelUiController : IDisposable private void OnWindowMoved(RetailWindowHandle window) { - if (_disposed || _synchronizingPlacement) return; + if (_disposed || _synchronizingGeometry) return; foreach (PanelEntry entry in _byPanel.Values) { - if (!entry.SharesMainPanelPlacement + if (!entry.SharesMainPanelGeometry || !ReferenceEquals(entry.Window, window)) continue; - _mainPanelPlacement = new PanelPlacement(window.Left, window.Top); - SynchronizeMainPanelSiblings(window); + CaptureAndSynchronizeMainPanelGeometry(window); return; } } - private void CaptureMainPanelPlacement( + private void OnWindowResized(RetailWindowHandle window) + { + if (_disposed || _synchronizingGeometry) return; + + foreach (PanelEntry entry in _byPanel.Values) + { + if (!entry.SharesMainPanelGeometry + || !ReferenceEquals(entry.Window, window)) + continue; + + CaptureAndSynchronizeMainPanelGeometry(window); + return; + } + } + + private void CaptureAndSynchronizeMainPanelGeometry(RetailWindowHandle source) + { + _mainPanelGeometry = new PanelGeometry( + source.Left, + source.Top, + source.Width, + source.Height); + SynchronizeMainPanelSiblings(source); + } + + private void CaptureMainPanelGeometry( PanelEntry entry, bool synchronizeSiblings) { - if (!entry.SharesMainPanelPlacement || entry.Window is not { } window) + if (!entry.SharesMainPanelGeometry || entry.Window is not { } window) return; - _mainPanelPlacement = new PanelPlacement(window.Left, window.Top); + _mainPanelGeometry = new PanelGeometry( + window.Left, + window.Top, + window.Width, + window.Height); if (synchronizeSiblings) SynchronizeMainPanelSiblings(window); } - private void PrepareMainPanelPlacement(PanelEntry entry) + private void PrepareMainPanelGeometry(PanelEntry entry) { - if (!entry.SharesMainPanelPlacement || entry.Window is not { } window) + if (!entry.SharesMainPanelGeometry || entry.Window is not { } window) return; - if (_mainPanelPlacement is not { } placement) + if (_mainPanelGeometry is not { } geometry) { - _mainPanelPlacement = new PanelPlacement(window.Left, window.Top); + _mainPanelGeometry = new PanelGeometry( + window.Left, + window.Top, + window.Width, + window.Height); SynchronizeMainPanelSiblings(window); return; } - MoveWindowTo(entry, placement); + ApplyWindowGeometry(entry, geometry); } private void SynchronizeMainPanelSiblings(RetailWindowHandle source) { - if (_mainPanelPlacement is not { } placement) return; + if (_mainPanelGeometry is not { } geometry) return; - _synchronizingPlacement = true; + _synchronizingGeometry = true; try { foreach (PanelEntry sibling in _byPanel.Values) { - if (!sibling.SharesMainPanelPlacement + if (!sibling.SharesMainPanelGeometry || sibling.Window is not { } window || ReferenceEquals(window, source)) continue; - MoveWindowTo(sibling, placement); + ApplyWindowGeometry(sibling, geometry); } } finally { - _synchronizingPlacement = false; + _synchronizingGeometry = false; } } - private void MoveWindowTo(PanelEntry entry, PanelPlacement placement) + private static void ApplyWindowGeometry(PanelEntry entry, PanelGeometry geometry) { - if (entry.Window is not { } window - || (window.Left == placement.Left && window.Top == placement.Top)) - return; + if (entry.Window is not { } window) return; - window.MoveTo(placement.Left, placement.Top); + if (window.Left != geometry.Left || window.Top != geometry.Top) + window.MoveTo(geometry.Left, geometry.Top); + if (window.Width != geometry.Width || window.Height != geometry.Height) + window.ResizeTo(geometry.Width, geometry.Height); } public void Dispose() @@ -283,14 +319,21 @@ public sealed class RetailPanelUiController : IDisposable _disposed = true; foreach (PanelEntry entry in _byPanel.Values) if (entry.Window is { } window) + { window.Moved -= OnWindowMoved; + window.Resized -= OnWindowResized; + } } - private readonly record struct PanelPlacement(float Left, float Top); + private readonly record struct PanelGeometry( + float Left, + float Top, + float Width, + float Height); private readonly record struct PanelEntry( string WindowName, bool RestorePrevious, RetailWindowHandle? Window, - bool SharesMainPanelPlacement); + bool SharesMainPanelGeometry); } diff --git a/src/AcDream.App/UI/RetailPanelCatalog.cs b/src/AcDream.App/UI/RetailPanelCatalog.cs index b2577b15..95dcae21 100644 --- a/src/AcDream.App/UI/RetailPanelCatalog.cs +++ b/src/AcDream.App/UI/RetailPanelCatalog.cs @@ -7,6 +7,7 @@ namespace AcDream.App.UI; /// public static class RetailPanelCatalog { + public const uint CharacterInformation = 3u; public const uint PositiveEffects = 4u; public const uint NegativeEffects = 5u; public const uint Inventory = 7u; @@ -18,6 +19,7 @@ public static class RetailPanelCatalog private static readonly (uint PanelId, string WindowName)[] Mounted = { + (CharacterInformation, WindowNames.CharacterInformation), (PositiveEffects, WindowNames.PositiveEffects), (NegativeEffects, WindowNames.NegativeEffects), (Inventory, WindowNames.Inventory), diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index fcf8e198..e69ee869 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -847,6 +847,8 @@ public sealed class RetailUiRuntime : IDisposable uint rootId = positive ? EffectsUiController.PositiveRootId : EffectsUiController.NegativeRootId; ElementInfo? rootInfo; ImportedLayout? layout; + EffectRowTemplateFactory? rowTemplates; + string selectPrompt; lock (_bindings.Assets.DatLock) { rootInfo = LayoutImporter.ImportInfos( @@ -861,10 +863,20 @@ public sealed class RetailUiRuntime : IDisposable _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont, new DatStringResolver(_bindings.Assets.Dats).Resolve); + rowTemplates = EffectRowTemplateFactory.TryLoad( + _bindings.Assets.Dats, + _bindings.Assets.ResolveSprite, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont); + var strings = new DatStringResolver(_bindings.Assets.Dats); + selectPrompt = strings.Resolve( + 0x23000001u, + DatStringResolver.ComputeHash("ID_Effects_Info_SelectASpell")) + ?? "SELECT A SPELL"; } - if (rootInfo is null || layout is null) + if (rootInfo is null || layout is null || rowTemplates is null) { - Console.WriteLine($"[M3] effects: root 0x{rootId:X8} not found."); + Console.WriteLine($"[M3] effects: root or row template for 0x{rootId:X8} not found."); return; } @@ -875,6 +887,8 @@ public sealed class RetailUiRuntime : IDisposable _bindings.Magic.ServerTime, _bindings.Assets.ResolveSprite, _bindings.Magic.ResolveSpellIcon, + rowTemplates, + selectPrompt, close: () => CloseWindow( positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects)); if (controller is null) @@ -893,14 +907,17 @@ public sealed class RetailUiRuntime : IDisposable new RetailWindowFrame.Options { WindowName = positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects, - Chrome = RetailWindowChrome.Imported, + Chrome = RetailWindowChrome.NineSlice, Left = Math.Max(0f, Host.Root.Width - root.Width - 12f), Top = 18f, Visible = false, - Resizable = false, ResizeX = false, - ResizeY = false, + ResizeY = true, + ResizableEdges = ResizeEdges.Bottom, ConstrainDragToParent = true, + ConstrainResizeToParent = true, + ContentAnchors = AnchorEdges.Left | AnchorEdges.Top + | AnchorEdges.Right | AnchorEdges.Bottom, ContentClickThrough = false, Controller = controller, }); @@ -919,6 +936,41 @@ public sealed class RetailUiRuntime : IDisposable MountLinkStatusPanel(); MountVitaePanel(); MountMiniGamePanel(); + MountCharacterInformationPanel(); + } + + private void MountCharacterInformationPanel() + { + ElementInfo? rootInfo; + ImportedLayout? layout; + lock (_bindings.Assets.DatLock) + { + rootInfo = LayoutImporter.ImportInfos( + _bindings.Assets.Dats, + CharacterController.LayoutId, + CharacterController.RootId); + layout = rootInfo is null + ? null + : LayoutImporter.Build( + rootInfo, + _bindings.Assets.ResolveSprite, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont, + new DatStringResolver(_bindings.Assets.Dats).Resolve); + } + if (rootInfo is null || layout is null) return; + + CharacterController.Bind( + layout, + _bindings.Character.Provider.BuildSheet, + _bindings.Assets.DefaultFont, + () => CloseWindow(WindowNames.CharacterInformation)); + RegisterIndicatorDetailPanel( + RetailPanelCatalog.CharacterInformation, + WindowNames.CharacterInformation, + rootInfo, + layout.Root, + controller: null); } private void MountLinkStatusPanel() @@ -1054,7 +1106,7 @@ public sealed class RetailUiRuntime : IDisposable string windowName, ElementInfo rootInfo, UiElement root, - IRetainedPanelController controller) + IRetainedPanelController? controller) { RetailWindowHandle handle = RetailWindowFrame.Mount( Host.Root, @@ -1063,14 +1115,17 @@ public sealed class RetailUiRuntime : IDisposable new RetailWindowFrame.Options { WindowName = windowName, - Chrome = RetailWindowChrome.Imported, + Chrome = RetailWindowChrome.NineSlice, Left = 18f, Top = 18f, Visible = false, - Resizable = false, ResizeX = false, - ResizeY = false, + ResizeY = true, + ResizableEdges = ResizeEdges.Bottom, ConstrainDragToParent = true, + ConstrainResizeToParent = true, + ContentAnchors = AnchorEdges.Left | AnchorEdges.Top + | AnchorEdges.Right | AnchorEdges.Bottom, ContentClickThrough = false, Controller = controller, }); diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs index fb54c08d..5fc596d0 100644 --- a/src/AcDream.App/UI/UiText.cs +++ b/src/AcDream.App/UI/UiText.cs @@ -152,6 +152,13 @@ public sealed class UiText : UiElement, IUiDatStateful /// The scroll model — also read by the linked UiScrollbar. public UiScrollable Scroll { get; } = new(); + /// + /// Keeps a view that is already at the end pinned there when content or + /// geometry changes. Chat uses the default; top-oriented reports such as + /// Character Information disable it. + /// + public bool PreserveEndOnLayout { get; set; } = true; + private const float WheelLines = 1f; // lines advanced per wheel notch (retail = 1 line per notch) // ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ── @@ -448,7 +455,7 @@ public sealed class UiText : UiElement, IUiDatStateful Scroll.SetExtents( (int)MathF.Ceiling(contentH), (int)MathF.Floor(innerH), - preserveEnd: true); + preserveEnd: PreserveEndOnLayout); // Overflow keeps the UiScrollable convention: ScrollY=0 is TOP/oldest and // ScrollY=MaxScroll is BOTTOM/newest. Fitting DAT-authored content instead diff --git a/src/AcDream.App/UI/WindowNames.cs b/src/AcDream.App/UI/WindowNames.cs index 21069b3e..b5acacef 100644 --- a/src/AcDream.App/UI/WindowNames.cs +++ b/src/AcDream.App/UI/WindowNames.cs @@ -7,6 +7,7 @@ public static class WindowNames public const string Vitals = "vitals"; public const string Toolbar = "toolbar"; public const string Character = "character"; + public const string CharacterInformation = "character-information"; public const string Inventory = "inventory"; public const string Chat = "chat"; public const string Radar = "radar"; diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs index 0f92ca07..c137c7dd 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs @@ -6,12 +6,37 @@ namespace AcDream.App.Tests.UI.Layout; /// /// Unit tests for and . -/// The controller CREATES the m_pMainText report element (0x1000011d) at bind time — retail's -/// gm*UI builds it at runtime; it is not a static dat element — and attaches it to the layout root. -/// No dats, no GL — pure data-wiring + report-content tests. +/// Production Character Information data authors m_pMainText (0x1000011d); +/// reduced unit-test layouts exercise the controller's fallback element. +/// No GL — pure data-wiring + report-content tests. /// public class CharacterControllerTests { + [Fact] + public void AuthoredCharacterInformationPanel_BindsTextScrollbarAndClose() + { + ImportedLayout layout = FixtureLoader.LoadCharacterInformation(); + int closes = 0; + + CharacterController.Bind( + layout, SampleData.SampleCharacter, close: () => closes++); + + Assert.Equal(CharacterController.RootId, layout.Root.DatElementId); + Assert.Equal((300f, 362f), (layout.Root.Width, layout.Root.Height)); + UiText text = Assert.IsType( + layout.FindElement(CharacterController.MainTextId)); + UiScrollbar scrollbar = Assert.IsType( + layout.FindElement(CharacterController.ScrollbarId)); + Assert.Same(text.Scroll, scrollbar.Model); + Assert.False(text.PreserveEndOnLayout); + Assert.Contains("Studio Player", text.LinesProvider().Select(line => line.Text)); + + UiButton close = Assert.IsType( + layout.FindElement(CharacterController.CloseId)); + close.OnEvent(new UiEvent(0, close, UiEventType.Click)); + Assert.Equal(1, closes); + } + /// Bind on an empty layout, then return the created m_pMainText element. private static UiText BindAndGetText(System.Func data) { diff --git a/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs index bb7f7819..e1e83757 100644 --- a/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs @@ -7,6 +7,28 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class EffectsUiControllerTests { private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); + private static EffectRowTemplateFactory Templates() + => new(FixtureLoader.LoadEffectRowTemplateInfos(), NoTex, defaultFont: null); + + [Fact] + public void AuthoredEffectRowTemplate_HasRetailIconNameDurationAndSelectionStates() + { + ElementInfo row = FixtureLoader.LoadEffectRowTemplateInfos(); + + Assert.Equal(EffectsUiController.RowTemplateId, row.Id); + Assert.Equal((300f, 32f), (row.Width, row.Height)); + Assert.Contains(UiButtonStateMachine.Normal, row.States.Keys); + Assert.Contains(UiButtonStateMachine.Highlight, row.States.Keys); + Assert.Equal((0f, 32f), + (FindInfo(row, EffectsUiController.RowIconId)!.X, + FindInfo(row, EffectsUiController.RowIconId)!.Width)); + Assert.Equal((37f, 188f), + (FindInfo(row, EffectsUiController.RowLabelId)!.X, + FindInfo(row, EffectsUiController.RowLabelId)!.Width)); + Assert.Equal((225f, 50f), + (FindInfo(row, EffectsUiController.RowDurationId)!.X, + FindInfo(row, EffectsUiController.RowDurationId)!.Width)); + } [Fact] public void Rebuild_DoesNotAutoSelectFirstEffect_AndClearsRemovedSelection() @@ -25,8 +47,10 @@ public sealed class EffectsUiControllerTests [EffectsUiController.InfoTextId] = info, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => id)!; + layout, spellbook, positive: true, () => 0d, NoTex, id => id, + Templates(), "SELECT A SPELL")!; + Assert.False(info.PreserveEndOnLayout); spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 42u)); spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( @@ -34,10 +58,15 @@ public sealed class EffectsUiControllerTests spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 3u, 60f, 1u, Bucket: 8u, SpellCategory: 42u)); Assert.Null(controller.SelectedSpellId); + Assert.Equal("SELECT A SPELL", Assert.Single(info.LinesProvider()).Text); Assert.Equal(1, list.GetNumUIItems()); - UiCatalogSlot row = Assert.IsType(list.GetItem(0)); + UiTemplateListSlot row = Assert.IsType(list.GetItem(0)); + UiText duration = Assert.IsType( + row.Content.FindElement(EffectsUiController.RowDurationId)); + Assert.Equal("1:00", Assert.Single(duration.LinesProvider()).Text); row.OnEvent(new UiEvent(0, row, UiEventType.Click)); Assert.Equal(42u, controller.SelectedSpellId); + Assert.Equal(["Boon", "", ""], info.LinesProvider().Select(line => line.Text)); spellbook.OnEnchantmentRemoved(1u, 42u); Assert.Null(controller.SelectedSpellId); @@ -57,14 +86,15 @@ public sealed class EffectsUiControllerTests [EffectsUiController.ListId] = list, }); using EffectsUiController controller = EffectsUiController.Bind( - layout, spellbook, positive: true, () => 0d, NoTex, id => id)!; + layout, spellbook, positive: true, () => 0d, NoTex, id => id, + Templates(), "SELECT A SPELL")!; spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 100u)); spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( 42u, 2u, 60f, 2u, Bucket: 2u, SpellCategory: 101u)); - UiCatalogSlot first = Assert.IsType(list.GetItem(0)); - UiCatalogSlot second = Assert.IsType(list.GetItem(1)); + UiTemplateListSlot first = Assert.IsType(list.GetItem(0)); + UiTemplateListSlot second = Assert.IsType(list.GetItem(1)); first.OnEvent(new UiEvent(0, first, UiEventType.Click)); Assert.Equal(42u, controller.SelectedSpellId); @@ -77,6 +107,32 @@ public sealed class EffectsUiControllerTests Assert.False(second.Selected); } + [Fact] + public void PermanentEffect_LeavesRetailDurationColumnEmpty() + { + var table = SpellTable.LoadFromReader(new StringReader( + "Spell ID,Name,Flags [Hex]\n42,Boon,0x4\n")); + var spellbook = new Spellbook(table); + var root = new UiPanel { Width = 160f, Height = 100f }; + var list = new UiItemList(NoTex) { Width = 160f, Height = 64f }; + root.AddChild(list); + var layout = new ImportedLayout(root, new Dictionary + { + [EffectsUiController.ListId] = list, + }); + using EffectsUiController controller = EffectsUiController.Bind( + layout, spellbook, positive: true, () => 0d, NoTex, id => id, + Templates(), "SELECT A SPELL")!; + + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 1u, -1f, 1u, Bucket: 1u, SpellCategory: 42u)); + + UiTemplateListSlot row = Assert.IsType(list.GetItem(0)); + UiText duration = Assert.IsType( + row.Content.FindElement(EffectsUiController.RowDurationId)); + Assert.Equal(string.Empty, Assert.Single(duration.LinesProvider()).Text); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -117,11 +173,13 @@ public sealed class EffectsUiControllerTests int closes = 0; using EffectsUiController? controller = EffectsUiController.Bind( - layout, spellbook, positive, () => 0d, NoTex, id => id, () => closes++); + layout, spellbook, positive, () => 0d, NoTex, id => id, + Templates(), "SELECT A SPELL", () => closes++); Assert.NotNull(controller); Assert.NotNull(layout.FindElement(EffectsUiController.ListId)); Assert.IsType(layout.FindElement(EffectsUiController.InfoTextId)); + Assert.IsType(layout.FindElement(EffectsUiController.ScrollbarId)); Assert.Equal(expectedRoot, layout.Root.DatElementId); UiButton close = Assert.IsType(layout.FindElement(EffectsUiController.CloseId)); close.OnEvent(new UiEvent(0, close, UiEventType.Click)); diff --git a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs index 5a18205b..214586bc 100644 --- a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs +++ b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs @@ -137,6 +137,16 @@ public static class FixtureLoader public static ElementInfo LoadNegativeEffectsInfos() => LoadInfos("effects_negative_2100001B.json"); + public static ElementInfo LoadEffectRowTemplateInfos() + => LoadInfos("effects_row_2100001B_10000128.json"); + + public static ImportedLayout LoadCharacterInformation() + => LayoutImporter.Build( + LoadCharacterInformationInfos(), _ => (0u, 0, 0), null); + + public static ElementInfo LoadCharacterInformationInfos() + => LoadInfos("character_info_2100006E_10000183.json"); + public static ImportedLayout LoadIndicators() => LayoutImporter.Build(LoadIndicatorsInfos(), _ => (0u, 0, 0), null); diff --git a/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs index caf419a7..810e85c9 100644 --- a/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/IndicatorBarControllerTests.cs @@ -98,7 +98,7 @@ public sealed class IndicatorBarControllerTests burden.OnEvent(new UiEvent(0, burden, UiEventType.Click)); - Assert.Equal([RetailPanelCatalog.Character], h.ToggledPanels); + Assert.Equal([RetailPanelCatalog.CharacterInformation], h.ToggledPanels); } [Theory] diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs b/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs index aa33afa5..ce38b102 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs @@ -64,6 +64,11 @@ public sealed class RetailLayoutFixtureGenerator Assert.Equal(expected, localStrings.Resolve( 0x23000001u, DatStringResolver.ComputeHash(key))); } + Assert.Equal( + "SELECT A SPELL", + localStrings.Resolve( + 0x23000001u, + DatStringResolver.ComputeHash("ID_Effects_Info_SelectASpell"))); uint masterDid = (uint)dats.Portal.Header.MasterMapId; Assert.True(dats.Portal.TryGet(masterDid, out var master)); @@ -120,6 +125,25 @@ public sealed class RetailLayoutFixtureGenerator File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), effectsJson); } + foreach ((uint layoutId, uint rootId, string fileName) in new[] + { + (EffectsUiController.LayoutId, EffectsUiController.RowTemplateId, + "effects_row_2100001B_10000128.json"), + (CharacterController.LayoutId, CharacterController.RootId, + "character_info_2100006E_10000183.json"), + }) + { + ElementInfo? panelPart = LayoutImporter.ImportInfos(dats, layoutId, rootId); + Assert.NotNull(panelPart); + File.WriteAllText( + Path.Combine(FixtureDirectory(), fileName), + JsonSerializer.Serialize(panelPart, new JsonSerializerOptions + { + IncludeFields = true, + WriteIndented = true, + })); + } + ElementInfo? miniGame = LayoutImporter.ImportInfos( dats, MiniGameUiController.LayoutId, MiniGameUiController.RootId); Assert.NotNull(miniGame); diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs index 23bd81c9..1ea87958 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs @@ -79,9 +79,9 @@ public sealed class RetailPanelUiControllerTests public void MainPanels_ShareOneCanonicalPlacementAcrossMoveCloseAndSwitch() { var root = new UiRoot { Width = 1280f, Height = 720f }; - RetailWindowHandle inventory = Mount(root, WindowNames.Inventory, 40f, 60f); - RetailWindowHandle character = Mount(root, WindowNames.Character, 540f, 18f); - RetailWindowHandle spellbook = Mount(root, WindowNames.Spellbook, 18f, 18f); + RetailWindowHandle inventory = Mount(root, WindowNames.Inventory, 40f, 60f, 360f); + RetailWindowHandle character = Mount(root, WindowNames.Character, 540f, 18f, 500f); + RetailWindowHandle spellbook = Mount(root, WindowNames.Spellbook, 18f, 18f, 420f); using var controller = Create(root); controller.RegisterMainPanel(7u, WindowNames.Inventory, inventory); controller.RegisterMainPanel(11u, WindowNames.Character, character); @@ -94,9 +94,12 @@ public sealed class RetailPanelUiControllerTests controller.SetPanelVisibility(7u, visible: true); inventory.MoveTo(312f, 147f); + inventory.ResizeTo(310f, 540f); Assert.Equal((312f, 147f), (character.Left, character.Top)); Assert.Equal((312f, 147f), (spellbook.Left, spellbook.Top)); + Assert.Equal((310f, 540f), (character.Width, character.Height)); + Assert.Equal((310f, 540f), (spellbook.Width, spellbook.Height)); Assert.True(characterMoves > 0); Assert.True(spellbookMoves > 0); @@ -104,12 +107,14 @@ public sealed class RetailPanelUiControllerTests Assert.False(inventory.IsVisible); Assert.True(character.IsVisible); Assert.Equal((312f, 147f), (character.Left, character.Top)); + Assert.Equal((310f, 540f), (character.Width, character.Height)); controller.SetPanelVisibility(11u, visible: false); controller.SetPanelVisibility(13u, visible: true); Assert.False(character.IsVisible); Assert.True(spellbook.IsVisible); Assert.Equal((312f, 147f), (spellbook.Left, spellbook.Top)); + Assert.Equal((310f, 540f), (spellbook.Width, spellbook.Height)); } [Fact] @@ -130,15 +135,18 @@ public sealed class RetailPanelUiControllerTests controller.SetPanelVisibility(7u, visible: true); inventory.MoveTo(280f, 120f); + inventory.ResizeTo(310f, 600f); controller.SetPanelVisibility(4u, visible: true); Assert.Equal((280f, 120f), (effect.Left, effect.Top)); + Assert.Equal((310f, 600f), (effect.Width, effect.Height)); Assert.False(inventory.IsVisible); Assert.True(effect.IsVisible); controller.SetPanelVisibility(4u, visible: false); Assert.True(inventory.IsVisible); Assert.Equal((280f, 120f), (inventory.Left, inventory.Top)); + Assert.Equal((310f, 600f), (inventory.Width, inventory.Height)); controller.SetPanelVisibility(11u, visible: true); Assert.Equal((280f, 120f), (character.Left, character.Top)); @@ -165,9 +173,10 @@ public sealed class RetailPanelUiControllerTests UiRoot root, string name, float left, - float top) + float top, + float contentHeight = 500f) { - var content = new UiPanel { Width = 300f, Height = 500f }; + var content = new UiPanel { Width = 300f, Height = contentHeight }; return RetailWindowFrame.Mount( root, content, diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/character_info_2100006E_10000183.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/character_info_2100006E_10000183.json new file mode 100644 index 00000000..50e23d7b --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/character_info_2100006E_10000183.json @@ -0,0 +1,2107 @@ +{ + "Id": 268435843, + "Type": 268435482, + "X": 0, + "Y": 0, + "Width": 300, + "Height": 362, + "OriginalParentWidth": 0, + "OriginalParentHeight": 0, + "HasOriginalParentSize": false, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "88": { + "Kind": 0, + "MasterPropertyId": 88, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "87": { + "Kind": 0, + "MasterPropertyId": 87, + "UnsignedValue": 268435461, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "268435529": { + "Kind": 1, + "MasterPropertyId": 268435529, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "268435497": { + "Kind": 0, + "MasterPropertyId": 268435497, + "UnsignedValue": 3, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435740, + "Type": 3, + "X": 0, + "Y": 25, + "Width": 300, + "Height": 337, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100682946, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100682946, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435741, + "Type": 12, + "X": 8, + "Y": 0, + "Width": 262, + "Height": 337, + "OriginalParentWidth": 300, + "OriginalParentHeight": 337, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "114": { + "Kind": 0, + "MasterPropertyId": 114, + "UnsignedValue": 268435742, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 1, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435742, + "Type": 11, + "X": 280, + "Y": 0, + "Width": 16, + "Height": 337, + "OriginalParentWidth": 300, + "OriginalParentHeight": 337, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100682847, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "121": { + "Kind": 1, + "MasterPropertyId": 121, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "137": { + "Kind": 4, + "MasterPropertyId": 137, + "UnsignedValue": 0, + "IntegerValue": 16, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "130": { + "Kind": 1, + "MasterPropertyId": 130, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "119": { + "Kind": 0, + "MasterPropertyId": 119, + "UnsignedValue": 268435570, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "131": { + "Kind": 1, + "MasterPropertyId": 131, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "120": { + "Kind": 0, + "MasterPropertyId": 120, + "UnsignedValue": 268435569, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100682847, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 1, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 56, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [ + { + "Id": 268436324, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682848, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682849, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682850, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682848, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682849, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682850, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436325, + "Type": 3, + "X": 0, + "Y": 3, + "Width": 16, + "Height": 10, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682851, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682852, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682853, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682851, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682852, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682853, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436326, + "Type": 3, + "X": 0, + "Y": 13, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682854, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682855, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682856, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682854, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682855, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682856, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435569, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682859, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682857, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682858, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682857, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682858, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682859, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268435570, + "Type": 1, + "X": 0, + "Y": 32, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682862, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682860, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682861, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682860, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682861, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682862, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + } + ] + } + ] + }, + { + "Id": 268435708, + "Type": 1, + "X": 276, + "Y": 0, + "Width": 24, + "Height": 25, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435461, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668308, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668307, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100668307, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100668308, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268435710, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 276, + "Height": 25, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100683002, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "23": { + "Kind": 5, + "MasterPropertyId": 23, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 189013650, + "TableId": 587202561, + "Override": 0, + "English": 1, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 1, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": { + "": { + "Item1": 100683002, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] +} \ No newline at end of file diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_row_2100001B_10000128.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_row_2100001B_10000128.json new file mode 100644 index 00000000..048ebdff --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_row_2100001B_10000128.json @@ -0,0 +1,961 @@ +{ + "Id": 268435752, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 300, + "Height": 32, + "OriginalParentWidth": 0, + "OriginalParentHeight": 0, + "HasOriginalParentSize": false, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668310, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "6": { + "Id": 6, + "Name": "Highlight", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668311, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100668310, + "Item2": 1 + }, + "Highlight": { + "Item1": 100668311, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [ + { + "Id": 268435753, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 32, + "Height": 32, + "OriginalParentWidth": 300, + "OriginalParentHeight": 32, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435754, + "Type": 12, + "X": 37, + "Y": 0, + "Width": 188, + "Height": 32, + "OriginalParentWidth": 300, + "OriginalParentHeight": 32, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 2, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "199": { + "Kind": 5, + "MasterPropertyId": 199, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 58579907, + "TableId": 587202561, + "Override": 0, + "English": 1, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "71": { + "Kind": 0, + "MasterPropertyId": 71, + "UnsignedValue": 268436373, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "72": { + "Kind": 2, + "MasterPropertyId": 72, + "UnsignedValue": 553648193, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "32": { + "Kind": 1, + "MasterPropertyId": 32, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "208": { + "Kind": 1, + "MasterPropertyId": 208, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 0, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435755, + "Type": 12, + "X": 225, + "Y": 0, + "Width": 50, + "Height": 32, + "OriginalParentWidth": 300, + "OriginalParentHeight": 32, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 3, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 2, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] +} \ No newline at end of file