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 <codex@openai.com>
This commit is contained in:
parent
a96767ba6d
commit
d1d603105f
24 changed files with 3696 additions and 147 deletions
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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/` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ using AcDream.App.UI;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
///
|
||||
/// <para>Retail fills a single scrollable <c>UIElement_Text</c> element (id 0x1000011d,
|
||||
/// <c>m_pMainText</c>) by calling a sequence of Update* methods that each
|
||||
|
|
@ -32,9 +33,14 @@ namespace AcDream.App.UI.Layout;
|
|||
/// </summary>
|
||||
public static class CharacterController
|
||||
{
|
||||
public const uint LayoutId = 0x2100006Eu;
|
||||
public const uint RootId = 0x10000183u;
|
||||
|
||||
/// <summary>Dat element id for the main report text (m_pMainText). Confirmed in
|
||||
/// <c>gmCharacterInfoUI::PostInit</c> 0x004b86f0: <c>GetChildRecursive(this, 0x1000011d)</c>.</summary>
|
||||
public const uint MainTextId = 0x1000011du;
|
||||
public const uint ScrollbarId = 0x1000011Eu;
|
||||
public const uint CloseId = 0x100000FCu;
|
||||
|
||||
/// <summary>White body text — matches retail's default UIElement_Text foreground.</summary>
|
||||
private static readonly Vector4 BodyColor = new(1f, 1f, 1f, 1f);
|
||||
|
|
@ -48,27 +54,27 @@ public static class CharacterController
|
|||
/// <see cref="UiText.LinesProvider"/> so the Studio or a live session can push a fresh
|
||||
/// <see cref="CharacterSheet"/> without re-binding.
|
||||
///
|
||||
/// <para>The element must resolve as a <see cref="UiText"/> (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.</para>
|
||||
/// <para>Production data resolves the element as a <see cref="UiText"/>
|
||||
/// (Type 12). A synthetic fallback is created only for deliberately reduced
|
||||
/// test layouts.</para>
|
||||
/// </summary>
|
||||
/// <param name="layout">Imported 0x2100002E layout tree.</param>
|
||||
/// <param name="layout">Imported 0x2100006E / 0x10000183 layout tree.</param>
|
||||
/// <param name="data">Provider returning the current <see cref="CharacterSheet"/>.</param>
|
||||
/// <param name="datFont">Retail dat font forwarded from the render stack. May be null
|
||||
/// (falls back to the BitmapFont debug path).</param>
|
||||
public static void Bind(
|
||||
ImportedLayout layout,
|
||||
Func<CharacterSheet> 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 ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
148
src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs
Normal file
148
src/AcDream.App/UI/Layout/EffectRowTemplateFactory.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates retail's effect-token template <c>0x10000128</c>. This is the
|
||||
/// retained equivalent of <c>InfoRegion::InfoRegion @ 0x004F1450</c> calling
|
||||
/// <c>UIElement_ListBox::AddItemFromTemplateList</c> for
|
||||
/// <c>EffectInfoRegion</c>.
|
||||
/// </summary>
|
||||
public sealed class EffectRowTemplateFactory
|
||||
{
|
||||
private readonly ElementInfo _template;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolveSprite;
|
||||
private readonly UiDatFont? _defaultFont;
|
||||
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||
|
||||
public EffectRowTemplateFactory(
|
||||
ElementInfo template,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null)
|
||||
{
|
||||
_template = template ?? throw new ArgumentNullException(nameof(template));
|
||||
_resolveSprite = resolveSprite ?? throw new ArgumentNullException(nameof(resolveSprite));
|
||||
_defaultFont = defaultFont;
|
||||
_fonts = fonts ?? new Dictionary<uint, UiDatFont?>();
|
||||
}
|
||||
|
||||
public float Width => _template.Width;
|
||||
public float Height => _template.Height;
|
||||
|
||||
public static EffectRowTemplateFactory? TryLoad(
|
||||
DatCollection dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? 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<uint, UiDatFont?>();
|
||||
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<UiText>(content, EffectsUiController.RowLabelId);
|
||||
UiText duration = Required<UiText>(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<T>(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<uint, UiDatFont?>? resolveFont,
|
||||
Dictionary<uint, UiDatFont?> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<double> _serverTime;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly EffectRowTemplateFactory _templates;
|
||||
private readonly string _selectPrompt;
|
||||
private readonly UiItemList _list;
|
||||
private readonly UiText? _info;
|
||||
private readonly UiButton? _close;
|
||||
private readonly Dictionary<uint, UiCatalogSlot> _rows = new();
|
||||
private readonly UiScrollbar? _scrollbar;
|
||||
private readonly Dictionary<uint, EffectRowTemplateFactory.EffectRow> _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<double> serverTime,
|
||||
Func<uint, uint> 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<double> serverTime,
|
||||
Func<uint, (uint Texture, int Width, int Height)> spriteResolve,
|
||||
Func<uint, uint> 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<UiText.Line>();
|
||||
return [new UiText.Line(_selectPrompt, _info.DefaultColor)];
|
||||
ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot
|
||||
.Cast<ActiveEnchantmentRecord?>()
|
||||
.FirstOrDefault(record => record?.SpellId == spellId);
|
||||
if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata))
|
||||
return Array.Empty<UiText.Line>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Registers one of retail <c>gmPanelUI</c>'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 <c>gmPanelUI</c>'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.
|
||||
/// </summary>
|
||||
public void RegisterMainPanel(
|
||||
uint panelId,
|
||||
|
|
@ -61,7 +61,7 @@ public sealed class RetailPanelUiController : IDisposable
|
|||
windowName,
|
||||
restorePrevious,
|
||||
window,
|
||||
sharesMainPanelPlacement: true);
|
||||
sharesMainPanelGeometry: true);
|
||||
}
|
||||
|
||||
/// <param name="restorePrevious">
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ namespace AcDream.App.UI;
|
|||
/// </summary>
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -152,6 +152,13 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
/// <summary>The scroll model — also read by the linked UiScrollbar.</summary>
|
||||
public UiScrollable Scroll { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -6,12 +6,37 @@ namespace AcDream.App.Tests.UI.Layout;
|
|||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="CharacterController"/> and <see cref="SampleData.SampleCharacter"/>.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<UiText>(
|
||||
layout.FindElement(CharacterController.MainTextId));
|
||||
UiScrollbar scrollbar = Assert.IsType<UiScrollbar>(
|
||||
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<UiButton>(
|
||||
layout.FindElement(CharacterController.CloseId));
|
||||
close.OnEvent(new UiEvent(0, close, UiEventType.Click));
|
||||
Assert.Equal(1, closes);
|
||||
}
|
||||
|
||||
/// <summary>Bind on an empty layout, then return the created m_pMainText element.</summary>
|
||||
private static UiText BindAndGetText(System.Func<CharacterSheet> data)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<UiCatalogSlot>(list.GetItem(0));
|
||||
UiTemplateListSlot row = Assert.IsType<UiTemplateListSlot>(list.GetItem(0));
|
||||
UiText duration = Assert.IsType<UiText>(
|
||||
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<UiCatalogSlot>(list.GetItem(0));
|
||||
UiCatalogSlot second = Assert.IsType<UiCatalogSlot>(list.GetItem(1));
|
||||
UiTemplateListSlot first = Assert.IsType<UiTemplateListSlot>(list.GetItem(0));
|
||||
UiTemplateListSlot second = Assert.IsType<UiTemplateListSlot>(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<uint, UiElement>
|
||||
{
|
||||
[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<UiTemplateListSlot>(list.GetItem(0));
|
||||
UiText duration = Assert.IsType<UiText>(
|
||||
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<UiText>(layout.FindElement(EffectsUiController.InfoTextId));
|
||||
Assert.IsType<UiScrollbar>(layout.FindElement(EffectsUiController.ScrollbarId));
|
||||
Assert.Equal(expectedRoot, layout.Root.DatElementId);
|
||||
UiButton close = Assert.IsType<UiButton>(layout.FindElement(EffectsUiController.CloseId));
|
||||
close.OnEvent(new UiEvent(0, close, UiEventType.Click));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<EnumIDMap>(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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue