diff --git a/AGENTS.md b/AGENTS.md index 82755c3d..dc7e3334 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,12 +108,12 @@ movement queries. ## Current state -**Currently working toward: M3 — Cast a spell. M2 — Kill a drudge LANDED 2026-07-15**: -the live melee/missile loop covers weapon switching, target-facing, attack/damage/death, loot, -inventory, and item giving. The M2/M3 projectile + DAT-effect foundation is automated through -Step 9; single-client missiles/protection effects are user-gated. **Next:** reconcile F.4/L.1d's -cast lifecycle against that foundation, then F.5 spellbook + active enchantments. **Carried:** final -two-client portal/observer VFX gate, #145-residual, #116, R6/TS-42, and Track MP0. Detail below. +**M3 — Cast a spell ACTIVE; automated implementation complete 2026-07-15.** Retail cast intent, +component/target gates, live enchantment wire, Magic-scoped input, spell bar, spellbook, component +book, effect indicators, shared main-panel switching, and positive/negative effects UI are implemented over the Step-9 projectile/DAT-effect +foundation. **Next:** connected single-client magic UI/casting visual gate, then the final two-client +portal-out/materialization observer gate. **Carried:** #145-residual, #116, R6/TS-42, Track MP0. +Start magic work at `claude-memory/project_magic_ui_and_casting.md`; detailed state is below. For canonical state, read in this order: - [`docs/plans/2026-05-12-milestones.md`](docs/plans/2026-05-12-milestones.md) — milestone targets + freeze list per milestone diff --git a/CLAUDE.md b/CLAUDE.md index 815748d9..08fb1219 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,12 +106,12 @@ movement queries. ## Current state -**Currently working toward: M3 — Cast a spell. M2 — Kill a drudge LANDED 2026-07-15**: -the live melee/missile loop covers weapon switching, target-facing, attack/damage/death, loot, -inventory, and item giving. The M2/M3 projectile + DAT-effect foundation is automated through -Step 9; single-client missiles/protection effects are user-gated. **Next:** reconcile F.4/L.1d's -cast lifecycle against that foundation, then F.5 spellbook + active enchantments. **Carried:** final -two-client portal/observer VFX gate, #145-residual, #116, R6/TS-42, and Track MP0. Detail below. +**M3 — Cast a spell ACTIVE; automated implementation complete 2026-07-15.** Retail cast intent, +component/target gates, live enchantment wire, Magic-scoped input, spell bar, spellbook, component +book, effect indicators, shared main-panel switching, and positive/negative effects UI are implemented over the Step-9 projectile/DAT-effect +foundation. **Next:** connected single-client magic UI/casting visual gate, then the final two-client +portal-out/materialization observer gate. **Carried:** #145-residual, #116, R6/TS-42, Track MP0. +Start magic work at `claude-memory/project_magic_ui_and_casting.md`; detailed state is below. For canonical state, read in this order: - [`docs/plans/2026-05-12-milestones.md`](docs/plans/2026-05-12-milestones.md) — milestone targets + freeze list per milestone diff --git a/docs/ISSUES.md b/docs/ISSUES.md index a2e4b71a..70c04b0a 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -5136,49 +5136,52 @@ responds as if the user clicked the item. ## #L.2 — Spellbook favorites panel -**Status:** OPEN +**Status:** DONE 2026-07-15 **Severity:** MEDIUM **Filed:** 2026-04-26 (deferred from Phase K) **Component:** ui / magic -**Description:** In `MagicCombat` scope, 1-9 should fire -`UseSpellSlot_1..9` (distinct from hotbar). Requires a small UI to -pin favorite spells + a spellbook tab nav. Cross-references issue -#L.3 (combat-mode dispatch). +**Resolution:** The authored `gmSpellcastingUI` page inside combat layout +`0x21000073` now exposes all eight server-persisted favorite tabs, equipped +caster endowment, spell selection/name, drag-remove/drop-add favorite edits, +cast button, and `UseSpellSlot_1..9`. Cast intent goes through the single +`SpellCastingController`; ACE remains authoritative for turning, motion, +components, mana, fizzle, impact, and completion. +The same slice also mounts the authored Helpful/Harmful effect indicators +(`0x21000071`, buttons `0x100000F5/F6`) and routes their panel IDs 4/5 through +the shared retail `gmPanelUI` lifecycle, so closed effect lists have an exact +reopen path and replace the current main panel instead of becoming orphaned. --- ## #L.3 — Combat-mode tracking + scope-aware Insert/PgUp/Delete/End/PgDn dispatch -**Status:** OPEN +**Status:** DONE 2026-07-15 **Severity:** MEDIUM **Filed:** 2026-04-26 (deferred from Phase K) **Component:** input / combat -**Description:** Insert/PgUp/Delete/End/PgDn mean different things in -melee / missile / magic combat modes (per retail keymap MeleeCombat / -MissileCombat / MagicCombat blocks). Phase K has the bindings and the -scope stack; what's missing: `CombatState.CurrentMode` field + -listener for the server-side `SetCombatMode` packet (likely 0x0053 or -similar — confirm against ACE source). When mode arrives, push the -appropriate scope; when leaving combat, pop. +**Resolution:** Every binding now persists its retail `InputScope`; schema 4 +migrates older files. `CombatState.CurrentMode` selects the Melee/Missile/Magic +shadow scope, held actions release atomically on any scope/binding transition, +and reentrant callbacks cannot replay a stale held-chord snapshot. Settings +conflict checks and rebinds preserve the scoped identity. --- ## #L.4 — F-key panels: Allegiance / Fellowship / Skills / Attributes / World / SpellComponents -**Status:** OPEN +**Status:** PARTIAL — magic/character/inventory surfaces shipped **Severity:** LOW **Filed:** 2026-04-26 (deferred from Phase K) **Component:** ui -**Description:** Retail F3-F6, F8-F12 toggle UI panels for various -character data. Phase K has the bindings (`ToggleAllegiancePanel`, -`ToggleFellowshipPanel`, `ToggleSpellbookPanel`, -`ToggleSpellComponentsPanel`, `ToggleAttributesPanel`, -`ToggleSkillsPanel`, `ToggleWorldPanel`, `ToggleInventoryPanel`); the -panels themselves don't exist. Each is its own design feature. -Inventory (F12) is the most-requested. +**Current state:** Retained Inventory, Skills, Attributes, Spellbook, and +SpellComponents panels now exist and their F-key actions route through the +window manager. The spell/component book is the authored `0x21000034` tree, +with learned-spell filters, exact DAT spell/component icons, component +categories, selection, scrolling, and server-persisted desired amounts. +Allegiance, Fellowship, and World remain separate open panel features. --- diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index 3f845c19..3c210847 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -128,6 +128,23 @@ window registration, plugin mounts, cursor feedback, layout persistence, and the retained tick/draw/restore/dispose paths. Panel-specific construction must not move back into `GameWindow.OnLoad`. +Magic follows the same boundary. Core `Spellbook` is the one learned/favorite/ +desired/enchantment state projection; Core.Net owns exact manifest and live +message parsing; App `SpellCastingController` owns validated cast intent; and +the retained spell bar, spell/component book, and effects controllers only +project that state and invoke supplied actions. ACE remains authoritative after +the targeted/untargeted request for turning, animation, mana/components, +fizzle, impact, damage, and completion. +The shared LayoutDesc importer resolves inherited controller trees with retail +`LayoutDesc::InqFullDesc`/`ElementDesc::Incorporate` child identity semantics; +panel controllers never reconstruct missing inherited widgets by hand. +`RetailPanelUiController` is the single retained owner of retail +`gmPanelUI::RecvNotice_SetPanelVisibility` semantics: one active main-panel +child, optional DAT-property-driven deferred restoration, and visibility +reconciliation for persistence. Toolbar buttons and the authored Helpful/ +Harmful effects indicators both send panel IDs into this owner; neither owns a +parallel window-lifecycle map. + --- ## Project Structure (current + target) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index a2c6cea9..b1358c2b 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -127,7 +127,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | `src/AcDream.Core/Items/ItemInstance.cs:187` | Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) | | AP-15 | WeenieError translation table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex | `src/AcDream.Core/Chat/WeenieErrorMessages.cs:26` | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs | | AP-16 | Point/spot lights selected per-object / per-cell as the **8 nearest reaching lights** (sphere-overlap, nearest-first) via `LightManager.SelectForObject`, capped at `MaxLightsPerObject=8`; called from `WbDrawDispatcher.ComputeEntityLightSet` (objects) and `EnvCellRenderer.GetCellLightSet` (cell shells). Retail's bake (`SetStaticLightingVertexColors`) sums ALL reaching static lights per vertex with no count cap. Retail's *hardware* path (`minimize_object_lighting` 0x0054d480) DOES cap at 8 per object, so the cap is faithful to retail's hardware path — not to its bake path. The `LightManager.Tick` UBO path survives for DIRECTIONAL (sun) lights only; `mesh_modern.vert`'s UBO loop skips point/spot entries (`posAndKind.w != 0 → continue`) — point lights reach the shader exclusively via the per-object SSBO (binding 5) | `src/AcDream.Core/Lighting/LightManager.cs:234` (`SelectForObject`); `MaxLightsPerObject` ~line 174; call sites `WbDrawDispatcher.ComputeEntityLightSet` + `EnvCellRenderer.GetCellLightSet` | Matches retail's hardware constraint (8 lights per object/cell); selection is nearest-sphere-overlap which faithfully allocates lights to the surfaces that actually see them | Surfaces reached by >8 point lights are dimmer than retail's uncapped bake — rare (a dungeon room has a handful of torches), but real; see AP-35 for the bake-vs-GPU-evaluate architecture difference | `minimize_object_lighting` 0x0054d480 (retail's 8-light hardware cap); `SetStaticLightingVertexColors` 0x0059cfe0 (retail's bake, no count cap) | -| AP-17 | Spell metadata from third-party CSV (3,956 rows, bad rows silently skipped), not the portal.dat SpellTable; Family feeds stacking decisions | `src/AcDream.Core/Spells/SpellTable.cs:10` | The dat spell-table port (obfuscated/encrypted aspects) wasn't done; CSV closed #11 fast and unblocked #6 stacking | Any CSV↔dat drift (wrong Family, missing rows) silently produces wrong buff-stacking winners and wrong panel info | portal.dat SpellTable 0x0E00000E | +| AP-17 | Descriptive spell metadata and Family still come from a third-party CSV (3,956 rows, bad rows silently skipped); retail DAT now supplies decrypted formulas, school, exact icons, level filters, and component preflight | `src/AcDream.Core/Spells/SpellTable.cs:10`; `src/AcDream.App/Spells/MagicRuntime.cs` | CSV originally closed #11 and unblocked #6 stacking; M3 moved every cast-critical formula/icon/level decision to portal.dat but has not yet replaced descriptive/Family metadata | CSV↔DAT drift can still produce wrong stacking winners or panel name/description/beneficial classification; component choice and level/icon presentation no longer drift here | portal.dat SpellTable 0x0E00000E | | ~~AP-18~~ | **RETIRED 2026-07-10 — faithful retail radar port.** Exact `RGBAColor_Radar*` floats were recovered from named static data and `gmRadarUI::GetBlipColor` was re-ported with `_blipColor` overrides plus portal/vendor/attackable-creature/admin/PK/PKLite/free-PK/fellowship precedence. The old implementation was not merely hue-tuned: it also had wrong portal/vendor colors and an incomplete dispatch matrix. | `src/AcDream.Core/Ui/RadarBlipColors.cs` + radar classification tests | — | — | `gmRadarUI::GetBlipColor` 0x004d76f0; static RGBA initializers at named decomp pc:1089736-1089804 | | AP-19 | `PortalSideEpsilon` 0.01 (≈1 cm) instead of retail F_EPSILON ≈ 0.0002 — a documented render-root-lag tolerance, NOT a retail constant. DO-NOT-RETRY: T2 (BR-4) tried the retail value; CornerFloodReplay refuted it | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:49` | Retail's tight epsilon only works with eye-exact swept curr_cell tracking; our viewer cell lags the eye by up to ~1 cm at pressed corners. Tighten after the #108-membership family + cdstW near-clip pin land | A 1 cm misclassification band at portal planes can flood or cull a portal the eye hasn't crossed — one-frame leaks / grey flashes at knife-edge doorway/corner positions | F_EPSILON @0x007c8c70; `PView::InitCell` 0x005a4b70 | | AP-20 | Sub-pixel view-polygon vertex merge fixed at 1080p-reference NDC units (2/1080); retail merges at ~1 actual screen pixel | `src/AcDream.App/Rendering/PortalProjection.cs:179` | Unit approximation whose coarseness only strengthens convergence — the merge is the flood's fixpoint floor (replaced MaxReprocessPerCell=16) | At 4K+ a legitimately visible 1–2 px sliver aperture collapses to degenerate and rejects — a thin/distant doorway stops admitting its flood slightly earlier than retail | `Render::copy_view` 0x0054dfc0 | @@ -210,7 +210,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` | | AP-108 | Paperdoll/AutoWield still omit full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **Primary replacement retired from this row 2026-07-14:** inventory activation and paperdoll drops share the confirmed blocker transaction, preserve explicit slot intent, relocate an already-worn item to an explicitly selected compatible slot, and emit retail's successful move-to-backpack status. **Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, and live doll work; primary weapon, incompatible shield, and mismatched ammo blockers sequence through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` | | AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` | -| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, spellbook/effects/favorite spell bars, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic `gmCombatUI` now covers the active M2 melee/missile loop; Wave 10 lands each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI | Named `gm*UI::PostInit` methods and LayoutDesc catalog | +| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic combat plus M3 spellbook/component/effects/favorite-spell surfaces now cover the active melee/missile/magic loops; later waves land each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI outside the current combat/magic/inventory/character loops | Named `gm*UI::PostInit` methods and LayoutDesc catalog | | ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` | | AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, `MaybeStopCompletely`, server-response queueing, and auto-repeat, but still omits `StartAttackRequest`'s `FinishJump` call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` | | AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` | diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 860151cb..379d05e4 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -1,6 +1,6 @@ # acdream — strategic roadmap -**Status:** Living document. Updated 2026-07-15. **M3 active.** M2's connected demo loop is complete: melee and missile weapon switching, target-facing, combat animation and damage/death presentation, loot/inventory, stack operations, and giving an item to a world target have all passed user gates. The projectile/PhysicsScript/particle/portal foundation is implemented through its Step 9 automated hardening gate; single-client missile and protection visuals pass, while the final two-client portal-out/materialization observer gate remains open. The immediate work order is an F.4/L.1d retail-conformance audit against that shipped foundation, implementation of the remaining cast lifecycle, then F.5 spellbook and active-enchantment UI. +**Status:** Living document. Updated 2026-07-15. **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, authored effect-indicator buttons, 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. **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. --- @@ -11,17 +11,19 @@ turn/windup/release/projectile/impact lifecycle, self-cast a buff and see its active enchantment, then recall through the DAT-driven portal presentation. -**Next concrete work, in order:** +**Current gate order:** -1. Reconcile current E.5 wire, M2/M3 projectile/VFX, motion, and enchantment - state against named-retail F.4/L.1d; record only the proven remaining gaps. -2. Port and conformance-test those cast-state and animation lifecycle gaps, - including facing, release, interruption/fizzle, recoil, and authoritative - completion. -3. Ship F.5's retained spellbook and active-enchantment surfaces over the - existing shared state/command seam. -4. Run the pending two-client portal-out/materialization observer visual gate - before M3 closes. +1. **Done (automated):** named-retail F.4/L.1d reconciliation, full live + enchantment messages, account/focus-aware component formula preflight, + retail target eligibility, authoritative cast request/completion ownership. +2. **Done (automated):** authored Magic combat page, eight favorite tabs, + equipped caster endowment, scoped retail keys, spellbook/component book, + positive/negative active-enchantment panels, Helpful/Harmful indicator + buttons, and the retail one-active-main-panel lifecycle. +3. **Pending user visual gate:** connected projectile/self-buff casts plus the + complete retained magic UI interaction checklist. +4. **Pending user visual gate:** two-client portal-out/materialization observer + presentation before M3 closes. Opportunistic UI/physics fixes are no longer the work-order driver. They enter the active slice only when they block this demo or represent a severe regression. @@ -527,13 +529,14 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar. - **Missile/portal VFX campaign Steps 0–5 implemented and independently reviewed 2026-07-14.** The retail oracle and packet fixtures are pinned, complete `PhysicsDesc` plus F754/F755 parsing and nine-channel gates are shipped, and App now has one canonical `LiveEntityRuntime` record per server-object incarnation. Logical register/unregister is separate from spatial rebucketing, so landblock churn and attached equipment retain identity without replaying renderer/script creation or reconstructing from stale spawn data. Canonical materialized and visible target/radar views are distinct; pickup/parent leave-world preserves owners while pausing root simulation; spawn publication is once per incarnation. `GpuWorldState` is spatial-only for live objects. Production PhysicsScript and Animation loading shares a narrow `DatCollection`-backed compatibility reader for retail's inherited blocking-particle payload, including mesh-side preloading; the proven concurrent-safe DatReaderWriter 2.1.7 read path avoids blocking update-thread effects behind streaming. Typed-table selection preserves DAT order and retail's first `intensity <= Mod` boundary, and live `PhysicsDesc` effect defaults replace rather than fall back to Setup. The Step 4 scheduler is now one serial FIFO per owner with duplicate stacking, catch-up dispatch, deterministic delayed `CallPES`, complete hook-router fan-out, update-frame clock publication, retail cell/Frozen eligibility, and structural rejection of malformed zero-time recursive DAT chains without rejecting valid timed weather loops. `EntityEffectController` retains pre-create F754/F755 in one mixed per-GUID FIFO, resolves local identity only through `LiveEntityRuntime`, replays once only after the canonical owner is fully ready and in-world, drops later plays while that existing owner is cell-less, routes attached-child updates through the eligible parent, and replaces/clears the live SoundTable on every PhysicsDesc application. Step 5 publishes current rigid root/indexed-part poses after animation and recursively composed held-child transforms, keeping render-only Setup scale out of particle and holding-location anchors; it then drains animation hooks, owner PhysicsScripts, moving emitters/lights, and particle simulation in fixed order. Normal/blocking/anonymous logical emitter identity is exact, including Stop retaining a blocking ID until final-particle retirement; moving and held Setup lights follow their current roots and share retail's PhysicsState Lighting/SetLight transition state; world-released versus parent-local particle behavior is preserved; missing emitter DAT produces an actionable diagnostic and no invented fallback. Drawable meshes no longer collapse stable Setup-part indices; nested attachments update parent-before-child, recursively withdraw on ancestor pose loss, and recover the complete descendant chain only on real publication edges. Loaded↔pending projection edges skip particle update/draw while retaining original absolute creation times and logical identity, and withdraw light presentation; re-entry evaluates elapsed state once without backlog emission or recreating an effect owner. Scripts, particles, lights, translucency, audio, and teardown all share canonical `WorldEntity.Id`; static allocators fail fast before namespace wrap. IA-7, AD-14, TS-10, TS-11, TS-12, TS-13, and AP-67 are retired; AD-32 now covers only the remaining non-effect, non-Parent pre-create packet families, and AD-43 registers the corrupt-DAT zero-time-cycle safety boundary. - **✓ M2 local attack receive funnel (live-gated 2026-07-15).** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`. - **✓ M2 basic retail combat bar (live-gated 2026-07-15).** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams. +- **M3 cast lifecycle + retained magic UI implemented 2026-07-15; visual gates pending.** `SpellCastingController` ports retail's immediate targeted/untargeted request boundary, component preflight, target eligibility, movement stop, and shared UI-busy lifetime while leaving turn/cast/fizzle/impact outcomes authoritative to ACE. Formula preflight reads decrypted portal.dat formulas, the SCID↔WCID map, canonical account randomization, exact school-focus map, infused-magic properties, and scarab-only substitution. Complete live enchantment packets preserve spell/layer identity, all StatMod fields, client-clock normalization, and exact purge buckets. Authored retained surfaces now include the Magic page of `gmCombatUI`, all eight favorite tabs, equipped caster endowment, Magic-scoped retail keys, `gmSpellbookUI`, `gmSpellComponentUI`, both `gmEffectsUI` windows, and the floating Helpful/Harmful effect indicators. `RetailPanelUiController` ports the shared one-active-panel lifecycle so toolbar and indicator launchers converge on one owner. Exact DAT icons and server-persisted filters/desired components are retained. #L.2/#L.3 close and #L.4/AP-110 narrow. Research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`. - **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets. Confirmation reuse now ports retail `DialogFactory` contexts, queue groups/priority, fresh DAT roots, property results, callbacks/close notices, and server abort handling; `/die`, gameplay request tuples, and guarded item-use prompts share its type-1 LayoutDesc `0x2100003C` presenter. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`, `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`. - **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158. - **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate passed). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout. - **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository`→`ClientObjectTable` / `ItemInstance`→`ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green. - **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars. - **✓ D.5.3 — Toolbar selected-object display (issue #141) SHIPPED.** Exact health/player/pet/Free-PK policy, selected-item mana (`0x100001A2`), stack entry (`0x100001A3`), stack slider (`0x100001A4`), formatted count/name, shared split quantity, click-to-use, peace/war, health/name, and paperdoll self/upper-item selection all passed live by 2026-07-11. Exact raw `ShortCutData` preservation is already shipped in Wave 4.2; the eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase. -- **☐ D.5.5+ — Core panels.** Inventory (`gmInventoryUI`/`gmBackpackUI`), equipment/paperdoll (`gmPaperDollUI`/`gm3DItemsUI` + the `UiViewport` 3D doll), vendor, trade, spellbook. Research drop done (`docs/research/2026-06-16-*`). Depends on **D.5.4** (data model) + the item-slot/list/icon spine (D.5.1/D.5.2) + the **window manager** (Plan 2: open/close/z-order/persist + faithful grip/dragbar drag-resize) + the drag-drop spine wired (`UiRoot` has the chain; the per-cell accept/drop hooks are still stubs in `UiField`). Also deferred from D.5.1: drag/reorder + the `AddShortcut`/`RemoveShortcut` mutate wire. +- **☐ D.5.5+ — Remaining core panels.** Inventory, equipment/paperdoll, spellbook/component book, and active-effects surfaces are shipped through the retained window manager. Vendor, trade, salvage, tinkering, and the remaining social/world panels stay open. Research drop: `docs/research/2026-06-16-*`; current magic research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`. - **D.6 — HUD.** **Radar/compass IMPLEMENTED 2026-07-10; jump power bar IMPLEMENTED 2026-07-13; live-world visual gates pending.** The radar is retained retail `gmRadarUI` (`LayoutDesc 0x21000074`): a 120×140 circular face (`0x06004CC1`) with orbiting N/E/S/W tokens, a fixed bright-green player marker, and coordinate footer (`0x06004CC0`). Projection is `radarPixelRadius / radarRange` with 75 m outdoor / 25 m indoor ranges — no 1.18 factor or scrolling strip. The separate `gmFloatyPowerBarUI` (`LayoutDesc 0x21000072`) now appears only while jump is pending and clips the authored JumpMode fill left-to-right from the movement controller's shared 0–1 charge. Both live in `AcDream.App/UI` behind focused controllers; `GameWindow` supplies state delegates only. Remaining D.6: target name plate, damage floaters, and other world-space HUD elements through the raw `TextRenderer` path. See slice 06 §A.2, `docs/research/2026-07-10-retail-radar-pseudocode.md`, `docs/research/2026-07-13-retail-jump-powerbar-pseudocode.md`, and the named `gmRadarUI`/`gmPowerbarUI` decomp. - **✓ SHIPPED — D.7 — Cursor manager (user-confirmed 2026-07-11).** All reachable `ClientUISystem::UpdateCursorState @ 0x00564630` branches resolve through production DAT EnumIDMap table 6, with event-ordered global-default versus captured/hovered `MediaDescCursor` layering from `UIElementManager::CheckCursor @ 0x0045ABF0`. OS fallback remains only AP-72 and emits a visible diagnostic. The live gate covered world/object, combat, targeted-use, text, move/resize, and drag contexts; combat-indicator mouse clicks and the quick key share the same toggle command. - ~~**D.8 — Sound.**~~ **Superseded — shipped as Phase E.2** (`SoundTable`/`Sound` dat decode, OpenAL 16-voice engine, per-entity 3D positional audio via `AudioHookSink`). Entry kept here for history; see the shipped table. diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 1e53cce6..900a535f 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -634,6 +634,18 @@ protection effects pass the single-client visual gate. M3 begins with a focused named-retail gap audit of F.4/L.1d—not a rewrite of this foundation—then fills the spellbook/active-enchantment UI and completes the two-client portal gate. +**Implementation update (2026-07-15):** The automated F.4/L.1d audit and port +are complete. Cast requests now use retail component-formula selection, +target compatibility, shared busy/completion ownership, and exact +targeted/untargeted messages. Live enchantment add/remove/dispel/purge state is +fully parsed with layer identity and client-clock normalization. The authored +Magic combat page, eight favorite tabs, caster endowment, spellbook/component +book, scoped keys, positive/negative effects windows, and the authored +Helpful/Harmful indicators are mounted in the retained runtime. 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. + **Phases to ship:** - **F.4** — Spell cast state machine (buffs + recalls first, projectile spells second). diff --git a/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md new file mode 100644 index 00000000..8ee37d36 --- /dev/null +++ b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md @@ -0,0 +1,277 @@ +# Retail magic UI and casting pseudocode + +This note is the implementation oracle for the M3 cast lifecycle and retained +spell UI. Retail addresses refer to the September 2013 EoR named client. +Wire layouts were cross-checked against ACE and the existing holtburger-derived +`PlayerDescriptionParser`. + +## Cast request ownership + +Retail: `ClientMagicSystem::CastSpell` `0x00568040` and +`ClientMagicSystem::FreeHandsAndCastSpell` `0x00566EF0`. + +```text +CastSpell(spellId, useSelectedTarget): + spell = SpellTable[spellId] + if components are required: + formula = GetAppropriateSpellFormula(spell) + if any required component is absent: + display "You do not have all of this spell's components." + return + + if spell targets self: + target = local player id (or untargeted when no SmartBox exists) + FreeHandsAndCastSpell(spellId, target) + return + + if spell has no target type: + MaybeStopCompletely() + send CastUntargetedSpell(spellId) + increment UI busy count + return + + target = selected object + if target == 0: + display "You must select a suitable target." + return + if the selected GUID no longer resolves to an object: + return silently + if ObjectCompatibleWithSpell(target, spellId): + FreeHandsAndCastSpell(spellId, target) + reselect target + +FreeHandsAndCastSpell(spellId, target): + MaybeStopCompletely() + if target == 0: send CastUntargetedSpell(spellId) + else: send CastTargetedSpell(target, spellId) + increment UI busy count +``` + +The request is sent immediately. ACE remains authoritative for turning, +casting motion, component consumption, mana use, fizzle, impact, damage, and +enchantment state. The client does not invent a parallel gameplay outcome. + +### Formula selection and target compatibility + +Retail: `ClientMagicSystem::GetAppropriateSpellFormula` `0x00567D50`, +`CSpellBase::InqScarabOnlyFormula` `0x00597050`, +`ACCWeenieObject::MagicPackIsOwned` `0x0058CB80`, and +`ClientMagicSystem::ObjectCompatibleWithSpellTargetType` `0x00567230`. + +```text +GetAppropriateSpellFormula(spell): + schoolFocusWcid = SchoolOfMagic2WCID(spell.school) + infusedProperty = [War 0x129, Life 0x128, Item 0x127, + Creature 0x126, Void 0x148][school] + if infusedProperty > 0 OR a directly carried side-pack has schoolFocusWcid: + retain every power component in formula order + strongest = highest retained component power + append prismatic taper SCID 0xBC: + power 1 => 1; power 2 => 2; power 3/4/7 => 3; + power 5/6/8/9/10 => 4 + return result + return account-customized formula + +ObjectCompatibleWithSpellTargetType(target, targetMask): + special = targetMask & 0x8107 + reject self when special == 0 + reject stacks + require (targetMask & target.type) != 0 OR special != 0 + require target.IsPlayer OR target.publicBitfield has BF_ATTACKABLE + require target.petOwner == 0 +``` + +The final Player/Attackable and pet gates apply after both the ordinary type +match and the special-mask bypass. Formula entries are SCIDs; inventory and +favorite-component events use WCIDs, resolved through DualEnumIDMap +`0x27000002`. The magic-school focus map is retail enum category `0x28`, key +`0x10000001`. + +## Authoritative enchantment events + +Retail dispatchers: + +- `CM_Magic::DispatchUI_UpdateEnchantment` `0x006A32F0` +- `CM_Magic::DispatchUI_RemoveEnchantment` `0x006A2FB0` +- `CM_Magic::DispatchUI_DispelEnchantment` `0x006A2F20` + +```text +0x02C2 UpdateEnchantment: + unpack one Enchantment record + +0x02C4 UpdateMultipleEnchantments: + u32 count + repeat count times: unpack Enchantment record + +Enchantment record (60 or 64 bytes): + u16 spellId + u16 layer + u16 spellCategory + u16 hasSpellSetId + u32 powerLevel + f64 startTime + f64 duration + u32 casterGuid + f32 degradeModifier + f32 degradeLimit + f64 lastDegraded + u32 statModType + u32 statModKey + f32 statModValue + if hasSpellSetId: u32 spellSetId + +0x02C3 RemoveEnchantment: + u16 spellId + u16 layer + +0x02C7 DispelEnchantment: + u16 spellId + u16 layer + +0x02C5 / 0x02C8 Remove/DispelMultipleEnchantments: + u32 count + repeat count times: u16 spellId, u16 layer + +0x02C6 PurgeEnchantments: + no payload; clear the authoritative active set +``` + +## Magic combat page (spell bar) + +Retail: `gmSpellcastingUI::PostInit` `0x004C5D00`, +`SpellCastSubMenu::Init` `0x004C5A90`, and `gmSpellcastingUI::Cast` +`0x004C6050`. + +The spell bar is the authored Magic page inside combat layout `0x21000073`. +It is not an advanced-combat placeholder. + +```text +combat basic page 0x1000005C +spellcasting page 0x10000061 +spellcasting content 0x100000A2 +spell name text 0x1000048B +spellcasting background 0x100000A0 +cast button 0x100000B2 +endowment icon 0x100000B1 + +favorite tab buttons 0x100000A3..0x100000A9, 0x100005C2 +favorite list groups 0x100000AA..0x100000B0, 0x100005C3 +item list within each group 0x100000B6 +``` + +There are eight favorite tabs. Each list is authoritative character state. +Selecting a spell updates its name and current spell. Cast invokes +`ClientMagicSystem::CastSpell(selectedSpell, true)`. PageUp/PageDown and the +retail spell input actions change tab/selection; the cast-current action casts +the current selection. Favorite edits are sent to ACE. + +Outbound character events (`CM_Character`): + +```text +0x01E3 AddSpellFavorite(spellId:u32, position:i32, tab:i32) +0x01E4 RemoveSpellFavorite(spellId:u32, tab:i32) +0x0286 SpellbookFilterEvent(filter:u32) +0x0224 SetDesiredComponentLevel(componentDid:u32, amount:i32) +``` + +## Spellbook and component book + +Retail: `gmSpellbookUI::PostInit` `0x0048B2B0` and +`gmSpellComponentUI::PostInit` `0x0048A190`. + +Both pages are authored in layout `0x21000034` (300 by 600 pixels): + +```text +window root 0x100002A8 +spellbook tab 0x100002A9 +component tab 0x100002AA +close button 0x100002AB +spellbook page 0x100002AC (base 0x10000294 / 0x21000032) +component page 0x100002AD (base 0x100002A7 / 0x21000033) + +spell list 0x10000295 +school buttons 0x10000298..0x1000029B, 0x100005C0 +level buttons 1..7 0x1000029C..0x100002A2 +level button 8 0x1000054E + +component item list 0x10000464 +component scrollbar 0x10000465 +``` + +Spellbook rows come only from the authoritative learned-spell set. Retail +sorts by `CSpellBase::_display_order`; school and level buttons apply the +server-persisted filter bitfield. Component rows use the installed component +table and the server-persisted desired purchase amounts. + +## Active enchantment panels + +Retail: `gmEffectsUI::PostInit` `0x004B7560` and +`gmEffectsUI::RebuildList` `0x004B8350`. + +```text +layout 0x2100001B +positive root 0x1000011F (inherits 0x10000122) +negative root 0x10000121 (inherits 0x10000122) +shared effects template 0x10000122 +effect list 0x10000123 +information text 0x10000126 +effect UI type property 0x1000000C +``` + +The `0x10000184`/`0x10000185` values used by `gmPanelUI` are panel-slot +identities, not roots in effects LayoutDesc `0x2100001B`. The two authored +effects roots carry their title/close overrides and inherit the list, +scrollbar, information area, and remaining chrome from `0x10000122`. + +Retail `gmPanelUI::SetupChildren` (`0x004BC9E0`) registers those children as +panel IDs 4 and 5. `RecvNotice_SetPanelVisibility` (`0x004BC6F0`) owns one +active child: showing either effect panel hides the previous main panel, while +closing the active child hides the shared host. DAT bool property `0x10000049` +selects the exceptional save-and-restore-previous behavior. The resolved +Helpful/positive root sets it, so closing Helpful restores the preceding +ordinary panel. Harmful/negative does not set it and leaves the host empty. + +The reopen path is not the main toolbar. Retail +`gmUIElement_EffectsIndicator::PostInit` (`0x004E6930`) reads effect type 1 or +2 from property `0x1000000C`, and `Update` (`0x004E6A50`) ghosts the button +when no matching enchantment exists. The floating indicators layout is +`0x21000071`; Helpful button `0x100000F5` opens panel 4 and Harmful button +`0x100000F6` opens panel 5. The docked equivalent is layout `0x21000015`. + +Retail `LayoutDesc::InqFullDesc` (`0x0069A520`) resolves the base first and +then calls `ElementDesc::Incorporate` (`0x0069B5A0`). Child tables are merged +by element identity: base-only children remain, same-identity children are +incorporated recursively, and derived-only children are appended with their +read order shifted by the retained base-only count. + +When visible, each panel rebuilds from the authoritative active-enchantment +snapshot and refreshes each token's duration at most once per second. +`CEnchantmentRegistry::GetEnchantmentsInEffect` (`0x00594540`) merges only the +multiplicative and additive registries, so Vitae and cooldown records neither +appear in these panels nor enable an indicator. Each incoming entry is dueled +against the current winner for its spell category (`Duel @ 0x005942B0`): higher +power wins, with later start time breaking an equal-power tie. Positive and +negative panels then test the winning spell's Beneficial flag and sort +alphabetically. +Live `AddEnchantmentToList @ 0x00593DF0` inserts a new identity at the bucket +head, while an existing identity refreshes in place; bulk UnPack retains its +received head-to-tail order. Since an exact power/start tie keeps the first +duel incumbent, the newest incrementally added identity wins an exact tie. + +The Helpful/Harmful indicators intentionally use a different projection. +`CountSpellsInList @ 0x00593CD0` feeds the registry's two indicator counters by +visiting every raw mult/add node before any category duel. Therefore opposite- +sign spells in the same category can enable both indicators while only the duel +winner appears in the effects list. +Remaining duration is `(startTime + duration) - current game time`. Selecting +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. + +## Portal presentation boundary + +Recall and portal presentation remains DAT-driven. `/ls` starts the retail +recall motion (`0x10000153`); its animation hooks/PES chain supplies the cloud. +Remote disappearance and materialization use authoritative Hidden/UnHide state +and typed physics scripts. No generic teleport effect, guessed color, or local +teleport outcome is added by the spell UI work. diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index bf7ef098..13d6dfa1 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -11,6 +11,10 @@ namespace AcDream.App.Rendering; public sealed class GameWindow : IDisposable { + private static double ClientTimerNow() => + System.Diagnostics.Stopwatch.GetTimestamp() + / (double)System.Diagnostics.Stopwatch.Frequency; + private readonly record struct SkyPesKey(int ObjectIndex, uint PesObjectId, bool PostScene); private readonly AcDream.App.RuntimeOptions _options; @@ -833,6 +837,7 @@ public sealed class GameWindow : IDisposable private AcDream.App.Combat.CombatAttackController? _combatAttackController; private AcDream.App.Combat.CombatTargetController? _combatTargetController; private AcDream.App.UI.ItemInteractionController? _itemInteractionController; + private AcDream.App.Spells.MagicRuntime? _magicRuntime; private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new(); // Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport // widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625). @@ -1289,6 +1294,8 @@ public sealed class GameWindow : IDisposable _inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher( _kbSource, _mouseSource, _keyBindings); _inputDispatcher.Fired += OnInputAction; + Combat.CombatModeChanged += SetInputCombatScope; + SetInputCombatScope(Combat.CurrentMode); // Retail CameraSet::ToggleMouseLook / Rotate (0x00457490 / // 0x00458310): the callback submits a filtered horizontal @@ -2085,11 +2092,8 @@ public sealed class GameWindow : IDisposable _vitalsVm ??= new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat, LocalPlayer); _uiHost = new AcDream.App.UI.UiHost(_gl, shadersDir, _debugFont); _uiHost.Root.UiLocked = _persistedGameplay.LockUI; - var spellComponentTable = - // DBObj file id is 0x0E00000F. Retail's IsComponentPack - // passes enum key 0x10000001 to DBObj::GetByEnum; that key is - // NOT a portal.dat file id (using it here reads unrelated bytes). - _dats!.Get(0x0E00000Fu); + AcDream.App.Spells.MagicCatalog magicCatalog = + AcDream.App.Spells.MagicCatalog.Load(_dats!); _itemInteractionController = new AcDream.App.UI.ItemInteractionController( Objects, playerGuid: () => _playerServerGuid, @@ -2110,7 +2114,7 @@ public sealed class GameWindow : IDisposable playerOnGround: GetDebugPlayerOnGround, inNonCombatMode: () => Combat.CurrentMode == AcDream.Core.Combat.CombatMode.NonCombat, - isComponentPack: wcid => spellComponentTable?.Components.ContainsKey(wcid) == true, + isComponentPack: magicCatalog.IsComponentPack, placeInBackpack: SendPickUp, sendSplitToWorld: (item, amount) => _liveSession?.SendStackableSplitTo3D(item, amount), @@ -2138,6 +2142,24 @@ public sealed class GameWindow : IDisposable sendRaiseVital: (statId, cost) => _liveSession?.SendRaiseVital(statId, cost), sendRaiseSkill: (statId, cost) => _liveSession?.SendRaiseSkill(statId, cost), sendTrainSkill: (statId, credits) => _liveSession?.SendTrainSkill(statId, credits)); + _magicRuntime = AcDream.App.Spells.MagicRuntime.Create( + magicCatalog, + SpellBook, + Objects, + selectedObject: () => _selection.SelectedObjectId, + localPlayerId: () => _playerServerGuid, + // SpellFormula::Randomize hashes the canonical account spelling + // delivered by CharacterList. + accountName: () => _liveSession?.Characters?.AccountName + ?? _options.LiveUser + ?? string.Empty, + stopCompletely: PreparePlayerForAttackRequest, + sendUntargeted: spellId => _liveSession?.SendCastUntargetedSpell(spellId), + sendTargeted: (target, spellId) => _liveSession?.SendCastTargetedSpell(target, spellId), + displayMessage: text => Chat.OnSystemMessage(text, 0x1Au), + incrementBusy: () => _itemInteractionController.IncrementBusyCount(), + canSend: () => _liveSession is not null + && _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld); _uiHost.Root.DragReleasedOutsideUi += OnUiDragReleasedOutside; // Feed Silk input to the UiRoot tree so windows drag / close / select. @@ -2228,6 +2250,31 @@ public sealed class GameWindow : IDisposable _combatAttackController, () => _persistedGameplay, SetRetailCombatGameplay), + Magic: new AcDream.App.UI.MagicRuntimeBindings( + SpellBook, + _magicRuntime.Casting, + Objects, + () => _playerServerGuid, + magicCatalog.Components, + (type, icon, under, over, effects) => + iconComposer.GetIcon(type, icon, under, over, effects), + (type, icon, under, over, effects) => + iconComposer.GetDragIcon(type, icon, under, over, effects), + iconComposer.GetSpellIcon, + iconComposer.GetSpellComponentIcon, + _selection, + magicCatalog.GetSpellLevel, + guid => _selection.Select( + guid, AcDream.Core.Selection.SelectionChangeSource.Inventory), + UseItemByGuid, + (tab, position, spellId) => + _liveSession?.SendAddSpellFavorite(spellId, position, tab), + (tab, spellId) => + _liveSession?.SendRemoveSpellFavorite(spellId, tab), + filters => _liveSession?.SendSpellbookFilter(filters), + (componentId, amount) => + _liveSession?.SendSetDesiredComponentLevel(componentId, amount), + ClientTimerNow), JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings( () => _playerController?.JumpCharge ?? default), Fps: new AcDream.App.UI.FpsRuntimeBindings( @@ -2763,6 +2810,9 @@ public sealed class GameWindow : IDisposable // them down before dropping the canonical GUID/timestamp snapshots. _equippedChildRenderer?.Clear(); Objects.Clear(); + SpellBook.Clear(); + _magicRuntime?.Reset(); + _itemInteractionController?.ClearBusy(); _selection.Reset(); _pendingPostArrivalAction = null; try @@ -2932,7 +2982,8 @@ public sealed class GameWindow : IDisposable onCharacterOptions: (options1, _) => _characterOptions1 = (AcDream.Core.Net.Messages.PlayerDescriptionParser.CharacterOptions1) - options1); + options1, + clientTime: ClientTimerNow); // Phase I.7: subscribe to CombatState events and emit // retail-faithful "You hit X for Y damage" chat lines into @@ -12423,6 +12474,17 @@ public sealed class GameWindow : IDisposable /// via ; /// this method handles transitional Press/Release events only. /// + private void SetInputCombatScope(AcDream.Core.Combat.CombatMode mode) + { + _inputDispatcher?.SetCombatScope(mode switch + { + AcDream.Core.Combat.CombatMode.Melee => AcDream.UI.Abstractions.Input.InputScope.MeleeCombat, + AcDream.Core.Combat.CombatMode.Missile => AcDream.UI.Abstractions.Input.InputScope.MissileCombat, + AcDream.Core.Combat.CombatMode.Magic => AcDream.UI.Abstractions.Input.InputScope.MagicCombat, + _ => null, + }); + } + private void OnInputAction( AcDream.UI.Abstractions.Input.InputAction action, AcDream.UI.Abstractions.Input.ActivationType activation) @@ -14584,6 +14646,7 @@ public sealed class GameWindow : IDisposable _uiHost = null; _itemInteractionController?.Dispose(); _itemInteractionController = null; + _magicRuntime = null; // Phase A.1: join the streamer worker thread before tearing down GL // state. The worker may still be processing a load job that references @@ -14632,6 +14695,7 @@ public sealed class GameWindow : IDisposable _debugFont?.Dispose(); _frameProfiler.Dispose(); // MP0: releases the GpuFrameTimer query ring _dats?.Dispose(); + Combat.CombatModeChanged -= SetInputCombatScope; _input?.Dispose(); _gl?.Dispose(); } diff --git a/src/AcDream.App/Spells/MagicRuntime.cs b/src/AcDream.App/Spells/MagicRuntime.cs new file mode 100644 index 00000000..d47c1da6 --- /dev/null +++ b/src/AcDream.App/Spells/MagicRuntime.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AcDream.Core.Items; +using AcDream.Core.Spells; +using DatReaderWriter; +using DatReaderWriter.DBObjs; + +namespace AcDream.App.Spells; + +/// Immutable presentation metadata for one retail spell component WCID. +public sealed record SpellComponentDescriptor( + uint WeenieClassId, + string Name, + uint Category, + uint IconId); + +/// +/// App-layer projection of retail's spell, component, and school-focus DAT tables. +/// Keeping this catalog outside GameWindow makes the SCID/WCID boundary and +/// rough spell-level calculation one independently testable ownership seam. +/// +public sealed class MagicCatalog +{ + private const uint SpellTableDid = 0x0E00000Eu; + private const uint SpellComponentTableDid = 0x0E00000Fu; + private const uint ComponentIdMapDid = 0x27000002u; + + private readonly IReadOnlyDictionary _formulaDefinitions; + private readonly IReadOnlyDictionary _wcidByScid; + private readonly IReadOnlyDictionary _magicPackWcidBySchool; + private readonly IReadOnlyDictionary _spellLevels; + + private MagicCatalog( + IReadOnlyDictionary components, + IReadOnlyDictionary formulaDefinitions, + IReadOnlyDictionary wcidByScid, + IReadOnlyDictionary magicPackWcidBySchool, + IReadOnlyDictionary spellLevels) + { + Components = components; + _formulaDefinitions = formulaDefinitions; + _wcidByScid = wcidByScid; + _magicPackWcidBySchool = magicPackWcidBySchool; + _spellLevels = spellLevels; + } + + public IReadOnlyDictionary Components { get; } + + public bool IsComponentPack(uint weenieClassId) + => Components.ContainsKey(weenieClassId); + + public int GetSpellLevel(uint spellId) + => _spellLevels.TryGetValue(spellId, out int level) ? level : 0; + + public SpellComponentRequirementService CreateRequirementService( + ClientObjectTable objects, + Func playerGuid, + Func accountName) + => new( + objects, + playerGuid, + accountName, + _formulaDefinitions, + _wcidByScid, + _magicPackWcidBySchool); + + public static MagicCatalog Load(DatCollection dats) + { + ArgumentNullException.ThrowIfNull(dats); + + var components = new Dictionary(); + var wcidByScid = new Dictionary(); + SpellComponentTable? componentTable = dats.Get(SpellComponentTableDid); + DualEnumIDMap? componentIds = dats.Get(ComponentIdMapDid); + if (componentTable is not null && componentIds is not null) + { + foreach (var pair in componentTable.Components) + { + if (!componentIds.ClientEnumToID.TryGetValue(pair.Key, out uint wcid)) + continue; + + wcidByScid[pair.Key] = wcid; + components[wcid] = new SpellComponentDescriptor( + wcid, + pair.Value.Name.Value, + pair.Value.Category, + pair.Value.Icon.DataId); + } + } + + var formulaDefinitions = new Dictionary(); + var spellLevels = new Dictionary(); + DatReaderWriter.DBObjs.SpellTable? spellTable = + dats.Get(SpellTableDid); + if (spellTable is not null) + { + foreach (var pair in spellTable.Spells) + { + uint[] formula = pair.Value.Components.Take(8).ToArray(); + formulaDefinitions[pair.Key] = new SpellFormulaDefinition( + pair.Value.FormulaVersion, + formula, + (uint)pair.Value.School); + spellLevels[pair.Key] = + RetailSpellFormula.InqSpellLevelByRoughHeuristic(formula); + } + } + + var magicPackWcidBySchool = new Dictionary(); + // Retail IsComponentPack resolves enum key 0x10000001 in category 0x28; + // that key is not itself a portal.dat file id. + uint magicPackMapDid = AcDream.App.UI.RetailDataIdResolver.Resolve( + dats, 0x10000001u, 0x28u); + if (magicPackMapDid != 0u + && dats.Portal.TryGet(magicPackMapDid, out EnumIDMap? magicPackMap) + && magicPackMap is not null) + { + foreach (var pair in magicPackMap.ClientEnumToID) + magicPackWcidBySchool[pair.Key] = pair.Value; + } + + return new MagicCatalog( + components, + formulaDefinitions, + wcidByScid, + magicPackWcidBySchool, + spellLevels); + } +} + +/// +/// App-layer owner for the live retail magic request path. The server owns +/// turning, motion, fizzle, mana/component consumption, effects, and results. +/// +public sealed class MagicRuntime +{ + private MagicRuntime(MagicCatalog catalog, SpellCastingController casting) + { + Catalog = catalog; + Casting = casting; + } + + public MagicCatalog Catalog { get; } + public SpellCastingController Casting { get; } + + public static MagicRuntime Create( + MagicCatalog catalog, + Spellbook spellbook, + ClientObjectTable objects, + Func selectedObject, + Func localPlayerId, + Func accountName, + Action stopCompletely, + Action sendUntargeted, + Action sendTargeted, + Action displayMessage, + Action incrementBusy, + Func canSend) + { + ArgumentNullException.ThrowIfNull(catalog); + ArgumentNullException.ThrowIfNull(spellbook); + ArgumentNullException.ThrowIfNull(objects); + ArgumentNullException.ThrowIfNull(displayMessage); + + SpellComponentRequirementService requirements = catalog.CreateRequirementService( + objects, localPlayerId, accountName); + bool TargetCompatible(uint target, SpellMetadata spell, bool showMessage) + { + if (objects.Get(target) is not { } targetObject) + return false; + + SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate( + localPlayerId(), targetObject, spell); + if (showMessage && !result.Allowed && result.Message is { } message) + displayMessage(message); + return result.Allowed; + } + + var casting = new SpellCastingController( + spellbook, + selectedObject, + localPlayerId, + stopCompletely, + sendUntargeted, + sendTargeted, + displayMessage, + targetCompatible: (target, spell) => TargetCompatible(target, spell, true), + hasRequiredComponents: requirements.HasRequiredComponents, + incrementBusy: incrementBusy, + canSend: canSend, + targetCompatibleSilent: (target, spell) => TargetCompatible(target, spell, false)); + return new MagicRuntime(catalog, casting); + } + + public void Reset() => Casting.Reset(); +} diff --git a/src/AcDream.App/Spells/RetailSpellTargetPolicy.cs b/src/AcDream.App/Spells/RetailSpellTargetPolicy.cs new file mode 100644 index 00000000..e21b1302 --- /dev/null +++ b/src/AcDream.App/Spells/RetailSpellTargetPolicy.cs @@ -0,0 +1,45 @@ +using AcDream.Core.Items; +using AcDream.Core.Spells; + +namespace AcDream.App.Spells; + +public readonly record struct SpellTargetPolicyResult(bool Allowed, string? Message) +{ + public static SpellTargetPolicyResult Accept { get; } = new(true, null); +} + +/// +/// Pure projection of ClientMagicSystem::ObjectCompatibleWithSpellTargetType +/// (0x00567230). The caller owns target lookup and message presentation. +/// +public static class RetailSpellTargetPolicy +{ + private const uint SpecialTargetMask = 0x00008107u; + + public static SpellTargetPolicyResult Evaluate( + uint localPlayerId, + ClientObject target, + SpellMetadata spell) + { + uint mask = spell.TargetMask; + uint special = mask & SpecialTargetMask; + if (target.ObjectId == localPlayerId && special == 0u) + return new(false, "You cannot cast this spell upon yourself."); + if (target.StackSize > 1) + return new(false, "Cannot cast spell on a stack of items."); + + if (((uint)target.Type & mask) == 0u && special == 0u) + return new(false, $"This spell cannot be cast on {target.Name}."); + + // Retail joins the ordinary type match and the 0x8107 special-mask + // bypass before these two gates. IsPlayer is PWD bit 3; otherwise the + // object must carry BF_ATTACKABLE. Pets are never legal spell targets. + var flags = (PublicWeenieFlags)target.PublicWeenieBitfield.GetValueOrDefault(); + bool playerOrAttackable = (flags + & (PublicWeenieFlags.Player | PublicWeenieFlags.Attackable)) != 0; + if (!playerOrAttackable || target.PetOwnerId != 0u) + return new(false, $"This spell cannot be cast on {target.Name}."); + + return SpellTargetPolicyResult.Accept; + } +} diff --git a/src/AcDream.App/Spells/SpellCastingController.cs b/src/AcDream.App/Spells/SpellCastingController.cs new file mode 100644 index 00000000..1eb6a759 --- /dev/null +++ b/src/AcDream.App/Spells/SpellCastingController.cs @@ -0,0 +1,157 @@ +using System; +using AcDream.Core.Spells; + +namespace AcDream.App.Spells; + +/// +/// App-layer owner for retail cast intent. It validates the client-owned +/// selection rules, stops movement, and emits exactly one targeted or +/// untargeted request. ACE owns every gameplay result after that boundary. +/// +/// +/// Port of ClientMagicSystem::CastSpell (0x00568040) and +/// FreeHandsAndCastSpell (0x00566EF0). This controller deliberately +/// does not consume mana/components or start local effects. +/// +public sealed class SpellCastingController +{ + private readonly Spellbook _spellbook; + private readonly Func _selectedObject; + private readonly Func _localPlayerId; + private readonly Action _stopCompletely; + private readonly Action _sendUntargeted; + private readonly Action _sendTargeted; + private readonly Action _displayMessage; + private readonly Func _targetCompatible; + private readonly Func _targetCompatibleSilent; + private readonly Func _hasRequiredComponents; + private readonly Action _incrementBusy; + private readonly Func _canSend; + + public SpellCastingController( + Spellbook spellbook, + Func selectedObject, + Func localPlayerId, + Action stopCompletely, + Action sendUntargeted, + Action sendTargeted, + Action displayMessage, + Func? targetCompatible = null, + Func? hasRequiredComponents = null, + Action? incrementBusy = null, + Func? canSend = null, + Func? targetCompatibleSilent = null) + { + _spellbook = spellbook ?? throw new ArgumentNullException(nameof(spellbook)); + _selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject)); + _localPlayerId = localPlayerId ?? throw new ArgumentNullException(nameof(localPlayerId)); + _stopCompletely = stopCompletely ?? throw new ArgumentNullException(nameof(stopCompletely)); + _sendUntargeted = sendUntargeted ?? throw new ArgumentNullException(nameof(sendUntargeted)); + _sendTargeted = sendTargeted ?? throw new ArgumentNullException(nameof(sendTargeted)); + _displayMessage = displayMessage ?? throw new ArgumentNullException(nameof(displayMessage)); + _targetCompatible = targetCompatible ?? ((_, _) => true); + _targetCompatibleSilent = targetCompatibleSilent ?? _targetCompatible; + _hasRequiredComponents = hasRequiredComponents ?? (_ => true); + _incrementBusy = incrementBusy ?? (() => { }); + _canSend = canSend ?? (() => true); + } + + public uint? LastRequestedSpellId { get; private set; } + public uint? LastRequestedTargetId { get; private set; } + + public bool IsTargetReady(uint spellId) + { + if (!_spellbook.Knows(spellId) + || !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell)) + return false; + if (spell.IsSelfTargeted || spell.IsUntargeted || spell.TargetMask == 0u) + return true; + return _selectedObject() is uint target and not 0u + && _targetCompatibleSilent(target, spell); + } + + public CastRequestResult Cast(uint spellId) + { + if (!_spellbook.Knows(spellId) || !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell)) + { + _displayMessage("You do not know that spell."); + return CastRequestResult.UnknownSpell; + } + + if (!_hasRequiredComponents(spellId)) + { + _displayMessage("You do not have all of this spell's components."); + return CastRequestResult.MissingComponents; + } + + uint? target; + bool untargeted; + if (spell.IsSelfTargeted) + { + uint playerId = _localPlayerId(); + target = playerId == 0 ? null : playerId; + untargeted = target is null; + } + else if (spell.IsUntargeted || spell.TargetMask == 0) + { + target = null; + untargeted = true; + } + else + { + target = _selectedObject(); + untargeted = false; + if (target is null or 0) + { + _displayMessage("You must select a suitable target."); + return CastRequestResult.NoTarget; + } + if (!_targetCompatible(target.Value, spell)) + return CastRequestResult.IncompatibleTarget; + } + + if (!_canSend()) + { + _displayMessage("You cannot cast a spell right now."); + return CastRequestResult.Unavailable; + } + + try + { + _stopCompletely(); + LastRequestedSpellId = spellId; + LastRequestedTargetId = target; + if (untargeted) + _sendUntargeted(spellId); + else + _sendTargeted(target!.Value, spellId); + // FreeHandsAndCastSpell @ 0x00566EF0 increments the shared UI + // busy reference only after Event_Cast has been emitted. The + // matching server UseDone event decrements it. + _incrementBusy(); + } + catch + { + LastRequestedSpellId = null; + LastRequestedTargetId = null; + throw; + } + return CastRequestResult.Sent; + } + + public void Reset() + { + LastRequestedSpellId = null; + LastRequestedTargetId = null; + } +} + +public enum CastRequestResult +{ + Sent, + UnknownSpell, + NoTarget, + IncompatibleTarget, + MissingComponents, + Unavailable, +} diff --git a/src/AcDream.App/Spells/SpellComponentRequirementService.cs b/src/AcDream.App/Spells/SpellComponentRequirementService.cs new file mode 100644 index 00000000..fa1b10f7 --- /dev/null +++ b/src/AcDream.App/Spells/SpellComponentRequirementService.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AcDream.Core.Items; +using AcDream.Core.Spells; + +namespace AcDream.App.Spells; + +public sealed record SpellFormulaDefinition( + uint FormulaVersion, + IReadOnlyList ComponentIds, + uint School = 0u); + +/// +/// Client-side retail component preflight for ClientMagicSystem::CastSpell +/// (0x00568040). Spell formula entries are SCIDs; carried inventory objects and +/// SetDesiredComponent use WCIDs, so all comparisons cross the DAT enum map. +/// +public sealed class SpellComponentRequirementService +{ + public const uint SpellComponentsRequiredProperty = 68u; + + private readonly ClientObjectTable _objects; + private readonly Func _playerGuid; + private readonly Func _accountName; + private readonly IReadOnlyDictionary _formulas; + private readonly IReadOnlyDictionary _wcidByScid; + private readonly IReadOnlyDictionary _magicPackWcidBySchool; + + public SpellComponentRequirementService( + ClientObjectTable objects, + Func playerGuid, + Func accountName, + IReadOnlyDictionary formulas, + IReadOnlyDictionary wcidByScid, + IReadOnlyDictionary? magicPackWcidBySchool = null) + { + _objects = objects ?? throw new ArgumentNullException(nameof(objects)); + _playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid)); + _accountName = accountName ?? throw new ArgumentNullException(nameof(accountName)); + _formulas = formulas ?? throw new ArgumentNullException(nameof(formulas)); + _wcidByScid = wcidByScid ?? throw new ArgumentNullException(nameof(wcidByScid)); + _magicPackWcidBySchool = magicPackWcidBySchool + ?? new Dictionary(); + } + + public bool HasRequiredComponents(uint spellId) + { + uint playerGuid = _playerGuid(); + ClientObject? player = _objects.Get(playerGuid); + if (player is not null + && !player.Properties.GetBool(SpellComponentsRequiredProperty, true)) + return true; + if (!_formulas.TryGetValue(spellId, out SpellFormulaDefinition? formula)) + return false; + + HashSet ownedWcids = _objects.Objects + .Where(item => IsOwnedByPlayer(item, playerGuid)) + .Select(item => item.WeenieClassId) + .ToHashSet(); + IReadOnlyList components = GetAppropriateFormula( + formula, player, playerGuid); + foreach (uint scid in components) + { + if (scid == 0u) continue; + if (!_wcidByScid.TryGetValue(scid, out uint wcid) + || !ownedWcids.Contains(wcid)) + return false; + } + return true; + } + + /// + /// Retail ClientMagicSystem::GetAppropriateSpellFormula + /// (0x00567D50): an infused school or a directly carried school focus uses + /// the scarab-only formula; otherwise the account-customized formula wins. + /// + private IReadOnlyList GetAppropriateFormula( + SpellFormulaDefinition formula, + ClientObject? player, + uint playerGuid) + { + bool infused = SchoolInfusionProperty(formula.School) is uint propertyId + && player?.Properties.GetInt(propertyId) > 0; + bool carriesMagicPack = _magicPackWcidBySchool.TryGetValue( + formula.School, out uint packWcid) + && _objects.Objects.Any(item => + item.ContainerId == playerGuid + && item.WeenieClassId == packWcid); + return infused || carriesMagicPack + ? RetailSpellFormula.InqScarabOnlyFormula(formula.ComponentIds) + : RetailSpellFormula.CustomizeForAccount( + formula.ComponentIds, formula.FormulaVersion, _accountName()); + } + + private static uint? SchoolInfusionProperty(uint school) => school switch + { + 1u => 0x129u, // AugmentationInfusedWarMagic + 2u => 0x128u, // AugmentationInfusedLifeMagic + 3u => 0x127u, // AugmentationInfusedItemMagic + 4u => 0x126u, // AugmentationInfusedCreatureMagic + 5u => 0x148u, // AugmentationInfusedVoidMagic + _ => null, + }; + + private bool IsOwnedByPlayer(ClientObject item, uint playerGuid) + { + if (playerGuid == 0u) return false; + ClientObject current = item; + for (int depth = 0; depth < 16; depth++) + { + if (current.WielderId == playerGuid || current.ContainerId == playerGuid) + return true; + if (current.ContainerId == 0u + || _objects.Get(current.ContainerId) is not { } parent) + return false; + current = parent; + } + return false; + } +} diff --git a/src/AcDream.App/UI/IconComposer.cs b/src/AcDream.App/UI/IconComposer.cs index eb15341f..583b853a 100644 --- a/src/AcDream.App/UI/IconComposer.cs +++ b/src/AcDream.App/UI/IconComposer.cs @@ -4,6 +4,7 @@ using System.Numerics; using AcDream.App.Rendering; using AcDream.Core.Items; using AcDream.Core.Textures; +using AcDream.Core.Spells; using DatReaderWriter; using DatReaderWriter.DBObjs; @@ -35,6 +36,8 @@ public sealed class IconComposer private readonly TextureCache _cache; private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new(); private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new(); + private readonly Dictionary _spellIcons = new(); + private readonly Dictionary _componentIcons = new(); private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, uint Texture); @@ -298,4 +301,71 @@ public sealed class IconComposer var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null); layers.Add((decoded.Rgba8, decoded.Width, decoded.Height)); } + + /// + /// Retail ClientMagicSystem::CompositeSpellIcon (0x00567550): power-level + /// backing, spell art, reversed/normal recolor, then self/fellow overlay. + /// + public uint GetSpellIcon(uint spellId) + { + if (_spellIcons.TryGetValue(spellId, out uint cached)) return cached; + DatReaderWriter.DBObjs.SpellTable? table = + _dats.Get(0x0E00000Eu); + if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return 0u; + + uint power = spell.Components.Count == 0 + ? 0u + : RetailSpellFormula.DeterminePowerLevelOfComponent(spell.Components[0]); + uint powerBacking = RetailDataIdResolver.Resolve(_dats, power, 0x10000006u); + var layers = new List<(byte[] rgba, int w, int h)>(); + AddLayer(layers, powerBacking); + AddLayer(layers, spell.Icon); + if (layers.Count == 0) return 0u; + + var composed = Compose(layers); + uint tintIndex = (spell.Bitfield & DatReaderWriter.Enums.SpellIndex.Reversed) != 0 + ? 1u : 2u; + uint tintDid = RetailDataIdResolver.Resolve(_dats, tintIndex, 0x10000007u); + if (TryDecode(tintDid, out DecodedTexture tint)) + ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h, + tint.Rgba8, tint.Width, tint.Height); + + uint overlayIndex = (spell.Bitfield & DatReaderWriter.Enums.SpellIndex.FellowshipSpell) != 0 + ? 4u + : (spell.Bitfield & DatReaderWriter.Enums.SpellIndex.SelfTargeted) != 0 + ? 3u : 0u; + if (overlayIndex != 0u) + { + uint overlayDid = RetailDataIdResolver.Resolve(_dats, overlayIndex, 0x10000007u); + if (TryDecode(overlayDid, out DecodedTexture overlay)) + composed = Compose([ + (composed.rgba, composed.w, composed.h), + (overlay.Rgba8, overlay.Width, overlay.Height)]); + } + + uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true); + _spellIcons[spellId] = texture; + return texture; + } + + /// + /// Retail ClientMagicSystem::CompositeSpellComponentIcon (0x00567720). + /// Components use their raw DAT art with pure white replaced by black. + /// + public uint GetSpellComponentIcon(uint iconId) + { + if (iconId == 0u) return 0u; + if (_componentIcons.TryGetValue(iconId, out uint cached)) return cached; + if (!TryDecode(iconId, out DecodedTexture icon)) return 0u; + byte[] rgba = (byte[])icon.Rgba8.Clone(); + for (int i = 0; i + 3 < rgba.Length; i += 4) + { + if (rgba[i] != 255 || rgba[i + 1] != 255 || rgba[i + 2] != 255 || rgba[i + 3] != 255) + continue; + rgba[i] = rgba[i + 1] = rgba[i + 2] = 0; + } + uint texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true); + _componentIcons[iconId] = texture; + return texture; + } } diff --git a/src/AcDream.App/UI/ItemInteractionController.cs b/src/AcDream.App/UI/ItemInteractionController.cs index 10e0074a..135ad8b1 100644 --- a/src/AcDream.App/UI/ItemInteractionController.cs +++ b/src/AcDream.App/UI/ItemInteractionController.cs @@ -125,6 +125,18 @@ public sealed class ItemInteractionController : IDisposable public bool CanMakeInventoryRequest => _readyForInventoryRequest() && _busyCount == 0 && !_autoWield.IsBusy; + /// + /// Increments retail's shared ClientUISystem busy reference after a + /// request issued by another retained controller has been sent. The + /// corresponding UseDone releases it. Retail spell casts complete through + /// Item__UseDone too; AttackDone belongs only to the combat attack owner. + /// + public void IncrementBusyCount() + { + _busyCount++; + StateChanged?.Invoke(); + } + /// /// Raised for retail confirmation/auxiliary actions whose retained panel is /// owned outside this controller (PK/NPK altar, volatile rare, trade, salvage). @@ -514,6 +526,14 @@ public sealed class ItemInteractionController : IDisposable StateChanged?.Invoke(); } + /// Session teardown drops every outstanding client UI busy reference. + public void ClearBusy() + { + if (_busyCount == 0) return; + _busyCount = 0; + StateChanged?.Invoke(); + } + private bool ConsumeUseThrottle() { long now = _nowMs(); diff --git a/src/AcDream.App/UI/Layout/CombatUiController.cs b/src/AcDream.App/UI/Layout/CombatUiController.cs index c0b5561b..41c84a0a 100644 --- a/src/AcDream.App/UI/Layout/CombatUiController.cs +++ b/src/AcDream.App/UI/Layout/CombatUiController.cs @@ -20,7 +20,8 @@ public sealed class CombatUiController : IRetainedPanelController { public const uint LayoutId = 0x21000073u; public const uint BasicPanelId = 0x1000005Cu; - public const uint AdvancedPanelId = 0x10000061u; + public const uint SpellcastingPanelId = 0x10000061u; + public const uint AdvancedPanelId = SpellcastingPanelId; public const uint PowerControlId = 0x1000004Fu; public const uint SpeedLabelId = 0x10000051u; public const uint PowerLabelId = 0x10000052u; @@ -36,7 +37,7 @@ public sealed class CombatUiController : IRetainedPanelController private readonly UiElement _root; private readonly UiElement _basicPanel; - private readonly UiElement _advancedPanel; + private readonly UiElement _spellcastingPanel; private readonly UiScrollbar _powerControl; private readonly UiButton _high; private readonly UiButton _medium; @@ -54,7 +55,7 @@ public sealed class CombatUiController : IRetainedPanelController private CombatUiController( ImportedLayout layout, UiElement basicPanel, - UiElement advancedPanel, + UiElement spellcastingPanel, UiScrollbar powerControl, UiButton high, UiButton medium, @@ -71,7 +72,7 @@ public sealed class CombatUiController : IRetainedPanelController { _root = layout.Root; _basicPanel = basicPanel; - _advancedPanel = advancedPanel; + _spellcastingPanel = spellcastingPanel; _powerControl = powerControl; _high = high; _medium = medium; @@ -85,11 +86,10 @@ public sealed class CombatUiController : IRetainedPanelController _setGameplay = setGameplay; _setWindowVisible = setWindowVisible; - // PlayerModule::AdvancedCombatUI false selects gmCombatUI's authored - // basic page. The advanced page belongs to the separate advanced- - // combat flow and stays hidden in the basic panel. + // Retail layout 0x21000073 contains two sibling pages: gmCombatUI's + // physical-attack controls and gmSpellcastingUI's favorite-spell bar. _basicPanel.Visible = true; - _advancedPanel.Visible = false; + _spellcastingPanel.Visible = false; _powerControl.SetScalarPosition(_attacks.DesiredPower); _powerControl.ScalarChanged = _attacks.SetDesiredPower; @@ -140,7 +140,7 @@ public sealed class CombatUiController : IRetainedPanelController ArgumentNullException.ThrowIfNull(setWindowVisible); if (layout.FindElement(BasicPanelId) is not { } basic - || layout.FindElement(AdvancedPanelId) is not { } advanced + || layout.FindElement(SpellcastingPanelId) is not { } spellcasting || layout.FindElement(PowerControlId) is not UiScrollbar power || layout.FindElement(HighButtonId) is not UiButton high || layout.FindElement(MediumButtonId) is not UiButton medium @@ -151,7 +151,7 @@ public sealed class CombatUiController : IRetainedPanelController return null; return new CombatUiController( - layout, basic, advanced, power, high, medium, low, + layout, basic, spellcasting, power, high, medium, low, repeatAttacks, autoTarget, keepInView, combat, attacks, gameplay, setGameplay, labels, setWindowVisible); } @@ -166,7 +166,9 @@ public sealed class CombatUiController : IRetainedPanelController private void OnCombatModeChanged(CombatMode mode) { - bool visible = CombatInputPlanner.SupportsTargetedAttack(mode); + bool visible = mode is CombatMode.Melee or CombatMode.Missile or CombatMode.Magic; + _basicPanel.Visible = mode is CombatMode.Melee or CombatMode.Missile; + _spellcastingPanel.Visible = mode == CombatMode.Magic; if (_root is IUiDatStateful stateful) { if (mode == CombatMode.Melee) diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index 36bb928b..70ccf7ee 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -77,6 +77,8 @@ public static class DatWidgetFactory { UiRadar.RetailClassId => new UiRadar(), // gmRadarUI (Register 0x004D8B80) 1 => BuildButton(info, resolve, elementFont, stringResolve), // UIElement_Button + EffectsIndicatorController.RetailClassId => BuildButton( + info, resolve, elementFont, stringResolve), // gmUIElement_EffectsIndicator 6 => new UiMenu(), // UIElement_Menu (reg :120163) 7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter 0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf diff --git a/src/AcDream.App/UI/Layout/EffectsIndicatorController.cs b/src/AcDream.App/UI/Layout/EffectsIndicatorController.cs new file mode 100644 index 00000000..04788967 --- /dev/null +++ b/src/AcDream.App/UI/Layout/EffectsIndicatorController.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using AcDream.Core.Spells; + +namespace AcDream.App.UI.Layout; + +/// +/// Port of retail gmUIElement_EffectsIndicator::PostInit @ 0x004E6930 +/// and Update @ 0x004E6A50: the authored Helpful and Harmful buttons +/// reflect whether matching enchantments exist and send panel visibility notices +/// for gmPanelUI slots 4 and 5. +/// +public sealed class EffectsIndicatorController : IRetainedPanelController +{ + public const uint LayoutId = 0x21000071u; + public const uint RetailClassId = 0x10000002u; + public const uint HelpfulButtonId = 0x100000F5u; + public const uint HarmfulButtonId = 0x100000F6u; + + private readonly Spellbook _spellbook; + private readonly UiButton _helpful; + private readonly UiButton _harmful; + private readonly Action _togglePanel; + private bool _disposed; + + private EffectsIndicatorController( + Spellbook spellbook, + UiButton helpful, + UiButton harmful, + Action togglePanel) + { + _spellbook = spellbook; + _helpful = helpful; + _harmful = harmful; + _togglePanel = togglePanel; + _helpful.OnClick = () => _togglePanel(RetailPanelCatalog.PositiveEffects); + _harmful.OnClick = () => _togglePanel(RetailPanelCatalog.NegativeEffects); + _spellbook.EnchantmentsChanged += Update; + Update(); + } + + public static EffectsIndicatorController? Bind( + ImportedLayout layout, + Spellbook spellbook, + Action togglePanel) + { + ArgumentNullException.ThrowIfNull(layout); + ArgumentNullException.ThrowIfNull(spellbook); + ArgumentNullException.ThrowIfNull(togglePanel); + if (layout.FindElement(HelpfulButtonId) is not UiButton helpful + || layout.FindElement(HarmfulButtonId) is not UiButton harmful) + return null; + return new EffectsIndicatorController(spellbook, helpful, harmful, togglePanel); + } + + private void Update() + { + bool helpful = HasMatchingEffect(beneficial: true); + bool harmful = HasMatchingEffect(beneficial: false); + _helpful.TrySetRetailState(helpful + ? UiButtonStateMachine.Normal + : UiButtonStateMachine.Ghosted); + _harmful.TrySetRetailState(harmful + ? UiButtonStateMachine.Normal + : UiButtonStateMachine.Ghosted); + } + + private bool HasMatchingEffect(bool beneficial) + // Retail SpellEffectMatchesUIType @ 0x004B76C0 supplies this helpful/harmful split. + => _spellbook.ActiveEnchantmentSnapshot.Any(record => + // Retail CountSpellsInList @ 0x00593CD0 counts every raw mult/add + // node. Unlike gmEffectsUI's list, indicator totals are not dueled + // by spell category first. + record.Bucket is 1u or 2u + && _spellbook.TryGetMetadata(record.SpellId, out SpellMetadata metadata) + && metadata.IsBeneficial == beneficial); + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _spellbook.EnchantmentsChanged -= Update; + _helpful.OnClick = null; + _harmful.OnClick = null; + } +} diff --git a/src/AcDream.App/UI/Layout/EffectsUiController.cs b/src/AcDream.App/UI/Layout/EffectsUiController.cs new file mode 100644 index 00000000..cc04b290 --- /dev/null +++ b/src/AcDream.App/UI/Layout/EffectsUiController.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using AcDream.Core.Spells; + +namespace AcDream.App.UI.Layout; + +/// Retail gmEffectsUI positive/negative instance binding. +public sealed class EffectsUiController : IRetainedPanelController +{ + public const uint LayoutId = 0x2100001Bu; + // gmPanelUI's 0x10000184/185 are panel-slot IDs, not roots in this LayoutDesc. + // The authored positive/negative gmEffectsUI instances inherit template 0x10000122. + public const uint PositiveRootId = 0x1000011Fu; + public const uint NegativeRootId = 0x10000121u; + public const uint CloseId = 0x100000FCu; + public const uint ListId = 0x10000123u; + public const uint InfoTextId = 0x10000126u; + + private readonly Spellbook _spellbook; + private readonly bool _positive; + private readonly Func _serverTime; + private readonly Func _resolveSpellIcon; + private readonly UiItemList _list; + private readonly UiText? _info; + private readonly UiButton? _close; + private readonly Dictionary _rows = new(); + private uint? _selectedSpellId; + private double _lastDurationUpdate = double.NaN; + private bool _disposed; + + internal uint? SelectedSpellId => _selectedSpellId; + + private EffectsUiController( + ImportedLayout layout, + Spellbook spellbook, + bool positive, + Func serverTime, + Func resolveSpellIcon, + UiItemList list, + Action? close) + { + _spellbook = spellbook; + _positive = positive; + _serverTime = serverTime; + _resolveSpellIcon = resolveSpellIcon; + _list = list; + _info = layout.FindElement(InfoTextId) as UiText; + _close = layout.FindElement(CloseId) as UiButton; + if (_close is not null) _close.OnClick = close; + _list.Columns = 1; + _list.CellWidth = Math.Max(1f, _list.Width); + _list.CellHeight = 32f; + ConfigureInfo(); + _spellbook.EnchantmentsChanged += Rebuild; + Rebuild(); + } + + public static EffectsUiController? Bind( + ImportedLayout layout, + Spellbook spellbook, + bool positive, + Func serverTime, + Func spriteResolve, + Func resolveSpellIcon, + Action? close = null) + { + UiElement? host = layout.FindElement(ListId); + if (host is null) return null; + UiItemList list; + if (host is UiItemList itemList) + list = itemList; + else + { + list = new UiItemList(spriteResolve) + { + Width = host.Width, + Height = host.Height, + Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom, + }; + host.AddChild(list); + } + return new EffectsUiController( + layout, spellbook, positive, serverTime, resolveSpellIcon, list, close); + } + + public void Tick() + { + double now = _serverTime(); + if (!double.IsFinite(now)) return; + if (double.IsFinite(_lastDurationUpdate) + && now >= _lastDurationUpdate + && now - _lastDurationUpdate < 1.0) + return; + _lastDurationUpdate = now; + foreach (ActiveEnchantmentRecord enchantment in VisibleEnchantments()) + if (_rows.TryGetValue(enchantment.Identity, out UiCatalogSlot? row)) + row.Detail = FormatRemaining(enchantment, now); + } + + private void Rebuild() + { + _lastDurationUpdate = double.NaN; + _rows.Clear(); + ActiveEnchantmentRecord[] enchantments = VisibleEnchantments().ToArray(); + using (_list.DeferLayout()) + { + _list.Flush(); + foreach (ActiveEnchantmentRecord enchantment in enchantments) + { + _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); + _rows[identity] = row; + _list.AddItem(row); + } + } + if (_selectedSpellId is uint selected + && !_rows.Values.Any(row => row.EntryId == selected)) + _selectedSpellId = null; + SyncSelection(); + } + + private IEnumerable VisibleEnchantments() + => _spellbook.EnchantmentsInEffectSnapshot + .Where(record => + { + if (!_spellbook.TryGetMetadata(record.SpellId, out SpellMetadata metadata)) + return false; + return metadata.IsBeneficial == _positive; + }) + .OrderBy(record => _spellbook.TryGetMetadata(record.SpellId, out SpellMetadata metadata) + ? metadata.Name : record.SpellId.ToString(CultureInfo.InvariantCulture), + StringComparer.OrdinalIgnoreCase); + + private static string FormatRemaining(ActiveEnchantmentRecord enchantment, double now) + { + if (enchantment.Duration < 0) return "Permanent"; + double remaining = Math.Max(0, enchantment.StartTime + enchantment.Duration - now); + if (!double.IsFinite(remaining)) return "--:--"; + remaining = Math.Min(remaining, TimeSpan.MaxValue.TotalSeconds); + TimeSpan time = TimeSpan.FromSeconds(remaining); + return time.TotalHours >= 1 + ? $"{(int)time.TotalHours}:{time.Minutes:00}:{time.Seconds:00}" + : $"{time.Minutes}:{time.Seconds:00}"; + } + + private void Select(uint spellId) + { + // Retail gmEffectsUI::SetSelectedSpell @ 0x004B8290 stores the + // token's spell stat, not its enchantment layer identity. + _selectedSpellId = _selectedSpellId == spellId ? null : spellId; + SyncSelection(); + } + + private void SyncSelection() + { + foreach (UiCatalogSlot row in _rows.Values) + row.Selected = row.EntryId == _selectedSpellId; + } + + private void ConfigureInfo() + { + if (_info is null) return; + _info.LinesProvider = () => + { + if (_selectedSpellId is not uint spellId) + return Array.Empty(); + ActiveEnchantmentRecord? selected = _spellbook.ActiveEnchantmentSnapshot + .Cast() + .FirstOrDefault(record => record?.SpellId == spellId); + if (selected is null || !_spellbook.TryGetMetadata(selected.Value.SpellId, out SpellMetadata metadata)) + return Array.Empty(); + return + [ + new UiText.Line(metadata.Name, _info.DefaultColor), + new UiText.Line(metadata.Description, _info.DefaultColor), + ]; + }; + } + + public void OnShown() => Rebuild(); + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _spellbook.EnchantmentsChanged -= Rebuild; + if (_close is not null) _close.OnClick = null; + } +} diff --git a/src/AcDream.App/UI/Layout/ElementReader.cs b/src/AcDream.App/UI/Layout/ElementReader.cs index 23741a17..10f6e974 100644 --- a/src/AcDream.App/UI/Layout/ElementReader.cs +++ b/src/AcDream.App/UI/Layout/ElementReader.cs @@ -131,8 +131,9 @@ public sealed class ElementInfo public string DefaultStateName = ""; /// - /// Resolved child elements (populated by the importer in Task 5). - /// Children come from the derived element's own tree, not the base element's. + /// Resolved child elements. The importer ports retail child-table incorporation: + /// base-only children remain, same-ID children merge recursively, and derived-only + /// children append after the retained inherited entries. /// public List Children = new(); @@ -264,7 +265,9 @@ public static class ElementReader /// base entries are the default; derived entries override (or add) per state name key. /// /// - /// : come from the derived element's own tree only. + /// : are not combined by this scalar merge helper; + /// separately ports retail's recursive child-table + /// incorporation. /// /// /// @@ -306,10 +309,9 @@ public static class ElementReader FontColor = derived.FontColor ?? base_.FontColor, // DefaultStateName: derived wins if set; otherwise inherit the base's default. DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName, - // Children come from the derived element's own tree, not the base prototype's. - // Defensive copy: prevent a later mutation of either the merged result or the input - // from corrupting the other. Safe because derived.Children is fully - // populated by the recursive importer BEFORE Merge is called and never mutated after). + // This helper merges one element snapshot only. LayoutImporter separately + // incorporates the child tables after the scalar/state merge. + // Defensive copy prevents later mutation of either input. Children = new List(derived.Children), }; // Start with base StateMedia as defaults, then let derived entries override. diff --git a/src/AcDream.App/UI/Layout/LayoutImporter.cs b/src/AcDream.App/UI/Layout/LayoutImporter.cs index 0f6c603e..c7fb23be 100644 --- a/src/AcDream.App/UI/Layout/LayoutImporter.cs +++ b/src/AcDream.App/UI/Layout/LayoutImporter.cs @@ -295,11 +295,9 @@ public static class LayoutImporter // ── Inheritance resolution ──────────────────────────────────────────────── - /// True when a pure-container leaf should inherit its base's subtree (the - /// gmInventoryUI sub-window mount): the derived element has no own children, no own - /// state media, and the base resolved to ≥1 child. Inert for media-bearing inheritors - /// (close button / title), elements with their own children, and childless style - /// prototypes (vitals/chat/toolbar text — the base prototype has no children). + /// True when a pure-container inheritor needs the mounted-base Z-layer + /// correction. Child inheritance itself is unconditional and follows retail + /// ElementDesc::Incorporate (0x0069B5A0). internal static bool ShouldMountBaseChildren(int derivedChildCount, int derivedMediaCount, int baseChildCount) => derivedChildCount == 0 && derivedMediaCount == 0 && baseChildCount > 0; @@ -316,7 +314,7 @@ public static class LayoutImporter // Read this element's own fields + media (no inheritance, no children yet). var self = ToInfo(d); var result = self; - List? baseChildren = null; + ElementInfo? baseInfo = null; // Apply BaseElement / BaseLayoutId inheritance if present. if (d.BaseElement != 0 && d.BaseLayoutId != 0 @@ -327,32 +325,23 @@ public static class LayoutImporter if (baseDesc is not null) { // Recurse the base chain (already guarded by the HashSet add above). - var baseInfo = Resolve(dats, baseDesc, baseChain); + baseInfo = Resolve(dats, baseDesc, baseChain); // Derived fields override the base; children are attached below. result = ElementReader.Merge(baseInfo, self); - baseChildren = baseInfo.Children; // capture for the sub-window mount } } - // Resolve + attach children. Each child gets a FRESH base-chain set: - // the cycle guard is per-element, not shared across siblings. - foreach (var kv in d.Children) - { - var child = Resolve(dats, kv.Value, new HashSet<(uint, uint)>()); - SetOriginalParentSize(child, result.Width, result.Height); - result.Children.Add(child); - } + // Retail LayoutDesc::InqFullDesc (0x0069A520) recursively resolves the base, + // then ElementDesc::Incorporate (0x0069B5A0) merges the complete child table: + // base-only children remain, same-ID children incorporate recursively, and + // derived-only children append after the retained base entries. + IncorporateChildren(dats, result, baseInfo?.Children, d); - // Sub-window mount: a pure-container leaf (no own children, no own media) that inherits - // from a base WITH content attaches the base's subtree. Targets the gmInventoryUI panels - // (0x100001CD/CE/CF — Type-0, media-less, childless, BaseLayoutId → a gm*UI window); - // inert for media-bearing inheritors (close button/title) and childless style prototypes - // (vitals/chat/toolbar). See ShouldMountBaseChildren + the B-Grid spec. - if (baseChildren is not null - && ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseChildren.Count)) + // A pure-container sub-window mount needs one additional layer correction. + // Child-table incorporation has already happened above for every inheritor. + if (baseInfo is not null + && ShouldMountBaseChildren(d.Children.Count, self.StateMedia.Count, baseInfo.Children.Count)) { - result.Children.AddRange(baseChildren); - // The mounted slot's layer WITHIN THE FRAME is its OWN ZLevel, not the mounted // sub-window root's. The gm*UI sub-window roots carry ZLevel 1000 (their standalone // top-window layer); ElementReader.Merge's zero-wins-base rule made the slot (own @@ -372,6 +361,57 @@ public static class LayoutImporter return result; } + private static void IncorporateChildren( + DatCollection dats, + ElementInfo result, + IReadOnlyList? baseChildren, + ElementDesc derived) + { + baseChildren ??= Array.Empty(); + var baseById = baseChildren.ToDictionary(child => child.Id); + int retainedBaseCount = baseChildren.Count(child => + !derived.Children.ContainsKey(child.Id)); + + foreach (ElementInfo baseChild in baseChildren) + { + bool hasOverlay = derived.Children.TryGetValue( + baseChild.Id, out ElementDesc? overlay); + ElementInfo child = hasOverlay + ? IncorporateResolvedChild(dats, baseChild, overlay!) + : baseChild; + // Base-only descendants retain the base layout's design parent size; + // UiLayoutPolicy then performs retail's base-to-derived parent resize. + if (hasOverlay) + SetOriginalParentSize(child, result.Width, result.Height); + result.Children.Add(child); + } + + // Every new child receives a fresh base-chain set. Retail offsets its read order + // by the count of inherited children not replaced by a same-ID overlay. + foreach (var pair in derived.Children) + { + if (baseById.ContainsKey(pair.Key)) continue; + ElementInfo child = Resolve(dats, pair.Value, new HashSet<(uint, uint)>()); + child.ReadOrder += checked((uint)retainedBaseCount); + SetOriginalParentSize(child, result.Width, result.Height); + result.Children.Add(child); + } + } + + private static ElementInfo IncorporateResolvedChild( + DatCollection dats, + ElementInfo baseChild, + ElementDesc derivedChild) + { + // ElementDesc::Incorporate consumes the partial child directly. It does not + // independently re-resolve that child's BaseElement when the inherited table + // already contains the same identity. + ElementInfo self = ToInfo(derivedChild); + ElementInfo result = ElementReader.Merge(baseChild, self); + IncorporateChildren(dats, result, baseChild.Children, derivedChild); + return result; + } + /// /// Read an 's own scalar fields + state media into a /// fresh . No inheritance is applied; children are not diff --git a/src/AcDream.App/UI/Layout/RetailPanelUiController.cs b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs new file mode 100644 index 00000000..738a45a8 --- /dev/null +++ b/src/AcDream.App/UI/Layout/RetailPanelUiController.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; + +namespace AcDream.App.UI.Layout; + +/// +/// Retained-window port of retail gmPanelUI::RecvNotice_SetPanelVisibility +/// at 0x004BC6F0. The original owns one active child panel and an optional +/// deferred child; this owner applies the same lifecycle to registered retained +/// windows without making the toolbar authoritative for non-toolbar panels. +/// +public sealed class RetailPanelUiController +{ + public const uint RestorePreviousPropertyId = 0x10000049u; + + private readonly Func _isVisible; + private readonly Func _show; + private readonly Func _hide; + private readonly Dictionary _byPanel = new(); + private readonly Dictionary _byWindow = new(StringComparer.Ordinal); + private uint? _activePanel; + private uint? _deferredPanel; + private bool _applying; + + public RetailPanelUiController( + Func isVisible, + Func show, + Func hide) + { + _isVisible = isVisible ?? throw new ArgumentNullException(nameof(isVisible)); + _show = show ?? throw new ArgumentNullException(nameof(show)); + _hide = hide ?? throw new ArgumentNullException(nameof(hide)); + } + + public uint? ActivePanelId => _activePanel; + + /// + /// Effective DAT bool property 0x10000049. Retail retains the previous + /// panel only when the newly shown panel has this property and the previous + /// panel does not. + /// + public void Register(uint panelId, string windowName, bool restorePrevious = false) + { + if (panelId == 0) throw new ArgumentOutOfRangeException(nameof(panelId)); + ArgumentException.ThrowIfNullOrWhiteSpace(windowName); + if (_byPanel.ContainsKey(panelId) || _byWindow.ContainsKey(windowName)) + throw new InvalidOperationException( + $"Panel {panelId} or retained window '{windowName}' is already registered."); + + _byPanel.Add(panelId, new PanelEntry(windowName, restorePrevious)); + _byWindow.Add(windowName, panelId); + if (_isVisible(windowName)) + ObserveWindowVisibility(windowName, visible: true); + } + + /// + /// Registers a panel directly from its fully inherited retail root so the + /// authored deferred-panel behavior cannot be dropped at the mount seam. + /// + public void Register(uint panelId, string windowName, ElementInfo authoredRoot) + { + ArgumentNullException.ThrowIfNull(authoredRoot); + Register( + panelId, + windowName, + authoredRoot.TryGetEffectiveBool( + RestorePreviousPropertyId, + out bool restorePrevious) + && restorePrevious); + } + + public bool IsPanelVisible(uint panelId) + => _byPanel.TryGetValue(panelId, out PanelEntry entry) + && _isVisible(entry.WindowName); + + public bool TogglePanel(uint panelId) + { + bool visible = !IsPanelVisible(panelId); + return SetPanelVisibility(panelId, visible) && visible; + } + + public bool SetPanelVisibility(uint panelId, bool visible) + { + if (!_byPanel.TryGetValue(panelId, out PanelEntry requested)) return false; + + _applying = true; + try + { + if (visible) + { + if (_activePanel == panelId) + return _show(requested.WindowName); + + uint? previousId = _activePanel; + PanelEntry? previous = previousId is uint id + && _byPanel.TryGetValue(id, out PanelEntry found) + && _isVisible(found.WindowName) + ? found + : null; + + _deferredPanel = requested.RestorePrevious + && previous is { RestorePrevious: false } + ? previousId + : null; + _activePanel = panelId; + if (previous is not null) + _hide(previous.Value.WindowName); + return _show(requested.WindowName); + } + + if (_activePanel != panelId) + { + if (_deferredPanel == panelId) _deferredPanel = null; + return _hide(requested.WindowName); + } + + bool hidden = _hide(requested.WindowName); + _activePanel = null; + if (_deferredPanel is not uint deferredId + || !_byPanel.TryGetValue(deferredId, out PanelEntry deferred)) + return hidden; + + _deferredPanel = null; + _activePanel = deferredId; + return _show(deferred.WindowName) || hidden; + } + finally + { + _applying = false; + } + } + + /// + /// Feeds visibility changes made by persistence or another retained-window + /// owner back through the canonical panel lifecycle. + /// + public void ObserveWindowVisibility(string windowName, bool visible) + { + if (_applying || !_byWindow.TryGetValue(windowName, out uint panelId)) return; + SetPanelVisibility(panelId, visible); + } + + private readonly record struct PanelEntry(string WindowName, bool RestorePrevious); +} diff --git a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs new file mode 100644 index 00000000..546155e8 --- /dev/null +++ b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs @@ -0,0 +1,457 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; +using AcDream.App.Spells; +using AcDream.Core.Items; +using AcDream.Core.Spells; + +namespace AcDream.App.UI.Layout; + +public enum SpellbookWindowPage { Spells, Components } + +/// +/// Binds retail's combined spellbook/component-book LayoutDesc 0x21000034. +/// The spell page mirrors gmSpellbookUI filters; the component page mirrors +/// ComponentTracker and exposes the 0..5000 desired purchase level field. +/// +public sealed class SpellbookWindowController : IRetainedPanelController +{ + public const uint LayoutId = 0x21000034u; + public const uint RootId = 0x100002A8u; + public const uint SpellTabId = 0x100002A9u; + public const uint ComponentTabId = 0x100002AAu; + public const uint CloseId = 0x100002ABu; + public const uint SpellPageId = 0x100002ACu; + public const uint ComponentPageId = 0x100002ADu; + public const uint SpellListId = 0x10000295u; + public const uint ComponentListId = 0x10000464u; + public const uint ComponentScrollbarId = 0x10000465u; + + private static readonly (uint Id, uint Mask)[] FilterButtons = + [ + (0x10000298u, 0x0001u), (0x10000299u, 0x0002u), + (0x1000029Au, 0x0004u), (0x1000029Bu, 0x0008u), + (0x100005C0u, 0x2000u), + (0x1000029Cu, 0x0010u), (0x1000029Du, 0x0020u), + (0x1000029Eu, 0x0040u), (0x1000029Fu, 0x0080u), + (0x100002A0u, 0x0100u), (0x100002A1u, 0x0200u), + (0x100002A2u, 0x0400u), (0x1000054Eu, 0x0800u), + ]; + + private readonly Spellbook _spellbook; + private readonly ClientObjectTable _objects; + private readonly Func _playerGuid; + private readonly IReadOnlyDictionary _components; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveComponentIcon; + private readonly Func _spellLevel; + private readonly Action _selectObject; + private readonly Action _addFavorite; + private readonly Action _sendFilter; + private readonly Action _setDesiredComponent; + private readonly Action _close; + private readonly UiElement _spellPage; + private readonly UiElement _componentPage; + private readonly UiButton _spellTab; + private readonly UiButton _componentTab; + private readonly UiButton _closeButton; + private readonly UiItemList _spellList; + private readonly UiItemList _componentList; + private readonly List<(UiButton Button, uint Mask)> _filters = new(); + private uint? _selectedSpell; + private bool _spellsDirty; + private bool _componentsDirty; + private bool _componentEditActive; + private bool _disposed; + + private SpellbookWindowController( + ImportedLayout layout, + Spellbook spellbook, + ClientObjectTable objects, + Func playerGuid, + IReadOnlyDictionary components, + Func resolveSpellIcon, + Func resolveComponentIcon, + Func spellLevel, + Action selectObject, + Action addFavorite, + Action sendFilter, + Action setDesiredComponent, + Action close, + UiElement spellPage, + UiElement componentPage, + UiButton spellTab, + UiButton componentTab, + UiButton closeButton, + UiItemList spellList, + UiItemList componentList) + { + _spellbook = spellbook; + _objects = objects; + _playerGuid = playerGuid; + _components = components; + _resolveSpellIcon = resolveSpellIcon; + _resolveComponentIcon = resolveComponentIcon; + _spellLevel = spellLevel; + _selectObject = selectObject; + _addFavorite = addFavorite; + _sendFilter = sendFilter; + _setDesiredComponent = setDesiredComponent; + _close = close; + _spellPage = spellPage; + _componentPage = componentPage; + _spellTab = spellTab; + _componentTab = componentTab; + _closeButton = closeButton; + _spellList = spellList; + _componentList = componentList; + + spellTab.OnClick = () => ShowPage(SpellbookWindowPage.Spells); + componentTab.OnClick = () => ShowPage(SpellbookWindowPage.Components); + closeButton.OnClick = close; + foreach ((uint id, uint mask) in FilterButtons) + { + if (layout.FindElement(id) is not UiButton button) continue; + uint filterMask = mask; + button.OnClick = () => ToggleFilter(filterMask); + _filters.Add((button, mask)); + } + + ConfigureSpellList(); + ConfigureComponentList(layout); + _spellbook.SpellbookChanged += OnSpellbookChanged; + _spellbook.DesiredComponentsChanged += OnDesiredComponentsChanged; + _objects.ObjectAdded += OnObjectChanged; + _objects.ObjectUpdated += OnObjectChanged; + _objects.ObjectRemoved += OnObjectRemoved; + _objects.ObjectMoved += OnObjectMoved; + _objects.ContainerContentsReplaced += OnContainerContentsReplaced; + ShowPage(SpellbookWindowPage.Spells); + RebuildAll(); + } + + public SpellbookWindowPage CurrentPage { get; private set; } + + public static SpellbookWindowController? Bind( + ImportedLayout layout, + Spellbook spellbook, + ClientObjectTable objects, + Func playerGuid, + IReadOnlyDictionary components, + Func resolveSpellIcon, + Func resolveComponentIcon, + Func spellLevel, + Action selectObject, + Action addFavorite, + Action sendFilter, + Action setDesiredComponent, + Action close) + { + if (layout.FindElement(SpellPageId) is not { } spellPage + || layout.FindElement(ComponentPageId) is not { } componentPage + || layout.FindElement(SpellTabId) is not UiButton spellTab + || layout.FindElement(ComponentTabId) is not UiButton componentTab + || layout.FindElement(CloseId) is not UiButton closeButton + || layout.FindElement(SpellListId) is not UiItemList spellList) + return null; + + UiElement? componentHost = layout.FindElement(ComponentListId); + if (componentHost is null) return null; + UiItemList componentList; + if (componentHost is UiItemList itemList) + componentList = itemList; + else + { + componentList = new UiItemList(spellList.SpriteResolve) + { + Width = componentHost.Width, + Height = componentHost.Height, + Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom, + }; + componentHost.AddChild(componentList); + } + + return new SpellbookWindowController( + layout, spellbook, objects, playerGuid, components, + resolveSpellIcon, resolveComponentIcon, + spellLevel, selectObject, + addFavorite, sendFilter, setDesiredComponent, close, + spellPage, componentPage, spellTab, componentTab, closeButton, + spellList, componentList); + } + + public void ShowPage(SpellbookWindowPage page) + { + CurrentPage = page; + _spellPage.Visible = page == SpellbookWindowPage.Spells; + _componentPage.Visible = page == SpellbookWindowPage.Components; + _spellTab.Selected = page == SpellbookWindowPage.Spells; + _componentTab.Selected = page == SpellbookWindowPage.Components; + } + + private void ConfigureSpellList() + { + _spellList.Columns = 6; + _spellList.CellWidth = 32f; + _spellList.CellHeight = 32f; + } + + private void ConfigureComponentList(ImportedLayout layout) + { + _componentList.Columns = 1; + _componentList.CellWidth = Math.Max(1f, _componentList.Width); + _componentList.CellHeight = 32f; + if (layout.FindElement(ComponentScrollbarId) is UiScrollbar scrollbar) + scrollbar.Model = _componentList.Scroll; + } + + private void ToggleFilter(uint mask) + { + uint filters = _spellbook.SpellbookFilters ^ mask; + _spellbook.SetSpellbookFilters(filters); + _sendFilter(filters); + } + + private void RebuildAll() + { + RebuildSpells(); + RebuildComponents(); + _spellsDirty = false; + _componentsDirty = false; + } + + private void RebuildSpells() + { + uint effective = _spellbook.SpellbookFilters; + foreach ((UiButton button, uint mask) in _filters) + button.Selected = (effective & mask) != 0; + + SpellMetadata[] spells = _spellbook.LearnedSpells + .Select(id => _spellbook.TryGetMetadata(id, out SpellMetadata metadata) ? metadata : null) + .Where(metadata => metadata is not null && IsVisible(metadata, effective)) + .OrderBy(metadata => metadata!.SortKey) + .ThenBy(metadata => metadata!.Name, StringComparer.OrdinalIgnoreCase) + .Cast() + .ToArray(); + + using (_spellList.DeferLayout()) + { + _spellList.Flush(); + foreach (SpellMetadata metadata in spells) + { + uint spellId = metadata.SpellId; + var slot = new UiCatalogSlot + { + EntryId = spellId, + CatalogIconTexture = _resolveSpellIcon(spellId), + Label = metadata.Name, + SpriteResolve = _spellList.SpriteResolve, + }; + slot.Clicked = () => SelectSpell(spellId); + // gmSpellbookUI double-click publishes AddSpellShortcut; the open + // gmSpellcastingUI tab chooses the server favorite-list destination. + slot.DoubleClicked = () => { SelectSpell(spellId); _addFavorite(spellId); }; + _spellList.AddItem(slot); + } + } + SyncSpellSelection(); + } + + private void RebuildComponents() + { + uint player = _playerGuid(); + var packs = _objects.Objects + .Where(item => (item.IsComponentPack || _components.ContainsKey(item.WeenieClassId)) + && IsOwnedByPlayer(item, player)) + .GroupBy(item => item.WeenieClassId) + .Select(group => new + { + ComponentId = group.Key, + First = group.First(), + Quantity = group.Sum(item => Math.Max(1, item.StackSize)), + }) + .OrderBy(group => _components.TryGetValue(group.ComponentId, out var descriptor) ? descriptor.Category : uint.MaxValue) + .ThenBy(group => group.First.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + float width = Math.Max(96f, _componentList.Width); + _componentList.CellWidth = width; + using (_componentList.DeferLayout()) + { + _componentList.Flush(); + uint? category = null; + foreach (var packGroup in packs) + { + ClientObject pack = packGroup.First; + uint componentId = packGroup.ComponentId; + _components.TryGetValue(componentId, out SpellComponentDescriptor? descriptor); + uint nextCategory = descriptor?.Category ?? 8u; + if (category != nextCategory) + { + category = nextCategory; + _componentList.AddItem(new UiCatalogSlot + { + EntryId = uint.MaxValue, + Label = ComponentCategoryName(nextCategory), + ShowLabel = true, + SpriteResolve = _componentList.SpriteResolve, + }); + } + uint desired = _spellbook.DesiredComponents.TryGetValue(componentId, out uint amount) ? amount : 0u; + var row = new UiCatalogSlot + { + EntryId = componentId, + CatalogIconTexture = _resolveComponentIcon( + pack.IconId != 0 ? pack.IconId : descriptor?.IconId ?? 0u), + Label = string.IsNullOrWhiteSpace(pack.Name) ? descriptor?.Name ?? $"Component {componentId}" : pack.Name, + Detail = packGroup.Quantity.ToString(CultureInfo.InvariantCulture), + ShowLabel = true, + SpriteResolve = _componentList.SpriteResolve, + }; + var desiredField = new UiField + { + Left = width - 47f, + Top = 4f, + Width = 43f, + Height = 24f, + MaxCharacters = 4, + ClearOnSubmit = false, + RecordHistory = false, + SelectAllOnFocus = true, + RightAligned = true, + CharacterFilter = char.IsAsciiDigit, + BackgroundColor = new Vector4(0f, 0f, 0f, 0.65f), + }; + desiredField.SetText(desired.ToString(CultureInfo.InvariantCulture)); + desiredField.OnFocusGained = () => _componentEditActive = true; + uint committed = desired; + void Commit(string value) + { + if (!uint.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out uint parsed) + || parsed > 5000u) + { + desiredField.SetText(committed.ToString(CultureInfo.InvariantCulture)); + return; + } + if (parsed == committed) return; + committed = parsed; + _spellbook.SetDesiredComponent(componentId, parsed); + _setDesiredComponent(componentId, parsed); + } + desiredField.OnSubmit = Commit; + desiredField.OnFocusLost = value => + { + Commit(value); + _componentEditActive = false; + }; + row.AddChild(desiredField); + row.Clicked = () => _selectObject(pack.ObjectId); + _componentList.AddItem(row); + } + } + } + + private static string ComponentCategoryName(uint category) => category switch + { + 0u => "Scarabs", + 1u => "Herbs", + 2u => "Powdered Gems", + 3u => "Alchemical Substances", + 4u => "Talismans", + 5u => "Tapers", + 6u => "Prismatic Components", + _ => "Other Components", + }; + + private bool IsOwnedByPlayer(ClientObject item, uint playerGuid) + { + if (playerGuid == 0) return item.ContainerId != 0 || item.WielderId != 0; + ClientObject current = item; + for (int depth = 0; depth < 16; depth++) + { + if (current.WielderId == playerGuid || current.ContainerId == playerGuid) return true; + if (current.ContainerId == 0 || _objects.Get(current.ContainerId) is not { } parent) return false; + current = parent; + } + return false; + } + + private bool IsVisible(SpellMetadata metadata, uint filters) + { + uint school = metadata.School switch + { + "Creature Enchantment" => 0x0001u, + "Item Enchantment" => 0x0002u, + "Life Magic" => 0x0004u, + "War Magic" => 0x0008u, + "Void Magic" => 0x2000u, + _ => 0u, + }; + int spellLevel = _spellLevel(metadata.SpellId); + if (spellLevel is < 1 or > 8) return false; + uint level = 0x10u << (spellLevel - 1); + return school != 0 && (filters & school) != 0 && (filters & level) != 0; + } + + private void SelectSpell(uint spellId) + { + _selectedSpell = spellId; + SyncSpellSelection(); + } + + private void SyncSpellSelection() + { + for (int i = 0; i < _spellList.GetNumUIItems(); i++) + if (_spellList.GetItem(i) is UiCatalogSlot slot) + slot.Selected = slot.EntryId == _selectedSpell; + } + + public void Tick() + { + if (_spellsDirty) + { + _spellsDirty = false; + RebuildSpells(); + } + if (_componentsDirty && !_componentEditActive) + { + _componentsDirty = false; + RebuildComponents(); + } + } + + private void OnSpellbookChanged() => _spellsDirty = true; + private void OnDesiredComponentsChanged() => _componentsDirty = true; + private void OnObjectChanged(ClientObject item) + { + if (IsComponent(item)) _componentsDirty = true; + } + private void OnObjectRemoved(ClientObject item) + { + if (IsComponent(item)) _componentsDirty = true; + } + private void OnObjectMoved(ClientObjectMove _) => _componentsDirty = true; + private void OnContainerContentsReplaced(uint _) => _componentsDirty = true; + private bool IsComponent(ClientObject item) + => item.IsComponentPack || _components.ContainsKey(item.WeenieClassId); + public void OnShown() => RebuildAll(); + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _spellbook.SpellbookChanged -= OnSpellbookChanged; + _spellbook.DesiredComponentsChanged -= OnDesiredComponentsChanged; + _objects.ObjectAdded -= OnObjectChanged; + _objects.ObjectUpdated -= OnObjectChanged; + _objects.ObjectRemoved -= OnObjectRemoved; + _objects.ObjectMoved -= OnObjectMoved; + _objects.ContainerContentsReplaced -= OnContainerContentsReplaced; + _spellTab.OnClick = null; + _componentTab.OnClick = null; + _closeButton.OnClick = null; + foreach ((UiButton button, _) in _filters) button.OnClick = null; + } +} diff --git a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs new file mode 100644 index 00000000..da09a01f --- /dev/null +++ b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AcDream.App.Spells; +using AcDream.Core.Combat; +using AcDream.Core.Items; +using AcDream.Core.Spells; +using AcDream.Core.Selection; +using AcDream.UI.Abstractions.Input; + +namespace AcDream.App.UI.Layout; + +/// +/// Retail gmSpellcastingUI binding for the authored magic page inside combat +/// LayoutDesc 0x21000073. Favorites remain server-persisted; casting emits one +/// request through . +/// +public sealed class SpellcastingUiController : IRetainedPanelController +{ + public const uint PageId = 0x10000061u; + public const uint SpellNameId = 0x1000048Bu; + public const uint EndowmentId = 0x100000B1u; + public const uint CastButtonId = 0x100000B2u; + public const uint FavoriteListId = 0x100000B6u; + + private static readonly uint[] TabIds = + [ + 0x100000A3u, 0x100000A4u, 0x100000A5u, 0x100000A6u, + 0x100000A7u, 0x100000A8u, 0x100000A9u, 0x100005C2u, + ]; + + private static readonly uint[] GroupIds = + [ + 0x100000AAu, 0x100000ABu, 0x100000ACu, 0x100000ADu, + 0x100000AEu, 0x100000AFu, 0x100000B0u, 0x100005C3u, + ]; + + private readonly Spellbook _spellbook; + private readonly SpellCastingController _casting; + private readonly SelectionState _selection; + private readonly ClientObjectTable _objects; + private readonly Func _playerGuid; + private readonly Func _resolveSpellIcon; + private readonly Func _resolveItemDragIcon; + private readonly Action _useItem; + private readonly Action? _addFavorite; + private readonly Action? _removeFavorite; + private readonly UiElement[] _tabs; + private readonly UiElement[] _groups; + private readonly UiItemList?[] _lists; + private readonly UiButton _cast; + private readonly UiText? _spellName; + private readonly UiElement _endowmentHost; + private readonly UiCatalogSlot _endowmentSlot; + private int _activeTab; + private readonly uint?[] _selected = new uint?[8]; + private readonly bool[] _endowmentSelected = new bool[8]; + private uint _endowmentItemId; + private uint _endowmentSpellId; + private bool _disposed; + private bool _favoritesDirty; + private bool _endowmentDirty; + + private SpellcastingUiController( + ImportedLayout layout, + Spellbook spellbook, + SpellCastingController casting, + ClientObjectTable objects, + Func playerGuid, + Func resolveSpellIcon, + Func resolveItemDragIcon, + Action useItem, + SelectionState selection, + Action? addFavorite, + Action? removeFavorite, + UiElement[] tabs, + UiElement[] groups, + UiItemList?[] lists, + UiButton cast, + UiElement endowmentHost) + { + _spellbook = spellbook; + _casting = casting; + _selection = selection; + _objects = objects; + _playerGuid = playerGuid; + _resolveSpellIcon = resolveSpellIcon; + _resolveItemDragIcon = resolveItemDragIcon; + _useItem = useItem; + _addFavorite = addFavorite; + _removeFavorite = removeFavorite; + _tabs = tabs; + _groups = groups; + _lists = lists; + _cast = cast; + _spellName = layout.FindElement(SpellNameId) as UiText; + _endowmentHost = endowmentHost; + foreach (UiElement child in _endowmentHost.Children) child.Visible = false; + _endowmentSlot = new UiCatalogSlot + { + Left = 0f, + Top = 0f, + Width = endowmentHost.Width, + Height = endowmentHost.Height, + SpriteResolve = lists.FirstOrDefault(list => list is not null)?.SpriteResolve, + }; + _endowmentSlot.Clicked = SelectEndowment; + _endowmentSlot.DoubleClicked = () => { SelectEndowment(); CastSelected(); }; + _endowmentHost.AddChild(_endowmentSlot); + + for (int i = 0; i < tabs.Length; i++) + { + int index = i; + SetClick(tabs[i], () => SelectTab(index)); + } + _cast.OnClick = CastSelected; + ConfigureSpellName(); + _spellbook.SpellbookChanged += OnSpellbookChanged; + _selection.Changed += OnSelectionChanged; + _objects.ObjectAdded += OnObjectChanged; + _objects.ObjectUpdated += OnObjectChanged; + _objects.ObjectRemoved += OnObjectChanged; + _objects.Cleared += OnObjectsCleared; + UpdateEndowment(); + SelectTab(0); + Rebuild(); + } + + public int ActiveTab => _activeTab; + + public static SpellcastingUiController? Bind( + ImportedLayout layout, + Spellbook spellbook, + SpellCastingController casting, + ClientObjectTable objects, + Func playerGuid, + Func resolveSpellIcon, + Func resolveItemDragIcon, + Action useItem, + SelectionState selection, + Action? addFavorite, + Action? removeFavorite) + { + if (layout.FindElement(CastButtonId) is not UiButton cast + || layout.FindElement(EndowmentId) is not { } endowmentHost) + return null; + + var tabs = new UiElement[8]; + var groups = new UiElement[8]; + var lists = new UiItemList?[8]; + for (int i = 0; i < 8; i++) + { + if (layout.FindElement(TabIds[i]) is not { } tab + || layout.FindElement(GroupIds[i]) is not { } group) + return null; + tabs[i] = tab; + groups[i] = group; + lists[i] = Descendants(group).OfType().FirstOrDefault(); + } + + return new SpellcastingUiController( + layout, spellbook, casting, objects, playerGuid, resolveSpellIcon, + resolveItemDragIcon, useItem, selection, + addFavorite, removeFavorite, + tabs, groups, lists, cast, endowmentHost); + } + + public void AddFavorite(uint spellId) + { + IReadOnlyList spells = _spellbook.GetFavorites(_activeTab); + if (spells.Contains(spellId)) + { + SelectSpell(spellId); + return; + } + int position = spells.Count; + _addFavorite?.Invoke(_activeTab, position, spellId); + _spellbook.SetFavorite(_activeTab, position, spellId); + SelectSpell(spellId); + } + + public bool Handle(InputAction action) + { + if (action is >= InputAction.UseSpellSlot_1 and <= InputAction.UseSpellSlot_9) + { + int index = (int)action - (int)InputAction.UseSpellSlot_1; + IReadOnlyList spells = _spellbook.GetFavorites(_activeTab); + if (index < spells.Count) + { + SelectSpell(spells[index]); + CastSelected(); + } + return true; + } + + switch (action) + { + case InputAction.CombatPrevSpellTab: SelectTab((_activeTab + 7) % 8); return true; + case InputAction.CombatNextSpellTab: SelectTab((_activeTab + 1) % 8); return true; + case InputAction.CombatFirstSpellTab: SelectTab(0); return true; + case InputAction.CombatLastSpellTab: SelectTab(7); return true; + case InputAction.CombatPrevSpell: MoveSelection(-1, false); return true; + case InputAction.CombatNextSpell: MoveSelection(1, false); return true; + case InputAction.CombatFirstSpell: MoveSelection(0, true); return true; + case InputAction.CombatLastSpell: MoveSelection(-1, true); return true; + case InputAction.CombatCastCurrentSpell: CastSelected(); return true; + default: return false; + } + } + + private void SelectTab(int tab) + { + _activeTab = Math.Clamp(tab, 0, 7); + for (int i = 0; i < 8; i++) + { + SetSelected(_tabs[i], i == _activeTab); + _groups[i].Visible = i == _activeTab; + } + IReadOnlyList favorites = _spellbook.GetFavorites(_activeTab); + if (_endowmentSelected[_activeTab] && _endowmentItemId != 0u) + _selected[_activeTab] = null; + else if (_selected[_activeTab] is not uint selected || !favorites.Contains(selected)) + { + _selected[_activeTab] = favorites.Count == 0 ? null : favorites[0]; + _endowmentSelected[_activeTab] = favorites.Count == 0 && _endowmentItemId != 0u; + } + SyncSelection(); + UpdateCastAvailability(); + } + + private void MoveSelection(int delta, bool edge) + { + IReadOnlyList spells = _spellbook.GetFavorites(_activeTab); + int count = spells.Count + (_endowmentItemId != 0u ? 1 : 0); + if (count == 0) return; + int current = _endowmentSelected[_activeTab] + ? 0 + : (_selected[_activeTab] is uint id ? spells.IndexOf(id) : -1) + + (_endowmentItemId != 0u ? 1 : 0); + int next = edge ? (delta < 0 ? count - 1 : 0) : (current + delta + count) % count; + if (_endowmentItemId != 0u && next == 0) SelectEndowment(); + else SelectSpell(spells[next - (_endowmentItemId != 0u ? 1 : 0)]); + } + + private void SelectSpell(uint spellId) + { + _endowmentSelected[_activeTab] = false; + _selected[_activeTab] = spellId; + SyncSelection(); + UpdateCastAvailability(); + } + + private void CastSelected() + { + if (_endowmentSelected[_activeTab] && _endowmentItemId != 0u) + _useItem(_endowmentItemId); + else if (_selected[_activeTab] is uint spellId) + _casting.Cast(spellId); + } + + private void SelectEndowment() + { + if (_endowmentItemId == 0u) return; + _endowmentSelected[_activeTab] = true; + _selected[_activeTab] = null; + SyncSelection(); + UpdateCastAvailability(); + } + + private void Rebuild() + { + for (int tab = 0; tab < 8; tab++) + { + UiItemList? list = _lists[tab]; + if (list is null) continue; + IReadOnlyList favorites = _spellbook.GetFavorites(tab); + using (list.DeferLayout()) + { + list.Flush(); + list.Columns = 9; + list.CellWidth = 32f; + list.CellHeight = 32f; + foreach (uint spellId in favorites) + { + uint id = spellId; + int position = list.GetNumUIItems(); + _spellbook.TryGetMetadata(id, out SpellMetadata? metadata); + var slot = new UiCatalogSlot + { + EntryId = id, + CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(id), + Label = metadata?.Name ?? $"Spell {id}", + SpriteResolve = list.SpriteResolve, + CatalogDragPayload = new SpellFavoriteDragPayload(tab, position, id), + DragBegan = payload => BeginFavoriteDrag((SpellFavoriteDragPayload)payload), + DragEnded = payload => EndFavoriteDrag((SpellFavoriteDragPayload)payload), + Dropped = payload => + { + if (payload is SpellFavoriteDragPayload favorite) + DropFavorite(favorite, tab, position); + }, + }; + slot.Clicked = () => SelectSpell(id); + slot.DoubleClicked = () => { SelectSpell(id); CastSelected(); }; + list.AddItem(slot); + } + } + } + SelectTab(_activeTab); + } + + private void BeginFavoriteDrag(SpellFavoriteDragPayload payload) + => _removeFavorite?.Invoke(payload.SourceTab, payload.SpellId); + + private void EndFavoriteDrag(SpellFavoriteDragPayload payload) + => _spellbook.RemoveFavorite(payload.SourceTab, payload.SpellId); + + private void DropFavorite(SpellFavoriteDragPayload payload, int targetTab, int targetPosition) + { + _addFavorite?.Invoke(targetTab, targetPosition, payload.SpellId); + _spellbook.SetFavorite(targetTab, targetPosition, payload.SpellId); + _selected[targetTab] = payload.SpellId; + } + + private void OnSpellbookChanged() => _favoritesDirty = true; + + private void OnObjectChanged(ClientObject _) => _endowmentDirty = true; + + private void OnObjectsCleared() => _endowmentDirty = true; + + public void Tick() + { + if (_endowmentDirty) + { + _endowmentDirty = false; + UpdateEndowment(); + SelectTab(_activeTab); + } + if (_favoritesDirty) + { + _favoritesDirty = false; + Rebuild(); + } + } + + private void SyncSelection() + { + for (int tab = 0; tab < 8; tab++) + { + UiItemList? list = _lists[tab]; + if (list is null) continue; + for (int i = 0; i < list.GetNumUIItems(); i++) + if (list.GetItem(i) is UiCatalogSlot slot) + slot.Selected = slot.EntryId == _selected[tab]; + _endowmentSlot.Selected = _endowmentItemId != 0u + && _endowmentSelected[_activeTab]; + } + } + + private void OnSelectionChanged(SelectionTransition _) => UpdateCastAvailability(); + + private void UpdateCastAvailability() + => _cast.Enabled = _endowmentSelected[_activeTab] + ? _endowmentItemId != 0u + : _selected[_activeTab] is uint spellId + && _casting.IsTargetReady(spellId); + + private void ConfigureSpellName() + { + if (_spellName is null) return; + _spellName.OneLine = true; + _spellName.Centered = true; + _spellName.Padding = 0; + _spellName.LinesProvider = () => + { + uint? chosenSpell = _endowmentSelected[_activeTab] + ? (_endowmentSpellId == 0u ? null : _endowmentSpellId) + : _selected[_activeTab]; + string name = chosenSpell is uint spellId + && _spellbook.TryGetMetadata(spellId, out SpellMetadata metadata) + ? metadata.Name : string.Empty; + return [new UiText.Line(name, _spellName.DefaultColor)]; + }; + } + + private void UpdateEndowment() + { + ClientObject? endowment = _objects.GetEquippedBy(_playerGuid()) + .FirstOrDefault(item => + (item.CurrentlyEquippedLocation & EquipMask.Held) != 0 + && (item.Type & ItemType.Caster) != 0 + && item.SpellId.GetValueOrDefault() != 0u); + + _endowmentItemId = endowment?.ObjectId ?? 0u; + _endowmentSpellId = endowment?.SpellId ?? 0u; + _endowmentHost.Visible = endowment is not null; + _endowmentSlot.EntryId = _endowmentItemId; + _endowmentSlot.CatalogIconTexture = _endowmentSpellId == 0u + ? 0u : _resolveSpellIcon(_endowmentSpellId); + _endowmentSlot.CatalogOverlayTexture = endowment is null + ? 0u : _resolveItemDragIcon(endowment); + string spellName = _spellbook.TryGetMetadata(_endowmentSpellId, out SpellMetadata metadata) + ? metadata.Name : $"Spell {_endowmentSpellId}"; + _endowmentSlot.Label = endowment is null + ? string.Empty : $"{endowment.GetAppropriateName()} ({spellName})"; + + for (int tab = 0; tab < 8; tab++) + { + if (_endowmentItemId != 0u && _selected[tab] is null) + _endowmentSelected[tab] = true; + else if (_endowmentItemId == 0u && _endowmentSelected[tab]) + _endowmentSelected[tab] = false; + } + } + + private static IEnumerable Descendants(UiElement root) + { + foreach (UiElement child in root.Children) + { + yield return child; + foreach (UiElement nested in Descendants(child)) yield return nested; + } + } + + public void OnShown() => SyncSelection(); + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _spellbook.SpellbookChanged -= OnSpellbookChanged; + _selection.Changed -= OnSelectionChanged; + _objects.ObjectAdded -= OnObjectChanged; + _objects.ObjectUpdated -= OnObjectChanged; + _objects.ObjectRemoved -= OnObjectChanged; + _objects.Cleared -= OnObjectsCleared; + foreach (UiElement tab in _tabs) SetClick(tab, null); + _cast.OnClick = null; + } + + private static void SetClick(UiElement element, Action? action) + { + element.ClickThrough = action is null; + switch (element) + { + case UiButton button: button.OnClick = action; break; + case UiText text: text.OnClick = action; break; + case UiDatElement dat: dat.OnClick = action; break; + } + } + + private static void SetSelected(UiElement element, bool selected) + { + if (element is UiButton button) + button.Selected = selected; + else if (element is UiText text) + text.DefaultColor = selected + ? new System.Numerics.Vector4(1f, 1f, 1f, 1f) + : new System.Numerics.Vector4(0.65f, 0.65f, 0.65f, 1f); + } +} + +public sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId); + +internal static class FavoriteListExtensions +{ + public static int IndexOf(this IReadOnlyList values, uint value) + { + for (int i = 0; i < values.Count; i++) if (values[i] == value) return i; + return -1; + } +} diff --git a/src/AcDream.App/UI/RetailPanelCatalog.cs b/src/AcDream.App/UI/RetailPanelCatalog.cs index 87292087..4df67e17 100644 --- a/src/AcDream.App/UI/RetailPanelCatalog.cs +++ b/src/AcDream.App/UI/RetailPanelCatalog.cs @@ -1,24 +1,36 @@ namespace AcDream.App.UI; /// -/// Retail toolbar panel ids carried by DAT property 0x10000029. -/// Retail reference: gmToolbarUI::PostInit @ 0x004BEA80 discovers the -/// seven buttons and reads this property from each one. -/// Only panels mounted in the retained runtime are mapped; other authored -/// toolbar buttons remain present but ghosted until their panel ships. +/// Retail gmPanelUI panel ids carried by DAT property 0x10000029. +/// The toolbar exposes only a subset of these ids; Helpful/Harmful effects use +/// the authored gmUIElement_EffectsIndicator buttons instead. /// public static class RetailPanelCatalog { + public const uint PositiveEffects = 4u; + public const uint NegativeEffects = 5u; public const uint Inventory = 7u; public const uint Character = 11u; + public const uint Magic = 13u; private static readonly (uint PanelId, string WindowName)[] Mounted = { + (PositiveEffects, WindowNames.PositiveEffects), + (NegativeEffects, WindowNames.NegativeEffects), (Inventory, WindowNames.Inventory), (Character, WindowNames.Character), + (Magic, WindowNames.Spellbook), + }; + + private static readonly (uint PanelId, string WindowName)[] Toolbar = + { + (Inventory, WindowNames.Inventory), + (Character, WindowNames.Character), + (Magic, WindowNames.Spellbook), }; public static IReadOnlyList<(uint PanelId, string WindowName)> MountedPanels => Mounted; + public static IReadOnlyList<(uint PanelId, string WindowName)> ToolbarPanels => Toolbar; public static bool TryGetWindowName(uint panelId, out string windowName) { diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index dee43b65..57cda9d6 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -3,12 +3,14 @@ using AcDream.App.Plugins; using AcDream.App.Combat; using AcDream.App.Input; using AcDream.App.Rendering; +using AcDream.App.Spells; using AcDream.App.UI.Layout; using AcDream.App.UI.Testing; using AcDream.Core.Combat; using AcDream.Core.Items; using AcDream.Core.Net.Messages; using AcDream.Core.Selection; +using AcDream.Core.Spells; using AcDream.UI.Abstractions; using AcDream.UI.Abstractions.Panels.Chat; using AcDream.UI.Abstractions.Panels.Settings; @@ -43,6 +45,26 @@ public sealed record CombatRuntimeBindings( Func Gameplay, Action SetGameplay); +public sealed record MagicRuntimeBindings( + Spellbook Spellbook, + SpellCastingController Casting, + ClientObjectTable Objects, + Func PlayerGuid, + IReadOnlyDictionary Components, + Func ResolveIcon, + Func ResolveDragIcon, + Func ResolveSpellIcon, + Func ResolveComponentIcon, + SelectionState Selection, + Func SpellLevel, + Action SelectObject, + Action UseItem, + Action AddFavorite, + Action RemoveFavorite, + Action SendSpellbookFilter, + Action SetDesiredComponent, + Func ServerTime); + public sealed record JumpPowerbarRuntimeBindings(Func Snapshot); public sealed record FpsRuntimeBindings( @@ -116,6 +138,7 @@ public sealed record RetailUiRuntimeBindings( ChatRuntimeBindings Chat, RadarRuntimeBindings Radar, CombatRuntimeBindings Combat, + MagicRuntimeBindings Magic, JumpPowerbarRuntimeBindings JumpPowerbar, FpsRuntimeBindings Fps, ToolbarRuntimeBindings Toolbar, @@ -139,6 +162,7 @@ public sealed class RetailUiRuntime : IDisposable private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity; private readonly RetailWindowLayoutPersistence? _persistence; private readonly RetailUiAutomationScriptRunner? _automation; + private readonly RetailPanelUiController _panelUi; private GameplayConfirmationController? _gameplayConfirmationController; private RetailItemConfirmationController? _itemConfirmationController; private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController; @@ -147,12 +171,19 @@ public sealed class RetailUiRuntime : IDisposable private RetailUiRuntime(RetailUiRuntimeBindings bindings) { _bindings = bindings; + _panelUi = new RetailPanelUiController( + bindings.Host.IsWindowVisible, + bindings.Host.ShowWindow, + bindings.Host.HideWindow); MountFpsDisplay(); MountVitals(); MountRadar(); MountChat(); MountToolbar(); MountCombat(); + MountSpellbook(); + MountEffects(); + MountIndicators(); MountJumpPowerbar(); MountDialogFactory(); MountCharacter(); @@ -192,6 +223,11 @@ public sealed class RetailUiRuntime : IDisposable public ToolbarController? ToolbarController { get; private set; } public ToolbarInputController? ToolbarInputController { get; private set; } public CombatUiController? CombatUiController { get; private set; } + public SpellcastingUiController? SpellcastingUiController { get; private set; } + public SpellbookWindowController? SpellbookWindowController { get; private set; } + public EffectsUiController? PositiveEffectsController { get; private set; } + public EffectsUiController? NegativeEffectsController { get; private set; } + public EffectsIndicatorController? EffectsIndicatorController { get; private set; } public JumpPowerbarController? JumpPowerbarController { get; private set; } public RetailFpsController? FpsController { get; private set; } public SelectedObjectController? SelectedObjectController { get; private set; } @@ -216,6 +252,10 @@ public sealed class RetailUiRuntime : IDisposable public void Tick(double deltaSeconds) { FpsController?.Tick(); + SpellbookWindowController?.Tick(); + SpellcastingUiController?.Tick(); + PositiveEffectsController?.Tick(); + NegativeEffectsController?.Tick(); JumpPowerbarController?.Tick(); SelectedObjectController?.Tick(deltaSeconds); DialogFactory?.Tick(); @@ -226,7 +266,33 @@ public sealed class RetailUiRuntime : IDisposable public void Draw(System.Numerics.Vector2 screenSize) => Host.Draw(screenSize); public bool HandleInputAction(AcDream.UI.Abstractions.Input.InputAction action) - => ToolbarInputController?.Handle(action) == true; + { + if (SpellcastingUiController?.Handle(action) == true) + return true; + if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleSpellbookPanel) + { + OpenSpellbook(SpellbookWindowPage.Spells); + return true; + } + if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleSpellComponentsPanel) + { + OpenSpellbook(SpellbookWindowPage.Components); + return true; + } + return ToolbarInputController?.Handle(action) == true; + } + + private void OpenSpellbook(SpellbookWindowPage page) + { + bool visible = Host.IsWindowVisible(WindowNames.Spellbook); + if (visible && SpellbookWindowController?.CurrentPage == page) + CloseWindow(WindowNames.Spellbook); + else + { + SpellbookWindowController?.ShowPage(page); + _panelUi.SetPanelVisibility(RetailPanelCatalog.Magic, visible: true); + } + } public bool HandleConfirmationRequest(GameEvents.CharacterConfirmationRequest request) => _gameplayConfirmationController?.HandleRequest(request) == true; @@ -258,15 +324,22 @@ public sealed class RetailUiRuntime : IDisposable public void RestoreNamedLayout(string profileName) => _persistence?.RestoreNamed(profileName); public bool ToggleWindow(string name) - => Host.ToggleWindow(name); + => RetailPanelCatalog.TryGetPanelId(name, out uint panelId) + ? _panelUi.TogglePanel(panelId) + : Host.ToggleWindow(name); public void CloseWindow(string name) - => Host.HideWindow(name); + { + if (RetailPanelCatalog.TryGetPanelId(name, out uint panelId)) + _panelUi.SetPanelVisibility(panelId, visible: false); + else + Host.HideWindow(name); + } public void SyncToolbarWindowButtons() { if (ToolbarController is null) return; - foreach (var (panelId, windowName) in RetailPanelCatalog.MountedPanels) + foreach (var (panelId, windowName) in RetailPanelCatalog.ToolbarPanels) ToolbarController.SetPanelOpen(panelId, Host.IsWindowVisible(windowName)); } @@ -284,6 +357,7 @@ public sealed class RetailUiRuntime : IDisposable private void OnWindowVisibilityChanged(string windowName, bool visible) { + _panelUi.ObserveWindowVisibility(windowName, visible); if (RetailPanelCatalog.TryGetPanelId(windowName, out uint panelId)) ToolbarController?.SetPanelOpen(panelId, visible); } @@ -583,7 +657,28 @@ public sealed class RetailUiRuntime : IDisposable return; } + SpellcastingUiController? spellcasting = Layout.SpellcastingUiController.Bind( + layout, + _bindings.Magic.Spellbook, + _bindings.Magic.Casting, + _bindings.Magic.Objects, + _bindings.Magic.PlayerGuid, + _bindings.Magic.ResolveSpellIcon, + item => _bindings.Magic.ResolveDragIcon( + item.Type, item.IconId, item.IconUnderlayId, + item.IconOverlayId, item.Effects), + _bindings.Magic.UseItem, + _bindings.Magic.Selection, + _bindings.Magic.AddFavorite, + _bindings.Magic.RemoveFavorite); + if (spellcasting is null) + Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073."); + CombatUiController = controller; + SpellcastingUiController = spellcasting; + IRetainedPanelController controllerOwner = spellcasting is null + ? controller + : new RetainedPanelControllerGroup(controller, spellcasting); UiElement root = layout.Root; RetailWindowFrame.Mount( Host.Root, @@ -601,10 +696,190 @@ public sealed class RetailUiRuntime : IDisposable ResizeY = false, ConstrainDragToParent = true, ContentClickThrough = false, - Controller = controller, + Controller = controllerOwner, }); controller.SyncVisibility(); - Console.WriteLine("[M2] retail combat bar from gmCombatUI LayoutDesc 0x21000073."); + Console.WriteLine(spellcasting is null + ? "[M2] retail combat from LayoutDesc 0x21000073; magic binding unavailable." + : "[M3] retail combat + spell bar from LayoutDesc 0x21000073."); + } + + private void MountSpellbook() + { + ImportedLayout? layout; + lock (_bindings.Assets.DatLock) + { + layout = LayoutImporter.Import( + _bindings.Assets.Dats, + SpellbookWindowController.LayoutId, + SpellbookWindowController.RootId, + _bindings.Assets.ResolveSprite, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont); + } + if (layout is null) + { + Console.WriteLine("[M3] spellbook: LayoutDesc 0x21000034 not found."); + return; + } + + SpellbookWindowController? controller = Layout.SpellbookWindowController.Bind( + layout, + _bindings.Magic.Spellbook, + _bindings.Magic.Objects, + _bindings.Magic.PlayerGuid, + _bindings.Magic.Components, + _bindings.Magic.ResolveSpellIcon, + _bindings.Magic.ResolveComponentIcon, + _bindings.Magic.SpellLevel, + _bindings.Magic.SelectObject, + spellId => SpellcastingUiController?.AddFavorite(spellId), + _bindings.Magic.SendSpellbookFilter, + _bindings.Magic.SetDesiredComponent, + () => CloseWindow(WindowNames.Spellbook)); + if (controller is null) + { + Console.WriteLine("[M3] spellbook: required controls missing in LayoutDesc 0x21000034."); + return; + } + + SpellbookWindowController = controller; + UiElement root = layout.Root; + RetailWindowFrame.Mount( + Host.Root, + root, + _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Spellbook, + Chrome = RetailWindowChrome.Imported, + Left = 18f, + Top = 18f, + Visible = false, + Resizable = false, + ResizeX = false, + ResizeY = false, + ConstrainDragToParent = true, + ContentClickThrough = false, + Controller = controller, + }); + _panelUi.Register(RetailPanelCatalog.Magic, WindowNames.Spellbook); + Console.WriteLine("[M3] retail spellbook/component book from LayoutDesc 0x21000034."); + } + + private void MountEffects() + { + MountEffectsInstance(positive: true); + MountEffectsInstance(positive: false); + } + + private void MountEffectsInstance(bool positive) + { + uint rootId = positive ? EffectsUiController.PositiveRootId : EffectsUiController.NegativeRootId; + ElementInfo? rootInfo; + ImportedLayout? layout; + lock (_bindings.Assets.DatLock) + { + rootInfo = LayoutImporter.ImportInfos( + _bindings.Assets.Dats, + EffectsUiController.LayoutId, + 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) + { + Console.WriteLine($"[M3] effects: root 0x{rootId:X8} not found."); + return; + } + + EffectsUiController? controller = Layout.EffectsUiController.Bind( + layout, + _bindings.Magic.Spellbook, + positive, + _bindings.Magic.ServerTime, + _bindings.Assets.ResolveSprite, + _bindings.Magic.ResolveSpellIcon, + close: () => CloseWindow( + positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects)); + if (controller is null) + { + Console.WriteLine($"[M3] effects: list missing under root 0x{rootId:X8}."); + return; + } + + if (positive) PositiveEffectsController = controller; + else NegativeEffectsController = controller; + UiElement root = layout.Root; + RetailWindowFrame.Mount( + Host.Root, + root, + _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects, + Chrome = RetailWindowChrome.Imported, + Left = Math.Max(0f, Host.Root.Width - root.Width - 12f), + Top = 18f, + Visible = false, + Resizable = false, + ResizeX = false, + ResizeY = false, + ConstrainDragToParent = true, + ContentClickThrough = false, + Controller = controller, + }); + _panelUi.Register( + positive ? RetailPanelCatalog.PositiveEffects : RetailPanelCatalog.NegativeEffects, + positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects, + rootInfo); + } + + private void MountIndicators() + { + ImportedLayout? layout = Import(EffectsIndicatorController.LayoutId); + if (layout is null) + { + Console.WriteLine("[M3] effects indicators: LayoutDesc 0x21000071 not found."); + return; + } + + EffectsIndicatorController? controller = Layout.EffectsIndicatorController.Bind( + layout, + _bindings.Magic.Spellbook, + panelId => _panelUi.TogglePanel(panelId)); + if (controller is null) + { + Console.WriteLine("[M3] effects indicators: authored Helpful/Harmful buttons missing."); + return; + } + + EffectsIndicatorController = controller; + RetailWindowFrame.Mount( + Host.Root, + layout.Root, + _bindings.Assets.ResolveSprite, + new RetailWindowFrame.Options + { + WindowName = WindowNames.Indicators, + Chrome = RetailWindowChrome.Imported, + Left = 10f, + Top = 96f, + Visible = true, + Resizable = false, + ResizeX = false, + ResizeY = false, + ConstrainDragToParent = true, + ContentClickThrough = false, + Controller = controller, + }); + Console.WriteLine("[M3] retail Helpful/Harmful effect indicators from LayoutDesc 0x21000071."); } private void MountJumpPowerbar() @@ -825,6 +1100,7 @@ public sealed class RetailUiRuntime : IDisposable ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, ContentClickThrough = false, }); + _panelUi.Register(RetailPanelCatalog.Character, WindowNames.Character); Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E)."); } @@ -906,6 +1182,7 @@ public sealed class RetailUiRuntime : IDisposable Host.Root.Height - root.Top), ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom, }); + _panelUi.Register(RetailPanelCatalog.Inventory, WindowNames.Inventory); uint contents, sideBag, mainPack; IReadOnlyDictionary paperdollEmptySprites; diff --git a/src/AcDream.App/UI/UiCatalogSlot.cs b/src/AcDream.App/UI/UiCatalogSlot.cs new file mode 100644 index 00000000..5a704adb --- /dev/null +++ b/src/AcDream.App/UI/UiCatalogSlot.cs @@ -0,0 +1,97 @@ +using System; +using System.Numerics; + +namespace AcDream.App.UI; + +/// +/// Icon-list cell for non-weenie catalog entries such as spells and components. +/// It reuses the retail UIItemList geometry without pretending a spell id is an +/// object GUID and without participating in item drag/drop. +/// +public sealed class UiCatalogSlot : UiItemSlot +{ + public uint EntryId { get; set; } + public uint CatalogIconTexture { get; set; } + /// Optional second 32x32 layer (retail spell endowment item icon). + public uint CatalogOverlayTexture { get; set; } + public string Label { get; set; } = string.Empty; + public string? Detail { get; set; } + public bool ShowLabel { get; init; } + + public new Action? Clicked { get; set; } + public new Action? DoubleClicked { get; set; } + public object? CatalogDragPayload { get; init; } + public Action? DragBegan { get; init; } + public Action? DragEnded { get; init; } + public Action? Dropped { get; init; } + + public override string? GetTooltipText() => string.IsNullOrWhiteSpace(Label) ? null : Label; + + public override bool IsDragSource => CatalogDragPayload is not null; + public override object? GetDragPayload() => CatalogDragPayload; + public override (uint tex, int w, int h)? GetDragGhost() => + CatalogDragPayload is not null && CatalogIconTexture != 0u + ? (CatalogIconTexture, 32, 32) + : null; + + internal override void SetDragSourceActive(bool active, object? payload) + { + if (!active && payload is not null) DragEnded?.Invoke(payload); + } + + public override bool OnEvent(in UiEvent e) + { + switch (e.Type) + { + case UiEventType.MouseDown: + return true; + case UiEventType.Click: + Clicked?.Invoke(); + return true; + case UiEventType.DoubleClick: + DoubleClicked?.Invoke(); + return true; + case UiEventType.DragBegin: + if (e.Payload is not null) DragBegan?.Invoke(e.Payload); + return true; + case UiEventType.DragEnter: + case UiEventType.DragOver: + return true; + case UiEventType.DropReleased: + if (e.Payload is not null) Dropped?.Invoke(e.Payload); + return true; + default: + return false; + } + } + + protected override void OnDraw(UiRenderContext ctx) + { + float iconSize = ShowLabel ? MathF.Min(32f, Height) : Width; + if (CatalogIconTexture != 0) + ctx.DrawSprite(CatalogIconTexture, 0f, 0f, iconSize, iconSize, 0f, 0f, 1f, 1f, Vector4.One); + else if (SpriteResolve is not null && EmptySprite != 0) + { + var (texture, _, _) = SpriteResolve(EmptySprite); + if (texture != 0) + ctx.DrawSprite(texture, 0f, 0f, iconSize, iconSize, 0f, 0f, 1f, 1f, Vector4.One); + } + + if (CatalogOverlayTexture != 0) + ctx.DrawSprite(CatalogOverlayTexture, 0f, 0f, iconSize, iconSize, 0f, 0f, 1f, 1f, Vector4.One); + + if (Selected && SpriteResolve is not null && SelectedSprite != 0) + { + var (texture, _, _) = SpriteResolve(SelectedSprite); + if (texture != 0) + ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); + } + + if (ShowLabel) + { + ctx.DrawString(Label, iconSize + 4f, 3f, new Vector4(0.92f, 0.88f, 0.70f, 1f)); + if (!string.IsNullOrWhiteSpace(Detail)) + ctx.DrawString(Detail!, MathF.Max(iconSize + 4f, Width - 48f), 3f, Vector4.One); + } + } +} diff --git a/src/AcDream.App/UI/UiField.cs b/src/AcDream.App/UI/UiField.cs index 0bb021eb..c1ce9f75 100644 --- a/src/AcDream.App/UI/UiField.cs +++ b/src/AcDream.App/UI/UiField.cs @@ -61,6 +61,7 @@ public sealed class UiField : UiElement private int _caret; private int? _selAnchor; // selection fixed end (null = no selection); span = [min,max] with _caret public string Text => _text; + public bool IsFocused => _focused; public int CaretPos => _caret; private readonly List _history = new(); diff --git a/src/AcDream.App/UI/UiItemSlot.cs b/src/AcDream.App/UI/UiItemSlot.cs index 340eb372..9126cad9 100644 --- a/src/AcDream.App/UI/UiItemSlot.cs +++ b/src/AcDream.App/UI/UiItemSlot.cs @@ -10,7 +10,7 @@ namespace AcDream.App.UI; /// pre-composited icon texture (set by the controller). Holds the bound weenie /// guid (retail UIElement_UIItem::itemID, +0x5FC). /// -public sealed class UiItemSlot : UiElement +public class UiItemSlot : UiElement { public UiItemSlot() { ClickThrough = false; } diff --git a/src/AcDream.App/UI/UiText.cs b/src/AcDream.App/UI/UiText.cs index b030d0da..74fe548e 100644 --- a/src/AcDream.App/UI/UiText.cs +++ b/src/AcDream.App/UI/UiText.cs @@ -22,6 +22,9 @@ namespace AcDream.App.UI; /// public sealed class UiText : UiElement { + /// Optional base-element click notice used by authored text tabs. + public Action? OnClick { get; set; } + public override bool HandlesClick => OnClick is not null || base.HandlesClick; /// Dat element id for imported UIElement_Text widgets. 0 for synthesized text. public uint ElementId { get; set; } @@ -360,6 +363,11 @@ public sealed class UiText : UiElement public override bool OnEvent(in UiEvent e) { + if (e.Type == UiEventType.Click && OnClick is not null) + { + OnClick(); + return true; + } switch (e.Type) { case UiEventType.Scroll: diff --git a/src/AcDream.App/UI/WindowNames.cs b/src/AcDream.App/UI/WindowNames.cs index 28dd52bb..792cc4ad 100644 --- a/src/AcDream.App/UI/WindowNames.cs +++ b/src/AcDream.App/UI/WindowNames.cs @@ -12,4 +12,8 @@ public static class WindowNames public const string Radar = "radar"; public const string Combat = "combat"; public const string JumpPowerbar = "jump-powerbar"; + public const string Spellbook = "spellbook"; + public const string Indicators = "indicators"; + public const string PositiveEffects = "effects-positive"; + public const string NegativeEffects = "effects-negative"; } diff --git a/src/AcDream.Core.Net/GameEventWiring.cs b/src/AcDream.Core.Net/GameEventWiring.cs index 8b93233d..dbc2e59a 100644 --- a/src/AcDream.Core.Net/GameEventWiring.cs +++ b/src/AcDream.Core.Net/GameEventWiring.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using AcDream.Core.Chat; using AcDream.Core.Combat; using AcDream.Core.Items; @@ -77,13 +78,15 @@ public static class GameEventWiring FriendsState? friends = null, SquelchState? squelch = null, Action>? onDesiredComponents = null, - Action? onCharacterOptions = null) + Action? onCharacterOptions = null, + Func? clientTime = null) { ArgumentNullException.ThrowIfNull(dispatcher); ArgumentNullException.ThrowIfNull(items); ArgumentNullException.ThrowIfNull(combat); ArgumentNullException.ThrowIfNull(spellbook); ArgumentNullException.ThrowIfNull(chat); + clientTime ??= static () => 0d; // ── Chat ────────────────────────────────────────────────── dispatcher.Register(GameEventType.ChannelBroadcast, e => @@ -283,21 +286,44 @@ public static class GameEventWiring dispatcher.Register(GameEventType.MagicUpdateEnchantment, e => { var p = GameEvents.ParseMagicUpdateEnchantment(e.Payload.Span); - if (p is not null) spellbook.OnEnchantmentAdded( - p.Value.SpellId, p.Value.LayerId, p.Value.Duration, p.Value.CasterGuid); + if (p is not null) spellbook.OnEnchantmentAdded(ToActiveEnchantment(p.Value, clientTime())); + }); + dispatcher.Register(GameEventType.MagicUpdateMultipleEnchantments, e => + { + var entries = GameEvents.ParseMagicUpdateMultipleEnchantments(e.Payload.Span); + if (entries is not null) + { + double receivedAt = clientTime(); + spellbook.OnEnchantmentsAdded(entries.Select(entry => + ToActiveEnchantment(entry, receivedAt))); + } }); dispatcher.Register(GameEventType.MagicRemoveEnchantment, e => { var p = GameEvents.ParseMagicRemoveEnchantment(e.Payload.Span); - if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.LayerId, p.Value.SpellId); + if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.Layer, p.Value.SpellId); + }); + dispatcher.Register(GameEventType.MagicRemoveMultipleEnchantments, e => + { + var entries = GameEvents.ParseMagicLayeredSpellList(e.Payload.Span); + if (entries is not null) + spellbook.OnEnchantmentsRemoved(entries.Select(item => ((uint)item.SpellId, (uint)item.Layer))); }); dispatcher.Register(GameEventType.MagicDispelEnchantment, e => { var p = GameEvents.ParseMagicDispelEnchantment(e.Payload.Span); - if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.LayerId, p.Value.SpellId); + if (p is not null) spellbook.OnEnchantmentRemoved(p.Value.Layer, p.Value.SpellId); + }); + dispatcher.Register(GameEventType.MagicDispelMultipleEnchantments, e => + { + var entries = GameEvents.ParseMagicLayeredSpellList(e.Payload.Span); + if (entries is not null) + spellbook.OnEnchantmentsRemoved(entries.Select(item => ((uint)item.SpellId, (uint)item.Layer))); }); dispatcher.Register(GameEventType.MagicPurgeEnchantments, _ => spellbook.OnPurgeAll()); + dispatcher.Register(GameEventType.MagicPurgeBadEnchantments, + _ => spellbook.OnPurgeBadEnchantments()); // ── Inventory ───────────────────────────────────────────── dispatcher.Register(GameEventType.WieldObject, e => @@ -407,8 +433,8 @@ public static class GameEventWiring // spellbook arrives via PlayerDescription (0x0013), which uses // a different wire format (see WorldSession + LocalPlayerState // — feeds vitals from PrivateUpdateVital instead). - foreach (uint sid in p.Value.SpellBook) - spellbook.OnSpellLearned(sid); + // The appraised spellbook belongs to that item. The local player's + // learned spell manifest arrives only in PlayerDescription. }); // ── Player ──────────────────────────────────────────────── @@ -441,6 +467,17 @@ public static class GameEventWiring onCharacterOptions?.Invoke(p.Value.Options1, p.Value.Options2); onDesiredComponents?.Invoke(p.Value.DesiredComps); + double receivedAt = clientTime(); + ActiveEnchantmentRecord[] enchantments = p.Value.Enchantments + .Select(entry => ToActiveEnchantment(entry, receivedAt)) + .ToArray(); + spellbook.ReplaceManifest( + p.Value.Spells, + enchantments, + p.Value.HotbarSpells, + p.Value.DesiredComps, + p.Value.SpellbookFilters); + // B-Wire: deliver the player's OWN properties to the player ClientObject. // (PD's "membership manifest" rule is about ITEMS, whose data comes from // CreateObject; the player's own stats legitimately come from PD.) Upsert @@ -508,9 +545,6 @@ public static class GameEventWiring } } - foreach (uint sid in p.Value.Spells.Keys) - spellbook.OnSpellLearned(sid); - // K-fix7 (2026-04-26): push Run + Jump skill values to the // PlayerMovementController so the runRate / jump-arc formulas // use the SERVER's authoritative skill instead of our @@ -566,21 +600,6 @@ public static class GameEventWiring // Issue #7 — enchantment block: feed each entry into the // Spellbook with full StatMod data so EnchantmentMath can // aggregate buffs in vital-max calc (issue #6 lights up). - foreach (var ench in p.Value.Enchantments) - { - spellbook.OnEnchantmentAdded(new AcDream.Core.Spells.ActiveEnchantmentRecord( - SpellId: ench.SpellId, - LayerId: ench.Layer, - Duration: (float)ench.Duration, - CasterGuid: ench.CasterGuid, - StatModType: ench.StatModType, - StatModKey: ench.StatModKey, - StatModValue: ench.StatModValue, - Bucket: (uint)ench.Bucket)); - if (dumpPd) - Console.WriteLine($"vitals: PD-ench spell={ench.SpellId} layer={ench.Layer} bucket={ench.Bucket} key={ench.StatModKey} val={ench.StatModValue}"); - } - // D.5.4: PlayerDescription is a membership MANIFEST, not the data // source. Record existence (+ equip slot); CreateObject fills the // actual weenie data via ObjectTableWiring. (Previously this seeded @@ -630,4 +649,46 @@ public static class GameEventWiring onShortcuts?.Invoke(p.Value.Shortcuts); }); } + + private static ActiveEnchantmentRecord ToActiveEnchantment( + PlayerDescriptionParser.EnchantmentEntry enchantment, + double receivedAt) => new( + SpellId: enchantment.SpellId, + LayerId: enchantment.Layer, + Duration: enchantment.Duration, + CasterGuid: enchantment.CasterGuid, + StatModType: enchantment.StatModType, + StatModKey: enchantment.StatModKey, + StatModValue: enchantment.StatModValue, + Bucket: enchantment.Bucket == 0 + ? ClassifyLiveEnchantmentBucket(enchantment.StatModType) + : (uint)enchantment.Bucket, + // Retail Enchantment::UnPack (0x005CB040) converts both relative + // wire timestamps to the monotonic client Timer domain at receipt. + StartTime: receivedAt + enchantment.StartTime, + SpellCategory: enchantment.SpellCategory, + PowerLevel: enchantment.PowerLevel, + DegradeModifier: enchantment.DegradeModifier, + DegradeLimit: enchantment.DegradeLimit, + LastTimeDegraded: receivedAt + enchantment.LastTimeDegraded, + SpellSetId: enchantment.SpellSetId); + + /// + /// Live 0x02C2/0x02C4 records do not carry PlayerDescription's outer + /// EnchantmentMask bucket. Retail reconstructs the registry list from the + /// StatMod type flags; this is the same ordering used by ACE's + /// EnchantmentRegistry.BuildCategories. + /// + private static uint ClassifyLiveEnchantmentBucket(uint statModType) + { + const uint Multiplicative = 0x00004000u; + const uint Additive = 0x00008000u; + const uint Vitae = 0x00800000u; + const uint Cooldown = 0x01000000u; + if ((statModType & Vitae) != 0) return 4u; + if ((statModType & Cooldown) != 0) return 8u; + if ((statModType & Multiplicative) != 0) return 1u; + if ((statModType & Additive) != 0) return 2u; + return 0u; + } } diff --git a/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs b/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs index 84ba9aee..9f3dda9d 100644 --- a/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs +++ b/src/AcDream.Core.Net/Messages/ClientCommandRequests.cs @@ -33,6 +33,9 @@ public static class ClientCommandRequests public const uint DisplayConsentOpcode = 0x0217u; public const uint RemoveConsentOpcode = 0x0218u; public const uint SetDesiredComponentLevelOpcode = 0x0224u; + public const uint AddSpellFavoriteOpcode = 0x01E3u; + public const uint RemoveSpellFavoriteOpcode = 0x01E4u; + public const uint SpellbookFilterOpcode = 0x0286u; public const uint LegacyFriendsOpcode = 0xF7CDu; // Named-retail anchors: @@ -177,6 +180,31 @@ public static class ClientCommandRequests return body; } + // CM_Character::Event_AddSpellFavorite @ 0x006A0F70. + public static byte[] BuildAddSpellFavorite( + uint sequence, uint spellId, int position, int tabIndex) + { + byte[] body = CreateBody(sequence, AddSpellFavoriteOpcode, 12); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), spellId); + BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(16), position); + BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(20), tabIndex); + return body; + } + + // CM_Character::Event_RemoveSpellFavorite @ 0x006A1890. + public static byte[] BuildRemoveSpellFavorite( + uint sequence, uint spellId, int tabIndex) + { + byte[] body = CreateBody(sequence, RemoveSpellFavoriteOpcode, 8); + BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(12), spellId); + BinaryPrimitives.WriteInt32LittleEndian(body.AsSpan(16), tabIndex); + return body; + } + + // CM_Character::Event_SpellbookFilterEvent @ 0x006A1A30. + public static byte[] BuildSpellbookFilter(uint sequence, uint filters) => + BuildUInt32(sequence, SpellbookFilterOpcode, filters); + private static byte[] BuildParameterless(uint sequence, uint opcode) { byte[] body = new byte[12]; diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index 83041165..530f03ac 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -207,6 +207,9 @@ public static class CreateObject // PublicWeenieDesc._ammoType, gated by WeenieHeader flag 0x100. // AMMO_NONE is the explicit wire value zero; null means absent. ushort? AmmoType = null, + // PublicWeenieDesc._spellID, gated by PWD_Packed_SpellID. The packed + // wire field is u16 and widens into retail's u32 runtime member. + uint? SpellId = null, // Complete immutable PhysicsDesc projection. Kept alongside the // legacy convenience fields while live-entity ownership migrates to // LiveEntityRuntime in Step 2. @@ -862,6 +865,7 @@ public static class CreateObject byte? radarBehavior = null; byte? combatUse = null; ushort? ammoType = null; + uint? spellId = null; uint iconOverlayId = 0; uint iconUnderlayId = 0; uint uiEffects = 0; @@ -1011,6 +1015,7 @@ public static class CreateObject if ((weenieFlags & 0x00400000u) != 0) // Spell u16 { if (body.Length - pos < 2) throw new FormatException("trunc Spell"); + spellId = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(pos)); pos += 2; } if ((weenieFlags & 0x02000000u) != 0) // HouseOwner u32 @@ -1113,6 +1118,7 @@ public static class CreateObject PluralName: pluralName, PetOwnerId: petOwnerId, AmmoType: ammoType, + SpellId: spellId, Physics: physics); } catch diff --git a/src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs b/src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs new file mode 100644 index 00000000..3448ba82 --- /dev/null +++ b/src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs @@ -0,0 +1,115 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; + +namespace AcDream.Core.Net.Messages; + +/// +/// Retail Enchantment::UnPack reader shared by PlayerDescription and +/// the 0x02C2/0x02C4 magic update events. The record is 60 bytes, plus the +/// optional four-byte spell-set id. +/// +/// +/// Retail reference: CM_Magic::DispatchUI_UpdateEnchantment +/// (0x006A32F0). Layout cross-checked against ACE +/// Network/Structure/Enchantment.cs. +/// +public static class EnchantmentWireReader +{ + public static PlayerDescriptionParser.EnchantmentEntry Read( + ReadOnlySpan source, + ref int position, + PlayerDescriptionParser.EnchantmentBucket bucket = 0) + { + if (source.Length - position < 60) + throw new FormatException("truncated enchantment record"); + + ushort spellId = ReadU16(source, ref position); + ushort layer = ReadU16(source, ref position); + ushort spellCategory = ReadU16(source, ref position); + ushort hasSpellSetId = ReadU16(source, ref position); + uint powerLevel = ReadU32(source, ref position); + double startTime = ReadF64(source, ref position); + double duration = ReadF64(source, ref position); + uint casterGuid = ReadU32(source, ref position); + float degradeModifier = ReadF32(source, ref position); + float degradeLimit = ReadF32(source, ref position); + double lastDegraded = ReadF64(source, ref position); + uint statModType = ReadU32(source, ref position); + uint statModKey = ReadU32(source, ref position); + float statModValue = ReadF32(source, ref position); + uint? spellSetId = hasSpellSetId != 0 ? ReadU32(source, ref position) : null; + + if (!double.IsFinite(startTime) + || !double.IsFinite(duration) + || !float.IsFinite(degradeModifier) + || !float.IsFinite(degradeLimit) + || !double.IsFinite(lastDegraded) + || !float.IsFinite(statModValue)) + { + throw new FormatException("non-finite enchantment value"); + } + + return new PlayerDescriptionParser.EnchantmentEntry( + spellId, layer, spellCategory, hasSpellSetId, powerLevel, + startTime, duration, casterGuid, degradeModifier, degradeLimit, + lastDegraded, statModType, statModKey, statModValue, spellSetId, + bucket); + } + + public static IReadOnlyList ReadList( + ReadOnlySpan source, + ref int position, + PlayerDescriptionParser.EnchantmentBucket bucket = 0) + { + if (source.Length - position < 4) + throw new FormatException("truncated enchantment list count"); + + uint count = ReadU32(source, ref position); + if (count > 0x4000) + throw new FormatException("unreasonable enchantment list count"); + + var result = new List((int)count); + for (uint i = 0; i < count; i++) + result.Add(Read(source, ref position, bucket)); + return result; + } + + private static ushort ReadU16(ReadOnlySpan source, ref int position) + { + Require(source, position, 2); + ushort value = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(position, 2)); + position += 2; + return value; + } + + private static uint ReadU32(ReadOnlySpan source, ref int position) + { + Require(source, position, 4); + uint value = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(position, 4)); + position += 4; + return value; + } + + private static float ReadF32(ReadOnlySpan source, ref int position) + { + Require(source, position, 4); + float value = BinaryPrimitives.ReadSingleLittleEndian(source.Slice(position, 4)); + position += 4; + return value; + } + + private static double ReadF64(ReadOnlySpan source, ref int position) + { + Require(source, position, 8); + double value = BinaryPrimitives.ReadDoubleLittleEndian(source.Slice(position, 8)); + position += 8; + return value; + } + + private static void Require(ReadOnlySpan source, int position, int count) + { + if (position < 0 || source.Length - position < count) + throw new FormatException("truncated enchantment record"); + } +} diff --git a/src/AcDream.Core.Net/Messages/GameEvents.cs b/src/AcDream.Core.Net/Messages/GameEvents.cs index ab831cbe..223a5d22 100644 --- a/src/AcDream.Core.Net/Messages/GameEvents.cs +++ b/src/AcDream.Core.Net/Messages/GameEvents.cs @@ -1,5 +1,6 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.Text; namespace AcDream.Core.Net.Messages; @@ -271,14 +272,19 @@ public static class GameEvents /// /// 0x02C3 MagicRemoveEnchantment — (layerId, spellId). /// - public readonly record struct MagicRemoveEnchantment(uint LayerId, uint SpellId); + public readonly record struct LayeredSpellId(ushort SpellId, ushort Layer) + { + public uint Packed => SpellId | ((uint)Layer << 16); + } + + public readonly record struct MagicRemoveEnchantment(ushort SpellId, ushort Layer); public static MagicRemoveEnchantment? ParseMagicRemoveEnchantment(ReadOnlySpan payload) { - if (payload.Length < 8) return null; + if (payload.Length < 4) return null; return new MagicRemoveEnchantment( - BinaryPrimitives.ReadUInt32LittleEndian(payload), - BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4))); + BinaryPrimitives.ReadUInt16LittleEndian(payload), + BinaryPrimitives.ReadUInt16LittleEndian(payload.Slice(2))); } /// 0x01A8 MagicRemoveSpell — spell id removed from spellbook. @@ -294,26 +300,20 @@ public static class GameEvents /// stat mods. We expose the first few fields that drive the enchant /// bar UI; the rest is available via the raw payload view. /// - public readonly record struct EnchantmentSummary( - uint SpellId, - uint LayerId, - float Duration, - uint CasterGuid); - - public static EnchantmentSummary? ParseMagicUpdateEnchantment(ReadOnlySpan payload) + public static PlayerDescriptionParser.EnchantmentEntry? ParseMagicUpdateEnchantment( + ReadOnlySpan payload) { - // Layout (ACE Enchantment.Pack): - // u32 spellId - // u32 layerId - // f32 duration - // u32 casterGuid - // ... (stat mods, category, power, etc.) - if (payload.Length < 16) return null; - return new EnchantmentSummary( - BinaryPrimitives.ReadUInt32LittleEndian(payload), - BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(4)), - BinaryPrimitives.ReadSingleLittleEndian(payload.Slice(8)), - BinaryPrimitives.ReadUInt32LittleEndian(payload.Slice(12))); + int position = 0; + try { return EnchantmentWireReader.Read(payload, ref position); } + catch (FormatException) { return null; } + } + + public static IReadOnlyList? + ParseMagicUpdateMultipleEnchantments(ReadOnlySpan payload) + { + int position = 0; + try { return EnchantmentWireReader.ReadList(payload, ref position); } + catch (FormatException) { return null; } } /// @@ -323,6 +323,23 @@ public static class GameEvents public static MagicRemoveEnchantment? ParseMagicDispelEnchantment(ReadOnlySpan payload) => ParseMagicRemoveEnchantment(payload); + public static IReadOnlyList? ParseMagicLayeredSpellList( + ReadOnlySpan payload) + { + if (payload.Length < 4) return null; + uint count = BinaryPrimitives.ReadUInt32LittleEndian(payload); + if (count > 0x4000 || payload.Length - 4 < checked((int)count * 4)) return null; + var result = new LayeredSpellId[count]; + for (int i = 0; i < result.Length; i++) + { + int offset = 4 + i * 4; + result[i] = new LayeredSpellId( + BinaryPrimitives.ReadUInt16LittleEndian(payload.Slice(offset, 2)), + BinaryPrimitives.ReadUInt16LittleEndian(payload.Slice(offset + 2, 2))); + } + return result; + } + // ── Appraise / identify ───────────────────────────────────────────────── /// 0x00C9 IdentifyObjectResponse header. diff --git a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs index efc1ca50..7358a20e 100644 --- a/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs +++ b/src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs @@ -328,7 +328,9 @@ public static class PlayerDescriptionParser CharacterOptionDataFlag optionFlags = CharacterOptionDataFlag.None; uint options1 = 0; uint options2 = 0; - uint spellbookFilters = 0; + // Retail CPlayerModule ctor (0x005D5245) enables every school and + // level filter before any optional PlayerDescription override. + uint spellbookFilters = 0x3FFFu; List shortcuts = new(); List> hotbarSpells = new(); List<(uint, uint)> desiredComps = new(); @@ -687,10 +689,7 @@ public static class PlayerDescriptionParser ReadOnlySpan src, ref int pos, List dest, EnchantmentBucket bucket) { - uint count = ReadU32(src, ref pos); - if (count > 0x4000) throw new FormatException("unreasonable enchantment list count"); - for (int i = 0; i < count; i++) - dest.Add(ReadEnchantment(src, ref pos, bucket)); + dest.AddRange(EnchantmentWireReader.ReadList(src, ref pos, bucket)); } private static EnchantmentEntry ReadEnchantment( @@ -703,29 +702,7 @@ public static class PlayerDescriptionParser // f32 degrade_modifier, f32 degrade_limit, f64 last_time_degraded, // u32 stat_mod_type, u32 stat_mod_key, f32 stat_mod_value, (28) // if has_spell_set_id != 0: u32 spell_set_id (0 or 4) - if (src.Length - pos < 60) throw new FormatException("truncated enchantment record"); - ushort spellId = ReadU16(src, ref pos); - ushort layer = ReadU16(src, ref pos); - ushort spellCategory = ReadU16(src, ref pos); - ushort hasSpellSetId = ReadU16(src, ref pos); - uint powerLevel = ReadU32(src, ref pos); - double startTime = ReadF64(src, ref pos); - double duration = ReadF64(src, ref pos); - uint casterGuid = ReadU32(src, ref pos); - float degradeModifier= ReadF32(src, ref pos); - float degradeLimit = ReadF32(src, ref pos); - double lastDegraded = ReadF64(src, ref pos); - uint statModType = ReadU32(src, ref pos); - uint statModKey = ReadU32(src, ref pos); - float statModValue = ReadF32(src, ref pos); - uint? spellSetId = null; - if (hasSpellSetId != 0) - spellSetId = ReadU32(src, ref pos); - return new EnchantmentEntry( - spellId, layer, spellCategory, hasSpellSetId, powerLevel, - startTime, duration, casterGuid, degradeModifier, degradeLimit, - lastDegraded, statModType, statModKey, statModValue, spellSetId, - bucket); + return EnchantmentWireReader.Read(src, ref pos, bucket); } /// Strict inventory + equipped block reader. Returns true if diff --git a/src/AcDream.Core.Net/ObjectTableWiring.cs b/src/AcDream.Core.Net/ObjectTableWiring.cs index be9d577a..1f6d3530 100644 --- a/src/AcDream.Core.Net/ObjectTableWiring.cs +++ b/src/AcDream.Core.Net/ObjectTableWiring.cs @@ -138,5 +138,6 @@ public static class ObjectTableWiring CombatUse: s.CombatUse, PluralName: s.PluralName, PetOwnerId: s.PetOwnerId, - AmmoType: s.AmmoType); + AmmoType: s.AmmoType, + SpellId: s.SpellId); } diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 695da2d6..b9c7ea01 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -131,6 +131,7 @@ public sealed class WorldSession : IDisposable string? PluralName = null, uint? PetOwnerId = null, ushort? AmmoType = null, + uint? SpellId = null, PhysicsSpawnData? Physics = null); /// @@ -191,6 +192,7 @@ public sealed class WorldSession : IDisposable PluralName: parsed.PluralName, PetOwnerId: parsed.PetOwnerId, AmmoType: parsed.AmmoType, + SpellId: parsed.SpellId, Physics: parsed.Physics); /// Fires when the session finishes parsing a CreateObject. @@ -1451,6 +1453,45 @@ public sealed class WorldSession : IDisposable seq, componentId: 0u, amount: uint.MaxValue)); } + public void SendSetDesiredComponentLevel(uint componentId, uint amount) + { + uint seq = NextGameActionSequence(); + SendGameAction(ClientCommandRequests.BuildSetDesiredComponentLevel( + seq, componentId, amount)); + } + + public void SendAddSpellFavorite(uint spellId, int position, int tabIndex) + { + uint seq = NextGameActionSequence(); + SendGameAction(ClientCommandRequests.BuildAddSpellFavorite( + seq, spellId, position, tabIndex)); + } + + public void SendRemoveSpellFavorite(uint spellId, int tabIndex) + { + uint seq = NextGameActionSequence(); + SendGameAction(ClientCommandRequests.BuildRemoveSpellFavorite( + seq, spellId, tabIndex)); + } + + public void SendSpellbookFilter(uint filters) + { + uint seq = NextGameActionSequence(); + SendGameAction(ClientCommandRequests.BuildSpellbookFilter(seq, filters)); + } + + public void SendCastUntargetedSpell(uint spellId) + { + uint seq = NextGameActionSequence(); + SendGameAction(CastSpellRequest.BuildUntargeted(seq, spellId)); + } + + public void SendCastTargetedSpell(uint targetGuid, uint spellId) + { + uint seq = NextGameActionSequence(); + SendGameAction(CastSpellRequest.BuildTargeted(seq, targetGuid, spellId)); + } + /// Send retail ChangeCombatMode (0x0053). public void SendChangeCombatMode(CombatMode mode) { diff --git a/src/AcDream.Core/Items/ClientObject.cs b/src/AcDream.Core/Items/ClientObject.cs index 9a810ed7..6247696a 100644 --- a/src/AcDream.Core/Items/ClientObject.cs +++ b/src/AcDream.Core/Items/ClientObject.cs @@ -191,6 +191,8 @@ public sealed class ClientObject public byte? CombatUse { get; set; } /// Retail PublicWeenieDesc._ammoType; zero is AMMO_NONE. public ushort? AmmoType { get; set; } + /// Retail PublicWeenieDesc._spellID; used by caster endowments. + public uint? SpellId { get; set; } /// Client-side trade state used by ItemHolder legality gates. public int TradeState { get; set; } /// Resolved membership in SpellComponentTable (retail IsComponentPack). @@ -258,7 +260,8 @@ public readonly record struct WeenieData( byte? CombatUse = null, string? PluralName = null, uint? PetOwnerId = null, - ushort? AmmoType = null); + ushort? AmmoType = null, + uint? SpellId = null); /// /// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0). diff --git a/src/AcDream.Core/Items/ClientObjectTable.cs b/src/AcDream.Core/Items/ClientObjectTable.cs index b24f3c9f..a103fcc5 100644 --- a/src/AcDream.Core/Items/ClientObjectTable.cs +++ b/src/AcDream.Core/Items/ClientObjectTable.cs @@ -700,6 +700,7 @@ public sealed class ClientObjectTable if (d.PetOwnerId is { } petOwnerId) obj.PetOwnerId = petOwnerId; if (d.CombatUse is { } combatUse) obj.CombatUse = combatUse; if (d.AmmoType is { } ammoType) obj.AmmoType = ammoType; + if (d.SpellId is { } spellId) obj.SpellId = spellId; if (d.RadarBlipColor is { } radarBlipColor) obj.RadarBlipColor = radarBlipColor; if (d.RadarBehavior is { } radarBehavior) obj.RadarBehavior = radarBehavior; if (d.ItemsCapacity is { } ic) obj.ItemsCapacity = ic; diff --git a/src/AcDream.Core/Spells/EnchantmentRegistryProjection.cs b/src/AcDream.Core/Spells/EnchantmentRegistryProjection.cs new file mode 100644 index 00000000..83ae76dc --- /dev/null +++ b/src/AcDream.Core/Spells/EnchantmentRegistryProjection.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; + +namespace AcDream.Core.Spells; + +/// +/// Pure port of retail's ordinary in-effect registry projection: +/// CEnchantmentRegistry::GetEnchantmentsInEffect @ 0x00594540, +/// GetEnchantmentsInEffectFromList @ 0x00594430, +/// Duel @ 0x005942B0, and Enchantment::Duel @ 0x005CACE0. +/// +public static class EnchantmentRegistryProjection +{ + /// + /// Merges multiplicative then additive records and retains one winner per + /// spell category. Higher power wins; equal power is replaced only by a + /// later start time. Vitae and cooldown registries do not participate. + /// + public static IReadOnlyList GetEnchantmentsInEffect( + IEnumerable enchantments) + { + ArgumentNullException.ThrowIfNull(enchantments); + ActiveEnchantmentRecord[] source = enchantments as ActiveEnchantmentRecord[] + ?? [.. enchantments]; + var result = new List(); + DuelList(source, bucket: 1u, result); + DuelList(source, bucket: 2u, result); + return result; + } + + private static void DuelList( + IReadOnlyList source, + uint bucket, + List result) + { + for (int sourceIndex = 0; sourceIndex < source.Count; sourceIndex++) + { + ActiveEnchantmentRecord candidate = source[sourceIndex]; + if (candidate.Bucket != bucket) continue; + + int incumbentIndex = result.FindIndex(existing => + existing.SpellCategory == candidate.SpellCategory); + if (incumbentIndex >= 0) + { + ActiveEnchantmentRecord incumbent = result[incumbentIndex]; + if (!CandidateWins(incumbent, candidate)) + continue; + // Retail removes the losing node, then appends the winner. + result.RemoveAt(incumbentIndex); + } + result.Add(candidate); + } + } + + private static bool CandidateWins( + ActiveEnchantmentRecord incumbent, + ActiveEnchantmentRecord candidate) + { + // Retail Enchantment::_power_level is signed int even though the wire + // field is read as four raw bytes. + int incumbentPower = unchecked((int)incumbent.PowerLevel); + int candidatePower = unchecked((int)candidate.PowerLevel); + return candidatePower > incumbentPower + || (candidatePower == incumbentPower + && candidate.StartTime > incumbent.StartTime); + } +} diff --git a/src/AcDream.Core/Spells/RetailSpellFormula.cs b/src/AcDream.Core/Spells/RetailSpellFormula.cs new file mode 100644 index 00000000..138c8518 --- /dev/null +++ b/src/AcDream.Core/Spells/RetailSpellFormula.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace AcDream.Core.Spells; + +/// +/// Pure retail spell-formula helpers. The formula is an eight-entry list of +/// spell-component enum IDs (SCIDs), not inventory weenie class IDs. +/// +public static class RetailSpellFormula +{ + private const uint LowestTaperId = 63u; + private const uint PrismaticTaperId = 0xBCu; + + /// + /// Retail MagicSystem::DeterminePowerLevelOfComponent (0x005BD240). + /// + public static uint DeterminePowerLevelOfComponent(uint componentId) => componentId switch + { + >= 1u and <= 6u => componentId, + 0x6Eu => 7u, + 0x70u => 8u, + 0xC0u => 9u, + 0xC1u => 10u, + _ => 0u, + }; + + /// + /// Retail CSpellBase::InqSpellLevelByRoughHeuristic (0x00597260). + /// This is the I-VIII level used by gmSpellbookUI's level filters. + /// + public static int InqSpellLevelByRoughHeuristic(IReadOnlyList components) + { + uint power = components.Count == 0 + ? 0u + : DeterminePowerLevelOfComponent(components[0]); + return power switch + { + < 7u => (int)power, + >= 9u => (int)power - 2, + _ => (int)power - 1, + }; + } + + /// + /// Retail CSpellBase::InqScarabOnlyFormula (0x00597050). + /// Foci and the infused-magic augmentations replace the ordinary taper + /// formula with its power components plus one to four prismatic tapers. + /// + public static IReadOnlyList InqScarabOnlyFormula( + IReadOnlyList components) + { + ArgumentNullException.ThrowIfNull(components); + + var result = new List(8); + uint strongestPower = 0u; + foreach (uint component in components.Take(8)) + { + if (component == 0u) + break; + if (!IsPowerComponent(component)) + continue; + + result.Add(component); + strongestPower = Math.Max( + strongestPower, + DeterminePowerLevelOfComponent(component)); + } + + int taperCount = strongestPower switch + { + 1u => 1, + 2u => 2, + 3u or 4u or 7u => 3, + 5u or 6u or 8u or 9u or 10u => 4, + _ => 0, + }; + while (taperCount-- > 0 && result.Count < 8) + result.Add(PrismaticTaperId); + return result; + } + + private static bool IsPowerComponent(uint component) => component is + >= 1u and <= 6u or 0x6Eu or 0x6Fu or 0x70u or 0xC0u or 0xC1u; + + /// + /// Retail PString's CP-1252 hash used by SpellFormula::RandomizeForName. + /// + public static uint ComputeNameHash(string value) + { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + long result = 0; + foreach (byte raw in Encoding.GetEncoding(1252).GetBytes(value ?? string.Empty)) + { + sbyte character = unchecked((sbyte)raw); + result = character + (result << 4); + if ((result & 0xF0000000L) != 0) + result = (result ^ ((result & 0xF0000000L) >> 24)) & 0x0FFFFFFF; + } + return unchecked((uint)result); + } + + /// + /// Retail SpellFormula::RandomizeForName (0x005BD050), cross-checked with + /// ACE.DatLoader SpellTable.GetSpellFormula. Only taper SCIDs change. + /// + public static IReadOnlyList CustomizeForAccount( + IReadOnlyList components, + uint formulaVersion, + string accountName) + { + uint[] result = components.Take(8).ToArray(); + return formulaVersion switch + { + 1u => RandomizeVersion1(result, accountName), + 2u => RandomizeVersion2(result, accountName), + 3u => RandomizeVersion3(result, accountName), + _ => result, + }; + } + + private static IReadOnlyList RandomizeVersion1(uint[] c, string accountName) + { + int count = c.Count(value => value != 0); + if (count < 5) return c; + + uint seed = ComputeNameHash(accountName) % 0x13D573u; + uint scarab = c[0]; + int herbIndex = count > 5 ? 2 : 1; + uint herb = c[herbIndex]; + int powderIndex = herbIndex + 1 + (count > 6 ? 1 : 0); + if (powderIndex + 1 >= c.Length) return c; + uint powder = c[powderIndex]; + uint potion = c[powderIndex + 1]; + int talismanIndex = powderIndex + 2 + (count > 7 ? 1 : 0); + if (talismanIndex >= c.Length) return c; + uint talisman = c[talismanIndex]; + + if (count > 5) + c[1] = unchecked(powder + 2u * herb + potion + talisman + scarab) % 12u + LowestTaperId; + if (count > 6) + { + uint denominator = unchecked(scarab + powder + potion); + if (denominator != 0) + c[3] = unchecked((scarab + herb + talisman + 2u * (powder + potion)) + * (seed / denominator)) % 12u + LowestTaperId; + } + if (count > 7) + { + uint denominator = unchecked(talisman + scarab); + if (denominator != 0) + c[6] = unchecked((powder + 2u * talisman + potion + herb + scarab) + * (seed / denominator)) % 12u + LowestTaperId; + } + return c; + } + + private static IReadOnlyList RandomizeVersion2(uint[] c, string accountName) + { + if (c.Length < 8) return c; + uint seed = ComputeNameHash(accountName) % 0x13D573u; + uint p1 = c[0], c4 = c[4], x = c[5], a = c[7]; + c[3] = unchecked(a + 3u * p1 + 2u * c4 * x + c[2] + c[1]) % 12u + LowestTaperId; + uint denominator = unchecked(c[1] * a + 2u * c4); + if (denominator != 0) + c[6] = unchecked((a + 3u * p1 * c[2] + 2u * x + c4) + * (seed / denominator)) % 12u + LowestTaperId; + return c; + } + + private static IReadOnlyList RandomizeVersion3(uint[] c, string accountName) + { + if (c.Length < 7) return c; + uint hash = ComputeNameHash(accountName); + uint h0 = unchecked(hash % 0x13D573u + c[0]) % 12u; + uint h1 = unchecked(hash % 0x4AEFDu + c[1]) % 12u; + uint h2 = unchecked(hash % 0x96A7Fu + c[2]) % 12u; + uint h4 = unchecked(hash % 0x100A03u + c[4]) % 12u; + uint h5 = unchecked(hash % 0xEB2EFu + c[5]) % 12u; + uint h7 = unchecked(hash % 0x121E7Du + (c.Length > 7 ? c[7] : 0u)) % 12u; + c[3] = unchecked(h0 + h1 + h2 + h4 + h5 + h2 * h5 + h0 * h1 + h7 * (h4 + 1u)) + % 12u + LowestTaperId; + c[6] = unchecked(h0 + h1 + h2 + h4 + hash % 0x65039u % 12u + + h7 * (h4 * (h0 * h1 * h2 * h5 + 7u) + 1u) + + h5 + 5u * h0 * h1 + 11u * h2 * h5) % 12u + LowestTaperId; + return c; + } +} diff --git a/src/AcDream.Core/Spells/SpellMetadata.cs b/src/AcDream.Core/Spells/SpellMetadata.cs index 8654fc7e..ca238876 100644 --- a/src/AcDream.Core/Spells/SpellMetadata.cs +++ b/src/AcDream.Core/Spells/SpellMetadata.cs @@ -41,4 +41,21 @@ public sealed record SpellMetadata( int ManaCost, bool IsDebuff, bool IsFellowship, - string Description); + string Description, + int SortKey, + int Difficulty, + uint Flags, + int Generation, + bool IsFastWindup, + bool IsOffensive, + bool IsUntargeted, + float Speed, + uint CasterEffect, + uint TargetEffect, + uint TargetMask, + int SpellType) +{ + public bool IsSelfTargeted => (Flags & 0x00000008u) != 0; + public bool IsBeneficial => (Flags & 0x00000004u) != 0; + public bool IsProjectile => (Flags & 0x00000100u) != 0; +} diff --git a/src/AcDream.Core/Spells/SpellModel.cs b/src/AcDream.Core/Spells/SpellModel.cs index 0e1c8d27..e1abe5e4 100644 --- a/src/AcDream.Core/Spells/SpellModel.cs +++ b/src/AcDream.Core/Spells/SpellModel.cs @@ -18,11 +18,9 @@ public enum MagicSchool : uint None = 0, WarMagic = 1, LifeMagic = 2, - CreatureEnchantment = 3, - ItemEnchantment = 4, - PortalMagic = 5, - // VoidMagic added in later retail revisions; uses LifeMagic skill. - VoidMagic = 6, + ItemEnchantment = 3, + CreatureEnchantment = 4, + VoidMagic = 5, } /// @@ -55,14 +53,23 @@ public enum SpellCategory : uint public enum SpellFlags : uint { None = 0, - Beneficial = 0x00000001, - Resistable = 0x00000002, - Projectile = 0x00000004, - EnchantmentDispel = 0x00000008, - PurgeOnReset = 0x00000010, + Resistable = 0x00000001, + PKSensitive = 0x00000002, + Beneficial = 0x00000004, + SelfTargeted = 0x00000008, + Reversed = 0x00000010, NotIndoors = 0x00000020, - Melee = 0x00000040, - Missile = 0x00000080, + NotOutdoors = 0x00000040, + NotResearchable = 0x00000080, + Projectile = 0x00000100, + CreatureSpell = 0x00000200, + ExcludedFromItemDescriptions = 0x00000400, + IgnoresManaConversion = 0x00000800, + NonTrackingProjectile = 0x00001000, + FellowshipSpell = 0x00002000, + FastCast = 0x00004000, + IndoorLongRange = 0x00008000, + DamageOverTime = 0x00010000, // more flags in r01 §1 } diff --git a/src/AcDream.Core/Spells/SpellTable.cs b/src/AcDream.Core/Spells/SpellTable.cs index cce06a97..638984ce 100644 --- a/src/AcDream.Core/Spells/SpellTable.cs +++ b/src/AcDream.Core/Spells/SpellTable.cs @@ -101,6 +101,18 @@ public sealed class SpellTable int? iIsDebuff = Get("IsDebuff"); int? iIsFellow = Get("IsFellowship"); int? iDescription = Get("Description"); + int? iSortKey = Get("SortKey"); + int? iDifficulty = Get("Difficulty"); + int? iFlags = Get("Flags [Hex]"); + int? iGeneration = Get("Generation"); + int? iFastWindup = Get("IsFastWindup"); + int? iOffensive = Get("IsOffensive"); + int? iUntargeted = Get("IsUntargetted"); + int? iSpeed = Get("Speed"); + int? iCasterFx = Get("CasterEffect"); + int? iTargetFx = Get("TargetEffect"); + int? iTargetMask = Get("TargetMask [Hex]"); + int? iSpellType = Get("Type"); if (iSpellId is null || iName is null) return new SpellTable(byId); @@ -125,10 +137,24 @@ public sealed class SpellTable bool isDebuff = ParseBool(fields, iIsDebuff); bool isFellow = ParseBool(fields, iIsFellow); string description = iDescription is int d && d < fields.Count ? fields[d] : ""; + int sortKey = (int)ParseUInt(fields, iSortKey); + int difficulty = (int)ParseUInt(fields, iDifficulty); + uint flags = ParseHexUInt(fields, iFlags); + int generation = (int)ParseUInt(fields, iGeneration); + bool fastWindup = ParseBool(fields, iFastWindup); + bool offensive = ParseBool(fields, iOffensive); + bool untargeted = ParseBool(fields, iUntargeted); + float speed = ParseFloat(fields, iSpeed); + uint casterEffect = ParseUInt(fields, iCasterFx); + uint targetEffect = ParseUInt(fields, iTargetFx); + uint targetMask = ParseHexUInt(fields, iTargetMask); + int spellType = (int)ParseUInt(fields, iSpellType); byId[spellId] = new SpellMetadata( spellId, name, school, family, iconId, words, duration, - mana, isDebuff, isFellow, description); + mana, isDebuff, isFellow, description, sortKey, difficulty, + flags, generation, fastWindup, offensive, untargeted, speed, + casterEffect, targetEffect, targetMask, spellType); } return new SpellTable(byId); diff --git a/src/AcDream.Core/Spells/Spellbook.cs b/src/AcDream.Core/Spells/Spellbook.cs index 66ca93a5..b2e57c29 100644 --- a/src/AcDream.Core/Spells/Spellbook.cs +++ b/src/AcDream.Core/Spells/Spellbook.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace AcDream.Core.Spells; @@ -17,9 +17,14 @@ namespace AcDream.Core.Spells; /// public sealed class Spellbook { - private readonly HashSet _learnedSpells = new(); - private readonly ConcurrentDictionary _activeByLayer = new(); + private readonly Dictionary _learnedSpells = new(); + private readonly Dictionary _activeById = new(); + private readonly Dictionary> _enchantmentOrderByBucket = new(); + private readonly List[] _favoriteSpells = Enumerable.Range(0, 8) + .Select(_ => new List()).ToArray(); + private readonly Dictionary _desiredComponents = new(); private readonly SpellTable _table; + private uint _spellbookFilters = 0x3FFFu; /// /// Build a Spellbook with an optional @@ -69,39 +74,98 @@ public sealed class Spellbook /// Fires when an enchantment is removed (expired / dispelled). public event Action? EnchantmentRemoved; + /// Fires when any manifest-backed magic UI state changes. + public event Action? StateChanged; + + /// Fires when learned spells, favorites, or spellbook filters change. + public event Action? SpellbookChanged; + + /// Fires when the active enchantment set changes. + public event Action? EnchantmentsChanged; + + /// Fires when the desired spell-component levels change. + public event Action? DesiredComponentsChanged; + /// All currently learned spell ids. - public IReadOnlyCollection LearnedSpells => _learnedSpells; + public IReadOnlyCollection LearnedSpells => _learnedSpells.Keys; + + /// Stable learned-spell snapshot, including server spell power. + public IReadOnlyList LearnedSpellSnapshot => _learnedSpells + .Select(pair => new LearnedSpellRecord(pair.Key, pair.Value)) + .OrderBy(spell => spell.SpellId) + .ToArray(); /// All currently-active enchantments. - public IEnumerable ActiveEnchantments => _activeByLayer.Values; + public IEnumerable ActiveEnchantments => _activeById.Values; + + public IReadOnlyList ActiveEnchantmentSnapshot => + _activeById.Values.OrderBy(record => record.Identity).ToArray(); + + /// + /// Retail ordinary-effect projection after mult/add filtering and the + /// spell-category duel. This is the source for effects-list rows; retail's + /// indicator counts scan the raw mult/add records before this projection. + /// + public IReadOnlyList EnchantmentsInEffectSnapshot + { + get + { + var ordered = new List(); + AppendBucket(bucket: 1u, ordered); + AppendBucket(bucket: 2u, ordered); + return EnchantmentRegistryProjection.GetEnchantmentsInEffect(ordered); + } + } + + public IReadOnlyList GetFavorites(int tabIndex) + { + if ((uint)tabIndex >= _favoriteSpells.Length) return Array.Empty(); + return _favoriteSpells[tabIndex].ToArray(); + } + + public IReadOnlyDictionary DesiredComponents => _desiredComponents; + + public uint SpellbookFilters => _spellbookFilters; public int LearnedCount => _learnedSpells.Count; - public int ActiveCount => _activeByLayer.Count; + public int ActiveCount => _activeById.Count; - public bool Knows(uint spellId) => _learnedSpells.Contains(spellId); + public bool Knows(uint spellId) => _learnedSpells.ContainsKey(spellId); // ── Inbound handlers ───────────────────────────────────────────────────── /// 0x02C1 MagicUpdateSpell: learn a spell. - public void OnSpellLearned(uint spellId) + public void OnSpellLearned(uint spellId, float power = 0f) { - if (_learnedSpells.Add(spellId)) + if (_learnedSpells.TryAdd(spellId, power)) + { SpellLearned?.Invoke(spellId); + NotifySpellbookChanged(); + } + else if (_learnedSpells[spellId] != power) + { + _learnedSpells[spellId] = power; + NotifySpellbookChanged(); + } } /// 0x01A8 MagicRemoveSpell: forget a spell. public void OnSpellForgotten(uint spellId) { if (_learnedSpells.Remove(spellId)) + { SpellForgotten?.Invoke(spellId); + NotifySpellbookChanged(); + } } /// 0x02C2 MagicUpdateEnchantment: enchantment added / refreshed. public void OnEnchantmentAdded(uint spellId, uint layerId, float duration, uint casterGuid) { var record = new ActiveEnchantmentRecord(spellId, layerId, duration, casterGuid); - _activeByLayer[layerId] = record; + UpsertLiveEnchantment(record); EnchantmentAdded?.Invoke(record); + NotifyEnchantmentsChanged(); } /// @@ -113,56 +177,305 @@ public sealed class Spellbook /// public void OnEnchantmentAdded(ActiveEnchantmentRecord record) { - _activeByLayer[record.LayerId] = record; + UpsertLiveEnchantment(record); EnchantmentAdded?.Invoke(record); + NotifyEnchantmentsChanged(); } /// 0x02C3 / 0x02C7 MagicRemove/DispelEnchantment. public void OnEnchantmentRemoved(uint layerId, uint spellId) { - if (_activeByLayer.TryRemove(layerId, out var record)) + uint identity = ActiveEnchantmentRecord.MakeIdentity(spellId, layerId); + if (RemoveActiveEnchantment(identity, out ActiveEnchantmentRecord record)) + { EnchantmentRemoved?.Invoke(record); - else - EnchantmentRemoved?.Invoke(new ActiveEnchantmentRecord(spellId, layerId, 0f, 0)); + NotifyEnchantmentsChanged(); + } } - /// 0x02C6 MagicPurgeEnchantments: clear all active buffs. + public void OnEnchantmentsAdded(IEnumerable records) + { + foreach (ActiveEnchantmentRecord record in records) + { + UpsertLiveEnchantment(record); + EnchantmentAdded?.Invoke(record); + } + NotifyEnchantmentsChanged(); + } + + public void OnEnchantmentsRemoved(IEnumerable<(uint SpellId, uint Layer)> spells) + { + bool changed = false; + foreach ((uint spellId, uint layer) in spells) + { + uint identity = ActiveEnchantmentRecord.MakeIdentity(spellId, layer); + if (!RemoveActiveEnchantment(identity, out ActiveEnchantmentRecord record)) continue; + changed = true; + EnchantmentRemoved?.Invoke(record); + } + if (changed) NotifyEnchantmentsChanged(); + } + + /// + /// Retail EnchantmentRegistry::PurgeEnchantments (0x00594DE0): purge only + /// finite-duration multiplicative/additive enchantments. Vitae, cooldowns, + /// and permanent enchantments live in separate registries and survive. + /// public void OnPurgeAll() { - foreach (var rec in _activeByLayer.Values) + ActiveEnchantmentRecord[] removed = _activeById.Values + .Where(IsPurgeable) + .ToArray(); + foreach (ActiveEnchantmentRecord record in removed) + RemoveActiveEnchantment(record.Identity, out _); + foreach (var rec in removed) EnchantmentRemoved?.Invoke(rec); - _activeByLayer.Clear(); + if (removed.Length != 0) NotifyEnchantmentsChanged(); + } + + public void OnPurgeBadEnchantments() + { + // Retail PurgeBadEnchantments (0x00594E00) tests the StatMod + // Beneficial bit directly; spell-table labels do not participate. + const uint BeneficialStatMod = 0x02000000u; + ActiveEnchantmentRecord[] removed = _activeById.Values + .Where(record => IsPurgeable(record) + && (record.StatModType.GetValueOrDefault() & BeneficialStatMod) == 0) + .ToArray(); + foreach (ActiveEnchantmentRecord record in removed) + RemoveActiveEnchantment(record.Identity, out _); + foreach (ActiveEnchantmentRecord record in removed) + EnchantmentRemoved?.Invoke(record); + if (removed.Length != 0) NotifyEnchantmentsChanged(); + } + + private static bool IsPurgeable(ActiveEnchantmentRecord record) => + (record.Bucket is 1u or 2u) && record.Duration != -1f; + + /// + /// PlayerDescription is a complete character manifest. Replace every + /// magic collection atomically so relogging or switching characters cannot + /// retain state from the prior snapshot. + /// + public void ReplaceManifest( + IReadOnlyDictionary learnedSpells, + IEnumerable enchantments, + IReadOnlyList> favorites, + IReadOnlyList<(uint Id, uint Amount)> desiredComponents, + uint spellbookFilters) + { + _learnedSpells.Clear(); + foreach ((uint spellId, float power) in learnedSpells) + _learnedSpells[spellId] = power; + + _activeById.Clear(); + _enchantmentOrderByBucket.Clear(); + foreach (ActiveEnchantmentRecord enchantment in enchantments) + UpsertManifestEnchantment(enchantment); + + for (int i = 0; i < _favoriteSpells.Length; i++) + { + _favoriteSpells[i].Clear(); + if (i < favorites.Count) + _favoriteSpells[i].AddRange(favorites[i]); + } + + _desiredComponents.Clear(); + foreach ((uint id, uint amount) in desiredComponents) + _desiredComponents[id] = amount; + _spellbookFilters = spellbookFilters; + SpellbookChanged?.Invoke(); + EnchantmentsChanged?.Invoke(); + DesiredComponentsChanged?.Invoke(); + StateChanged?.Invoke(); + } + + public void SetFavorite(int tabIndex, int position, uint spellId) + { + if ((uint)tabIndex >= _favoriteSpells.Length || position < 0) return; + List tab = _favoriteSpells[tabIndex]; + if (position > tab.Count) position = tab.Count; + tab.Remove(spellId); + tab.Insert(Math.Min(position, tab.Count), spellId); + NotifySpellbookChanged(); + } + + public void RemoveFavorite(int tabIndex, uint spellId) + { + if ((uint)tabIndex >= _favoriteSpells.Length) return; + if (_favoriteSpells[tabIndex].Remove(spellId)) NotifySpellbookChanged(); + } + + public void SetSpellbookFilters(uint filters) + { + if (_spellbookFilters == filters) return; + _spellbookFilters = filters; + NotifySpellbookChanged(); + } + + public void SetDesiredComponent(uint componentId, uint amount) + { + uint current = _desiredComponents.TryGetValue(componentId, out uint existing) + ? existing : 0u; + if (current == amount) return; + if (amount == 0) _desiredComponents.Remove(componentId); + else _desiredComponents[componentId] = amount; + DesiredComponentsChanged?.Invoke(); + StateChanged?.Invoke(); } public void Clear() { _learnedSpells.Clear(); - _activeByLayer.Clear(); + _activeById.Clear(); + _enchantmentOrderByBucket.Clear(); + foreach (List tab in _favoriteSpells) tab.Clear(); + _desiredComponents.Clear(); + _spellbookFilters = 0x3FFFu; + SpellbookChanged?.Invoke(); + EnchantmentsChanged?.Invoke(); + DesiredComponentsChanged?.Invoke(); + StateChanged?.Invoke(); + } + + private void NotifySpellbookChanged() + { + SpellbookChanged?.Invoke(); + StateChanged?.Invoke(); + } + + private void NotifyEnchantmentsChanged() + { + EnchantmentsChanged?.Invoke(); + StateChanged?.Invoke(); + } + + private void AppendBucket(uint bucket, List destination) + { + if (!_enchantmentOrderByBucket.TryGetValue(bucket, out List? order)) return; + foreach (uint identity in order) + if (_activeById.TryGetValue(identity, out ActiveEnchantmentRecord record) + && record.Bucket == bucket) + destination.Add(record); + } + + /// + /// Retail AddEnchantmentToList @ 0x00593DF0 inserts a new live + /// mult/add record at the list head. Refreshing an existing identity keeps + /// its node position; moving buckets creates a new head node. + /// + private void UpsertLiveEnchantment(ActiveEnchantmentRecord record) + { + uint identity = record.Identity; + if (_activeById.TryGetValue(identity, out ActiveEnchantmentRecord previous)) + { + _activeById[identity] = record; + if (previous.Bucket == record.Bucket) + { + EnsureOrdered(identity, record.Bucket, insertAtHead: true); + return; + } + RemoveFromOrder(previous); + } + else + { + _activeById.Add(identity, record); + } + GetBucketOrder(record.Bucket).Insert(0, identity); + } + + /// Bulk UnPack preserves the received head-to-tail list order. + private void UpsertManifestEnchantment(ActiveEnchantmentRecord record) + { + uint identity = record.Identity; + if (_activeById.TryGetValue(identity, out ActiveEnchantmentRecord previous)) + { + _activeById[identity] = record; + if (previous.Bucket == record.Bucket) + { + EnsureOrdered(identity, record.Bucket, insertAtHead: false); + return; + } + RemoveFromOrder(previous); + } + else + { + _activeById.Add(identity, record); + } + GetBucketOrder(record.Bucket).Add(identity); + } + + private bool RemoveActiveEnchantment( + uint identity, + out ActiveEnchantmentRecord record) + { + if (!_activeById.Remove(identity, out record)) return false; + RemoveFromOrder(record); + return true; + } + + private void EnsureOrdered(uint identity, uint bucket, bool insertAtHead) + { + List order = GetBucketOrder(bucket); + if (order.Contains(identity)) return; + if (insertAtHead) order.Insert(0, identity); + else order.Add(identity); + } + + private List GetBucketOrder(uint bucket) + { + if (_enchantmentOrderByBucket.TryGetValue(bucket, out List? order)) + return order; + order = new List(); + _enchantmentOrderByBucket.Add(bucket, order); + return order; + } + + private void RemoveFromOrder(ActiveEnchantmentRecord record) + { + if (!_enchantmentOrderByBucket.TryGetValue(record.Bucket, out List? order)) + return; + order.Remove(record.Identity); + if (order.Count == 0) + _enchantmentOrderByBucket.Remove(record.Bucket); } } +public readonly record struct LearnedSpellRecord(uint SpellId, float Power); + /// /// Summary of one active enchantment layer on the player. The /// optional StatMod fields (issue #12) carry the wire-level -/// `_smod` triad (type, key, val) when available — only -/// `PlayerDescription`'s enchantment block currently populates these -/// (). -/// `MagicUpdateEnchantment` events still produce records with these -/// fields null until the wire parser is extended. +/// `_smod` triad (type, key, val). Both the complete +/// `PlayerDescription` snapshot and live MagicUpdateEnchantment events +/// populate the same record shape. /// /// /// tells EnchantmentMath whether this /// enchantment's StatMod is multiplicative (0x01), additive -/// (0x02), cooldown (0x04), or vitae (0x08) per +/// (0x02), vitae (0x04), or cooldown (0x08) per /// the retail EnchantmentMask classification. /// /// public readonly record struct ActiveEnchantmentRecord( uint SpellId, uint LayerId, - float Duration, + double Duration, uint CasterGuid, uint? StatModType = null, uint? StatModKey = null, float? StatModValue = null, - uint Bucket = 0); + uint Bucket = 0, + double StartTime = 0, + uint SpellCategory = 0, + uint PowerLevel = 0, + float DegradeModifier = 0, + float DegradeLimit = 0, + double LastTimeDegraded = 0, + uint? SpellSetId = null) +{ + public uint Identity => MakeIdentity(SpellId, LayerId); + + public static uint MakeIdentity(uint spellId, uint layerId) => + (spellId & 0xFFFFu) | ((layerId & 0xFFFFu) << 16); +} diff --git a/src/AcDream.UI.Abstractions/Input/Binding.cs b/src/AcDream.UI.Abstractions/Input/Binding.cs index 7f497405..08b3b6ec 100644 --- a/src/AcDream.UI.Abstractions/Input/Binding.cs +++ b/src/AcDream.UI.Abstractions/Input/Binding.cs @@ -13,4 +13,5 @@ namespace AcDream.UI.Abstractions.Input; public readonly record struct Binding( KeyChord Chord, InputAction Action, - ActivationType Activation = ActivationType.Press); + ActivationType Activation = ActivationType.Press, + InputScope Scope = InputScope.Game); diff --git a/src/AcDream.UI.Abstractions/Input/InputDispatcher.cs b/src/AcDream.UI.Abstractions/Input/InputDispatcher.cs index e62dc5e2..9de7a208 100644 --- a/src/AcDream.UI.Abstractions/Input/InputDispatcher.cs +++ b/src/AcDream.UI.Abstractions/Input/InputDispatcher.cs @@ -36,6 +36,7 @@ public sealed class InputDispatcher private readonly IMouseSource _mouse; private KeyBindings _bindings; private readonly Stack _scopes = new(); + private InputScope? _combatScope; private readonly HashSet _heldHoldChords = new(); // Double-click detection. _lastMouseDownButton == null means no recent press. @@ -72,7 +73,33 @@ public sealed class InputDispatcher } /// Topmost scope on the stack — what the dispatcher looks up first. - public InputScope ActiveScope => _scopes.Peek(); + public InputScope ActiveScope => _scopes.Peek() == InputScope.Game && _combatScope is { } combat + ? combat + : _scopes.Peek(); + + /// Set the mode-dependent combat layer that shadows normal game chords. + public void SetCombatScope(InputScope? scope) + { + if (scope is not null && scope is not ( + InputScope.MeleeCombat or InputScope.MissileCombat or InputScope.MagicCombat)) + throw new ArgumentOutOfRangeException(nameof(scope)); + if (_combatScope == scope) return; + ReleaseHeldHoldBindings(); + _combatScope = scope; + } + + private Binding? FindActive(KeyChord chord, ActivationType activation) + { + foreach (InputScope scope in _scopes) + { + if (scope == InputScope.Game && _combatScope is { } combat + && _bindings.Find(chord, activation, combat) is { } combatBinding) + return combatBinding; + if (_bindings.Find(chord, activation, scope) is { } binding) + return binding; + } + return null; + } /// True iff a is in progress. public bool IsCapturing => _captureCallback is not null; @@ -115,15 +142,18 @@ public sealed class InputDispatcher /// public void SetBindings(KeyBindings bindings) { - _bindings = bindings ?? throw new ArgumentNullException(nameof(bindings)); - _heldHoldChords.Clear(); + ArgumentNullException.ThrowIfNull(bindings); + ReleaseHeldHoldBindings(); + _bindings = bindings; } /// /// Per-frame "is this action's chord currently held" query. Walks every /// binding for the given action; returns true if any of them has its /// chord currently held in the underlying keyboard/mouse state AND the - /// modifier mask matches. + /// modifier mask matches. Scope precedence is resolved through the same + /// path as transition dispatch, so a combat binding + /// on a chord shadows the normal Game action during held polling too. /// /// /// Used by per-frame movement polling (MovementInput.Forward @@ -149,11 +179,28 @@ public sealed class InputDispatcher if (_mouse.WantCaptureKeyboard) return false; foreach (var b in _bindings.ForAction(action)) { - if (IsChordHeld(b.Chord)) return true; + if (!IsChordHeld(b.Chord)) continue; + Binding? active = FindActiveHeld(b.Chord, b.Activation); + if (active?.Action == action) return true; } return false; } + private Binding? FindActiveHeld(KeyChord candidate, ActivationType activation) + { + // IsChordHeld deliberately lets a bare movement chord coexist with the + // retail Shift-to-walk modifier. Resolve an explicitly bound current + // modifier chord first so a higher combat scope can still shadow it; + // only fall back to the authored bare chord when Shift has no binding. + var actual = candidate with { Modifiers = _keyboard.CurrentModifiers }; + Binding? active = FindActive(actual, activation); + if (active is not null) return active; + return candidate.Modifiers == ModifierMask.None + && _keyboard.CurrentModifiers == ModifierMask.Shift + ? FindActive(candidate, activation) + : null; + } + /// True iff the given chord's primary key is currently down on /// the appropriate device AND the keyboard's current modifier mask /// matches the chord's required modifier mask. Match semantics: @@ -208,7 +255,11 @@ public sealed class InputDispatcher }; /// Push a scope onto the active stack. Top wins. - public void PushScope(InputScope scope) => _scopes.Push(scope); + public void PushScope(InputScope scope) + { + ReleaseHeldHoldBindings(); + _scopes.Push(scope); + } /// Pop the topmost scope. is the /// scope the caller believes is on top; mismatch throws to catch @@ -218,9 +269,22 @@ public sealed class InputDispatcher if (_scopes.Peek() != expected) throw new InvalidOperationException( $"PopScope expected {expected} but top is {_scopes.Peek()}"); + ReleaseHeldHoldBindings(); _scopes.Pop(); } + private void ReleaseHeldHoldBindings() + { + if (_heldHoldChords.Count == 0) return; + var releases = new List(_heldHoldChords.Count); + foreach (KeyChord chord in _heldHoldChords) + if (FindActive(chord, ActivationType.Hold) is { } binding) + releases.Add(binding); + _heldHoldChords.Clear(); + foreach (Binding binding in releases) + Fired?.Invoke(binding.Action, ActivationType.Release); + } + /// /// Per-frame tick. Re-fires /// activations for every chord that's currently held. Call once per @@ -238,7 +302,12 @@ public sealed class InputDispatcher for (int i = 0; i < snapshot.Length; i++) { var chord = snapshot[i]; - var hold = _bindings.Find(chord, ActivationType.Hold); + // A preceding Hold callback may have changed scope or bindings. + // Those transitions synchronously release and clear every held + // chord; never dispatch a stale snapshot entry afterward. + if (!_heldHoldChords.Contains(chord)) + continue; + var hold = FindActive(chord, ActivationType.Hold); if (hold is not null) Fired?.Invoke(hold.Value.Action, ActivationType.Hold); } @@ -273,10 +342,10 @@ public sealed class InputDispatcher if (_mouse.WantCaptureKeyboard) return; var chord = new KeyChord(key, mods, Device: 0); - var press = _bindings.Find(chord, ActivationType.Press); + var press = FindActive(chord, ActivationType.Press); if (press is not null) Fired?.Invoke(press.Value.Action, ActivationType.Press); - var hold = _bindings.Find(chord, ActivationType.Hold); + var hold = FindActive(chord, ActivationType.Hold); if (hold is not null) { // Emit a Press transition so subscribers can latch state, then @@ -305,7 +374,7 @@ public sealed class InputDispatcher // mid-press. var chord = new KeyChord(key, mods, Device: 0); - var release = _bindings.Find(chord, ActivationType.Release); + var release = FindActive(chord, ActivationType.Release); if (release is not null) Fired?.Invoke(release.Value.Action, ActivationType.Release); // Any matching Hold binding gets a Release transition. Walk the @@ -320,7 +389,7 @@ public sealed class InputDispatcher foreach (var held in toRemove) { _heldHoldChords.Remove(held); - var hold = _bindings.Find(held, ActivationType.Hold); + var hold = FindActive(held, ActivationType.Hold); if (hold is not null) Fired?.Invoke(hold.Value.Action, ActivationType.Release); } } @@ -330,10 +399,10 @@ public sealed class InputDispatcher if (_mouse.WantCaptureMouse) return; var chord = new KeyChord(MouseButtonToKey(button), mods, Device: 1); - var press = _bindings.Find(chord, ActivationType.Press); + var press = FindActive(chord, ActivationType.Press); if (press is not null) Fired?.Invoke(press.Value.Action, ActivationType.Press); - var hold = _bindings.Find(chord, ActivationType.Hold); + var hold = FindActive(chord, ActivationType.Hold); if (hold is not null) { Fired?.Invoke(hold.Value.Action, ActivationType.Press); @@ -348,7 +417,7 @@ public sealed class InputDispatcher if (_lastMouseDownButton == button && nowMs - _lastMouseDownTickMs <= DoubleClickThresholdMs) { - var dbl = _bindings.Find(chord, ActivationType.DoubleClick); + var dbl = FindActive(chord, ActivationType.DoubleClick); if (dbl is not null) Fired?.Invoke(dbl.Value.Action, ActivationType.DoubleClick); _lastMouseDownButton = null; // consumed; require fresh pair for next } @@ -363,7 +432,7 @@ public sealed class InputDispatcher { var chord = new KeyChord(MouseButtonToKey(button), mods, Device: 1); - var release = _bindings.Find(chord, ActivationType.Release); + var release = FindActive(chord, ActivationType.Release); if (release is not null) Fired?.Invoke(release.Value.Action, ActivationType.Release); var keyForLookup = MouseButtonToKey(button); @@ -376,7 +445,7 @@ public sealed class InputDispatcher foreach (var held in toRemove) { _heldHoldChords.Remove(held); - var hold = _bindings.Find(held, ActivationType.Hold); + var hold = FindActive(held, ActivationType.Hold); if (hold is not null) Fired?.Invoke(hold.Value.Action, ActivationType.Release); } } diff --git a/src/AcDream.UI.Abstractions/Input/KeyBindings.cs b/src/AcDream.UI.Abstractions/Input/KeyBindings.cs index dd09d760..ae9dd5c6 100644 --- a/src/AcDream.UI.Abstractions/Input/KeyBindings.cs +++ b/src/AcDream.UI.Abstractions/Input/KeyBindings.cs @@ -26,7 +26,7 @@ namespace AcDream.UI.Abstractions.Input; /// public sealed class KeyBindings { - private const int CurrentSchemaVersion = 3; + private const int CurrentSchemaVersion = 4; private readonly List _bindings = new(); @@ -47,11 +47,22 @@ public sealed class KeyBindings /// activation type matches the requested phase. Null if none. /// public Binding? Find(KeyChord chord, ActivationType activation) + { + for (int i = 0; i < _bindings.Count; i++) + { + Binding binding = _bindings[i]; + if (binding.Chord == chord && binding.Activation == activation) + return binding; + } + return null; + } + + public Binding? Find(KeyChord chord, ActivationType activation, InputScope scope) { for (int i = 0; i < _bindings.Count; i++) { var b = _bindings[i]; - if (b.Chord == chord && b.Activation == activation) return b; + if (b.Chord == chord && b.Activation == activation && b.Scope == scope) return b; } return null; } @@ -242,37 +253,37 @@ public sealed class KeyBindings // ── Combat (mode-dependent — dormant in K, lights up in Phase L) ── b.Add(new(new KeyChord(Key.GraveAccent, ModifierMask.None), InputAction.CombatToggleCombat)); // Melee mode (active when MeleeCombat scope pushed). - b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatDecreaseAttackPower)); - b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatIncreaseAttackPower)); + b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatDecreaseAttackPower, Scope: InputScope.MeleeCombat)); + b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatIncreaseAttackPower, Scope: InputScope.MeleeCombat)); // Retail HandleCombatAction consumes key-down AND key-up for attack // height actions: down starts charging, up ends the request. Hold is // our binding representation for that transition pair. - b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatLowAttack, ActivationType.Hold)); - b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatMediumAttack, ActivationType.Hold)); - b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatHighAttack, ActivationType.Hold)); + b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatLowAttack, ActivationType.Hold, InputScope.MeleeCombat)); + b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatMediumAttack, ActivationType.Hold, InputScope.MeleeCombat)); + b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatHighAttack, ActivationType.Hold, InputScope.MeleeCombat)); // Missile + Magic + Spell-tab — same chords; resolved by scope at // runtime per InputDispatcher's stack lookup. Add the bindings; // subscribers arrive in Phase L when CombatState.CurrentMode is // wired. - b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatDecreaseMissileAccuracy)); - b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatIncreaseMissileAccuracy)); - b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatAimLow)); - b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatAimMedium)); - b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatAimHigh)); - b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatPrevSpellTab)); - b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatNextSpellTab)); - b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatPrevSpell)); - b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatCastCurrentSpell)); - b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatNextSpell)); - b.Add(new(new KeyChord(Key.Insert, ModifierMask.Ctrl), InputAction.CombatFirstSpellTab)); - b.Add(new(new KeyChord(Key.PageUp, ModifierMask.Ctrl), InputAction.CombatLastSpellTab)); - b.Add(new(new KeyChord(Key.Delete, ModifierMask.Ctrl), InputAction.CombatFirstSpell)); - b.Add(new(new KeyChord(Key.PageDown, ModifierMask.Ctrl), InputAction.CombatLastSpell)); + b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatDecreaseMissileAccuracy, Scope: InputScope.MissileCombat)); + b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatIncreaseMissileAccuracy, Scope: InputScope.MissileCombat)); + b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatAimLow, Scope: InputScope.MissileCombat)); + b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatAimMedium, Scope: InputScope.MissileCombat)); + b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatAimHigh, Scope: InputScope.MissileCombat)); + b.Add(new(new KeyChord(Key.Insert, ModifierMask.None), InputAction.CombatPrevSpellTab, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.PageUp, ModifierMask.None), InputAction.CombatNextSpellTab, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.Delete, ModifierMask.None), InputAction.CombatPrevSpell, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.End, ModifierMask.None), InputAction.CombatCastCurrentSpell, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.PageDown, ModifierMask.None), InputAction.CombatNextSpell, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.Insert, ModifierMask.Ctrl), InputAction.CombatFirstSpellTab, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.PageUp, ModifierMask.Ctrl), InputAction.CombatLastSpellTab, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.Delete, ModifierMask.Ctrl), InputAction.CombatFirstSpell, Scope: InputScope.MagicCombat)); + b.Add(new(new KeyChord(Key.PageDown, ModifierMask.Ctrl), InputAction.CombatLastSpell, Scope: InputScope.MagicCombat)); for (int i = 1; i <= 9; i++) { var k = (Key)((int)Key.Number0 + i); var action = (InputAction)((int)InputAction.UseSpellSlot_1 + i - 1); - b.Add(new(new KeyChord(k, ModifierMask.None), action)); + b.Add(new(new KeyChord(k, ModifierMask.None), action, Scope: InputScope.MagicCombat)); } // ── Emotes ────────────────────────────────────────────── @@ -424,7 +435,17 @@ public sealed class KeyBindings var chord = new KeyChord(silkKey, mods, device); action = MigrateLegacyQuickSlotIntent(version, action, chord, activation); activation = MigrateCombatAttackActivation(version, action, activation); - loaded.Add(new(chord, action, activation)); + InputScope scope = defaults.ForAction(action) + .Select(binding => binding.Scope) + .DefaultIfEmpty(InputScope.Game) + .First(); + if (bindingEl.TryGetProperty("scope", out var scopeEl) + && scopeEl.ValueKind == JsonValueKind.String + && Enum.TryParse(scopeEl.GetString(), out InputScope parsedScope)) + { + scope = parsedScope; + } + loaded.Add(new(chord, action, activation, scope)); } } } @@ -480,6 +501,8 @@ public sealed class KeyBindings entry["device"] = (int)binding.Chord.Device; if (binding.Activation != ActivationType.Press) entry["activation"] = binding.Activation.ToString(); + if (binding.Scope != InputScope.Game) + entry["scope"] = binding.Scope.ToString(); list.Add(entry); } diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsVM.cs b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsVM.cs index 7988cf92..be14d6a4 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsVM.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsVM.cs @@ -253,7 +253,10 @@ public sealed class SettingsVM // captured chord + same activation type, but on a DIFFERENT // action. (Same-action bindings are fine — that's already in // _draft for this action and gets removed when we apply.) - var existing = _draft.Find(chord, RebindOriginal!.Value.Activation); + var existing = _draft.Find( + chord, + RebindOriginal!.Value.Activation, + RebindOriginal.Value.Scope); if (existing is not null && existing.Value.Action != RebindInProgress!.Value) { PendingConflict = new ConflictPrompt( @@ -293,7 +296,11 @@ public sealed class SettingsVM private void ApplyRebind(KeyChord chord) { _draft.Remove(RebindOriginal!.Value); - _draft.Add(new Binding(chord, RebindInProgress!.Value, RebindOriginal.Value.Activation)); + _draft.Add(new Binding( + chord, + RebindInProgress!.Value, + RebindOriginal.Value.Activation, + RebindOriginal.Value.Scope)); RebindInProgress = null; RebindOriginal = null; } diff --git a/tests/AcDream.App.Tests/Spells/RetailSpellTargetPolicyTests.cs b/tests/AcDream.App.Tests/Spells/RetailSpellTargetPolicyTests.cs new file mode 100644 index 00000000..f18ce13f --- /dev/null +++ b/tests/AcDream.App.Tests/Spells/RetailSpellTargetPolicyTests.cs @@ -0,0 +1,115 @@ +using AcDream.App.Spells; +using AcDream.Core.Items; +using AcDream.Core.Spells; + +namespace AcDream.App.Tests.Spells; + +public sealed class RetailSpellTargetPolicyTests +{ + [Fact] + public void RejectsStackedTarget() + { + SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate( + 1u, + new ClientObject { ObjectId = 2u, Name = "Scarabs", Type = ItemType.SpellComponents, StackSize = 2 }, + Spell(targetMask: (uint)ItemType.SpellComponents)); + + Assert.False(result.Allowed); + Assert.Contains("stack", result.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void RejectsSelfWithoutRetailSpecialTargetBits() + { + SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate( + 1u, + new ClientObject { ObjectId = 1u, Name = "Self", Type = ItemType.Creature }, + Spell(targetMask: (uint)ItemType.Creature)); + + Assert.False(result.Allowed); + Assert.Contains("yourself", result.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void AcceptsSelfWithSpecialTargetBits() + { + SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate( + 1u, + new ClientObject + { + ObjectId = 1u, + Name = "Self", + Type = ItemType.Creature, + PublicWeenieBitfield = (uint)PublicWeenieFlags.Player, + }, + Spell(targetMask: 0x8101u)); + + Assert.True(result.Allowed); + } + + [Theory] + [InlineData(PublicWeenieFlags.None, 0u, false)] + [InlineData(PublicWeenieFlags.Attackable, 0u, true)] + [InlineData(PublicWeenieFlags.Player, 0u, true)] + [InlineData(PublicWeenieFlags.Attackable, 99u, false)] + public void SpecialTargetBits_RequirePlayerOrAttackableNonPet( + PublicWeenieFlags flags, + uint petOwner, + bool expected) + { + SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate( + 1u, + new ClientObject + { + ObjectId = 2u, + Name = "Target", + Type = ItemType.Creature, + PublicWeenieBitfield = (uint)flags, + PetOwnerId = petOwner, + }, + Spell(targetMask: 0x8101u)); + + Assert.Equal(expected, result.Allowed); + } + + [Fact] + public void RejectsTypeMaskMismatchWithObjectName() + { + SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate( + 1u, + new ClientObject { ObjectId = 2u, Name = "Sword", Type = ItemType.MeleeWeapon }, + Spell(targetMask: (uint)ItemType.Creature)); + + Assert.False(result.Allowed); + Assert.Contains("Sword", result.Message); + } + + [Theory] + [InlineData(PublicWeenieFlags.Attackable, 0u, true)] + [InlineData(PublicWeenieFlags.None, 0u, false)] + [InlineData(PublicWeenieFlags.Player, 0u, true)] + [InlineData(PublicWeenieFlags.Attackable, 9u, false)] + public void DirectTypeMatch_StillRequiresPlayerOrAttackableNonPet( + PublicWeenieFlags flags, + uint petOwner, + bool expected) + { + SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate( + 1u, + new ClientObject + { + ObjectId = 2u, + Name = "Target", + Type = ItemType.Misc, + PublicWeenieBitfield = (uint)flags, + PetOwnerId = petOwner, + }, + Spell(targetMask: (uint)ItemType.Misc)); + + Assert.Equal(expected, result.Allowed); + } + + private static SpellMetadata Spell(uint targetMask) => new( + 1, "Test", "War Magic", 1, 0, "", 0, 0, false, false, "", + 0, 0, 0, 1, false, false, false, 0, 0, 0, targetMask, 0); +} diff --git a/tests/AcDream.App.Tests/Spells/SpellCastingControllerTests.cs b/tests/AcDream.App.Tests/Spells/SpellCastingControllerTests.cs new file mode 100644 index 00000000..6c55c73b --- /dev/null +++ b/tests/AcDream.App.Tests/Spells/SpellCastingControllerTests.cs @@ -0,0 +1,119 @@ +using AcDream.App.Spells; +using AcDream.Core.Spells; + +namespace AcDream.App.Tests.Spells; + +public sealed class SpellCastingControllerTests +{ + [Fact] + public void Cast_SelfTargeted_SendsLocalPlayerAsTarget() + { + Spellbook book = MakeBook(flags: 0x8, untargeted: true, targetMask: 0x10); + uint target = 0; + var controller = MakeController(book, selected: 99, player: 42, + sendTargeted: (id, _) => target = id); + + Assert.Equal(CastRequestResult.Sent, controller.Cast(1)); + Assert.Equal(42u, target); + } + + [Fact] + public void Cast_TargetedWithoutSelection_DoesNotSend() + { + Spellbook book = MakeBook(flags: 0, untargeted: false, targetMask: 0x10); + int sends = 0; + string message = ""; + var controller = MakeController(book, selected: null, player: 42, + sendTargeted: (_, _) => sends++, display: value => message = value); + + Assert.Equal(CastRequestResult.NoTarget, controller.Cast(1)); + Assert.Equal(0, sends); + Assert.Contains("select", message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Cast_Untargeted_StopsThenSendsThenIncrementsBusy() + { + Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); + var order = new List(); + var controller = MakeController(book, selected: null, player: 42, + stop: () => order.Add("stop"), + sendUntargeted: _ => order.Add("send"), + incrementBusy: () => order.Add("busy")); + + Assert.Equal(CastRequestResult.Sent, controller.Cast(1)); + Assert.Equal(["stop", "send", "busy"], order); + } + + [Fact] + public void Cast_MissingComponents_DoesNotIncrementBusyOrSend() + { + Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); + int sends = 0, busy = 0; + var controller = MakeController(book, selected: null, player: 42, + sendUntargeted: _ => sends++, hasComponents: _ => false, + incrementBusy: () => busy++); + + Assert.Equal(CastRequestResult.MissingComponents, controller.Cast(1)); + Assert.Equal(0, sends); + Assert.Equal(0, busy); + } + + [Fact] + public void Cast_Disconnected_DoesNotIncrementBusy() + { + Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); + int busy = 0; + var controller = MakeController(book, selected: null, player: 42, + incrementBusy: () => busy++, canSend: () => false); + + Assert.Equal(CastRequestResult.Unavailable, controller.Cast(1)); + Assert.Equal(0, busy); + } + + [Fact] + public void Cast_SendThrows_DoesNotIncrementBusyAndRethrows() + { + Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); + int increments = 0; + var controller = MakeController(book, selected: null, player: 42, + sendUntargeted: _ => throw new InvalidOperationException("wire"), + incrementBusy: () => increments++); + + Assert.Throws(() => controller.Cast(1)); + Assert.Equal(0, increments); + Assert.Null(controller.LastRequestedSpellId); + } + + private static SpellCastingController MakeController( + Spellbook book, + uint? selected, + uint player, + Action? stop = null, + Action? sendUntargeted = null, + Action? sendTargeted = null, + Action? display = null, + Func? hasComponents = null, + Action? incrementBusy = null, + Func? canSend = null) => new( + book, + () => selected, + () => player, + stop ?? (() => { }), + sendUntargeted ?? (_ => { }), + sendTargeted ?? ((_, _) => { }), + display ?? (_ => { }), + hasRequiredComponents: hasComponents, + incrementBusy: incrementBusy, + canSend: canSend); + + private static Spellbook MakeBook(uint flags, bool untargeted, uint targetMask) + { + const string header = "Spell ID,Name,Flags [Hex],IsUntargetted,TargetMask [Hex]"; + string row = $"1,Test,0x{flags:X},{untargeted},0x{targetMask:X}"; + SpellTable table = SpellTable.LoadFromReader(new StringReader($"{header}\n{row}")); + var book = new Spellbook(table); + book.OnSpellLearned(1); + return book; + } +} diff --git a/tests/AcDream.App.Tests/Spells/SpellComponentRequirementServiceTests.cs b/tests/AcDream.App.Tests/Spells/SpellComponentRequirementServiceTests.cs new file mode 100644 index 00000000..0f75d6e5 --- /dev/null +++ b/tests/AcDream.App.Tests/Spells/SpellComponentRequirementServiceTests.cs @@ -0,0 +1,180 @@ +using AcDream.App.Spells; +using AcDream.Core.Items; +using AcDream.Core.Spells; + +namespace AcDream.App.Tests.Spells; + +public sealed class SpellComponentRequirementServiceTests +{ + [Fact] + public void RequiresEveryFormulaScidMappedToOwnedWcid() + { + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 2u, + WeenieClassId = 100u, + ContainerId = 1u, + IsComponentPack = true, + }); + var service = Service(objects, [10u, 11u], new Dictionary + { + [10u] = 100u, + [11u] = 101u, + }); + + Assert.False(service.HasRequiredComponents(50u)); + + objects.AddOrUpdate(new ClientObject + { + ObjectId = 3u, + WeenieClassId = 101u, + ContainerId = 1u, + IsComponentPack = true, + }); + Assert.True(service.HasRequiredComponents(50u)); + } + + [Fact] + public void ServerDisabledComponents_BypassesInventoryPreflight() + { + var objects = new ClientObjectTable(); + var player = new ClientObject { ObjectId = 1u, Name = "Player" }; + player.Properties.Bools[SpellComponentRequirementService.SpellComponentsRequiredProperty] = false; + objects.AddOrUpdate(player); + + Assert.True(Service(objects, [10u], new Dictionary()) + .HasRequiredComponents(50u)); + } + + [Fact] + public void PersonalizedFormula_UsesExactCanonicalAccountSpelling() + { + const string canonical = "CanonicalAccount"; + const string loginSpelling = "canonicalaccount"; + uint[] formula = [6u, 63u, 10u, 64u, 20u, 30u, 65u, 40u]; + IReadOnlyList canonicalFormula = RetailSpellFormula.CustomizeForAccount( + formula, 3u, canonical); + IReadOnlyList loginFormula = RetailSpellFormula.CustomizeForAccount( + formula, 3u, loginSpelling); + Assert.NotEqual(canonicalFormula, loginFormula); + + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); + var map = canonicalFormula.Where(scid => scid != 0u) + .Distinct() + .ToDictionary(scid => scid, scid => scid + 1000u); + foreach (uint wcid in map.Values) + objects.AddOrUpdate(new ClientObject + { + ObjectId = wcid + 10000u, + WeenieClassId = wcid, + ContainerId = 1u, + }); + string account = canonical; + var service = new SpellComponentRequirementService( + objects, () => 1u, () => account, + new Dictionary { [50u] = new(3u, formula) }, + map); + + Assert.True(service.HasRequiredComponents(50u)); + account = loginSpelling; + Assert.False(service.HasRequiredComponents(50u)); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void InfusedSchoolOrDirectlyCarriedFocus_UsesScarabOnlyFormula( + bool infused, + bool carriesFocus) + { + var objects = new ClientObjectTable(); + var player = new ClientObject { ObjectId = 1u, Name = "Player" }; + if (infused) + player.Properties.Ints[0x129u] = 1; + objects.AddOrUpdate(player); + if (carriesFocus) + { + objects.AddOrUpdate(new ClientObject + { + ObjectId = 2u, + WeenieClassId = 900u, + ContainerId = 1u, + Type = ItemType.Container, + }); + } + // War I scarab-only formula is SCID 1 + one prismatic taper (0xBC). + objects.AddOrUpdate(new ClientObject + { + ObjectId = 3u, + WeenieClassId = 101u, + ContainerId = 1u, + }); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 4u, + WeenieClassId = 188u, + ContainerId = 1u, + }); + var service = new SpellComponentRequirementService( + objects, () => 1u, () => "testaccount", + new Dictionary + { + [50u] = new(0u, [1u, 63u, 10u], School: 1u), + }, + new Dictionary + { + [1u] = 101u, + [0xBCu] = 188u, + }, + new Dictionary { [1u] = 900u }); + + Assert.True(service.HasRequiredComponents(50u)); + } + + [Fact] + public void FocusNestedInsidePack_DoesNotCountAsRetailMagicPackOwnership() + { + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 2u, + WeenieClassId = 700u, + ContainerId = 1u, + Type = ItemType.Container, + }); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 3u, + WeenieClassId = 900u, + ContainerId = 2u, + Type = ItemType.Container, + }); + var service = new SpellComponentRequirementService( + objects, () => 1u, () => "testaccount", + new Dictionary + { + [50u] = new(0u, [1u, 63u], School: 1u), + }, + new Dictionary(), + new Dictionary { [1u] = 900u }); + + Assert.False(service.HasRequiredComponents(50u)); + } + + private static SpellComponentRequirementService Service( + ClientObjectTable objects, + IReadOnlyList components, + IReadOnlyDictionary map) => new( + objects, + () => 1u, + () => "testaccount", + new Dictionary + { + [50u] = new(0u, components), + }, + map); +} diff --git a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs index b3754149..a8434966 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs @@ -11,12 +11,12 @@ public sealed class CombatUiControllerTests private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); [Fact] - public void CombatMode_ShowsOnlyTargetedModes_AndSelectsMediumByDefault() + public void CombatMode_ShowsPhysicalAndMagicPages_AndSelectsMediumByDefault() { double now = 0d; var combat = new CombatState(); using var attacks = CreateAttacks(combat, () => now, []); - var (layout, _, _, power, high, medium, low) = BuildLayout(); + var (layout, basic, spellcasting, power, high, medium, low) = BuildLayout(); var visibility = new List(); GameplaySettings gameplay = GameplaySettings.Default; using var controller = CombatUiController.Bind( @@ -27,7 +27,9 @@ public sealed class CombatUiControllerTests combat.SetCombatMode(CombatMode.Melee); combat.SetCombatMode(CombatMode.Magic); - Assert.Equal([false, true, false], visibility); + Assert.Equal([false, true, true], visibility); + Assert.False(basic.Visible); + Assert.True(spellcasting.Visible); Assert.False(high.Selected); Assert.True(medium.Selected); Assert.False(low.Selected); diff --git a/tests/AcDream.App.Tests/UI/Layout/EffectsIndicatorControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/EffectsIndicatorControllerTests.cs new file mode 100644 index 00000000..59f0dade --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/EffectsIndicatorControllerTests.cs @@ -0,0 +1,102 @@ +using AcDream.App.UI; +using AcDream.App.UI.Layout; +using AcDream.Core.Spells; + +namespace AcDream.App.Tests.UI.Layout; + +public sealed class EffectsIndicatorControllerTests +{ + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); + + [Fact] + public void AuthoredFixture_BuildsRetailIndicatorButtons() + { + ElementInfo info = FixtureLoader.LoadIndicatorsInfos(); + Assert.Equal(0x10000610u, info.Id); + Assert.Equal(150f, info.Width); + Assert.Equal(30f, info.Height); + + ImportedLayout layout = LayoutImporter.Build(info, NoTex, datFont: null); + Assert.IsType(layout.FindElement(EffectsIndicatorController.HelpfulButtonId)); + Assert.IsType(layout.FindElement(EffectsIndicatorController.HarmfulButtonId)); + } + + [Fact] + public void Enchantments_GhostAndEnableMatchingIndicator_ThenClickCorrectPanel() + { + var table = SpellTable.LoadFromReader(new StringReader( + "Spell ID,Name,Flags [Hex]\n42,Boon,0x4\n43,Bane,0x0\n")); + var spellbook = new Spellbook(table); + ImportedLayout layout = BuildSyntheticLayout(); + var toggled = new List(); + using EffectsIndicatorController controller = EffectsIndicatorController.Bind( + layout, spellbook, toggled.Add)!; + UiButton helpful = Assert.IsType( + layout.FindElement(EffectsIndicatorController.HelpfulButtonId)); + UiButton harmful = Assert.IsType( + layout.FindElement(EffectsIndicatorController.HarmfulButtonId)); + + Assert.False(helpful.Enabled); + Assert.False(harmful.Enabled); + + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 10u, 60f, 1u, Bucket: 4u, SpellCategory: 42u)); + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 43u, 11u, 60f, 2u, Bucket: 8u, SpellCategory: 43u)); + Assert.False(helpful.Enabled); + Assert.False(harmful.Enabled); + + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 99u, PowerLevel: 1u)); + Assert.True(helpful.Enabled); + Assert.False(harmful.Enabled); + helpful.OnEvent(new UiEvent(0, helpful, UiEventType.Click)); + Assert.Equal([RetailPanelCatalog.PositiveEffects], toggled); + + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 43u, 1u, 60f, 2u, Bucket: 2u, SpellCategory: 99u, PowerLevel: 7u)); + // The harmful spell wins the shared category in the effects list, but + // retail's raw registry counters keep both indicator types enabled. + Assert.True(helpful.Enabled); + Assert.True(harmful.Enabled); + harmful.OnEvent(new UiEvent(0, harmful, UiEventType.Click)); + Assert.Equal( + [RetailPanelCatalog.PositiveEffects, RetailPanelCatalog.NegativeEffects], + toggled); + + spellbook.OnEnchantmentRemoved(1u, 42u); + Assert.False(helpful.Enabled); + Assert.True(harmful.Enabled); + } + + private static ImportedLayout BuildSyntheticLayout() + { + var root = new ElementInfo { Id = 1u, Type = 3u, Width = 150f, Height = 30f }; + var helpful = ButtonInfo(EffectsIndicatorController.HelpfulButtonId); + var harmful = ButtonInfo(EffectsIndicatorController.HarmfulButtonId); + return LayoutImporter.BuildFromInfos(root, [helpful, harmful], NoTex, null); + } + + private static ElementInfo ButtonInfo(uint id) + { + var info = new ElementInfo + { + Id = id, + Type = EffectsIndicatorController.RetailClassId, + Width = 20f, + Height = 20f, + DefaultStateId = UiButtonStateMachine.Normal, + }; + info.States[UiButtonStateMachine.Normal] = new UiStateInfo + { + Id = UiButtonStateMachine.Normal, + Name = "Normal", + }; + info.States[UiButtonStateMachine.Ghosted] = new UiStateInfo + { + Id = UiButtonStateMachine.Ghosted, + Name = "Ghosted", + }; + return info; + } +} diff --git a/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs new file mode 100644 index 00000000..bb7f7819 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs @@ -0,0 +1,198 @@ +using AcDream.App.UI; +using AcDream.App.UI.Layout; +using AcDream.Core.Spells; + +namespace AcDream.App.Tests.UI.Layout; + +public sealed class EffectsUiControllerTests +{ + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); + + [Fact] + public void Rebuild_DoesNotAutoSelectFirstEffect_AndClearsRemovedSelection() + { + 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 }; + var info = new UiText { Width = 160f, Height = 32f }; + root.AddChild(list); + root.AddChild(info); + var layout = new ImportedLayout(root, new Dictionary + { + [EffectsUiController.ListId] = list, + [EffectsUiController.InfoTextId] = info, + }); + using EffectsUiController controller = EffectsUiController.Bind( + layout, spellbook, positive: true, () => 0d, NoTex, id => id)!; + + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 42u)); + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 2u, 60f, 1u, Bucket: 4u, SpellCategory: 42u)); + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 3u, 60f, 1u, Bucket: 8u, SpellCategory: 42u)); + Assert.Null(controller.SelectedSpellId); + Assert.Equal(1, list.GetNumUIItems()); + UiCatalogSlot row = Assert.IsType(list.GetItem(0)); + row.OnEvent(new UiEvent(0, row, UiEventType.Click)); + Assert.Equal(42u, controller.SelectedSpellId); + + spellbook.OnEnchantmentRemoved(1u, 42u); + Assert.Null(controller.SelectedSpellId); + } + + [Fact] + public void DuplicateLayersOfSameSpell_ShareRetailSelection() + { + var table = SpellTable.LoadFromReader(new StringReader( + "Spell ID,Name,Flags [Hex]\n42,Boon,0x4\n")); + var spellbook = new Spellbook(table); + var root = new UiPanel { Width = 160f, Height = 100f }; + var list = new UiItemList(NoTex) { Width = 160f, Height = 64f }; + root.AddChild(list); + var layout = new ImportedLayout(root, new Dictionary + { + [EffectsUiController.ListId] = list, + }); + using EffectsUiController controller = EffectsUiController.Bind( + layout, spellbook, positive: true, () => 0d, NoTex, id => id)!; + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 1u, 60f, 1u, Bucket: 1u, SpellCategory: 100u)); + spellbook.OnEnchantmentAdded(new ActiveEnchantmentRecord( + 42u, 2u, 60f, 2u, Bucket: 2u, SpellCategory: 101u)); + + UiCatalogSlot first = Assert.IsType(list.GetItem(0)); + UiCatalogSlot second = Assert.IsType(list.GetItem(1)); + first.OnEvent(new UiEvent(0, first, UiEventType.Click)); + + Assert.Equal(42u, controller.SelectedSpellId); + Assert.True(first.Selected); + Assert.True(second.Selected); + + second.OnEvent(new UiEvent(0, second, UiEventType.Click)); + Assert.Null(controller.SelectedSpellId); + Assert.False(first.Selected); + Assert.False(second.Selected); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AuthoredEffectsFixture_InheritsListAndInfo_AndBinds(bool positive) + { + ElementInfo info = positive + ? FixtureLoader.LoadPositiveEffectsInfos() + : FixtureLoader.LoadNegativeEffectsInfos(); + uint expectedRoot = positive + ? EffectsUiController.PositiveRootId + : EffectsUiController.NegativeRootId; + Assert.Equal(expectedRoot, info.Id); + Assert.Equal(0x1000001Bu, info.Type); + Assert.Equal(300f, info.Width); + Assert.Equal(362f, info.Height); + Assert.True(info.TryGetEffectiveProperty(0x1000000Cu, out UiPropertyValue effectType)); + Assert.Equal(UiPropertyKind.Enum, effectType.Kind); + Assert.Equal(positive ? 1u : 2u, effectType.UnsignedValue); + Assert.Equal( + positive, + info.TryGetEffectiveBool(0x10000049u, out bool restorePrevious) + && restorePrevious); + + ElementInfo[] children = info.Children.OrderBy(child => child.ReadOrder).ToArray(); + Assert.Equal( + [0x100000FCu, 0x10000120u, 0x10000123u, 0x10000124u, 0x10000125u], + children.Select(child => child.Id)); + Assert.Equal([1u, 2u, 3u, 4u, 5u], children.Select(child => child.ReadOrder)); + Assert.Equal(children.Length, children.Select(child => child.Id).Distinct().Count()); + ElementInfo listInfo = Assert.Single(children, child => child.Id == EffectsUiController.ListId); + Assert.Equal(5u, listInfo.Type); + Assert.Equal(300f, listInfo.Width); + Assert.Equal(249f, listInfo.Height); + Assert.Equal(12u, FindInfo(info, EffectsUiController.InfoTextId)?.Type); + + ImportedLayout layout = LayoutImporter.Build(info, NoTex, datFont: null); + var spellbook = new Spellbook(); + int closes = 0; + + using EffectsUiController? controller = EffectsUiController.Bind( + layout, spellbook, positive, () => 0d, NoTex, id => id, () => closes++); + + Assert.NotNull(controller); + Assert.NotNull(layout.FindElement(EffectsUiController.ListId)); + Assert.IsType(layout.FindElement(EffectsUiController.InfoTextId)); + Assert.Equal(expectedRoot, layout.Root.DatElementId); + UiButton close = Assert.IsType(layout.FindElement(EffectsUiController.CloseId)); + close.OnEvent(new UiEvent(0, close, UiEventType.Click)); + Assert.Equal(1, closes); + + uint panelId = positive + ? RetailPanelCatalog.PositiveEffects + : RetailPanelCatalog.NegativeEffects; + Assert.True(RetailPanelCatalog.TryGetWindowName(panelId, out string windowName)); + Assert.Equal( + positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects, + windowName); + Assert.DoesNotContain( + RetailPanelCatalog.ToolbarPanels, + entry => entry.PanelId == panelId); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AuthoredRestorePreviousProperty_DrivesRetailPanelLifecycle(bool positive) + { + ElementInfo info = positive + ? FixtureLoader.LoadPositiveEffectsInfos() + : FixtureLoader.LoadNegativeEffectsInfos(); + var visible = new Dictionary(StringComparer.Ordinal) + { + [WindowNames.Inventory] = false, + [positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects] = false, + }; + var panelUi = new RetailPanelUiController( + name => visible[name], + name => + { + visible[name] = true; + return true; + }, + name => + { + visible[name] = false; + return true; + }); + uint effectsPanel = positive + ? RetailPanelCatalog.PositiveEffects + : RetailPanelCatalog.NegativeEffects; + string effectsWindow = positive + ? WindowNames.PositiveEffects + : WindowNames.NegativeEffects; + panelUi.Register(RetailPanelCatalog.Inventory, WindowNames.Inventory); + panelUi.Register( + effectsPanel, + effectsWindow, + info); + + panelUi.SetPanelVisibility(RetailPanelCatalog.Inventory, visible: true); + panelUi.SetPanelVisibility(effectsPanel, visible: true); + panelUi.SetPanelVisibility(effectsPanel, visible: false); + + Assert.Equal(positive, visible[WindowNames.Inventory]); + Assert.False(visible[effectsWindow]); + Assert.Equal( + positive ? RetailPanelCatalog.Inventory : null, + panelUi.ActivePanelId); + } + + private static ElementInfo? FindInfo(ElementInfo root, uint id) + { + if (root.Id == id) return root; + foreach (ElementInfo child in root.Children) + if (FindInfo(child, id) is { } found) + return found; + return null; + } +} diff --git a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs index 11f6d633..abc300cd 100644 --- a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs +++ b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs @@ -113,6 +113,24 @@ public static class FixtureLoader public static ElementInfo LoadFpsDisplayInfos() => LoadInfos("smartbox_fps_2100000F.json"); + public static ImportedLayout LoadPositiveEffects() + => LayoutImporter.Build(LoadPositiveEffectsInfos(), _ => (0u, 0, 0), null); + + public static ElementInfo LoadPositiveEffectsInfos() + => LoadInfos("effects_positive_2100001B.json"); + + public static ImportedLayout LoadNegativeEffects() + => LayoutImporter.Build(LoadNegativeEffectsInfos(), _ => (0u, 0, 0), null); + + public static ElementInfo LoadNegativeEffectsInfos() + => LoadInfos("effects_negative_2100001B.json"); + + public static ImportedLayout LoadIndicators() + => LayoutImporter.Build(LoadIndicatorsInfos(), _ => (0u, 0, 0), null); + + public static ElementInfo LoadIndicatorsInfos() + => LoadInfos("indicators_21000071.json"); + // ── Shared loader ──────────────────────────────────────────────────────── private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName) diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs b/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs index 2c959a75..fbd334f4 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs @@ -22,6 +22,7 @@ public sealed class RetailLayoutFixtureGenerator (0x21000024u, "paperdoll_21000024.json"), (0x2100002Eu, "character_2100002E.json"), (0x2100006Cu, "vitals_2100006C.json"), + (EffectsIndicatorController.LayoutId, "indicators_21000071.json"), (0x21000072u, "powerbar_21000072.json"), (0x21000073u, "combat_21000073.json"), (0x21000074u, "radar_21000074.json"), @@ -73,7 +74,7 @@ public sealed class RetailLayoutFixtureGenerator Assert.Equal(0x4000001Au, fps!.FontDid); var fpsStrings = new DatStringResolver(dats); Assert.Equal("FPS: ", fpsStrings.Resolve(0x23000001u, 0x0DCFFF73u, 0)); - Assert.Equal("\nDEG: ", fpsStrings.Resolve(0x23000001u, 0x0DCFFF73u, 1)); + Assert.Equal(@"\nDEG: ", fpsStrings.Resolve(0x23000001u, 0x0DCFFF73u, 1)); var fpsJson = JsonSerializer.Serialize(fps, new JsonSerializerOptions { IncludeFields = true, @@ -83,6 +84,23 @@ public sealed class RetailLayoutFixtureGenerator Path.Combine(FixtureDirectory(), $"smartbox_fps_{smartboxLayoutDid:X8}.json"), fpsJson); + foreach ((uint rootId, string fileName) in new[] + { + (EffectsUiController.PositiveRootId, "effects_positive_2100001B.json"), + (EffectsUiController.NegativeRootId, "effects_negative_2100001B.json"), + }) + { + ElementInfo? effects = LayoutImporter.ImportInfos( + dats, EffectsUiController.LayoutId, rootId); + Assert.NotNull(effects); + var effectsJson = JsonSerializer.Serialize(effects, new JsonSerializerOptions + { + IncludeFields = true, + WriteIndented = true, + }); + File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), effectsJson); + } + foreach (var (layoutId, fileName) in Layouts) { var info = LayoutImporter.ImportInfos(dats, layoutId); diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs new file mode 100644 index 00000000..7c000e7b --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/RetailPanelUiControllerTests.cs @@ -0,0 +1,90 @@ +using AcDream.App.UI.Layout; + +namespace AcDream.App.Tests.UI.Layout; + +public sealed class RetailPanelUiControllerTests +{ + [Fact] + public void ShowingPanel_HidesCurrent_AndClosingLeavesHostEmpty() + { + var visible = new Dictionary(StringComparer.Ordinal) + { + ["inventory"] = false, + ["helpful"] = false, + }; + var controller = Create(visible); + controller.Register(7u, "inventory"); + controller.Register(4u, "helpful"); + + Assert.True(controller.SetPanelVisibility(7u, true)); + Assert.True(visible["inventory"]); + Assert.Equal(7u, controller.ActivePanelId); + + Assert.True(controller.SetPanelVisibility(4u, true)); + Assert.False(visible["inventory"]); + Assert.True(visible["helpful"]); + Assert.Equal(4u, controller.ActivePanelId); + + Assert.False(controller.TogglePanel(4u)); + Assert.False(visible["helpful"]); + Assert.Null(controller.ActivePanelId); + } + + [Fact] + public void RestorePreviousProperty_ReopensOnlyNonRestoringPreviousPanel() + { + var visible = new Dictionary(StringComparer.Ordinal) + { + ["ordinary"] = false, + ["transient"] = false, + }; + var controller = Create(visible); + controller.Register(1u, "ordinary", restorePrevious: false); + controller.Register(2u, "transient", restorePrevious: true); + + controller.SetPanelVisibility(1u, true); + controller.SetPanelVisibility(2u, true); + Assert.False(visible["ordinary"]); + Assert.True(visible["transient"]); + + controller.SetPanelVisibility(2u, false); + Assert.True(visible["ordinary"]); + Assert.False(visible["transient"]); + Assert.Equal(1u, controller.ActivePanelId); + } + + [Fact] + public void ObservedPersistenceShow_UsesSameExclusiveLifecycle() + { + var visible = new Dictionary(StringComparer.Ordinal) + { + ["first"] = false, + ["second"] = false, + }; + var controller = Create(visible); + controller.Register(1u, "first"); + controller.Register(2u, "second"); + controller.SetPanelVisibility(1u, true); + + visible["second"] = true; + controller.ObserveWindowVisibility("second", visible: true); + + Assert.False(visible["first"]); + Assert.True(visible["second"]); + Assert.Equal(2u, controller.ActivePanelId); + } + + private static RetailPanelUiController Create(Dictionary visible) + => new( + name => visible[name], + name => + { + visible[name] = true; + return true; + }, + name => + { + visible[name] = false; + return true; + }); +} diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs new file mode 100644 index 00000000..43207884 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs @@ -0,0 +1,173 @@ +using AcDream.App.Spells; +using AcDream.App.UI; +using AcDream.App.UI.Layout; +using AcDream.Core.Items; +using AcDream.Core.Selection; +using AcDream.Core.Spells; + +namespace AcDream.App.Tests.UI.Layout; + +public sealed class SpellcastingUiControllerTests +{ + private static (uint, int, int) NoTex(uint _) => (0u, 0, 0); + + [Fact] + public void ImportedFixture_BindsAllTabs_AndTracksEquippedEndowment() + { + ImportedLayout layout = LayoutImporter.Build( + FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); + var spellbook = new Spellbook(); + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); + var used = new List(); + using SpellcastingUiController? controller = Bind( + layout, spellbook, objects, used.Add); + + Assert.True(controller is not null, DescribeBinding(layout)); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 2u, + Name = "Orb", + Type = ItemType.Caster, + WielderId = 1u, + CurrentlyEquippedLocation = EquipMask.Held, + SpellId = 2670u, + IconId = 0x06001234u, + }); + controller.Tick(); + + UiElement host = Assert.IsAssignableFrom( + layout.FindElement(SpellcastingUiController.EndowmentId)); + Assert.True(host.Visible); + UiCatalogSlot slot = Assert.IsType(host.Children[^1]); + Assert.Equal(2u, slot.EntryId); + Assert.Equal(2670u, slot.CatalogIconTexture); + Assert.Equal(2u, slot.CatalogOverlayTexture); + + slot.OnEvent(new UiEvent(0, slot, UiEventType.Click)); + var cast = Assert.IsType( + layout.FindElement(SpellcastingUiController.CastButtonId)); + cast.OnEvent(new UiEvent(0, cast, UiEventType.Click)); + Assert.Equal([2u], used); + } + + [Fact] + public void FavoriteDrop_IgnoresForeignInventoryPayload() + { + ImportedLayout layout = LayoutImporter.Build( + FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); + var spellbook = new Spellbook(); + spellbook.OnSpellLearned(42u, 1f); + spellbook.SetFavorite(0, 0, 42u); + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); + using SpellcastingUiController? controller = Bind( + layout, spellbook, objects, _ => { }); + + Assert.True(controller is not null, DescribeBinding(layout)); + controller.Tick(); + UiElement group = Assert.IsAssignableFrom(layout.FindElement(0x100000AAu)); + UiItemList list = Descendants(group).OfType().First(); + UiCatalogSlot slot = Assert.IsType(list.GetItem(0)); + var foreign = new ItemDragPayload(9u, ItemDragSource.Inventory, 0, new UiItemSlot(), null); + + bool handled = slot.OnEvent(new UiEvent( + 0, slot, UiEventType.DropReleased, Payload: foreign)); + + Assert.True(handled); + Assert.Equal([42u], spellbook.GetFavorites(0)); + } + + [Fact] + public void UnrelatedObjectUpdate_DoesNotRecreateFavoriteSlots() + { + ImportedLayout layout = LayoutImporter.Build( + FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); + var spellbook = new Spellbook(); + spellbook.OnSpellLearned(42u, 1f); + spellbook.SetFavorite(0, 0, 42u); + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); + using SpellcastingUiController controller = Bind( + layout, spellbook, objects, _ => { })!; + UiElement group = layout.FindElement(0x100000AAu)!; + UiItemList list = Descendants(group).OfType().First(); + UiItemSlot before = list.GetItem(0)!; + + objects.AddOrUpdate(new ClientObject { ObjectId = 99u, Name = "Rock" }); + controller.Tick(); + + Assert.Same(before, list.GetItem(0)); + } + + [Fact] + public void ObjectTableClear_RemovesEquippedEndowmentAndCannotUseStaleGuid() + { + ImportedLayout layout = LayoutImporter.Build( + FixtureLoader.LoadCombatInfos(), NoTex, datFont: null); + var spellbook = new Spellbook(); + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" }); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 2u, + Name = "Orb", + Type = ItemType.Caster, + WielderId = 1u, + CurrentlyEquippedLocation = EquipMask.Held, + SpellId = 2670u, + }); + var used = new List(); + using SpellcastingUiController controller = Bind( + layout, spellbook, objects, used.Add)!; + UiElement host = layout.FindElement(SpellcastingUiController.EndowmentId)!; + UiCatalogSlot slot = Assert.IsType(host.Children[^1]); + slot.OnEvent(new UiEvent(0, slot, UiEventType.Click)); + + objects.Clear(); + controller.Tick(); + + Assert.False(host.Visible); + Assert.Equal(0u, slot.EntryId); + slot.OnEvent(new UiEvent(0, slot, UiEventType.DoubleClick)); + Assert.Empty(used); + } + + private static SpellcastingUiController? Bind( + ImportedLayout layout, + Spellbook spellbook, + ClientObjectTable objects, + Action useItem) + { + var casting = new SpellCastingController( + spellbook, () => null, () => 1u, () => { }, _ => { }, (_, _) => { }, _ => { }); + return SpellcastingUiController.Bind( + layout, spellbook, casting, objects, () => 1u, + spellId => spellId, + item => item.ObjectId, + useItem, + new SelectionState(), + (_, _, _) => { }, + (_, _) => { }); + } + + private static IEnumerable Descendants(UiElement root) + { + foreach (UiElement child in root.Children) + { + yield return child; + foreach (UiElement nested in Descendants(child)) yield return nested; + } + } + + private static string DescribeBinding(ImportedLayout layout) + { + uint[] ids = + [ + SpellcastingUiController.CastButtonId, SpellcastingUiController.EndowmentId, + 0x100000A3u, 0x100000AAu, 0x100005C2u, 0x100005C3u, + ]; + return string.Join(", ", ids.Select(id => + $"{id:X8}={layout.FindElement(id)?.GetType().Name ?? "missing"}")); + } +} diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json index ce83762d..32a70ae5 100644 --- a/tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/character_2100002E.json @@ -3177,7 +3177,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741825, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -4127,7 +4127,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741825, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -6911,7 +6911,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -7043,7 +7043,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -7261,7 +7261,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -7393,7 +7393,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -7407,88 +7407,12 @@ "Children": [] }, { - "Id": 268436038, - "Type": 1, - "X": 260, - "Y": 26, - "Width": 30, - "Height": 26, - "OriginalParentWidth": 300, - "OriginalParentHeight": 55, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 2, - "Right": 2, - "Bottom": 1, - "ReadOrder": 6, - "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": 100682934, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - }, - "13": { - "Id": 13, - "Name": "Ghosted", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100682935, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 13, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100682934, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100682935, - "Item2": 3 - } - }, - "StateCursors": {}, - "DefaultStateName": "Ghosted", - "Children": [] - }, - { - "Id": 268436039, - "Type": 7, - "X": 5, - "Y": 20, - "Width": 240, - "Height": 17, + "Id": 268436046, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 300, + "Height": 20, "OriginalParentWidth": 300, "OriginalParentHeight": 55, "HasOriginalParentSize": true, @@ -7504,16 +7428,276 @@ "Name": "", "PassToChildren": false, "IncorporationFlags": 30, - "Image": { - "File": 100667814, - "DrawMode": 1 - }, + "Image": null, "Cursor": null, "Properties": { "Values": { - "111": { + "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": 1073741828, + "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": {} + }, + { + "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": 0, + "Green": 255, + "Red": 0, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 0, + "Green": 0, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 127, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { "Kind": 0, - "MasterPropertyId": 111, + "MasterPropertyId": 20, "UnsignedValue": 1, "IntegerValue": 0, "FloatValue": 0, @@ -7540,10 +7724,10 @@ "ArrayValue": [], "StructValue": {} }, - "104": { - "Kind": 1, - "MasterPropertyId": 104, - "UnsignedValue": 0, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -7568,14 +7752,26 @@ }, "ArrayValue": [], "StructValue": {} - }, - "106": { - "Kind": 1, - "MasterPropertyId": 106, + } + } + } + }, + "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": true, + "BoolValue": false, "StringInfoValue": { "Token": 0, "StringId": 0, @@ -7595,7 +7791,37 @@ "Y": 0, "Z": 0 }, - "ArrayValue": [], + "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": {} } } @@ -7603,142 +7829,18 @@ } }, "DefaultStateId": 0, - "FontDid": 0, + "FontDid": 1073741828, "HJustify": 1, "VJustify": 1, - "FontColor": null, - "StateMedia": { - "": { - "Item1": 100667814, - "Item2": 1 - } + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 }, + "StateMedia": {}, "StateCursors": {}, "DefaultStateName": "", - "Children": [ - { - "Id": 2, - "Type": 3, - "X": 0, - "Y": 1, - "Width": 240, - "Height": 15, - "OriginalParentWidth": 240, - "OriginalParentHeight": 17, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 1, - "Right": 1, - "Bottom": 1, - "ReadOrder": 1, - "ZLevel": 0, - "States": { - "4294967295": { - "Id": 4294967295, - "Name": "", - "PassToChildren": false, - "IncorporationFlags": 30, - "Image": { - "File": 100667274, - "DrawMode": 1 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 0, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "": { - "Item1": 100667274, - "Item2": 1 - } - }, - "StateCursors": {}, - "DefaultStateName": "", - "Children": [] - } - ] - }, - { - "Id": 268436971, - "Type": 1, - "X": 260, - "Y": 0, - "Width": 30, - "Height": 26, - "OriginalParentWidth": 300, - "OriginalParentHeight": 55, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 2, - "Right": 2, - "Bottom": 1, - "ReadOrder": 7, - "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": 100692267, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - }, - "13": { - "Id": 13, - "Name": "Ghosted", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100692268, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 13, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100692267, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100692268, - "Item2": 3 - } - }, - "StateCursors": {}, - "DefaultStateName": "Ghosted", "Children": [] }, { @@ -7946,7 +8048,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -8078,7 +8180,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -8296,7 +8398,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -8428,7 +8530,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -8440,6 +8542,341 @@ "StateCursors": {}, "DefaultStateName": "", "Children": [] + }, + { + "Id": 268436038, + "Type": 1, + "X": 260, + "Y": 26, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 7, + "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": 100682934, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682935, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682934, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100682935, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] + }, + { + "Id": 268436039, + "Type": 7, + "X": 5, + "Y": 20, + "Width": 240, + "Height": 17, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100667814, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "111": { + "Kind": 0, + "MasterPropertyId": 111, + "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": {} + }, + "104": { + "Kind": 1, + "MasterPropertyId": 104, + "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": {} + }, + "106": { + "Kind": 1, + "MasterPropertyId": 106, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100667814, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 2, + "Type": 3, + "X": 0, + "Y": 1, + "Width": 240, + "Height": 15, + "OriginalParentWidth": 240, + "OriginalParentHeight": 17, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100667274, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100667274, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268436971, + "Type": 1, + "X": 260, + "Y": 0, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 8, + "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": 100692267, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100692268, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100692267, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100692268, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] } ] }, @@ -8818,7 +9255,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -10916,7 +11353,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -11826,7 +12263,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -15188,7 +15625,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -16325,7 +16762,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -17056,7 +17493,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -17188,7 +17625,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -17406,7 +17843,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -17538,7 +17975,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -17552,96 +17989,20 @@ "Children": [] }, { - "Id": 268436038, - "Type": 1, - "X": 260, - "Y": 26, - "Width": 30, - "Height": 26, - "OriginalParentWidth": 300, - "OriginalParentHeight": 55, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 2, - "Right": 2, - "Bottom": 1, - "ReadOrder": 5, - "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": 100682934, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - }, - "13": { - "Id": 13, - "Name": "Ghosted", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100682935, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 13, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100682934, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100682935, - "Item2": 3 - } - }, - "StateCursors": {}, - "DefaultStateName": "Ghosted", - "Children": [] - }, - { - "Id": 268436971, - "Type": 1, - "X": 260, + "Id": 268436046, + "Type": 12, + "X": 0, "Y": 0, - "Width": 30, - "Height": 26, + "Width": 300, + "Height": 20, "OriginalParentWidth": 300, "OriginalParentHeight": 55, "HasOriginalParentSize": true, "Left": 1, - "Top": 2, - "Right": 2, + "Top": 1, + "Right": 1, "Bottom": 1, - "ReadOrder": 6, + "ReadOrder": 1, "ZLevel": 0, "States": { "4294967295": { @@ -17652,21 +18013,329 @@ "Image": null, "Cursor": null, "Properties": { - "Values": {} - } - }, - "1": { - "Id": 1, - "Name": "Normal", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100692267, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} + "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": 1073741828, + "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": {} + }, + { + "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": 0, + "Green": 255, + "Red": 0, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 0, + "Green": 0, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 127, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } } }, "13": { @@ -17674,33 +18343,86 @@ "Name": "Ghosted", "PassToChildren": false, "IncorporationFlags": 0, - "Image": { - "File": 100692268, - "DrawMode": 3 - }, + "Image": null, "Cursor": null, "Properties": { - "Values": {} + "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": 13, - "FontDid": 0, + "DefaultStateId": 0, + "FontDid": 1073741828, "HJustify": 1, "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100692267, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100692268, - "Item2": 3 - } + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 }, + "StateMedia": {}, "StateCursors": {}, - "DefaultStateName": "Ghosted", + "DefaultStateName": "", "Children": [] }, { @@ -17908,7 +18630,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -18040,7 +18762,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -18258,7 +18980,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -18390,7 +19112,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -18402,6 +19124,158 @@ "StateCursors": {}, "DefaultStateName": "", "Children": [] + }, + { + "Id": 268436038, + "Type": 1, + "X": 260, + "Y": 26, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 6, + "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": 100682934, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682935, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682934, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100682935, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] + }, + { + "Id": 268436971, + "Type": 1, + "X": 260, + "Y": 0, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 7, + "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": 100692267, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100692268, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100692267, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100692268, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] } ] } @@ -18984,7 +19858,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -19116,7 +19990,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -19334,7 +20208,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -19466,7 +20340,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -19480,88 +20354,12 @@ "Children": [] }, { - "Id": 268436038, - "Type": 1, - "X": 260, - "Y": 26, - "Width": 30, - "Height": 26, - "OriginalParentWidth": 300, - "OriginalParentHeight": 55, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 2, - "Right": 2, - "Bottom": 1, - "ReadOrder": 6, - "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": 100682934, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - }, - "13": { - "Id": 13, - "Name": "Ghosted", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100682935, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 13, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100682934, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100682935, - "Item2": 3 - } - }, - "StateCursors": {}, - "DefaultStateName": "Ghosted", - "Children": [] - }, - { - "Id": 268436039, - "Type": 7, - "X": 5, - "Y": 20, - "Width": 240, - "Height": 17, + "Id": 268436046, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 300, + "Height": 20, "OriginalParentWidth": 300, "OriginalParentHeight": 55, "HasOriginalParentSize": true, @@ -19577,16 +20375,276 @@ "Name": "", "PassToChildren": false, "IncorporationFlags": 30, - "Image": { - "File": 100667814, - "DrawMode": 1 - }, + "Image": null, "Cursor": null, "Properties": { "Values": { - "111": { + "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": 1073741828, + "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": {} + }, + { + "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": 0, + "Green": 255, + "Red": 0, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 0, + "Green": 0, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 127, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { "Kind": 0, - "MasterPropertyId": 111, + "MasterPropertyId": 20, "UnsignedValue": 1, "IntegerValue": 0, "FloatValue": 0, @@ -19613,10 +20671,10 @@ "ArrayValue": [], "StructValue": {} }, - "104": { - "Kind": 1, - "MasterPropertyId": 104, - "UnsignedValue": 0, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -19641,14 +20699,26 @@ }, "ArrayValue": [], "StructValue": {} - }, - "106": { - "Kind": 1, - "MasterPropertyId": 106, + } + } + } + }, + "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": true, + "BoolValue": false, "StringInfoValue": { "Token": 0, "StringId": 0, @@ -19668,7 +20738,37 @@ "Y": 0, "Z": 0 }, - "ArrayValue": [], + "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": {} } } @@ -19676,142 +20776,18 @@ } }, "DefaultStateId": 0, - "FontDid": 0, + "FontDid": 1073741828, "HJustify": 1, "VJustify": 1, - "FontColor": null, - "StateMedia": { - "": { - "Item1": 100667814, - "Item2": 1 - } + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 }, + "StateMedia": {}, "StateCursors": {}, "DefaultStateName": "", - "Children": [ - { - "Id": 2, - "Type": 3, - "X": 0, - "Y": 1, - "Width": 240, - "Height": 15, - "OriginalParentWidth": 240, - "OriginalParentHeight": 17, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 1, - "Right": 1, - "Bottom": 1, - "ReadOrder": 1, - "ZLevel": 0, - "States": { - "4294967295": { - "Id": 4294967295, - "Name": "", - "PassToChildren": false, - "IncorporationFlags": 30, - "Image": { - "File": 100667274, - "DrawMode": 1 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 0, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "": { - "Item1": 100667274, - "Item2": 1 - } - }, - "StateCursors": {}, - "DefaultStateName": "", - "Children": [] - } - ] - }, - { - "Id": 268436971, - "Type": 1, - "X": 260, - "Y": 0, - "Width": 30, - "Height": 26, - "OriginalParentWidth": 300, - "OriginalParentHeight": 55, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 2, - "Right": 2, - "Bottom": 1, - "ReadOrder": 7, - "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": 100692267, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - }, - "13": { - "Id": 13, - "Name": "Ghosted", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100692268, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 13, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100692267, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100692268, - "Item2": 3 - } - }, - "StateCursors": {}, - "DefaultStateName": "Ghosted", "Children": [] }, { @@ -20019,7 +20995,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -20151,7 +21127,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -20369,7 +21345,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -20501,7 +21477,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -20513,6 +21489,341 @@ "StateCursors": {}, "DefaultStateName": "", "Children": [] + }, + { + "Id": 268436038, + "Type": 1, + "X": 260, + "Y": 26, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 7, + "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": 100682934, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682935, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682934, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100682935, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] + }, + { + "Id": 268436039, + "Type": 7, + "X": 5, + "Y": 20, + "Width": 240, + "Height": 17, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100667814, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "111": { + "Kind": 0, + "MasterPropertyId": 111, + "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": {} + }, + "104": { + "Kind": 1, + "MasterPropertyId": 104, + "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": {} + }, + "106": { + "Kind": 1, + "MasterPropertyId": 106, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100667814, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 2, + "Type": 3, + "X": 0, + "Y": 1, + "Width": 240, + "Height": 15, + "OriginalParentWidth": 240, + "OriginalParentHeight": 17, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100667274, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100667274, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268436971, + "Type": 1, + "X": 260, + "Y": 0, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 8, + "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": 100692267, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100692268, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100692267, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100692268, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] } ] }, @@ -20891,7 +22202,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -22989,7 +24300,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -23899,7 +25210,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -27261,7 +28572,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -28398,7 +29709,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -29129,7 +30440,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -29261,7 +30572,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -29479,7 +30790,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -29611,7 +30922,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -29625,96 +30936,20 @@ "Children": [] }, { - "Id": 268436038, - "Type": 1, - "X": 260, - "Y": 26, - "Width": 30, - "Height": 26, - "OriginalParentWidth": 300, - "OriginalParentHeight": 55, - "HasOriginalParentSize": true, - "Left": 1, - "Top": 2, - "Right": 2, - "Bottom": 1, - "ReadOrder": 5, - "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": 100682934, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - }, - "13": { - "Id": 13, - "Name": "Ghosted", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100682935, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} - } - } - }, - "DefaultStateId": 13, - "FontDid": 0, - "HJustify": 1, - "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100682934, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100682935, - "Item2": 3 - } - }, - "StateCursors": {}, - "DefaultStateName": "Ghosted", - "Children": [] - }, - { - "Id": 268436971, - "Type": 1, - "X": 260, + "Id": 268436046, + "Type": 12, + "X": 0, "Y": 0, - "Width": 30, - "Height": 26, + "Width": 300, + "Height": 20, "OriginalParentWidth": 300, "OriginalParentHeight": 55, "HasOriginalParentSize": true, "Left": 1, - "Top": 2, - "Right": 2, + "Top": 1, + "Right": 1, "Bottom": 1, - "ReadOrder": 6, + "ReadOrder": 1, "ZLevel": 0, "States": { "4294967295": { @@ -29725,21 +30960,329 @@ "Image": null, "Cursor": null, "Properties": { - "Values": {} - } - }, - "1": { - "Id": 1, - "Name": "Normal", - "PassToChildren": false, - "IncorporationFlags": 0, - "Image": { - "File": 100692267, - "DrawMode": 3 - }, - "Cursor": null, - "Properties": { - "Values": {} + "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": 1073741828, + "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": {} + }, + { + "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": 0, + "Green": 255, + "Red": 0, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 0, + "Green": 0, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + { + "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": 127, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } } }, "13": { @@ -29747,33 +31290,86 @@ "Name": "Ghosted", "PassToChildren": false, "IncorporationFlags": 0, - "Image": { - "File": 100692268, - "DrawMode": 3 - }, + "Image": null, "Cursor": null, "Properties": { - "Values": {} + "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": 13, - "FontDid": 0, + "DefaultStateId": 0, + "FontDid": 1073741828, "HJustify": 1, "VJustify": 1, - "FontColor": null, - "StateMedia": { - "Normal": { - "Item1": 100692267, - "Item2": 3 - }, - "Ghosted": { - "Item1": 100692268, - "Item2": 3 - } + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 }, + "StateMedia": {}, "StateCursors": {}, - "DefaultStateName": "Ghosted", + "DefaultStateName": "", "Children": [] }, { @@ -29981,7 +31577,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 2, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -30113,7 +31709,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -30331,7 +31927,7 @@ "20": { "Kind": 0, "MasterPropertyId": 20, - "UnsignedValue": 1, + "UnsignedValue": 3, "IntegerValue": 0, "FloatValue": 0, "BoolValue": false, @@ -30463,7 +32059,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 2, "VJustify": 1, "FontColor": { "X": 1, @@ -30475,6 +32071,158 @@ "StateCursors": {}, "DefaultStateName": "", "Children": [] + }, + { + "Id": 268436038, + "Type": 1, + "X": 260, + "Y": 26, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 6, + "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": 100682934, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682935, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682934, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100682935, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] + }, + { + "Id": 268436971, + "Type": 1, + "X": 260, + "Y": 0, + "Width": 30, + "Height": 26, + "OriginalParentWidth": 300, + "OriginalParentHeight": 55, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 7, + "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": 100692267, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100692268, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100692267, + "Item2": 3 + }, + "Ghosted": { + "Item1": 100692268, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [] } ] } diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json index a2324378..260fe152 100644 --- a/tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/chat_21000006.json @@ -1097,7 +1097,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741825, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -3320,7 +3320,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 2, "FontColor": { "X": 0.8, @@ -6248,7 +6248,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741824, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/combat_21000073.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/combat_21000073.json index e18c55c4..75d7d04f 100644 --- a/tests/AcDream.App.Tests/UI/Layout/fixtures/combat_21000073.json +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/combat_21000073.json @@ -2035,7 +2035,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741825, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -19338,7 +19338,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741826, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -20278,7 +20278,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741826, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -20859,7 +20859,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741826, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, @@ -21469,7 +21469,7 @@ }, "DefaultStateId": 0, "FontDid": 1073741826, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/dialogs_2100003C.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/dialogs_2100003C.json index 7df33b7d..d19d45f1 100644 --- a/tests/AcDream.App.Tests/UI/Layout/fixtures/dialogs_2100003C.json +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/dialogs_2100003C.json @@ -287,6 +287,382 @@ "StateCursors": {}, "DefaultStateName": "", "Children": [ + { + "Id": 268435977, + "Type": 3, + "X": 0, + "Y": 13, + "Width": 4, + "Height": 53, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 1, + "ReadOrder": 6, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687166, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687166, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435978, + "Type": 3, + "X": 74, + "Y": 13, + "Width": 4, + "Height": 53, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 7, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687166, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687166, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435979, + "Type": 3, + "X": 13, + "Y": 73, + "Width": 52, + "Height": 6, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 8, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687165, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687165, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436145, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 13, + "Height": 13, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687161, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687161, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436146, + "Type": 3, + "X": 65, + "Y": 0, + "Width": 13, + "Height": 13, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687162, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687162, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436147, + "Type": 3, + "X": 0, + "Y": 65, + "Width": 13, + "Height": 14, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687163, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687163, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436148, + "Type": 3, + "X": 65, + "Y": 65, + "Width": 13, + "Height": 14, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 4, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687164, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687164, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436149, + "Type": 3, + "X": 13, + "Y": 0, + "Width": 52, + "Height": 6, + "OriginalParentWidth": 78, + "OriginalParentHeight": 79, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 5, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687165, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687165, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, { "Id": 268436271, "Type": 3, @@ -301,7 +677,7 @@ "Top": 2, "Right": 3, "Bottom": 1, - "ReadOrder": 2, + "ReadOrder": 10, "ZLevel": 0, "States": { "4294967295": { @@ -2069,7 +2445,7 @@ "Top": 1, "Right": 1, "Bottom": 1, - "ReadOrder": 1, + "ReadOrder": 9, "ZLevel": 0, "States": { "4294967295": { diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_negative_2100001B.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_negative_2100001B.json new file mode 100644 index 00000000..2899d29d --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_negative_2100001B.json @@ -0,0 +1,3295 @@ +{ + "Id": 268435745, + "Type": 268435483, + "X": 0, + "Y": 0, + "Width": 300, + "Height": 362, + "OriginalParentWidth": 0, + "OriginalParentHeight": 0, + "HasOriginalParentSize": false, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "88": { + "Kind": 0, + "MasterPropertyId": 88, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "268435468": { + "Kind": 0, + "MasterPropertyId": 268435468, + "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": {} + }, + "87": { + "Kind": 0, + "MasterPropertyId": 87, + "UnsignedValue": 268435463, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435744, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 276, + "Height": 25, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100683002, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "23": { + "Kind": 5, + "MasterPropertyId": 23, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 224554165, + "TableId": 587202561, + "Override": 0, + "English": 1, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 1, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": { + "": { + "Item1": 100683002, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435747, + "Type": 5, + "X": 0, + "Y": 25, + "Width": 300, + "Height": 249, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "100": { + "Kind": 7, + "MasterPropertyId": 100, + "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": 8, + "MasterPropertyId": 101, + "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": { + "99": { + "Kind": 2, + "MasterPropertyId": 99, + "UnsignedValue": 553648155, + "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": {} + }, + "98": { + "Kind": 0, + "MasterPropertyId": 98, + "UnsignedValue": 268435752, + "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": {} + }, + "114": { + "Kind": 0, + "MasterPropertyId": 114, + "UnsignedValue": 268435748, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435748, + "Type": 11, + "X": 280, + "Y": 27, + "Width": 16, + "Height": 249, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 4, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100682847, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "121": { + "Kind": 1, + "MasterPropertyId": 121, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "137": { + "Kind": 4, + "MasterPropertyId": 137, + "UnsignedValue": 0, + "IntegerValue": 16, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "130": { + "Kind": 1, + "MasterPropertyId": 130, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "119": { + "Kind": 0, + "MasterPropertyId": 119, + "UnsignedValue": 268435570, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "131": { + "Kind": 1, + "MasterPropertyId": 131, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "120": { + "Kind": 0, + "MasterPropertyId": 120, + "UnsignedValue": 268435569, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100682847, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 1, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 56, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [ + { + "Id": 268436324, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682848, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682849, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682850, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682848, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682849, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682850, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436325, + "Type": 3, + "X": 0, + "Y": 3, + "Width": 16, + "Height": 10, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682851, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682852, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682853, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682851, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682852, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682853, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436326, + "Type": 3, + "X": 0, + "Y": 13, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682854, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682855, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682856, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682854, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682855, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682856, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435569, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682859, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682857, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682858, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682857, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682858, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682859, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268435570, + "Type": 1, + "X": 0, + "Y": 32, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682862, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682860, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682861, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682860, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682861, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682862, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + } + ] + }, + { + "Id": 268435749, + "Type": 3, + "X": 0, + "Y": 274, + "Width": 300, + "Height": 88, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 5, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100668309, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100668309, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435750, + "Type": 12, + "X": 0, + "Y": 8, + "Width": 280, + "Height": 78, + "OriginalParentWidth": 300, + "OriginalParentHeight": 88, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "114": { + "Kind": 0, + "MasterPropertyId": 114, + "UnsignedValue": 268435751, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 1, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435751, + "Type": 11, + "X": 280, + "Y": 0, + "Width": 16, + "Height": 88, + "OriginalParentWidth": 300, + "OriginalParentHeight": 88, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100682847, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "121": { + "Kind": 1, + "MasterPropertyId": 121, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "137": { + "Kind": 4, + "MasterPropertyId": 137, + "UnsignedValue": 0, + "IntegerValue": 16, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "130": { + "Kind": 1, + "MasterPropertyId": 130, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "119": { + "Kind": 0, + "MasterPropertyId": 119, + "UnsignedValue": 268435570, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "131": { + "Kind": 1, + "MasterPropertyId": 131, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "120": { + "Kind": 0, + "MasterPropertyId": 120, + "UnsignedValue": 268435569, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100682847, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 1, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 56, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [ + { + "Id": 268436324, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682848, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682849, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682850, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682848, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682849, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682850, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436325, + "Type": 3, + "X": 0, + "Y": 3, + "Width": 16, + "Height": 10, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682851, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682852, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682853, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682851, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682852, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682853, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436326, + "Type": 3, + "X": 0, + "Y": 13, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682854, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682855, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682856, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682854, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682855, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682856, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435569, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682859, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682857, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682858, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682857, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682858, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682859, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268435570, + "Type": 1, + "X": 0, + "Y": 32, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682862, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682860, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682861, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682860, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682861, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682862, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + } + ] + } + ] + }, + { + "Id": 268435708, + "Type": 1, + "X": 276, + "Y": 0, + "Width": 24, + "Height": 25, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435463, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668308, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668307, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100668307, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100668308, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + } + ] +} \ No newline at end of file diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_positive_2100001B.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_positive_2100001B.json new file mode 100644 index 00000000..11721e7a --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/effects_positive_2100001B.json @@ -0,0 +1,3324 @@ +{ + "Id": 268435743, + "Type": 268435483, + "X": 0, + "Y": 0, + "Width": 300, + "Height": 362, + "OriginalParentWidth": 0, + "OriginalParentHeight": 0, + "HasOriginalParentSize": false, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 0, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "88": { + "Kind": 0, + "MasterPropertyId": 88, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "268435468": { + "Kind": 0, + "MasterPropertyId": 268435468, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "87": { + "Kind": 0, + "MasterPropertyId": 87, + "UnsignedValue": 268435462, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "268435529": { + "Kind": 1, + "MasterPropertyId": 268435529, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435744, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 276, + "Height": 25, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100683002, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "23": { + "Kind": 5, + "MasterPropertyId": 23, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 3828901, + "TableId": 587202561, + "Override": 0, + "English": 1, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 1, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": { + "": { + "Item1": 100683002, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435747, + "Type": 5, + "X": 0, + "Y": 25, + "Width": 300, + "Height": 249, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "100": { + "Kind": 7, + "MasterPropertyId": 100, + "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": 8, + "MasterPropertyId": 101, + "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": { + "99": { + "Kind": 2, + "MasterPropertyId": 99, + "UnsignedValue": 553648155, + "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": {} + }, + "98": { + "Kind": 0, + "MasterPropertyId": 98, + "UnsignedValue": 268435752, + "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": {} + }, + "114": { + "Kind": 0, + "MasterPropertyId": 114, + "UnsignedValue": 268435748, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435748, + "Type": 11, + "X": 280, + "Y": 27, + "Width": 16, + "Height": 249, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 4, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100682847, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "121": { + "Kind": 1, + "MasterPropertyId": 121, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "137": { + "Kind": 4, + "MasterPropertyId": 137, + "UnsignedValue": 0, + "IntegerValue": 16, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "130": { + "Kind": 1, + "MasterPropertyId": 130, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "119": { + "Kind": 0, + "MasterPropertyId": 119, + "UnsignedValue": 268435570, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "131": { + "Kind": 1, + "MasterPropertyId": 131, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "120": { + "Kind": 0, + "MasterPropertyId": 120, + "UnsignedValue": 268435569, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100682847, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 1, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 56, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [ + { + "Id": 268436324, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682848, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682849, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682850, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682848, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682849, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682850, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436325, + "Type": 3, + "X": 0, + "Y": 3, + "Width": 16, + "Height": 10, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682851, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682852, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682853, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682851, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682852, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682853, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436326, + "Type": 3, + "X": 0, + "Y": 13, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682854, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682855, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682856, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682854, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682855, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682856, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435569, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682859, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682857, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682858, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682857, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682858, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682859, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268435570, + "Type": 1, + "X": 0, + "Y": 32, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682862, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682860, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682861, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682860, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682861, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682862, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + } + ] + }, + { + "Id": 268435749, + "Type": 3, + "X": 0, + "Y": 274, + "Width": 300, + "Height": 88, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 5, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100668309, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100668309, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435750, + "Type": 12, + "X": 0, + "Y": 8, + "Width": 280, + "Height": 78, + "OriginalParentWidth": 300, + "OriginalParentHeight": 88, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "114": { + "Kind": 0, + "MasterPropertyId": 114, + "UnsignedValue": 268435751, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 1, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435751, + "Type": 11, + "X": 280, + "Y": 0, + "Width": 16, + "Height": 88, + "OriginalParentWidth": 300, + "OriginalParentHeight": 88, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100682847, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": { + "121": { + "Kind": 1, + "MasterPropertyId": 121, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "137": { + "Kind": 4, + "MasterPropertyId": 137, + "UnsignedValue": 0, + "IntegerValue": 16, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "130": { + "Kind": 1, + "MasterPropertyId": 130, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "119": { + "Kind": 0, + "MasterPropertyId": 119, + "UnsignedValue": 268435570, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "131": { + "Kind": 1, + "MasterPropertyId": 131, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "120": { + "Kind": 0, + "MasterPropertyId": 120, + "UnsignedValue": 268435569, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100682847, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 1, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 56, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [ + { + "Id": 268436324, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682848, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682849, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682850, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682848, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682849, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682850, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436325, + "Type": 3, + "X": 0, + "Y": 3, + "Width": 16, + "Height": 10, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682851, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682852, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682853, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682851, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682852, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682853, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436326, + "Type": 3, + "X": 0, + "Y": 13, + "Width": 16, + "Height": 3, + "OriginalParentWidth": 16, + "OriginalParentHeight": 16, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682854, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682855, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682856, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682854, + "Item2": 3 + }, + "Normal_rollover": { + "Item1": 100682855, + "Item2": 3 + }, + "Normal_pressed": { + "Item1": 100682856, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435569, + "Type": 1, + "X": 0, + "Y": 0, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682859, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682857, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682858, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682857, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682858, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682859, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268435570, + "Type": 1, + "X": 0, + "Y": 32, + "Width": 16, + "Height": 16, + "OriginalParentWidth": 16, + "OriginalParentHeight": 48, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 62, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "15": { + "Kind": 1, + "MasterPropertyId": 15, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "16": { + "Kind": 3, + "MasterPropertyId": 16, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.5, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "17": { + "Kind": 3, + "MasterPropertyId": 17, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0.1, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "19": { + "Kind": 1, + "MasterPropertyId": 19, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682862, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682860, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "2": { + "Id": 2, + "Name": "Normal_rollover", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682861, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100682860, + "Item2": 1 + }, + "Normal_rollover": { + "Item1": 100682861, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100682862, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + } + ] + } + ] + }, + { + "Id": 268435708, + "Type": 1, + "X": 276, + "Y": 0, + "Width": 24, + "Height": 25, + "OriginalParentWidth": 300, + "OriginalParentHeight": 362, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435462, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668308, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100668307, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100668307, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100668308, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + } + ] +} \ No newline at end of file diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/indicators_21000071.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/indicators_21000071.json new file mode 100644 index 00000000..4d70d6b8 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/indicators_21000071.json @@ -0,0 +1,2719 @@ +{ + "Id": 268437008, + "Type": 268435538, + "X": 0, + "Y": 0, + "Width": 150, + "Height": 30, + "OriginalParentWidth": 0, + "OriginalParentHeight": 0, + "HasOriginalParentSize": false, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 0, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": true, + "IncorporationFlags": 31, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "60": { + "Kind": 4, + "MasterPropertyId": 60, + "UnsignedValue": 0, + "IntegerValue": 30, + "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": {} + }, + "61": { + "Kind": 4, + "MasterPropertyId": 61, + "UnsignedValue": 0, + "IntegerValue": 150, + "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": {} + }, + "62": { + "Kind": 4, + "MasterPropertyId": 62, + "UnsignedValue": 0, + "IntegerValue": 30, + "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": {} + }, + "63": { + "Kind": 4, + "MasterPropertyId": 63, + "UnsignedValue": 0, + "IntegerValue": 150, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268437071, + "Type": 2, + "X": 0, + "Y": 25, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 13, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688169, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": { + "43": { + "Kind": 1, + "MasterPropertyId": 43, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688169, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437072, + "Type": 2, + "X": 5, + "Y": 25, + "Width": 140, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 14, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688172, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688172, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437073, + "Type": 2, + "X": 145, + "Y": 25, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 15, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688169, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": { + "44": { + "Kind": 1, + "MasterPropertyId": 44, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688169, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437074, + "Type": 2, + "X": 145, + "Y": 5, + "Width": 5, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 16, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688173, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": { + "44": { + "Kind": 1, + "MasterPropertyId": 44, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688173, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435699, + "Type": 268435460, + "X": 105, + "Y": 5, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 22, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435466, + "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": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693157, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693157, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693158, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100693157, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100693157, + "Item2": 1 + }, + "Ghosted": { + "Item1": 100693158, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [ + { + "Id": 268435698, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 20, + "OriginalParentHeight": 20, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100682984, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal_pressed": { + "Item1": 100682984, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435700, + "Type": 268435462, + "X": 65, + "Y": 5, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 20, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435468, + "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": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693152, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693152, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693153, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100693152, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100693152, + "Item2": 1 + }, + "Ghosted": { + "Item1": 100693153, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [ + { + "Id": 268435698, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 20, + "OriginalParentHeight": 20, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693147, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal_pressed": { + "Item1": 100693147, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435701, + "Type": 268435458, + "X": 25, + "Y": 5, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 18, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "268435468": { + "Kind": 0, + "MasterPropertyId": 268435468, + "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": {} + }, + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435462, + "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": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693148, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693148, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693149, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100693148, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100693148, + "Item2": 1 + }, + "Ghosted": { + "Item1": 100693149, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [ + { + "Id": 268435698, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 20, + "OriginalParentHeight": 20, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693147, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal_pressed": { + "Item1": 100693147, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268435702, + "Type": 268435458, + "X": 45, + "Y": 5, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 19, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "268435468": { + "Kind": 0, + "MasterPropertyId": 268435468, + "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": {} + }, + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435463, + "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": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693150, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": { + "File": 100693150, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693151, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 13, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100693150, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100693150, + "Item2": 1 + }, + "Ghosted": { + "Item1": 100693151, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Ghosted", + "Children": [ + { + "Id": 268435698, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 20, + "OriginalParentHeight": 20, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693147, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal_pressed": { + "Item1": 100693147, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268437059, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693187, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693187, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435703, + "Type": 268435457, + "X": 85, + "Y": 5, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 21, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435461, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "14": { + "Id": 14, + "Name": "Unencumbered", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693154, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "15": { + "Id": 15, + "Name": "Encumbered", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693155, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "16": { + "Id": 16, + "Name": "Heavily_encumbered", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693156, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 14, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Unencumbered": { + "Item1": 100693154, + "Item2": 1 + }, + "Encumbered": { + "Item1": 100693155, + "Item2": 1 + }, + "Heavily_encumbered": { + "Item1": 100693156, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Unencumbered", + "Children": [ + { + "Id": 268435698, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 20, + "OriginalParentHeight": 20, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693147, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal_pressed": { + "Item1": 100693147, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268437060, + "Type": 3, + "X": 5, + "Y": 0, + "Width": 140, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693183, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693183, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435704, + "Type": 268435459, + "X": 5, + "Y": 5, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 17, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "18": { + "Kind": 0, + "MasterPropertyId": 18, + "UnsignedValue": 268435465, + "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": {} + } + } + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": true, + "IncorporationFlags": 1, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "17": { + "Id": 17, + "Name": "Connection_good", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693144, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "18": { + "Id": 18, + "Name": "Connection_uncertain", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693145, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "19": { + "Id": 19, + "Name": "Connection_bad", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693146, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "20": { + "Id": 20, + "Name": "Connection_disconnected", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693146, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Connection_good": { + "Item1": 100693144, + "Item2": 1 + }, + "Connection_uncertain": { + "Item1": 100693145, + "Item2": 1 + }, + "Connection_bad": { + "Item1": 100693146, + "Item2": 1 + }, + "Connection_disconnected": { + "Item1": 100693146, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268435698, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 20, + "OriginalParentHeight": 20, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 1, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693147, + "DrawMode": 3 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal_pressed": { + "Item1": 100693147, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] + }, + { + "Id": 268437061, + "Type": 3, + "X": 145, + "Y": 0, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693188, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693188, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437062, + "Type": 3, + "X": 0, + "Y": 5, + "Width": 5, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 1, + "ReadOrder": 4, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693184, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693184, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268435706, + "Type": 1, + "X": 125, + "Y": 5, + "Width": 20, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 23, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "3": { + "Id": 3, + "Name": "Normal_pressed", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693170, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "1": { + "Id": 1, + "Name": "Normal", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100693169, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 1, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "Normal": { + "Item1": 100693169, + "Item2": 1 + }, + "Normal_pressed": { + "Item1": 100693170, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "Normal", + "Children": [] + }, + { + "Id": 268437063, + "Type": 3, + "X": 0, + "Y": 25, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 2, + "Bottom": 1, + "ReadOrder": 5, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693189, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693189, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437064, + "Type": 3, + "X": 5, + "Y": 25, + "Width": 140, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 6, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693185, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693185, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437065, + "Type": 3, + "X": 145, + "Y": 25, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 2, + "Right": 1, + "Bottom": 1, + "ReadOrder": 7, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693190, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693190, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437066, + "Type": 3, + "X": 145, + "Y": 5, + "Width": 5, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 1, + "ReadOrder": 8, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100693186, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "59": { + "Kind": 1, + "MasterPropertyId": 59, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100693186, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437067, + "Type": 2, + "X": 0, + "Y": 0, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 2, + "ReadOrder": 9, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688169, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": { + "43": { + "Kind": 1, + "MasterPropertyId": 43, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688169, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437068, + "Type": 2, + "X": 5, + "Y": 0, + "Width": 140, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 10, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688170, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688170, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437069, + "Type": 2, + "X": 145, + "Y": 0, + "Width": 5, + "Height": 5, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 2, + "Top": 1, + "Right": 1, + "Bottom": 2, + "ReadOrder": 11, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688169, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": { + "44": { + "Kind": 1, + "MasterPropertyId": 44, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688169, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268437070, + "Type": 2, + "X": 0, + "Y": 5, + "Width": 5, + "Height": 20, + "OriginalParentWidth": 150, + "OriginalParentHeight": 30, + "HasOriginalParentSize": true, + "Left": 1, + "Top": 1, + "Right": 2, + "Bottom": 1, + "ReadOrder": 12, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100688171, + "DrawMode": 1 + }, + "Cursor": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + }, + "Properties": { + "Values": { + "43": { + "Kind": 1, + "MasterPropertyId": 43, + "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": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100688171, + "Item2": 1 + } + }, + "StateCursors": { + "": { + "File": 100688153, + "HotspotX": 16, + "HotspotY": 16, + "IsValid": true + } + }, + "DefaultStateName": "", + "Children": [] + } + ] +} \ No newline at end of file diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json index ddb15324..434139a6 100644 --- a/tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/inventory_21000023.json @@ -8940,7 +8940,7 @@ }, "DefaultStateId": 1, "FontDid": 1073741826, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json index ccde08a2..3a8edb2c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/paperdoll_21000024.json @@ -8151,7 +8151,7 @@ }, "DefaultStateId": 1, "FontDid": 1073741826, - "HJustify": 1, + "HJustify": 0, "VJustify": 1, "FontColor": { "X": 1, diff --git a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs index 1118b8c4..46df0568 100644 --- a/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs +++ b/tests/AcDream.App.Tests/UI/UiItemSlotTests.cs @@ -55,6 +55,17 @@ public class UiItemSlotTests Assert.True(fired); } + [Fact] + public void CatalogSlot_keeps_catalog_identity_separate_from_object_guid() + { + var slot = new UiCatalogSlot { EntryId = 1234u, CatalogIconTexture = 99u }; + + Assert.Equal(1234u, slot.EntryId); + Assert.Equal(0u, slot.ItemId); + Assert.False(slot.IsDragSource); + Assert.Null(slot.GetDragPayload()); + } + // ── Shortcut number tests ──────────────────────────────────────────────── // Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465). diff --git a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs index 25b39dcc..96b69868 100644 --- a/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs +++ b/tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs @@ -460,8 +460,8 @@ public sealed class GameEventWiringTests public void WireAll_MagicPurgeEnchantments_CallsOnPurgeAll() { var (d, _, _, book, _) = MakeAll(); - book.OnEnchantmentAdded(1, 1, 100f, 0); - book.OnEnchantmentAdded(2, 2, 100f, 0); + book.OnEnchantmentAdded(new(1, 1, 100, 0, Bucket: 1)); + book.OnEnchantmentAdded(new(2, 2, 100, 0, Bucket: 2)); Assert.Equal(2, book.ActiveCount); var env = GameEventEnvelope.TryParse(WrapEnvelope(GameEventType.MagicPurgeEnchantments, Array.Empty())); @@ -470,6 +470,32 @@ public sealed class GameEventWiringTests Assert.Equal(0, book.ActiveCount); } + [Fact] + public void WireAll_MagicUpdateEnchantment_ConvertsRelativeTimesAndClassifiesBucket() + { + var dispatcher = new GameEventDispatcher(); + var book = new Spellbook(); + GameEventWiring.WireAll( + dispatcher, + new ClientObjectTable(), + new CombatState(), + book, + new ChatLog(), + clientTime: () => 100d); + + byte[] payload = BuildEnchantment( + spellId: 42, layer: 3, duration: 60, caster: 0xBEEF, + startTime: 10, lastDegraded: 2, statModType: 0x00008000u); + dispatcher.Dispatch(GameEventEnvelope.TryParse( + WrapEnvelope(GameEventType.MagicUpdateEnchantment, payload))!.Value); + + ActiveEnchantmentRecord record = Assert.Single(book.ActiveEnchantmentSnapshot); + Assert.Equal(110d, record.StartTime); + Assert.Equal(102d, record.LastTimeDegraded); + Assert.Equal(2u, record.Bucket); + Assert.Equal(60d, record.Duration); + } + [Fact] public void WireAll_WeenieError_RoutesToChatLog() { @@ -1064,4 +1090,27 @@ public sealed class GameEventWiringTests Assert.Equal((0x50C4A54Au, 0x948700u), observed); } + private static byte[] BuildEnchantment( + ushort spellId, + ushort layer, + double duration, + uint caster, + double startTime, + double lastDegraded, + uint statModType) + { + byte[] payload = new byte[60]; + int offset = 0; + WriteU16(spellId); WriteU16(layer); WriteU16(3); WriteU16(0); + WriteU32(8); WriteF64(startTime); WriteF64(duration); WriteU32(caster); + WriteF32(0.1f); WriteF32(-1f); WriteF64(lastDegraded); + WriteU32(statModType); WriteU32(7); WriteF32(1.25f); + return payload; + + void WriteU16(ushort value) { BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(offset), value); offset += 2; } + void WriteU32(uint value) { BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(offset), value); offset += 4; } + void WriteF32(float value) { BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(offset), value); offset += 4; } + void WriteF64(double value) { BinaryPrimitives.WriteDoubleLittleEndian(payload.AsSpan(offset), value); offset += 8; } + } + } diff --git a/tests/AcDream.Core.Net.Tests/Messages/CastSpellTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CastSpellTests.cs index f1253ffc..de2875ca 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/CastSpellTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/CastSpellTests.cs @@ -42,37 +42,92 @@ public sealed class CastSpellTests public void ParseMagicUpdateSpell_RoundTrip() { byte[] payload = new byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x3E1u); - Assert.Equal(0x3E1u, GameEvents.ParseMagicUpdateSpell(payload)); + BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x123403E1u); + var parsed = GameEvents.ParseMagicUpdateSpell(payload); + Assert.Equal(0x123403E1u, parsed); } [Fact] public void ParseMagicUpdateEnchantment_RoundTrip() { - byte[] payload = new byte[16]; - BinaryPrimitives.WriteUInt32LittleEndian(payload, 42u); // spellId - BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 7u); // layerId - BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(8), 300.0f); // duration - BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(12), 0xBEEFu); // caster + byte[] payload = BuildEnchantment(spellId: 42, layer: 7, duration: 300, caster: 0xBEEF); var parsed = GameEvents.ParseMagicUpdateEnchantment(payload); Assert.NotNull(parsed); - Assert.Equal(42u, parsed!.Value.SpellId); - Assert.Equal(7u, parsed.Value.LayerId); - Assert.Equal(300f, parsed.Value.Duration, 4); + Assert.Equal((ushort)42, parsed!.Value.SpellId); + Assert.Equal((ushort)7, parsed.Value.Layer); + Assert.Equal(300d, parsed.Value.Duration, 4); Assert.Equal(0xBEEFu, parsed.Value.CasterGuid); + Assert.Equal(0x20u, parsed.Value.StatModType); + Assert.Equal(7u, parsed.Value.StatModKey); + Assert.Equal(1.25f, parsed.Value.StatModValue); } [Fact] public void ParseMagicRemoveEnchantment_RoundTrip() { - byte[] payload = new byte[8]; - BinaryPrimitives.WriteUInt32LittleEndian(payload, 7u); // layerId - BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(4), 42u); // spellId + byte[] payload = new byte[4]; + BinaryPrimitives.WriteUInt16LittleEndian(payload, 42); + BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(2), 7); var parsed = GameEvents.ParseMagicRemoveEnchantment(payload); Assert.NotNull(parsed); - Assert.Equal(7u, parsed!.Value.LayerId); - Assert.Equal(42u, parsed.Value.SpellId); + Assert.Equal((ushort)7, parsed!.Value.Layer); + Assert.Equal((ushort)42, parsed.Value.SpellId); + } + + [Fact] + public void ParseMagicUpdateMultipleEnchantments_ReadsEveryRecord() + { + byte[] first = BuildEnchantment(42, 1, 60, 0xAA); + byte[] second = BuildEnchantment(43, 2, 120, 0xBB); + byte[] payload = new byte[4 + first.Length + second.Length]; + BinaryPrimitives.WriteUInt32LittleEndian(payload, 2); + first.CopyTo(payload, 4); + second.CopyTo(payload, 4 + first.Length); + + var parsed = GameEvents.ParseMagicUpdateMultipleEnchantments(payload); + + Assert.NotNull(parsed); + Assert.Equal(2, parsed!.Count); + Assert.Equal((ushort)42, parsed[0].SpellId); + Assert.Equal((ushort)43, parsed[1].SpellId); + } + + [Fact] + public void ParseMagicUpdateEnchantment_RejectsTruncatedOptionalSpellSetId() + { + byte[] payload = BuildEnchantment(42, 1, 60, 0xAA); + BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(6), 1); + + Assert.Null(GameEvents.ParseMagicUpdateEnchantment(payload)); + } + + [Fact] + public void ParseMagicLayeredSpellList_RejectsTruncation() + { + byte[] payload = new byte[8]; + BinaryPrimitives.WriteUInt32LittleEndian(payload, 2); + BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(4), 42); + BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(6), 1); + + Assert.Null(GameEvents.ParseMagicLayeredSpellList(payload)); + } + + private static byte[] BuildEnchantment( + ushort spellId, ushort layer, double duration, uint caster) + { + byte[] payload = new byte[60]; + int offset = 0; + WriteU16(spellId); WriteU16(layer); WriteU16(3); WriteU16(0); + WriteU32(8); WriteF64(10); WriteF64(duration); WriteU32(caster); + WriteF32(0.1f); WriteF32(-1f); WriteF64(0); + WriteU32(0x20); WriteU32(7); WriteF32(1.25f); + return payload; + + void WriteU16(ushort value) { BinaryPrimitives.WriteUInt16LittleEndian(payload.AsSpan(offset), value); offset += 2; } + void WriteU32(uint value) { BinaryPrimitives.WriteUInt32LittleEndian(payload.AsSpan(offset), value); offset += 4; } + void WriteF32(float value) { BinaryPrimitives.WriteSingleLittleEndian(payload.AsSpan(offset), value); offset += 4; } + void WriteF64(double value) { BinaryPrimitives.WriteDoubleLittleEndian(payload.AsSpan(offset), value); offset += 8; } } } diff --git a/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs b/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs index fdcdbf47..c8fef2c2 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/ClientCommandRequestsTests.cs @@ -147,6 +147,31 @@ public sealed class ClientCommandRequestsTests Assert.Equal(25u, Read(body, 16)); } + [Fact] + public void AddSpellFavorite_MatchesRetailThreeIntegerPayload() + { + byte[] body = ClientCommandRequests.BuildAddSpellFavorite(9u, 42u, 3, 7); + + Assert.Equal(24, body.Length); + Assert.Equal(ClientCommandRequests.AddSpellFavoriteOpcode, Read(body, 8)); + Assert.Equal(42u, Read(body, 12)); + Assert.Equal(3u, Read(body, 16)); + Assert.Equal(7u, Read(body, 20)); + } + + [Fact] + public void RemoveFavoriteAndFilter_MatchRetailPayloads() + { + byte[] remove = ClientCommandRequests.BuildRemoveSpellFavorite(2u, 42u, 6); + byte[] filter = ClientCommandRequests.BuildSpellbookFilter(3u, 0xA5u); + + Assert.Equal(ClientCommandRequests.RemoveSpellFavoriteOpcode, Read(remove, 8)); + Assert.Equal(42u, Read(remove, 12)); + Assert.Equal(6u, Read(remove, 16)); + Assert.Equal(ClientCommandRequests.SpellbookFilterOpcode, Read(filter, 8)); + Assert.Equal(0xA5u, Read(filter, 12)); + } + [Fact] public void LegacyFriendsCommand_IsAControlMessageWithoutGameActionEnvelope() { diff --git a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs index 9f08c413..86e971bb 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/CreateObjectTests.cs @@ -628,6 +628,22 @@ public sealed class CreateObjectTests Assert.Equal(0x50000001u, parsed.Value.PetOwnerId); } + [Fact] + public void WeenieHeader_spellId_isPreservedForCasterEndowment() + { + byte[] body = BuildMinimalCreateObjectWithWeenieHeader( + guid: 0x50000025u, + name: "Orb", + itemType: (uint)ItemType.Caster, + weenieFlags: 0x00400000u, + spellId: 0x0A6E); + + var parsed = CreateObject.TryParse(body); + + Assert.NotNull(parsed); + Assert.Equal(0x0A6Eu, parsed.Value.SpellId); + } + private static byte[] BuildMinimalCreateObjectWithWeenieHeader( uint guid, string name, @@ -673,7 +689,8 @@ public sealed class CreateObjectTests uint materialType = 0, uint cooldownId = 0, double cooldownDuration = 0, - uint petOwnerId = 0) + uint petOwnerId = 0, + ushort spellId = 0) { var bytes = new List(); WriteU32(bytes, CreateObject.Opcode); @@ -760,7 +777,7 @@ public sealed class CreateObjectTests bytes.AddRange(tmp.ToArray()); } if ((weenieFlags & 0x00200000u) != 0) WriteU16(bytes, burden ?? 0); // Burden u16 - if ((weenieFlags & 0x00400000u) != 0) WriteU16(bytes, 0); // Spell u16 + if ((weenieFlags & 0x00400000u) != 0) WriteU16(bytes, spellId); // Spell u16 if ((weenieFlags & 0x02000000u) != 0) WriteU32(bytes, 0); // HouseOwner u32 // HouseRestrictions (0x04000000): not parameterized (zero entries). // Wire: Version(u32) + OpenStatus(u32) + MonarchId(u32) + count(u16) + numBuckets(u16) + entries. diff --git a/tests/AcDream.Core.Net.Tests/Messages/GameEventDispatcherTests.cs b/tests/AcDream.Core.Net.Tests/Messages/GameEventDispatcherTests.cs index 6b3f17ae..f44bb55d 100644 --- a/tests/AcDream.Core.Net.Tests/Messages/GameEventDispatcherTests.cs +++ b/tests/AcDream.Core.Net.Tests/Messages/GameEventDispatcherTests.cs @@ -237,7 +237,8 @@ public sealed class GameEventDispatcherTests public void ParseMagicUpdateSpell_RoundTrip() { byte[] payload = new byte[4]; - BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x3E1u); // Flame Bolt I - Assert.Equal(0x3E1u, GameEvents.ParseMagicUpdateSpell(payload)); + BinaryPrimitives.WriteUInt32LittleEndian(payload, 0x123403E1u); + var parsed = GameEvents.ParseMagicUpdateSpell(payload); + Assert.Equal(0x123403E1u, parsed); } } diff --git a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs index eed761fd..6d924f93 100644 --- a/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs +++ b/tests/AcDream.Core.Net.Tests/PlayerDescriptionParserTests.cs @@ -379,7 +379,7 @@ public sealed class PlayerDescriptionParserTests writer.Write(0u); // option_flags = None — no further sections writer.Write(0xDEADBEEFu); // options1 sentinel - // No more bytes — spellbook_filters is optional (defaults to 0). + // No more bytes — spellbook_filters is optional (retail defaults all on). var parsed = PlayerDescriptionParser.TryParse(sb.ToArray()); Assert.NotNull(parsed); @@ -392,7 +392,7 @@ public sealed class PlayerDescriptionParserTests // pre-existing regression guard if they accidentally consume into // the wrong field's wire bytes. Assert.Equal(0u, parsed.Value.Options2); - Assert.Equal(0u, parsed.Value.SpellbookFilters); + Assert.Equal(0x3FFFu, parsed.Value.SpellbookFilters); Assert.Empty(parsed.Value.HotbarSpells); Assert.Empty(parsed.Value.DesiredComps); Assert.True(parsed.Value.GameplayOptions.IsEmpty); diff --git a/tests/AcDream.Core.Tests/Spells/EnchantmentRegistryProjectionTests.cs b/tests/AcDream.Core.Tests/Spells/EnchantmentRegistryProjectionTests.cs new file mode 100644 index 00000000..af3ec7ec --- /dev/null +++ b/tests/AcDream.Core.Tests/Spells/EnchantmentRegistryProjectionTests.cs @@ -0,0 +1,91 @@ +using AcDream.Core.Spells; + +namespace AcDream.Core.Tests.Spells; + +public sealed class EnchantmentRegistryProjectionTests +{ + [Fact] + public void GetEnchantmentsInEffect_ExcludesVitaeAndCooldown() + { + IReadOnlyList result = + EnchantmentRegistryProjection.GetEnchantmentsInEffect( + [ + Enchantment(1u, 1u, category: 1u), + Enchantment(2u, 4u, category: 2u), + Enchantment(3u, 8u, category: 3u), + Enchantment(4u, 2u, category: 4u), + ]); + + Assert.Equal([1u, 4u], result.Select(record => record.SpellId)); + } + + [Fact] + public void GetEnchantmentsInEffect_CategoryDuel_PrefersHigherPower() + { + IReadOnlyList result = + EnchantmentRegistryProjection.GetEnchantmentsInEffect( + [ + Enchantment(1u, 1u, category: 12u, power: 1u), + Enchantment(2u, 1u, category: 12u, power: 7u), + ]); + + Assert.Equal(2u, Assert.Single(result).SpellId); + } + + [Fact] + public void GetEnchantmentsInEffect_PowerComparisonUsesRetailSignedInt() + { + IReadOnlyList result = + EnchantmentRegistryProjection.GetEnchantmentsInEffect( + [ + Enchantment(1u, 1u, category: 12u, power: 1u), + Enchantment(2u, 1u, category: 12u, power: uint.MaxValue), + ]); + + Assert.Equal(1u, Assert.Single(result).SpellId); + } + + [Fact] + public void GetEnchantmentsInEffect_EqualPower_PrefersLaterStartAndKeepsExactTie() + { + IReadOnlyList result = + EnchantmentRegistryProjection.GetEnchantmentsInEffect( + [ + Enchantment(1u, 1u, category: 12u, power: 7u, start: 10d), + Enchantment(2u, 1u, category: 12u, power: 7u, start: 20d), + Enchantment(3u, 2u, category: 12u, power: 7u, start: 20d), + ]); + + Assert.Equal(2u, Assert.Single(result).SpellId); + } + + [Fact] + public void GetEnchantmentsInEffect_ReplacingWinner_AppendsInRetailOrder() + { + IReadOnlyList result = + EnchantmentRegistryProjection.GetEnchantmentsInEffect( + [ + Enchantment(1u, 1u, category: 10u, power: 1u), + Enchantment(2u, 1u, category: 20u, power: 1u), + Enchantment(3u, 2u, category: 10u, power: 2u), + ]); + + Assert.Equal([2u, 3u], result.Select(record => record.SpellId)); + } + + private static ActiveEnchantmentRecord Enchantment( + uint spellId, + uint bucket, + uint category, + uint power = 1u, + double start = 0d) + => new( + SpellId: spellId, + LayerId: spellId, + Duration: 60d, + CasterGuid: 1u, + Bucket: bucket, + StartTime: start, + SpellCategory: category, + PowerLevel: power); +} diff --git a/tests/AcDream.Core.Tests/Spells/RetailSpellFormulaTests.cs b/tests/AcDream.Core.Tests/Spells/RetailSpellFormulaTests.cs new file mode 100644 index 00000000..9007ffed --- /dev/null +++ b/tests/AcDream.Core.Tests/Spells/RetailSpellFormulaTests.cs @@ -0,0 +1,59 @@ +using AcDream.Core.Spells; + +namespace AcDream.Core.Tests.Spells; + +public sealed class RetailSpellFormulaTests +{ + [Theory] + [InlineData(1u, 1)] + [InlineData(6u, 6)] + [InlineData(0x6Eu, 6)] + [InlineData(0x70u, 7)] + [InlineData(0xC0u, 7)] + [InlineData(0xC1u, 8)] + [InlineData(7u, 0)] + public void RoughHeuristic_UsesFormulaPowerComponent(uint component, int expected) + => Assert.Equal(expected, + RetailSpellFormula.InqSpellLevelByRoughHeuristic([component])); + + [Fact] + public void CustomizeForAccount_IsDeterministic_AndOnlyChangesTapers() + { + uint[] formula = [6u, 63u, 10u, 64u, 20u, 30u, 65u, 40u]; + + IReadOnlyList first = RetailSpellFormula.CustomizeForAccount( + formula, 3u, "testaccount"); + IReadOnlyList second = RetailSpellFormula.CustomizeForAccount( + formula, 3u, "testaccount"); + + Assert.Equal(first, second); + Assert.Equal(formula[0], first[0]); + Assert.Equal(formula[1], first[1]); + Assert.Equal(formula[2], first[2]); + Assert.InRange(first[3], 63u, 74u); + Assert.Equal(formula[4], first[4]); + Assert.Equal(formula[5], first[5]); + Assert.InRange(first[6], 63u, 74u); + Assert.Equal(formula[7], first[7]); + } + + [Theory] + [InlineData(1u, 1)] + [InlineData(2u, 2)] + [InlineData(3u, 3)] + [InlineData(0x6Eu, 3)] + [InlineData(0x70u, 4)] + [InlineData(0xC1u, 4)] + public void ScarabOnlyFormula_KeepsPowerAndAppendsRetailTaperCount( + uint powerComponent, + int expectedTapers) + { + IReadOnlyList result = RetailSpellFormula.InqScarabOnlyFormula( + [powerComponent, 63u, 10u, 64u]); + + Assert.Equal(powerComponent, result[0]); + Assert.Equal(expectedTapers, result.Count(component => component == 0xBCu)); + Assert.DoesNotContain(63u, result); + Assert.DoesNotContain(10u, result); + } +} diff --git a/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs b/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs index a85e5041..ce887cdf 100644 --- a/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs +++ b/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs @@ -61,6 +61,72 @@ public sealed class SpellbookTests Assert.Equal(600f, e.Duration, 4); } + [Fact] + public void OnEnchantmentAdded_SameLayerDifferentSpell_RemainsDistinct() + { + var book = new Spellbook(); + book.OnEnchantmentAdded(42, 7, 300f, 0); + book.OnEnchantmentAdded(43, 7, 600f, 0); + + Assert.Equal(2, book.ActiveCount); + } + + [Fact] + public void LiveEnchantments_NewIdentityInsertsAtRetailListHead() + { + var book = new Spellbook(); + book.OnEnchantmentAdded(Effect(1u, layer: 1u, category: 12u)); + book.OnEnchantmentAdded(Effect(2u, layer: 2u, category: 12u)); + + Assert.Equal(2u, Assert.Single(book.EnchantmentsInEffectSnapshot).SpellId); + } + + [Fact] + public void LiveEnchantment_ExistingIdentityRefreshKeepsListPosition() + { + var book = new Spellbook(); + book.OnEnchantmentAdded(Effect(1u, layer: 1u, category: 12u)); + book.OnEnchantmentAdded(Effect(2u, layer: 2u, category: 12u)); + book.OnEnchantmentAdded(Effect(1u, layer: 1u, category: 12u)); + + Assert.Equal(2u, Assert.Single(book.EnchantmentsInEffectSnapshot).SpellId); + } + + [Fact] + public void ManifestEnchantments_PreserveReceivedListOrder() + { + var book = new Spellbook(); + book.ReplaceManifest( + new Dictionary(), + [Effect(1u, layer: 1u, category: 12u), Effect(2u, layer: 2u, category: 12u)], + [], + [], + 0x3FFFu); + + Assert.Equal(1u, Assert.Single(book.EnchantmentsInEffectSnapshot).SpellId); + } + + [Fact] + public void ReplaceManifest_ReplacesFavoritesFiltersAndDesiredComponents() + { + var book = new Spellbook(); + book.OnSpellLearned(999); + + book.ReplaceManifest( + new Dictionary { [42] = 123f }, + [new ActiveEnchantmentRecord(50, 2, 60, 1)], + [new uint[] { 42, 43 }], + [(100u, 25u)], + 0xA5u); + + Assert.False(book.Knows(999)); + Assert.True(book.Knows(42)); + Assert.Equal(new uint[] { 42, 43 }, book.GetFavorites(0)); + Assert.Equal(25u, book.DesiredComponents[100]); + Assert.Equal(0xA5u, book.SpellbookFilters); + Assert.Equal(1, book.ActiveCount); + } + [Fact] public void OnEnchantmentRemoved_FiresEvent() { @@ -76,18 +142,49 @@ public sealed class SpellbookTests } [Fact] - public void OnPurgeAll_RemovesAllEnchantments_FiresPerRecord() + public void OnPurgeAll_RemovesOnlyFiniteMultAndAddEnchantments() { var book = new Spellbook(); - book.OnEnchantmentAdded(1, 1, 100f, 0); - book.OnEnchantmentAdded(2, 2, 100f, 0); - book.OnEnchantmentAdded(3, 3, 100f, 0); + book.OnEnchantmentAdded(new(1, 1, 100, 0, Bucket: 1)); + book.OnEnchantmentAdded(new(2, 2, 100, 0, Bucket: 2)); + book.OnEnchantmentAdded(new(3, 3, -1, 0, Bucket: 2)); + book.OnEnchantmentAdded(new(4, 4, 100, 0, Bucket: 4)); + book.OnEnchantmentAdded(new(5, 5, 100, 0, Bucket: 8)); int removals = 0; book.EnchantmentRemoved += _ => removals++; book.OnPurgeAll(); - Assert.Equal(3, removals); - Assert.Equal(0, book.ActiveCount); + Assert.Equal(2, removals); + Assert.Equal(3, book.ActiveCount); + Assert.Equal([3u, 4u, 5u], + book.ActiveEnchantmentSnapshot.Select(record => record.SpellId).Order().ToArray()); } + + [Fact] + public void OnPurgeBad_UsesStatModBeneficialFlag_AndPreservesPermanent() + { + const uint Beneficial = 0x02000000u; + var book = new Spellbook(); + book.OnEnchantmentAdded(new(1, 1, 100, 0, StatModType: 0, Bucket: 1)); + book.OnEnchantmentAdded(new(2, 2, 100, 0, StatModType: Beneficial, Bucket: 2)); + book.OnEnchantmentAdded(new(3, 3, -1, 0, StatModType: 0, Bucket: 2)); + book.OnEnchantmentAdded(new(4, 4, 100, 0, StatModType: 0, Bucket: 8)); + + book.OnPurgeBadEnchantments(); + + Assert.Equal([2u, 3u, 4u], + book.ActiveEnchantmentSnapshot.Select(record => record.SpellId).Order().ToArray()); + } + + private static ActiveEnchantmentRecord Effect(uint spellId, uint layer, uint category) + => new( + SpellId: spellId, + LayerId: layer, + Duration: 60d, + CasterGuid: 1u, + Bucket: 1u, + StartTime: 10d, + SpellCategory: category, + PowerLevel: 7u); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherIsActionHeldTests.cs b/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherIsActionHeldTests.cs index e10d56e3..163f763a 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherIsActionHeldTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherIsActionHeldTests.cs @@ -38,6 +38,47 @@ public class InputDispatcherIsActionHeldTests Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward)); } + [Fact] + public void IsActionHeld_CombatBindingOnSameChord_ShadowsGamePolling() + { + var (dispatcher, kb, _, bindings) = Build(); + var chord = new KeyChord(Key.W, ModifierMask.None); + bindings.Add(new Binding(chord, InputAction.MovementForward)); + bindings.Add(new Binding( + chord, + InputAction.CombatCastCurrentSpell, + Scope: InputScope.MagicCombat)); + dispatcher.SetCombatScope(InputScope.MagicCombat); + + kb.EmitKeyDown(Key.W, ModifierMask.None); + + Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward)); + Assert.True(dispatcher.IsActionHeld(InputAction.CombatCastCurrentSpell)); + + dispatcher.SetCombatScope(null); + Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward)); + Assert.False(dispatcher.IsActionHeld(InputAction.CombatCastCurrentSpell)); + } + + [Fact] + public void IsActionHeld_ExplicitShiftCombatChord_ShadowsBareMovementFallback() + { + var (dispatcher, kb, _, bindings) = Build(); + bindings.Add(new Binding( + new KeyChord(Key.W, ModifierMask.None), + InputAction.MovementForward)); + bindings.Add(new Binding( + new KeyChord(Key.W, ModifierMask.Shift), + InputAction.CombatCastCurrentSpell, + Scope: InputScope.MagicCombat)); + dispatcher.SetCombatScope(InputScope.MagicCombat); + + kb.EmitKeyDown(Key.W, ModifierMask.Shift); + + Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward)); + Assert.True(dispatcher.IsActionHeld(InputAction.CombatCastCurrentSpell)); + } + [Fact] public void IsActionHeld_returns_false_when_no_binding_for_action() { diff --git a/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherTests.cs b/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherTests.cs index 6e6906a8..936aeb20 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Input/InputDispatcherTests.cs @@ -82,6 +82,44 @@ public class InputDispatcherTests Assert.Equal(InputScope.Game, dispatcher.ActiveScope); } + [Fact] + public void Magic_combat_scope_shadows_game_binding_for_same_chord() + { + var (dispatcher, kb, _, bindings, fired) = Build(); + var chord = new KeyChord(Key.Number1, ModifierMask.None); + bindings.Add(new Binding(chord, InputAction.UseQuickSlot_1)); + bindings.Add(new Binding(chord, InputAction.UseSpellSlot_1, Scope: InputScope.MagicCombat)); + + kb.EmitKeyDown(Key.Number1, ModifierMask.None); + dispatcher.SetCombatScope(InputScope.MagicCombat); + kb.EmitKeyDown(Key.Number1, ModifierMask.None); + + Assert.Equal( + [(InputAction.UseQuickSlot_1, ActivationType.Press), + (InputAction.UseSpellSlot_1, ActivationType.Press)], + fired); + } + + [Fact] + public void Changing_combat_scope_releases_hold_resolved_in_previous_scope() + { + var (dispatcher, kb, _, bindings, fired) = Build(); + bindings.Add(new Binding( + new KeyChord(Key.Delete, ModifierMask.None), + InputAction.CombatLowAttack, + ActivationType.Hold, + InputScope.MeleeCombat)); + dispatcher.SetCombatScope(InputScope.MeleeCombat); + + kb.EmitKeyDown(Key.Delete, ModifierMask.None); + dispatcher.SetCombatScope(InputScope.MagicCombat); + + Assert.Equal( + [(InputAction.CombatLowAttack, ActivationType.Press), + (InputAction.CombatLowAttack, ActivationType.Release)], + fired); + } + [Fact] public void PushScope_changes_ActiveScope() { @@ -149,6 +187,40 @@ public class InputDispatcherTests Assert.Empty(fired); // no longer held } + [Fact] + public void Hold_callback_scope_change_DoesNotDispatchStaleSnapshotChord() + { + var (dispatcher, kb, _, bindings, fired) = Build(); + bindings.Add(new Binding( + new KeyChord(Key.Delete, ModifierMask.None), + InputAction.CombatLowAttack, + ActivationType.Hold, + InputScope.MeleeCombat)); + bindings.Add(new Binding( + new KeyChord(Key.End, ModifierMask.None), + InputAction.CombatHighAttack, + ActivationType.Hold, + InputScope.MeleeCombat)); + dispatcher.SetCombatScope(InputScope.MeleeCombat); + kb.EmitKeyDown(Key.Delete, ModifierMask.None); + kb.EmitKeyDown(Key.End, ModifierMask.None); + fired.Clear(); + + bool changed = false; + dispatcher.Fired += (_, activation) => + { + if (activation != ActivationType.Hold || changed) + return; + changed = true; + dispatcher.SetCombatScope(InputScope.MagicCombat); + }; + + dispatcher.Tick(); + + Assert.Single(fired, entry => entry.Item2 == ActivationType.Hold); + Assert.Equal(2, fired.Count(entry => entry.Item2 == ActivationType.Release)); + } + [Fact] public void Release_binding_fires_only_on_KeyUp() { diff --git a/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsJsonTests.cs b/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsJsonTests.cs index 5cbc9f67..1e6f1d3d 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsJsonTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsJsonTests.cs @@ -49,7 +49,8 @@ public class KeyBindingsJsonTests Assert.Contains(loaded.All, x => x.Chord == b.Chord && x.Action == b.Action - && x.Activation == b.Activation); + && x.Activation == b.Activation + && x.Scope == b.Scope); } } finally diff --git a/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsTests.cs b/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsTests.cs index 3cb86d6a..9cf82274 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Input/KeyBindingsTests.cs @@ -57,6 +57,20 @@ public class KeyBindingsTests Assert.Null(b.Find(new KeyChord(Key.S, ModifierMask.None), ActivationType.Press)); } + [Fact] + public void RetailDefaults_resolve_shared_number_key_by_scope() + { + var bindings = KeyBindings.RetailDefaults(); + var chord = new KeyChord(Key.Number1, ModifierMask.None); + + Assert.Equal( + InputAction.UseQuickSlot_1, + bindings.Find(chord, ActivationType.Press, InputScope.Game)?.Action); + Assert.Equal( + InputAction.UseSpellSlot_1, + bindings.Find(chord, ActivationType.Press, InputScope.MagicCombat)?.Action); + } + [Fact] public void ForAction_returns_all_chords_bound_to_action() {