diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 46cb90d8..6ae1974b 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -531,7 +531,7 @@ 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. The spellbook now resolves retail UIItem prototype `0x10000343` for its one-column icon/name/separator rows and selected overlay, binds the authored scrollbar, ports exact filters/display ordering, drags learned spells into the open favorite bar without mutating their source, and reuses `RetailDialogFactory` for server-authoritative spell deletion (`CM_Magic::Event_RemoveSpell`, `0x01A8`). `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`. +- **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, `gmSpellbookUI`, `gmSpellComponentUI`, both `gmEffectsUI` windows, and the floating Helpful/Harmful effect indicators. Spell rows resolve UIItem prototype `0x10000343`; the component page now instantiates LayoutDesc `0x21000033` category/component templates `0x10000466/67`, including localized headings, exact row textures and columns, owned/desired counts, selection, and the 0..5000 desired-value contract. The authored scrollbars, exact filters/display ordering, learned-spell drag, and shared server-authoritative delete dialog are connected. `RetailPanelUiController` ports the shared one-active-panel lifecycle so toolbar and indicator launchers converge on one owner. #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. diff --git a/docs/plans/2026-05-12-milestones.md b/docs/plans/2026-05-12-milestones.md index 34389ab9..369622f3 100644 --- a/docs/plans/2026-05-12-milestones.md +++ b/docs/plans/2026-05-12-milestones.md @@ -6,8 +6,8 @@ LANDED 2026-07-15.** The complete connected melee/missile, death/loot, inventory, and item-giving demo is user-gated. The shared projectile/effect foundation is hardened through Step 9 and its single-client visual gate passes. -Next is the connected F.5 spellbook/favorite-bar/delete visual gate, followed -by cast/enchantment presentation and the two-client portal/observer VFX gate. +Next is the connected F.5 component-book/favorite-bar visual gate, followed by +cast/enchantment presentation and the two-client portal/observer VFX gate. Carried: #145-residual, #116 slide-response, R6/TS-42, and Track MP0. @@ -656,6 +656,9 @@ its authored scrollbar, ports exact school/level filter masks and stable display-order sorting, carries learned spells into the open favorite bar, and routes Delete through the shared retail confirmation factory plus server-authoritative `CM_Magic::Event_RemoveSpell (0x01A8)` response path. +The component-book follow-up now instantiates retail's category and component +listbox templates from LayoutDesc `0x21000033`, preserving localized headings, +row/selection textures, icon/name/count columns, and desired-value editing. **Phases to ship:** - **F.4** — Spell cast state machine (buffs + recalls first, projectile diff --git a/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md index 678fba78..2065ddcd 100644 --- a/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md +++ b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md @@ -292,6 +292,102 @@ 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. +### Component-book listbox templates and synchronization + +Named-retail anchors: + +- `gmSpellComponentUI::PostInit @ 0x0048A190` +- `gmSpellComponentUI::ListenToElementMessage @ 0x0048A420` +- `gmSpellComponentUI::SyncComponentRegionWithData @ 0x0048A620` +- `gmSpellComponentUI::UpdateComponents @ 0x0048A910` +- `gmSpellComponentUI::RecvNotice_SelectionChanged @ 0x00489C40` +- `gmSpellComponentUI::UpdateBuyRates @ 0x00489FE0` + +The component page is not a generic icon catalog. LayoutDesc `0x21000033` +authors a listbox with two entry templates, referenced in order by list +property `0x64`: + +```text +component list 0x10000464 (284 x 600) +component scrollbar 0x10000465 (16 x 600) + +template 0 / category 0x10000466 (284 x 32, UIElement_Text) + background 0x06001392 + centered category font 0x40000004 + +template 1 / component 0x10000467 (284 x 32, UIElement_Field) + normal background 0x06005E21 + Highlight background 0x06005E22 + icon region 0x10000468 (x=0, 32 x 32) + name 0x10000469 (x=42, width=180, height=32) + owned count 0x1000046A (x=224, y=0, width=60, height=15) + desired purchase count 0x1000046B (x=224, y=15, width=60, height=15) +``` + +The seven localized headings are string hashes in local string table +`0x23000001`: `SCARABS`, `HERBS`, `POWDERED GEMS`, `ALCHEMICAL SUBSTANCES`, +`TALISMANS`, `TAPERS`, and `PEAS`. They correspond in order to retail +`SpellComponentCategory` values 0 through 6. Category 6 is Peas; treating it +as an invented "Prismatic Components" group is not retail behavior. + +```text +PostInit(): + componentList = GetChildRecursive(0x10000464) as ListBox + register component/player/selection notices + if PlayerDescription already arrived: UpdateBuyRates(all components) + +UpdateComponents(change): + tracker = PlayerSystem.ComponentTracker + insertion = 0 + for category in 0..6: + if tracker.componentLists[category] is empty: + remove a stale category entry for this category, if present + continue + + if entry[insertion] is not this category header: + header = list.AddItemFromTemplateList(templateIndex=0, insertion) + header.attribute[0x1000004D] = category + header.text = localizedCategoryTitle[category] + insertion++ + + reconcile component rows against tracker.componentLists[category] + each new row = list.AddItemFromTemplateList(templateIndex=1, insertion) + row.attribute[0x1000004C] = componentWCID + row[0x10000468] = MakeIcon(componentWCID) + row[0x10000469] = componentName + row[0x1000046A] = totalOwnedCount + row[0x1000046B] = desiredPurchaseCount + row[0x1000046B].inputFilter = NumberInputFilter + insertion++ + +ListenToElementMessage(message): + if selection changed: + if selected entry is a category: + clear selected world object + else if selected entry is a component: + select the first owned object represented by that component region + + if edit completed on 0x1000046B: + parse integer + if 0 <= value <= 5000: + send SetDesiredComponentLevel(componentWCID, value) + update local PlayerModule projection + else: + restore the authoritative desired value in the field + +RecvNotice_SelectionChanged(): + if the selected world object belongs to a tracked component region: + select and expose its matching component row + else: + clear component-list selection +``` + +The retained port instantiates these resolved templates through +`ComponentBookTemplateFactory` and `UiTemplateListSlot`, the architectural +equivalent of `AddItemFromTemplateList`. Controllers bind only the live icon, +texts, selection, and edit callbacks. They do not duplicate the row sprites, +fonts, or geometry. + ### Spellbook row, selection, filtering, dragging, and deletion Named-retail anchors: diff --git a/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs new file mode 100644 index 00000000..41ce9139 --- /dev/null +++ b/src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs @@ -0,0 +1,188 @@ +using DatReaderWriter; + +namespace AcDream.App.UI.Layout; + +/// +/// Instantiates retail's category and component listbox templates from LayoutDesc +/// 0x21000033. This is the retained equivalent of +/// UIElement_ListBox::AddItemFromTemplateList used by +/// gmSpellComponentUI::UpdateComponents @ 0x0048A910. +/// +public sealed class ComponentBookTemplateFactory +{ + public const uint LayoutId = 0x21000033u; + public const uint CategoryTemplateId = 0x10000466u; + public const uint ComponentTemplateId = 0x10000467u; + public const uint IconId = 0x10000468u; + public const uint NameId = 0x10000469u; + public const uint OwnedCountId = 0x1000046Au; + public const uint DesiredCountId = 0x1000046Bu; + + private const uint LocalStringTableId = 0x23000001u; + private const uint HighlightState = UiButtonStateMachine.Highlight; + + private static readonly (string Key, string Fallback)[] CategoryStrings = + [ + ("ID_SpellComp_Category_Scarabs", "SCARABS"), + ("ID_SpellComp_Category_Herbs", "HERBS"), + ("ID_SpellComp_Category_Gems", "POWDERED GEMS"), + ("ID_SpellComp_Category_Alchemical", "ALCHEMICAL SUBSTANCES"), + ("ID_SpellComp_Category_Talismans", "TALISMANS"), + ("ID_SpellComp_Category_Tapers", "TAPERS"), + ("ID_SpellComp_Category_Peas", "PEAS"), + ]; + + private readonly ElementInfo _categoryTemplate; + private readonly ElementInfo _componentTemplate; + private readonly Func _resolveSprite; + private readonly UiDatFont? _defaultFont; + private readonly IReadOnlyDictionary _fonts; + private readonly string[] _categoryNames; + + public ComponentBookTemplateFactory( + ElementInfo categoryTemplate, + ElementInfo componentTemplate, + Func resolveSprite, + UiDatFont? defaultFont, + IReadOnlyDictionary? fonts = null, + IReadOnlyList? categoryNames = null) + { + _categoryTemplate = categoryTemplate; + _componentTemplate = componentTemplate; + _resolveSprite = resolveSprite; + _defaultFont = defaultFont; + _fonts = fonts ?? new Dictionary(); + _categoryNames = categoryNames?.ToArray() + ?? CategoryStrings.Select(value => value.Fallback).ToArray(); + } + + /// + /// Loads and resolves the two template definitions while the caller holds the + /// shared DAT lock. Fonts and localized category strings are captured eagerly, + /// so later inventory refreshes instantiate rows without reading DAT files. + /// + public static ComponentBookTemplateFactory? TryLoad( + DatCollection dats, + Func resolveSprite, + UiDatFont? defaultFont, + Func? resolveFont) + { + ElementInfo? category = LayoutImporter.ImportInfos( + dats, LayoutId, CategoryTemplateId); + ElementInfo? component = LayoutImporter.ImportInfos( + dats, LayoutId, ComponentTemplateId); + if (category is null || component is null) + return null; + + var fonts = new Dictionary(); + CaptureFonts(category, resolveFont, fonts); + CaptureFonts(component, resolveFont, fonts); + + var strings = new DatStringResolver(dats); + string[] categoryNames = CategoryStrings + .Select(value => strings.Resolve( + LocalStringTableId, + DatStringResolver.ComputeHash(value.Key)) + ?? value.Fallback) + .ToArray(); + + return new ComponentBookTemplateFactory( + category, component, resolveSprite, defaultFont, fonts, categoryNames); + } + + public UiTemplateListSlot CreateCategoryRow(uint category) + { + ImportedLayout content = Build(_categoryTemplate); + if (content.Root is not UiText title) + throw new InvalidOperationException( + "Retail component category template did not resolve to UIElement_Text."); + + string label = category < _categoryNames.Length + ? _categoryNames[category] + : "OTHER COMPONENTS"; + title.LinesProvider = () => [new UiText.Line(label, title.DefaultColor)]; + + return new UiTemplateListSlot( + content, uint.MaxValue, content.Root is IUiDatStateful stateful + ? stateful.ActiveRetailStateId + : UiStateInfo.DirectStateId, selectedState: null); + } + + public ComponentRow CreateComponentRow( + uint componentId, + uint iconTexture, + string name, + int ownedCount, + uint desiredCount) + { + ImportedLayout content = Build(_componentTemplate); + UiElement iconHost = Required(content, IconId); + UiText nameText = Required(content, NameId); + UiText ownedText = Required(content, OwnedCountId); + UiField desiredField = Required(content, DesiredCountId); + + var icon = new UiTextureElement + { + Width = iconHost.Width, + Height = iconHost.Height, + Anchors = AnchorEdges.Left | AnchorEdges.Top + | AnchorEdges.Right | AnchorEdges.Bottom, + Texture = iconTexture, + }; + iconHost.AddChild(icon); + + nameText.LinesProvider = () => [new UiText.Line(name, nameText.DefaultColor)]; + ownedText.LinesProvider = () => + [new UiText.Line(ownedCount.ToString(System.Globalization.CultureInfo.InvariantCulture), + ownedText.DefaultColor)]; + + // NumberInputFilter + the 0..5000 validation are installed by the controller, + // matching gmSpellComponentUI::ListenToElementMessage @ 0x0048A420. + desiredField.ClearOnSubmit = false; + desiredField.RecordHistory = false; + desiredField.SelectAllOnFocus = true; + desiredField.CharacterFilter = char.IsAsciiDigit; + desiredField.SetText(desiredCount.ToString( + System.Globalization.CultureInfo.InvariantCulture)); + + uint normalState = content.Root is IUiDatStateful stateful + ? stateful.ActiveRetailStateId + : UiStateInfo.DirectStateId; + var slot = new UiTemplateListSlot( + content, componentId, normalState, HighlightState); + return new ComponentRow(slot, desiredField); + } + + private ImportedLayout Build(ElementInfo template) + => LayoutImporter.Build( + template, + _resolveSprite, + _defaultFont, + did => _fonts.TryGetValue(did, out UiDatFont? font) ? font : _defaultFont); + + private static T Required(ImportedLayout content, uint id) + where T : UiElement + => content.FindElement(id) as T + ?? throw new InvalidOperationException( + $"Retail component template element 0x{id:X8} did not resolve to {typeof(T).Name}."); + + private static UiElement Required(ImportedLayout content, uint id) + => content.FindElement(id) + ?? throw new InvalidOperationException( + $"Retail component template element 0x{id:X8} is missing."); + + private static void CaptureFonts( + ElementInfo info, + Func? resolveFont, + Dictionary fonts) + { + if (info.FontDid != 0u && !fonts.ContainsKey(info.FontDid)) + fonts[info.FontDid] = resolveFont?.Invoke(info.FontDid); + foreach (ElementInfo child in info.Children) + CaptureFonts(child, resolveFont, fonts); + } + + public readonly record struct ComponentRow( + UiTemplateListSlot Slot, + UiField DesiredField); +} diff --git a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs index 7301ff1a..1dbfc5d8 100644 --- a/src/AcDream.App/UI/Layout/SpellbookWindowController.cs +++ b/src/AcDream.App/UI/Layout/SpellbookWindowController.cs @@ -2,9 +2,9 @@ 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.Selection; using AcDream.Core.Spells; namespace AcDream.App.UI.Layout; @@ -48,6 +48,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController private readonly ClientObjectTable _objects; private readonly Func _playerGuid; private readonly IReadOnlyDictionary _components; + private readonly SelectionState _selection; private readonly Func _resolveSpellIcon; private readonly Func _resolveComponentIcon; private readonly Func _spellLevel; @@ -66,10 +67,12 @@ public sealed class SpellbookWindowController : IRetainedPanelController private readonly UiButton _deleteButton; private readonly UiItemList _spellList; private readonly UiItemList _componentList; + private readonly ComponentBookTemplateFactory _componentTemplates; private readonly List<(UiButton Button, uint Mask)> _filters = new(); private readonly SpellbookRowStyle _rowStyle; private readonly UiDatFont? _rowFont; private uint? _selectedSpell; + private uint? _selectedComponent; private bool _spellsDirty; private bool _componentsDirty; private bool _componentEditActive; @@ -81,6 +84,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController ClientObjectTable objects, Func playerGuid, IReadOnlyDictionary components, + SelectionState selection, Func resolveSpellIcon, Func resolveComponentIcon, Func spellLevel, @@ -99,6 +103,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController UiButton deleteButton, UiItemList spellList, UiItemList componentList, + ComponentBookTemplateFactory componentTemplates, SpellbookRowStyle rowStyle, UiDatFont? rowFont) { @@ -106,6 +111,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController _objects = objects; _playerGuid = playerGuid; _components = components; + _selection = selection; _resolveSpellIcon = resolveSpellIcon; _resolveComponentIcon = resolveComponentIcon; _spellLevel = spellLevel; @@ -124,6 +130,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController _deleteButton = deleteButton; _spellList = spellList; _componentList = componentList; + _componentTemplates = componentTemplates; _rowStyle = rowStyle; _rowFont = rowFont; @@ -148,6 +155,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController _objects.ObjectRemoved += OnObjectRemoved; _objects.ObjectMoved += OnObjectMoved; _objects.ContainerContentsReplaced += OnContainerContentsReplaced; + _selection.Changed += OnSelectionChanged; ShowPage(SpellbookWindowPage.Spells); RebuildAll(); } @@ -160,6 +168,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController ClientObjectTable objects, Func playerGuid, IReadOnlyDictionary components, + SelectionState selection, Func resolveSpellIcon, Func resolveComponentIcon, Func spellLevel, @@ -170,6 +179,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController Action> showConfirmation, Action setDesiredComponent, Action close, + ComponentBookTemplateFactory componentTemplates, SpellbookRowStyle rowStyle, UiDatFont? rowFont) { @@ -199,13 +209,14 @@ public sealed class SpellbookWindowController : IRetainedPanelController } return new SpellbookWindowController( - layout, spellbook, objects, playerGuid, components, + layout, spellbook, objects, playerGuid, components, selection, resolveSpellIcon, resolveComponentIcon, spellLevel, selectObject, addFavorite, sendFilter, removeSpell, showConfirmation, setDesiredComponent, close, spellPage, componentPage, spellTab, componentTab, closeButton, - deleteButton, spellList, componentList, rowStyle, rowFont); + deleteButton, spellList, componentList, componentTemplates, + rowStyle, rowFont); } public void ShowPage(SpellbookWindowPage page) @@ -337,40 +348,29 @@ public sealed class SpellbookWindowController : IRetainedPanelController if (category != nextCategory) { category = nextCategory; - _componentList.AddItem(new UiCatalogSlot + UiTemplateListSlot categoryRow = + _componentTemplates.CreateCategoryRow(nextCategory); + categoryRow.Clicked = () => { - EntryId = uint.MaxValue, - Label = ComponentCategoryName(nextCategory), - ShowLabel = true, - SpriteResolve = _componentList.SpriteResolve, - }); + _selectedComponent = null; + SyncComponentSelection(); + _selectObject(0u); + }; + _componentList.AddItem(categoryRow); } 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)); + string componentName = string.IsNullOrWhiteSpace(pack.Name) + ? descriptor?.Name ?? $"Component {componentId}" + : pack.Name; + ComponentBookTemplateFactory.ComponentRow row = + _componentTemplates.CreateComponentRow( + componentId, + _resolveComponentIcon( + pack.IconId != 0 ? pack.IconId : descriptor?.IconId ?? 0u), + componentName, + packGroup.Quantity, + desired); + UiField desiredField = row.DesiredField; desiredField.OnFocusGained = () => _componentEditActive = true; uint committed = desired; void Commit(string value) @@ -392,25 +392,18 @@ public sealed class SpellbookWindowController : IRetainedPanelController Commit(value); _componentEditActive = false; }; - row.AddChild(desiredField); - row.Clicked = () => _selectObject(pack.ObjectId); - _componentList.AddItem(row); + row.Slot.Clicked = () => + { + _selectedComponent = componentId; + SyncComponentSelection(); + _selectObject(pack.ObjectId); + }; + _componentList.AddItem(row.Slot); } } + SyncComponentSelectionFromWorld(); } - 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; @@ -479,6 +472,23 @@ public sealed class SpellbookWindowController : IRetainedPanelController slot.Selected = slot.EntryId == _selectedSpell; } + private void SyncComponentSelection() + { + for (int i = 0; i < _componentList.GetNumUIItems(); i++) + if (_componentList.GetItem(i) is UiTemplateListSlot slot) + slot.SetSelected(slot.EntryId == _selectedComponent); + } + + private void SyncComponentSelectionFromWorld() + { + _selectedComponent = _selection.SelectedObjectId is uint selected + && _objects.Get(selected) is { } item + && IsComponent(item) + ? item.WeenieClassId + : null; + SyncComponentSelection(); + } + public void Tick() { if (_spellsDirty) @@ -505,6 +515,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController } private void OnObjectMoved(ClientObjectMove _) => _componentsDirty = true; private void OnContainerContentsReplaced(uint _) => _componentsDirty = true; + private void OnSelectionChanged(SelectionTransition _) => SyncComponentSelectionFromWorld(); private bool IsComponent(ClientObject item) => item.IsComponentPack || _components.ContainsKey(item.WeenieClassId); public void OnShown() => RebuildAll(); @@ -520,6 +531,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController _objects.ObjectRemoved -= OnObjectRemoved; _objects.ObjectMoved -= OnObjectMoved; _objects.ContainerContentsReplaced -= OnContainerContentsReplaced; + _selection.Changed -= OnSelectionChanged; RetailTabBinding.SetClick(_spellTab, null); RetailTabBinding.SetClick(_componentTab, null); _closeButton.OnClick = null; diff --git a/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs b/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs new file mode 100644 index 00000000..97793596 --- /dev/null +++ b/src/AcDream.App/UI/Layout/UiTemplateListSlot.cs @@ -0,0 +1,72 @@ +namespace AcDream.App.UI.Layout; + +/// +/// One runtime listbox entry instantiated from an authored LayoutDesc template. +/// Retail UIElement_ListBox::AddItemFromTemplateList creates exactly this +/// ownership shape: the list owns the entry while the template owns its visuals. +/// +public sealed class UiTemplateListSlot : UiItemSlot +{ + private readonly IUiDatStateful? _statefulRoot; + private readonly uint _normalState; + private readonly uint? _selectedState; + private bool? _stateWasSelected; + + public UiTemplateListSlot( + ImportedLayout content, + uint entryId, + uint normalState, + uint? selectedState) + { + Content = content; + EntryId = entryId; + _normalState = normalState; + _selectedState = selectedState; + _statefulRoot = content.Root as IUiDatStateful; + + Width = content.Root.Width; + Height = content.Root.Height; + content.Root.Left = 0f; + content.Root.Top = 0f; + content.Root.Anchors = AnchorEdges.Left | AnchorEdges.Top + | AnchorEdges.Right | AnchorEdges.Bottom; + AddChild(content.Root); + } + + public ImportedLayout Content { get; } + public uint EntryId { get; } + public new Action? Clicked { get; set; } + + public override bool HandlesClick => Clicked is not null; + + public void SetSelected(bool selected) + { + Selected = selected; + ApplySelectionState(); + } + + public override bool OnEvent(in UiEvent e) + { + if (e.Type == UiEventType.MouseDown) + return true; + if (e.Type == UiEventType.Click && Clicked is not null) + { + Clicked(); + return true; + } + return false; + } + + protected override void OnDraw(UiRenderContext ctx) + => ApplySelectionState(); + + private void ApplySelectionState() + { + if (_stateWasSelected == Selected) + return; + + _stateWasSelected = Selected; + _statefulRoot?.TrySetRetailState( + Selected && _selectedState.HasValue ? _selectedState.Value : _normalState); + } +} diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 807c08f4..0d1c04c4 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -709,6 +709,7 @@ public sealed class RetailUiRuntime : IDisposable { ImportedLayout? layout; SpellbookRowStyle? rowStyle; + ComponentBookTemplateFactory? componentTemplates; lock (_bindings.Assets.DatLock) { layout = LayoutImporter.Import( @@ -719,10 +720,16 @@ public sealed class RetailUiRuntime : IDisposable _bindings.Assets.DefaultFont, _bindings.Assets.ResolveFont); rowStyle = SpellbookRowStyle.TryLoad(_bindings.Assets.Dats); + componentTemplates = ComponentBookTemplateFactory.TryLoad( + _bindings.Assets.Dats, + _bindings.Assets.ResolveSprite, + _bindings.Assets.DefaultFont, + _bindings.Assets.ResolveFont); } - if (layout is null || rowStyle is null) + if (layout is null || rowStyle is null || componentTemplates is null) { - Console.WriteLine("[M3] spellbook: layout or authored UIItem row prototype not found."); + Console.WriteLine( + "[M3] spellbook: layout, spell row, or component list templates not found."); return; } @@ -737,6 +744,7 @@ public sealed class RetailUiRuntime : IDisposable _bindings.Magic.Objects, _bindings.Magic.PlayerGuid, _bindings.Magic.Components, + _bindings.Magic.Selection, _bindings.Magic.ResolveSpellIcon, _bindings.Magic.ResolveComponentIcon, _bindings.Magic.SpellLevel, @@ -747,6 +755,7 @@ public sealed class RetailUiRuntime : IDisposable (message, completed) => ShowConfirmation(message, completed), _bindings.Magic.SetDesiredComponent, () => CloseWindow(WindowNames.Spellbook), + componentTemplates, rowStyle.Value, rowFont); if (controller is null) diff --git a/src/AcDream.App/UI/UiTextureElement.cs b/src/AcDream.App/UI/UiTextureElement.cs new file mode 100644 index 00000000..e061ed87 --- /dev/null +++ b/src/AcDream.App/UI/UiTextureElement.cs @@ -0,0 +1,23 @@ +using System.Numerics; +using AcDream.App.Rendering; + +namespace AcDream.App.UI; + +/// +/// Runtime-owned texture content inside an authored retained-UI region. LayoutDesc +/// still owns the region's geometry and chrome; controllers supply only the live +/// texture handle (for example a composed item icon). +/// +public sealed class UiTextureElement : UiElement +{ + public UiTextureElement() => ClickThrough = true; + + public uint Texture { get; set; } + public Vector4 Tint { get; set; } = Vector4.One; + + protected override void OnDraw(UiRenderContext ctx) + { + if (Texture != 0u) + ctx.DrawSprite(Texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Tint); + } +} diff --git a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs index 79a75e0b..3f5087bb 100644 --- a/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs +++ b/tests/AcDream.App.Tests/UI/Layout/FixtureLoader.cs @@ -101,6 +101,12 @@ public static class FixtureLoader public static ElementInfo LoadSpellbookInfos() => LoadInfos("spellbook_21000034.json"); + public static ElementInfo LoadComponentCategoryTemplateInfos() + => LoadInfos("component_category_21000033_10000466.json"); + + public static ElementInfo LoadComponentRowTemplateInfos() + => LoadInfos("component_row_21000033_10000467.json"); + public static ImportedLayout LoadPowerbar() => LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null); diff --git a/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs b/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs index 1bada79a..c8277988 100644 --- a/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs +++ b/tests/AcDream.App.Tests/UI/Layout/RetailLayoutFixtureGenerator.cs @@ -47,6 +47,22 @@ public sealed class RetailLayoutFixtureGenerator "Asheron's Call"); using var dats = new DatCollection(datDir, DatAccessType.Read); + var localStrings = new DatStringResolver(dats); + foreach ((string key, string expected) in new[] + { + ("ID_SpellComp_Category_Scarabs", "SCARABS"), + ("ID_SpellComp_Category_Herbs", "HERBS"), + ("ID_SpellComp_Category_Gems", "POWDERED GEMS"), + ("ID_SpellComp_Category_Alchemical", "ALCHEMICAL SUBSTANCES"), + ("ID_SpellComp_Category_Talismans", "TALISMANS"), + ("ID_SpellComp_Category_Tapers", "TAPERS"), + ("ID_SpellComp_Category_Peas", "PEAS"), + }) + { + Assert.Equal(expected, localStrings.Resolve( + 0x23000001u, DatStringResolver.ComputeHash(key))); + } + uint masterDid = (uint)dats.Portal.Header.MasterMapId; Assert.True(dats.Portal.TryGet(masterDid, out var master)); Assert.True(master!.ClientEnumToID.TryGetValue(5u, out uint uiMapDid)); @@ -102,6 +118,26 @@ public sealed class RetailLayoutFixtureGenerator File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), effectsJson); } + foreach ((uint rootId, string fileName) in new[] + { + (0x10000466u, + "component_category_21000033_10000466.json"), + (0x10000467u, + "component_row_21000033_10000467.json"), + }) + { + ElementInfo? componentTemplate = LayoutImporter.ImportInfos( + dats, 0x21000033u, rootId); + Assert.NotNull(componentTemplate); + var componentJson = JsonSerializer.Serialize(componentTemplate, + new JsonSerializerOptions + { + IncludeFields = true, + WriteIndented = true, + }); + File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), componentJson); + } + foreach (var (layoutId, fileName) in Layouts) { var info = LayoutImporter.ImportInfos(dats, layoutId); diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs index 9088aa31..4123490a 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellbookWindowControllerTests.cs @@ -2,6 +2,7 @@ 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; using System.Numerics; @@ -238,6 +239,130 @@ public sealed class SpellbookWindowControllerTests Assert.Empty(VisibleSpellIds(list)); } + [Fact] + public void Components_UseAuthoredCategoryAndItemTemplates_WithRetailCountsAndSelection() + { + ImportedLayout layout = FixtureLoader.LoadSpellbook(); + var objects = new ClientObjectTable(); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 10u, + WeenieClassId = 100u, + Name = "Copper Scarab", + Type = ItemType.SpellComponents, + IconId = 0x06000100u, + StackSize = 2, + ContainerId = 1u, + IsComponentPack = true, + }); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 11u, + WeenieClassId = 101u, + Name = "Diamond Scarab", + Type = ItemType.SpellComponents, + IconId = 0x06000101u, + StackSize = 1, + ContainerId = 1u, + IsComponentPack = true, + }); + objects.AddOrUpdate(new ClientObject + { + ObjectId = 12u, + WeenieClassId = 200u, + Name = "Amaranth", + Type = ItemType.SpellComponents, + IconId = 0x06000200u, + StackSize = 3, + ContainerId = 1u, + IsComponentPack = true, + }); + var components = new Dictionary + { + [100u] = new(100u, "Copper Scarab", 0u, 0x06000100u), + [101u] = new(101u, "Diamond Scarab", 0u, 0x06000101u), + [200u] = new(200u, "Amaranth", 1u, 0x06000200u), + }; + Spellbook book = CreateSpellbook(); + book.SetDesiredComponent(100u, 7u); + var selected = new List(); + var desired = new List<(uint Component, uint Amount)>(); + var selection = new SelectionState(); + + using SpellbookWindowController controller = Bind( + layout, + book, + objects: objects, + components: components, + selection: selection, + selectObject: objectId => + { + selected.Add(objectId); + selection.Select(objectId, SelectionChangeSource.Inventory); + }, + setDesiredComponent: (component, amount) => desired.Add((component, amount)), + resolveComponentIcon: icon => icon + 0x1000u)!; + controller.ShowPage(SpellbookWindowPage.Components); + + UiItemList list = Assert.IsType( + layout.FindElement(SpellbookWindowController.ComponentListId)?.Children.Single()); + Assert.Equal(5, list.GetNumUIItems()); + Assert.Same( + list.Scroll, + Assert.IsType( + layout.FindElement(SpellbookWindowController.ComponentScrollbarId)).Model); + + UiTemplateListSlot scarabs = Assert.IsType(list.GetItem(0)); + UiText scarabTitle = Assert.IsType(scarabs.Content.Root); + Assert.Equal("SCARABS", Assert.Single(scarabTitle.LinesProvider()).Text); + Assert.Equal(0x06001392u, scarabTitle.BackgroundSprite); + + UiTemplateListSlot copper = Assert.IsType(list.GetItem(1)); + UiDatElement copperRoot = Assert.IsType(copper.Content.Root); + Assert.Equal((0x06005E21u, 1), copperRoot.ActiveMedia()); + Assert.Equal("Copper Scarab", Assert.Single( + Assert.IsType(copper.Content.FindElement( + ComponentBookTemplateFactory.NameId)).LinesProvider()).Text); + Assert.Equal("2", Assert.Single( + Assert.IsType(copper.Content.FindElement( + ComponentBookTemplateFactory.OwnedCountId)).LinesProvider()).Text); + UiField desiredField = Assert.IsType(copper.Content.FindElement( + ComponentBookTemplateFactory.DesiredCountId)); + Assert.Equal((224f, 15f, 60f, 15f), + (desiredField.Left, desiredField.Top, desiredField.Width, desiredField.Height)); + Assert.True(desiredField.RightAligned); + Assert.Equal("7", desiredField.Text); + UiElement iconHost = Assert.IsAssignableFrom(copper.Content.FindElement( + ComponentBookTemplateFactory.IconId)); + Assert.Equal( + 0x06001100u, + Assert.IsType(Assert.Single(iconHost.Children)).Texture); + + copper.OnEvent(new UiEvent(0u, copper, UiEventType.Click)); + Assert.Equal([10u], selected); + Assert.True(copper.Selected); + Assert.Equal(UiButtonStateMachine.Highlight, copperRoot.ActiveRetailStateId); + Assert.Equal((0x06005E22u, 1), copperRoot.ActiveMedia()); + + desiredField.SetText("21"); + desiredField.OnSubmit!(desiredField.Text); + Assert.Equal([(100u, 21u)], desired); + desiredField.SetText("5001"); + desiredField.OnSubmit!(desiredField.Text); + Assert.Equal("21", desiredField.Text); + Assert.Equal([(100u, 21u)], desired); + + UiTemplateListSlot herbs = Assert.IsType(list.GetItem(3)); + Assert.Equal("HERBS", Assert.Single( + Assert.IsType(herbs.Content.Root).LinesProvider()).Text); + UiTemplateListSlot amaranth = Assert.IsType(list.GetItem(4)); + selection.Select(12u, SelectionChangeSource.Radar); + Assert.True(amaranth.Selected); + Assert.False(copper.Selected); + selection.Select(0xDEADBEEFu, SelectionChangeSource.World); + Assert.False(amaranth.Selected); + } + private static SpellbookWindowController? Bind( ImportedLayout layout, Spellbook spellbook, @@ -245,23 +370,35 @@ public sealed class SpellbookWindowControllerTests Action? sendFilter = null, Action? removeSpell = null, Action>? showConfirmation = null, - Action? close = null) + Action? close = null, + ClientObjectTable? objects = null, + IReadOnlyDictionary? components = null, + SelectionState? selection = null, + Action? selectObject = null, + Action? setDesiredComponent = null, + Func? resolveComponentIcon = null) => SpellbookWindowController.Bind( layout, spellbook, - new ClientObjectTable(), + objects ?? new ClientObjectTable(), () => 1u, - new Dictionary(), + components ?? new Dictionary(), + selection ?? new SelectionState(), spellId => spellId + 1000u, - iconId => iconId, + resolveComponentIcon ?? (iconId => iconId), spellId => spellId == 103u ? 8 : 1, - _ => { }, + selectObject ?? (_ => { }), addFavorite ?? (_ => { }), sendFilter ?? (_ => { }), removeSpell ?? (_ => { }), showConfirmation ?? ((_, _) => { }), - (_, _) => { }, + setDesiredComponent ?? ((_, _) => { }), close ?? (() => { }), + new ComponentBookTemplateFactory( + FixtureLoader.LoadComponentCategoryTemplateInfos(), + FixtureLoader.LoadComponentRowTemplateInfos(), + _ => (0u, 0, 0), + defaultFont: null), RowStyle, rowFont: null); diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/component_category_21000033_10000466.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/component_category_21000033_10000466.json new file mode 100644 index 00000000..e6162133 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/component_category_21000033_10000466.json @@ -0,0 +1,358 @@ +{ + "Id": 268436582, + "Type": 12, + "X": 0, + "Y": 0, + "Width": 284, + "Height": 32, + "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": 30, + "Image": { + "File": 100668306, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 10, + "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": {} + } + ], + "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": { + "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": 1073741828, + "HJustify": 1, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": { + "": { + "Item1": 100668306, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] +} \ No newline at end of file diff --git a/tests/AcDream.App.Tests/UI/Layout/fixtures/component_row_21000033_10000467.json b/tests/AcDream.App.Tests/UI/Layout/fixtures/component_row_21000033_10000467.json new file mode 100644 index 00000000..9585ddf4 --- /dev/null +++ b/tests/AcDream.App.Tests/UI/Layout/fixtures/component_row_21000033_10000467.json @@ -0,0 +1,1395 @@ +{ + "Id": 268436583, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 284, + "Height": 32, + "OriginalParentWidth": 0, + "OriginalParentHeight": 0, + "HasOriginalParentSize": false, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100687393, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + }, + "6": { + "Id": 6, + "Name": "Highlight", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": { + "File": 100687394, + "DrawMode": 1 + }, + "Cursor": null, + "Properties": { + "Values": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": { + "": { + "Item1": 100687393, + "Item2": 1 + }, + "Highlight": { + "Item1": 100687394, + "Item2": 1 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [ + { + "Id": 268436584, + "Type": 3, + "X": 0, + "Y": 0, + "Width": 32, + "Height": 32, + "OriginalParentWidth": 284, + "OriginalParentHeight": 32, + "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": {} + } + } + }, + "DefaultStateId": 0, + "FontDid": 0, + "HJustify": 1, + "VJustify": 1, + "FontColor": null, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436585, + "Type": 12, + "X": 42, + "Y": 0, + "Width": 180, + "Height": 32, + "OriginalParentWidth": 284, + "OriginalParentHeight": 32, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 2, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741825, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 2, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "13": { + "Id": 13, + "Name": "Ghosted", + "PassToChildren": false, + "IncorporationFlags": 0, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 76, + "Green": 76, + "Red": 76, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + } + } + } + } + }, + "DefaultStateId": 0, + "FontDid": 1073741825, + "HJustify": 0, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436586, + "Type": 12, + "X": 224, + "Y": 0, + "Width": 60, + "Height": 15, + "OriginalParentWidth": 284, + "OriginalParentHeight": 32, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 3, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": null, + "Cursor": null, + "Properties": { + "Values": { + "35": { + "Kind": 4, + "MasterPropertyId": 35, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "26": { + "Kind": 7, + "MasterPropertyId": 26, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 2, + "MasterPropertyId": 24, + "UnsignedValue": 1073741824, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 3, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "36": { + "Kind": 4, + "MasterPropertyId": 36, + "UnsignedValue": 0, + "IntegerValue": 8, + "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": 1073741824, + "HJustify": 2, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": {}, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + }, + { + "Id": 268436587, + "Type": 12, + "X": 224, + "Y": 15, + "Width": 60, + "Height": 15, + "OriginalParentWidth": 284, + "OriginalParentHeight": 32, + "HasOriginalParentSize": true, + "Left": 0, + "Top": 0, + "Right": 0, + "Bottom": 0, + "ReadOrder": 4, + "ZLevel": 0, + "States": { + "4294967295": { + "Id": 4294967295, + "Name": "", + "PassToChildren": false, + "IncorporationFlags": 30, + "Image": { + "File": 100682946, + "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": 1073741824, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "37": { + "Kind": 4, + "MasterPropertyId": 37, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "27": { + "Kind": 7, + "MasterPropertyId": 27, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [ + { + "Kind": 6, + "MasterPropertyId": 25, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 255, + "Green": 255, + "Red": 255, + "Alpha": 255 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + ], + "StructValue": {} + }, + "20": { + "Kind": 0, + "MasterPropertyId": 20, + "UnsignedValue": 3, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "21": { + "Kind": 0, + "MasterPropertyId": 21, + "UnsignedValue": 1, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "22": { + "Kind": 1, + "MasterPropertyId": 22, + "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": {} + }, + "209": { + "Kind": 1, + "MasterPropertyId": 209, + "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": {} + }, + "36": { + "Kind": 4, + "MasterPropertyId": 36, + "UnsignedValue": 0, + "IntegerValue": 8, + "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": {} + }, + "203": { + "Kind": 1, + "MasterPropertyId": 203, + "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": {} + }, + "39": { + "Kind": 1, + "MasterPropertyId": 39, + "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": {} + }, + "30": { + "Kind": 4, + "MasterPropertyId": 30, + "UnsignedValue": 0, + "IntegerValue": 5, + "FloatValue": 0, + "BoolValue": false, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + }, + "32": { + "Kind": 1, + "MasterPropertyId": 32, + "UnsignedValue": 0, + "IntegerValue": 0, + "FloatValue": 0, + "BoolValue": true, + "StringInfoValue": { + "Token": 0, + "StringId": 0, + "TableId": 0, + "Override": 0, + "English": 0, + "Comment": 0 + }, + "ColorValue": { + "Blue": 0, + "Green": 0, + "Red": 0, + "Alpha": 0 + }, + "VectorValue": { + "X": 0, + "Y": 0, + "Z": 0 + }, + "ArrayValue": [], + "StructValue": {} + } + } + } + }, + "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": 1073741824, + "HJustify": 2, + "VJustify": 1, + "FontColor": { + "X": 1, + "Y": 1, + "Z": 1, + "W": 1 + }, + "StateMedia": { + "": { + "Item1": 100682946, + "Item2": 3 + } + }, + "StateCursors": {}, + "DefaultStateName": "", + "Children": [] + } + ] +} \ No newline at end of file