feat(ui): port retail component book rows
Instantiate gmSpellComponentUI category and component entries from LayoutDesc 0x21000033 instead of synthesizing catalog rows. Preserve localized category titles, authored textures and geometry, live icon/count/desired bindings, selected-object synchronization, and retail's inclusive 0..5000 desired-level contract. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
15bb61e05d
commit
a1175e6aac
13 changed files with 2395 additions and 60 deletions
|
|
@ -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.
|
- **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 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.
|
- **✓ 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`.
|
- **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.
|
- **✓ 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.
|
- **✓ 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.
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
LANDED 2026-07-15.** The complete connected melee/missile, death/loot,
|
LANDED 2026-07-15.** The complete connected melee/missile, death/loot,
|
||||||
inventory, and item-giving demo is user-gated. The shared projectile/effect
|
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.
|
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
|
Next is the connected F.5 component-book/favorite-bar visual gate, followed by
|
||||||
by cast/enchantment presentation and the two-client portal/observer VFX gate.
|
cast/enchantment presentation and the two-client portal/observer VFX gate.
|
||||||
Carried:
|
Carried:
|
||||||
#145-residual, #116 slide-response, R6/TS-42, and Track MP0.
|
#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
|
display-order sorting, carries learned spells into the open favorite bar, and
|
||||||
routes Delete through the shared retail confirmation factory plus
|
routes Delete through the shared retail confirmation factory plus
|
||||||
server-authoritative `CM_Magic::Event_RemoveSpell (0x01A8)` response path.
|
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:**
|
**Phases to ship:**
|
||||||
- **F.4** — Spell cast state machine (buffs + recalls first, projectile
|
- **F.4** — Spell cast state machine (buffs + recalls first, projectile
|
||||||
|
|
|
||||||
|
|
@ -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
|
server-persisted filter bitfield. Component rows use the installed component
|
||||||
table and the server-persisted desired purchase amounts.
|
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
|
### Spellbook row, selection, filtering, dragging, and deletion
|
||||||
|
|
||||||
Named-retail anchors:
|
Named-retail anchors:
|
||||||
|
|
|
||||||
188
src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs
Normal file
188
src/AcDream.App/UI/Layout/ComponentBookTemplateFactory.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
using DatReaderWriter;
|
||||||
|
|
||||||
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates retail's category and component listbox templates from LayoutDesc
|
||||||
|
/// 0x21000033. This is the retained equivalent of
|
||||||
|
/// <c>UIElement_ListBox::AddItemFromTemplateList</c> used by
|
||||||
|
/// <c>gmSpellComponentUI::UpdateComponents @ 0x0048A910</c>.
|
||||||
|
/// </summary>
|
||||||
|
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<uint, (uint tex, int w, int h)> _resolveSprite;
|
||||||
|
private readonly UiDatFont? _defaultFont;
|
||||||
|
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||||
|
private readonly string[] _categoryNames;
|
||||||
|
|
||||||
|
public ComponentBookTemplateFactory(
|
||||||
|
ElementInfo categoryTemplate,
|
||||||
|
ElementInfo componentTemplate,
|
||||||
|
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||||
|
UiDatFont? defaultFont,
|
||||||
|
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null,
|
||||||
|
IReadOnlyList<string>? categoryNames = null)
|
||||||
|
{
|
||||||
|
_categoryTemplate = categoryTemplate;
|
||||||
|
_componentTemplate = componentTemplate;
|
||||||
|
_resolveSprite = resolveSprite;
|
||||||
|
_defaultFont = defaultFont;
|
||||||
|
_fonts = fonts ?? new Dictionary<uint, UiDatFont?>();
|
||||||
|
_categoryNames = categoryNames?.ToArray()
|
||||||
|
?? CategoryStrings.Select(value => value.Fallback).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static ComponentBookTemplateFactory? TryLoad(
|
||||||
|
DatCollection dats,
|
||||||
|
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||||
|
UiDatFont? defaultFont,
|
||||||
|
Func<uint, UiDatFont?>? 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<uint, UiDatFont?>();
|
||||||
|
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<UiText>(content, NameId);
|
||||||
|
UiText ownedText = Required<UiText>(content, OwnedCountId);
|
||||||
|
UiField desiredField = Required<UiField>(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<T>(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<uint, UiDatFont?>? resolveFont,
|
||||||
|
Dictionary<uint, UiDatFont?> fonts)
|
||||||
|
{
|
||||||
|
if (info.FontDid != 0u && !fonts.ContainsKey(info.FontDid))
|
||||||
|
fonts[info.FontDid] = resolveFont?.Invoke(info.FontDid);
|
||||||
|
foreach (ElementInfo child in info.Children)
|
||||||
|
CaptureFonts(child, resolveFont, fonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct ComponentRow(
|
||||||
|
UiTemplateListSlot Slot,
|
||||||
|
UiField DesiredField);
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,9 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
|
||||||
using AcDream.App.Spells;
|
using AcDream.App.Spells;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
|
|
||||||
namespace AcDream.App.UI.Layout;
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
@ -48,6 +48,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
private readonly ClientObjectTable _objects;
|
private readonly ClientObjectTable _objects;
|
||||||
private readonly Func<uint> _playerGuid;
|
private readonly Func<uint> _playerGuid;
|
||||||
private readonly IReadOnlyDictionary<uint, SpellComponentDescriptor> _components;
|
private readonly IReadOnlyDictionary<uint, SpellComponentDescriptor> _components;
|
||||||
|
private readonly SelectionState _selection;
|
||||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||||
private readonly Func<uint, uint> _resolveComponentIcon;
|
private readonly Func<uint, uint> _resolveComponentIcon;
|
||||||
private readonly Func<uint, int> _spellLevel;
|
private readonly Func<uint, int> _spellLevel;
|
||||||
|
|
@ -66,10 +67,12 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
private readonly UiButton _deleteButton;
|
private readonly UiButton _deleteButton;
|
||||||
private readonly UiItemList _spellList;
|
private readonly UiItemList _spellList;
|
||||||
private readonly UiItemList _componentList;
|
private readonly UiItemList _componentList;
|
||||||
|
private readonly ComponentBookTemplateFactory _componentTemplates;
|
||||||
private readonly List<(UiButton Button, uint Mask)> _filters = new();
|
private readonly List<(UiButton Button, uint Mask)> _filters = new();
|
||||||
private readonly SpellbookRowStyle _rowStyle;
|
private readonly SpellbookRowStyle _rowStyle;
|
||||||
private readonly UiDatFont? _rowFont;
|
private readonly UiDatFont? _rowFont;
|
||||||
private uint? _selectedSpell;
|
private uint? _selectedSpell;
|
||||||
|
private uint? _selectedComponent;
|
||||||
private bool _spellsDirty;
|
private bool _spellsDirty;
|
||||||
private bool _componentsDirty;
|
private bool _componentsDirty;
|
||||||
private bool _componentEditActive;
|
private bool _componentEditActive;
|
||||||
|
|
@ -81,6 +84,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
ClientObjectTable objects,
|
ClientObjectTable objects,
|
||||||
Func<uint> playerGuid,
|
Func<uint> playerGuid,
|
||||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
||||||
|
SelectionState selection,
|
||||||
Func<uint, uint> resolveSpellIcon,
|
Func<uint, uint> resolveSpellIcon,
|
||||||
Func<uint, uint> resolveComponentIcon,
|
Func<uint, uint> resolveComponentIcon,
|
||||||
Func<uint, int> spellLevel,
|
Func<uint, int> spellLevel,
|
||||||
|
|
@ -99,6 +103,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
UiButton deleteButton,
|
UiButton deleteButton,
|
||||||
UiItemList spellList,
|
UiItemList spellList,
|
||||||
UiItemList componentList,
|
UiItemList componentList,
|
||||||
|
ComponentBookTemplateFactory componentTemplates,
|
||||||
SpellbookRowStyle rowStyle,
|
SpellbookRowStyle rowStyle,
|
||||||
UiDatFont? rowFont)
|
UiDatFont? rowFont)
|
||||||
{
|
{
|
||||||
|
|
@ -106,6 +111,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_playerGuid = playerGuid;
|
_playerGuid = playerGuid;
|
||||||
_components = components;
|
_components = components;
|
||||||
|
_selection = selection;
|
||||||
_resolveSpellIcon = resolveSpellIcon;
|
_resolveSpellIcon = resolveSpellIcon;
|
||||||
_resolveComponentIcon = resolveComponentIcon;
|
_resolveComponentIcon = resolveComponentIcon;
|
||||||
_spellLevel = spellLevel;
|
_spellLevel = spellLevel;
|
||||||
|
|
@ -124,6 +130,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
_deleteButton = deleteButton;
|
_deleteButton = deleteButton;
|
||||||
_spellList = spellList;
|
_spellList = spellList;
|
||||||
_componentList = componentList;
|
_componentList = componentList;
|
||||||
|
_componentTemplates = componentTemplates;
|
||||||
_rowStyle = rowStyle;
|
_rowStyle = rowStyle;
|
||||||
_rowFont = rowFont;
|
_rowFont = rowFont;
|
||||||
|
|
||||||
|
|
@ -148,6 +155,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
_objects.ObjectRemoved += OnObjectRemoved;
|
_objects.ObjectRemoved += OnObjectRemoved;
|
||||||
_objects.ObjectMoved += OnObjectMoved;
|
_objects.ObjectMoved += OnObjectMoved;
|
||||||
_objects.ContainerContentsReplaced += OnContainerContentsReplaced;
|
_objects.ContainerContentsReplaced += OnContainerContentsReplaced;
|
||||||
|
_selection.Changed += OnSelectionChanged;
|
||||||
ShowPage(SpellbookWindowPage.Spells);
|
ShowPage(SpellbookWindowPage.Spells);
|
||||||
RebuildAll();
|
RebuildAll();
|
||||||
}
|
}
|
||||||
|
|
@ -160,6 +168,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
ClientObjectTable objects,
|
ClientObjectTable objects,
|
||||||
Func<uint> playerGuid,
|
Func<uint> playerGuid,
|
||||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
||||||
|
SelectionState selection,
|
||||||
Func<uint, uint> resolveSpellIcon,
|
Func<uint, uint> resolveSpellIcon,
|
||||||
Func<uint, uint> resolveComponentIcon,
|
Func<uint, uint> resolveComponentIcon,
|
||||||
Func<uint, int> spellLevel,
|
Func<uint, int> spellLevel,
|
||||||
|
|
@ -170,6 +179,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
Action<string, Action<bool>> showConfirmation,
|
Action<string, Action<bool>> showConfirmation,
|
||||||
Action<uint, uint> setDesiredComponent,
|
Action<uint, uint> setDesiredComponent,
|
||||||
Action close,
|
Action close,
|
||||||
|
ComponentBookTemplateFactory componentTemplates,
|
||||||
SpellbookRowStyle rowStyle,
|
SpellbookRowStyle rowStyle,
|
||||||
UiDatFont? rowFont)
|
UiDatFont? rowFont)
|
||||||
{
|
{
|
||||||
|
|
@ -199,13 +209,14 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SpellbookWindowController(
|
return new SpellbookWindowController(
|
||||||
layout, spellbook, objects, playerGuid, components,
|
layout, spellbook, objects, playerGuid, components, selection,
|
||||||
resolveSpellIcon, resolveComponentIcon,
|
resolveSpellIcon, resolveComponentIcon,
|
||||||
spellLevel, selectObject,
|
spellLevel, selectObject,
|
||||||
addFavorite, sendFilter, removeSpell, showConfirmation,
|
addFavorite, sendFilter, removeSpell, showConfirmation,
|
||||||
setDesiredComponent, close,
|
setDesiredComponent, close,
|
||||||
spellPage, componentPage, spellTab, componentTab, closeButton,
|
spellPage, componentPage, spellTab, componentTab, closeButton,
|
||||||
deleteButton, spellList, componentList, rowStyle, rowFont);
|
deleteButton, spellList, componentList, componentTemplates,
|
||||||
|
rowStyle, rowFont);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowPage(SpellbookWindowPage page)
|
public void ShowPage(SpellbookWindowPage page)
|
||||||
|
|
@ -337,40 +348,29 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
if (category != nextCategory)
|
if (category != nextCategory)
|
||||||
{
|
{
|
||||||
category = nextCategory;
|
category = nextCategory;
|
||||||
_componentList.AddItem(new UiCatalogSlot
|
UiTemplateListSlot categoryRow =
|
||||||
|
_componentTemplates.CreateCategoryRow(nextCategory);
|
||||||
|
categoryRow.Clicked = () =>
|
||||||
{
|
{
|
||||||
EntryId = uint.MaxValue,
|
_selectedComponent = null;
|
||||||
Label = ComponentCategoryName(nextCategory),
|
SyncComponentSelection();
|
||||||
ShowLabel = true,
|
_selectObject(0u);
|
||||||
SpriteResolve = _componentList.SpriteResolve,
|
};
|
||||||
});
|
_componentList.AddItem(categoryRow);
|
||||||
}
|
}
|
||||||
uint desired = _spellbook.DesiredComponents.TryGetValue(componentId, out uint amount) ? amount : 0u;
|
uint desired = _spellbook.DesiredComponents.TryGetValue(componentId, out uint amount) ? amount : 0u;
|
||||||
var row = new UiCatalogSlot
|
string componentName = string.IsNullOrWhiteSpace(pack.Name)
|
||||||
{
|
? descriptor?.Name ?? $"Component {componentId}"
|
||||||
EntryId = componentId,
|
: pack.Name;
|
||||||
CatalogIconTexture = _resolveComponentIcon(
|
ComponentBookTemplateFactory.ComponentRow row =
|
||||||
pack.IconId != 0 ? pack.IconId : descriptor?.IconId ?? 0u),
|
_componentTemplates.CreateComponentRow(
|
||||||
Label = string.IsNullOrWhiteSpace(pack.Name) ? descriptor?.Name ?? $"Component {componentId}" : pack.Name,
|
componentId,
|
||||||
Detail = packGroup.Quantity.ToString(CultureInfo.InvariantCulture),
|
_resolveComponentIcon(
|
||||||
ShowLabel = true,
|
pack.IconId != 0 ? pack.IconId : descriptor?.IconId ?? 0u),
|
||||||
SpriteResolve = _componentList.SpriteResolve,
|
componentName,
|
||||||
};
|
packGroup.Quantity,
|
||||||
var desiredField = new UiField
|
desired);
|
||||||
{
|
UiField desiredField = row.DesiredField;
|
||||||
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;
|
desiredField.OnFocusGained = () => _componentEditActive = true;
|
||||||
uint committed = desired;
|
uint committed = desired;
|
||||||
void Commit(string value)
|
void Commit(string value)
|
||||||
|
|
@ -392,25 +392,18 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
Commit(value);
|
Commit(value);
|
||||||
_componentEditActive = false;
|
_componentEditActive = false;
|
||||||
};
|
};
|
||||||
row.AddChild(desiredField);
|
row.Slot.Clicked = () =>
|
||||||
row.Clicked = () => _selectObject(pack.ObjectId);
|
{
|
||||||
_componentList.AddItem(row);
|
_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)
|
private bool IsOwnedByPlayer(ClientObject item, uint playerGuid)
|
||||||
{
|
{
|
||||||
if (playerGuid == 0) return item.ContainerId != 0 || item.WielderId != 0;
|
if (playerGuid == 0) return item.ContainerId != 0 || item.WielderId != 0;
|
||||||
|
|
@ -479,6 +472,23 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
slot.Selected = slot.EntryId == _selectedSpell;
|
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()
|
public void Tick()
|
||||||
{
|
{
|
||||||
if (_spellsDirty)
|
if (_spellsDirty)
|
||||||
|
|
@ -505,6 +515,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
}
|
}
|
||||||
private void OnObjectMoved(ClientObjectMove _) => _componentsDirty = true;
|
private void OnObjectMoved(ClientObjectMove _) => _componentsDirty = true;
|
||||||
private void OnContainerContentsReplaced(uint _) => _componentsDirty = true;
|
private void OnContainerContentsReplaced(uint _) => _componentsDirty = true;
|
||||||
|
private void OnSelectionChanged(SelectionTransition _) => SyncComponentSelectionFromWorld();
|
||||||
private bool IsComponent(ClientObject item)
|
private bool IsComponent(ClientObject item)
|
||||||
=> item.IsComponentPack || _components.ContainsKey(item.WeenieClassId);
|
=> item.IsComponentPack || _components.ContainsKey(item.WeenieClassId);
|
||||||
public void OnShown() => RebuildAll();
|
public void OnShown() => RebuildAll();
|
||||||
|
|
@ -520,6 +531,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
||||||
_objects.ObjectRemoved -= OnObjectRemoved;
|
_objects.ObjectRemoved -= OnObjectRemoved;
|
||||||
_objects.ObjectMoved -= OnObjectMoved;
|
_objects.ObjectMoved -= OnObjectMoved;
|
||||||
_objects.ContainerContentsReplaced -= OnContainerContentsReplaced;
|
_objects.ContainerContentsReplaced -= OnContainerContentsReplaced;
|
||||||
|
_selection.Changed -= OnSelectionChanged;
|
||||||
RetailTabBinding.SetClick(_spellTab, null);
|
RetailTabBinding.SetClick(_spellTab, null);
|
||||||
RetailTabBinding.SetClick(_componentTab, null);
|
RetailTabBinding.SetClick(_componentTab, null);
|
||||||
_closeButton.OnClick = null;
|
_closeButton.OnClick = null;
|
||||||
|
|
|
||||||
72
src/AcDream.App/UI/Layout/UiTemplateListSlot.cs
Normal file
72
src/AcDream.App/UI/Layout/UiTemplateListSlot.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One runtime listbox entry instantiated from an authored LayoutDesc template.
|
||||||
|
/// Retail <c>UIElement_ListBox::AddItemFromTemplateList</c> creates exactly this
|
||||||
|
/// ownership shape: the list owns the entry while the template owns its visuals.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -709,6 +709,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
{
|
{
|
||||||
ImportedLayout? layout;
|
ImportedLayout? layout;
|
||||||
SpellbookRowStyle? rowStyle;
|
SpellbookRowStyle? rowStyle;
|
||||||
|
ComponentBookTemplateFactory? componentTemplates;
|
||||||
lock (_bindings.Assets.DatLock)
|
lock (_bindings.Assets.DatLock)
|
||||||
{
|
{
|
||||||
layout = LayoutImporter.Import(
|
layout = LayoutImporter.Import(
|
||||||
|
|
@ -719,10 +720,16 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Assets.DefaultFont,
|
_bindings.Assets.DefaultFont,
|
||||||
_bindings.Assets.ResolveFont);
|
_bindings.Assets.ResolveFont);
|
||||||
rowStyle = SpellbookRowStyle.TryLoad(_bindings.Assets.Dats);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -737,6 +744,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Magic.Objects,
|
_bindings.Magic.Objects,
|
||||||
_bindings.Magic.PlayerGuid,
|
_bindings.Magic.PlayerGuid,
|
||||||
_bindings.Magic.Components,
|
_bindings.Magic.Components,
|
||||||
|
_bindings.Magic.Selection,
|
||||||
_bindings.Magic.ResolveSpellIcon,
|
_bindings.Magic.ResolveSpellIcon,
|
||||||
_bindings.Magic.ResolveComponentIcon,
|
_bindings.Magic.ResolveComponentIcon,
|
||||||
_bindings.Magic.SpellLevel,
|
_bindings.Magic.SpellLevel,
|
||||||
|
|
@ -747,6 +755,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
(message, completed) => ShowConfirmation(message, completed),
|
(message, completed) => ShowConfirmation(message, completed),
|
||||||
_bindings.Magic.SetDesiredComponent,
|
_bindings.Magic.SetDesiredComponent,
|
||||||
() => CloseWindow(WindowNames.Spellbook),
|
() => CloseWindow(WindowNames.Spellbook),
|
||||||
|
componentTemplates,
|
||||||
rowStyle.Value,
|
rowStyle.Value,
|
||||||
rowFont);
|
rowFont);
|
||||||
if (controller is null)
|
if (controller is null)
|
||||||
|
|
|
||||||
23
src/AcDream.App/UI/UiTextureElement.cs
Normal file
23
src/AcDream.App/UI/UiTextureElement.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
|
||||||
|
namespace AcDream.App.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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).
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -101,6 +101,12 @@ public static class FixtureLoader
|
||||||
public static ElementInfo LoadSpellbookInfos()
|
public static ElementInfo LoadSpellbookInfos()
|
||||||
=> LoadInfos("spellbook_21000034.json");
|
=> 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()
|
public static ImportedLayout LoadPowerbar()
|
||||||
=> LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null);
|
=> LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,22 @@ public sealed class RetailLayoutFixtureGenerator
|
||||||
"Asheron's Call");
|
"Asheron's Call");
|
||||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
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;
|
uint masterDid = (uint)dats.Portal.Header.MasterMapId;
|
||||||
Assert.True(dats.Portal.TryGet<EnumIDMap>(masterDid, out var master));
|
Assert.True(dats.Portal.TryGet<EnumIDMap>(masterDid, out var master));
|
||||||
Assert.True(master!.ClientEnumToID.TryGetValue(5u, out uint uiMapDid));
|
Assert.True(master!.ClientEnumToID.TryGetValue(5u, out uint uiMapDid));
|
||||||
|
|
@ -102,6 +118,26 @@ public sealed class RetailLayoutFixtureGenerator
|
||||||
File.WriteAllText(Path.Combine(FixtureDirectory(), fileName), effectsJson);
|
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)
|
foreach (var (layoutId, fileName) in Layouts)
|
||||||
{
|
{
|
||||||
var info = LayoutImporter.ImportInfos(dats, layoutId);
|
var info = LayoutImporter.ImportInfos(dats, layoutId);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using AcDream.App.Spells;
|
||||||
using AcDream.App.UI;
|
using AcDream.App.UI;
|
||||||
using AcDream.App.UI.Layout;
|
using AcDream.App.UI.Layout;
|
||||||
using AcDream.Core.Items;
|
using AcDream.Core.Items;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.Spells;
|
using AcDream.Core.Spells;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
|
|
@ -238,6 +239,130 @@ public sealed class SpellbookWindowControllerTests
|
||||||
Assert.Empty(VisibleSpellIds(list));
|
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<uint, SpellComponentDescriptor>
|
||||||
|
{
|
||||||
|
[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<uint>();
|
||||||
|
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<UiItemList>(
|
||||||
|
layout.FindElement(SpellbookWindowController.ComponentListId)?.Children.Single());
|
||||||
|
Assert.Equal(5, list.GetNumUIItems());
|
||||||
|
Assert.Same(
|
||||||
|
list.Scroll,
|
||||||
|
Assert.IsType<UiScrollbar>(
|
||||||
|
layout.FindElement(SpellbookWindowController.ComponentScrollbarId)).Model);
|
||||||
|
|
||||||
|
UiTemplateListSlot scarabs = Assert.IsType<UiTemplateListSlot>(list.GetItem(0));
|
||||||
|
UiText scarabTitle = Assert.IsType<UiText>(scarabs.Content.Root);
|
||||||
|
Assert.Equal("SCARABS", Assert.Single(scarabTitle.LinesProvider()).Text);
|
||||||
|
Assert.Equal(0x06001392u, scarabTitle.BackgroundSprite);
|
||||||
|
|
||||||
|
UiTemplateListSlot copper = Assert.IsType<UiTemplateListSlot>(list.GetItem(1));
|
||||||
|
UiDatElement copperRoot = Assert.IsType<UiDatElement>(copper.Content.Root);
|
||||||
|
Assert.Equal((0x06005E21u, 1), copperRoot.ActiveMedia());
|
||||||
|
Assert.Equal("Copper Scarab", Assert.Single(
|
||||||
|
Assert.IsType<UiText>(copper.Content.FindElement(
|
||||||
|
ComponentBookTemplateFactory.NameId)).LinesProvider()).Text);
|
||||||
|
Assert.Equal("2", Assert.Single(
|
||||||
|
Assert.IsType<UiText>(copper.Content.FindElement(
|
||||||
|
ComponentBookTemplateFactory.OwnedCountId)).LinesProvider()).Text);
|
||||||
|
UiField desiredField = Assert.IsType<UiField>(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<UiElement>(copper.Content.FindElement(
|
||||||
|
ComponentBookTemplateFactory.IconId));
|
||||||
|
Assert.Equal(
|
||||||
|
0x06001100u,
|
||||||
|
Assert.IsType<UiTextureElement>(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<UiTemplateListSlot>(list.GetItem(3));
|
||||||
|
Assert.Equal("HERBS", Assert.Single(
|
||||||
|
Assert.IsType<UiText>(herbs.Content.Root).LinesProvider()).Text);
|
||||||
|
UiTemplateListSlot amaranth = Assert.IsType<UiTemplateListSlot>(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(
|
private static SpellbookWindowController? Bind(
|
||||||
ImportedLayout layout,
|
ImportedLayout layout,
|
||||||
Spellbook spellbook,
|
Spellbook spellbook,
|
||||||
|
|
@ -245,23 +370,35 @@ public sealed class SpellbookWindowControllerTests
|
||||||
Action<uint>? sendFilter = null,
|
Action<uint>? sendFilter = null,
|
||||||
Action<uint>? removeSpell = null,
|
Action<uint>? removeSpell = null,
|
||||||
Action<string, Action<bool>>? showConfirmation = null,
|
Action<string, Action<bool>>? showConfirmation = null,
|
||||||
Action? close = null)
|
Action? close = null,
|
||||||
|
ClientObjectTable? objects = null,
|
||||||
|
IReadOnlyDictionary<uint, SpellComponentDescriptor>? components = null,
|
||||||
|
SelectionState? selection = null,
|
||||||
|
Action<uint>? selectObject = null,
|
||||||
|
Action<uint, uint>? setDesiredComponent = null,
|
||||||
|
Func<uint, uint>? resolveComponentIcon = null)
|
||||||
=> SpellbookWindowController.Bind(
|
=> SpellbookWindowController.Bind(
|
||||||
layout,
|
layout,
|
||||||
spellbook,
|
spellbook,
|
||||||
new ClientObjectTable(),
|
objects ?? new ClientObjectTable(),
|
||||||
() => 1u,
|
() => 1u,
|
||||||
new Dictionary<uint, SpellComponentDescriptor>(),
|
components ?? new Dictionary<uint, SpellComponentDescriptor>(),
|
||||||
|
selection ?? new SelectionState(),
|
||||||
spellId => spellId + 1000u,
|
spellId => spellId + 1000u,
|
||||||
iconId => iconId,
|
resolveComponentIcon ?? (iconId => iconId),
|
||||||
spellId => spellId == 103u ? 8 : 1,
|
spellId => spellId == 103u ? 8 : 1,
|
||||||
_ => { },
|
selectObject ?? (_ => { }),
|
||||||
addFavorite ?? (_ => { }),
|
addFavorite ?? (_ => { }),
|
||||||
sendFilter ?? (_ => { }),
|
sendFilter ?? (_ => { }),
|
||||||
removeSpell ?? (_ => { }),
|
removeSpell ?? (_ => { }),
|
||||||
showConfirmation ?? ((_, _) => { }),
|
showConfirmation ?? ((_, _) => { }),
|
||||||
(_, _) => { },
|
setDesiredComponent ?? ((_, _) => { }),
|
||||||
close ?? (() => { }),
|
close ?? (() => { }),
|
||||||
|
new ComponentBookTemplateFactory(
|
||||||
|
FixtureLoader.LoadComponentCategoryTemplateInfos(),
|
||||||
|
FixtureLoader.LoadComponentRowTemplateInfos(),
|
||||||
|
_ => (0u, 0, 0),
|
||||||
|
defaultFont: null),
|
||||||
RowStyle,
|
RowStyle,
|
||||||
rowFont: null);
|
rowFont: null);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue