fix(ui): restore modern spell formula presentation
Resolve spell-examination component cells through their DAT icon DIDs, project scarab and prismatic-taper formulas when ACE disables component enforcement, and version authored window geometry so stale examination sizes reset once without losing user layout behavior. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
3e31b0ac70
commit
d4ce64f56b
22 changed files with 307 additions and 41 deletions
|
|
@ -115,8 +115,8 @@ and the final two-client portal-out/materialization observer flow are
|
||||||
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||||
Release builds; the appraisal and retained catalog-input corrections pass
|
Release builds; the appraisal formula-media/geometry correction passes
|
||||||
3,553 App tests / 3 skips and 7,984 complete-solution tests / 5 skips.
|
3,555 App tests / 3 skips and 7,986 complete-solution tests / 5 skips.
|
||||||
|
|
||||||
**Current work order:** the six-slice pre-M4 world-interaction completion
|
**Current work order:** the six-slice pre-M4 world-interaction completion
|
||||||
program in `docs/plans/2026-07-23-world-interaction-completion.md`: favorite
|
program in `docs/plans/2026-07-23-world-interaction-completion.md`: favorite
|
||||||
|
|
@ -126,8 +126,9 @@ transactions. Slices 1 and 2 plus the adjacent shared-cooldown presentation are
|
||||||
user-accepted. Slice 3's first connected gate exposed and corrected the
|
user-accepted. Slice 3's first connected gate exposed and corrected the
|
||||||
monster-response flag mismatch, non-retail shared-panel mounting, and missing
|
monster-response flag mismatch, non-retail shared-panel mounting, and missing
|
||||||
inscription transaction. The creature and common-item report corrections are
|
inscription transaction. The creature and common-item report corrections are
|
||||||
user-accepted; favorite-spell press-time selection and right-click local
|
user-accepted. Favorite-spell press-time selection and right-click local
|
||||||
examination are the current visual gate.
|
examination are implemented; the current visual gate is its corrected modern
|
||||||
|
scarab/prismatic formula, DAT component icons, and authored 310 x 400 extent.
|
||||||
|
|
||||||
**Structural prerequisite before new M4 subsystem work:** all eight
|
**Structural prerequisite before new M4 subsystem work:** all eight
|
||||||
behavior-preserving `GameWindow` decomposition slices and the automated
|
behavior-preserving `GameWindow` decomposition slices and the automated
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ and the final two-client portal-out/materialization observer flow are
|
||||||
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
user-gated. Deterministic world-lifecycle automation protects fresh login,
|
||||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||||
Release builds; the appraisal and retained catalog-input corrections pass
|
Release builds; the appraisal formula-media/geometry correction passes
|
||||||
3,553 App tests / 3 skips and 7,984 complete-solution tests / 5 skips.
|
3,555 App tests / 3 skips and 7,986 complete-solution tests / 5 skips.
|
||||||
|
|
||||||
**Current work order:** the six-slice pre-M4 world-interaction completion
|
**Current work order:** the six-slice pre-M4 world-interaction completion
|
||||||
program in `docs/plans/2026-07-23-world-interaction-completion.md`: favorite
|
program in `docs/plans/2026-07-23-world-interaction-completion.md`: favorite
|
||||||
|
|
@ -124,8 +124,9 @@ transactions. Slices 1 and 2 plus the adjacent shared-cooldown presentation are
|
||||||
user-accepted. Slice 3's first connected gate exposed and corrected the
|
user-accepted. Slice 3's first connected gate exposed and corrected the
|
||||||
monster-response flag mismatch, non-retail shared-panel mounting, and missing
|
monster-response flag mismatch, non-retail shared-panel mounting, and missing
|
||||||
inscription transaction. The creature and common-item report corrections are
|
inscription transaction. The creature and common-item report corrections are
|
||||||
user-accepted; favorite-spell press-time selection and right-click local
|
user-accepted. Favorite-spell press-time selection and right-click local
|
||||||
examination are the current visual gate.
|
examination are implemented; the current visual gate is its corrected modern
|
||||||
|
scarab/prismatic formula, DAT component icons, and authored 310 x 400 extent.
|
||||||
|
|
||||||
**Structural prerequisite before new M4 subsystem work:** all eight
|
**Structural prerequisite before new M4 subsystem work:** all eight
|
||||||
behavior-preserving `GameWindow` decomposition slices and the automated
|
behavior-preserving `GameWindow` decomposition slices and the automated
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,13 @@ component formula. It sends no Appraise request and takes no busy reference,
|
||||||
so the status-bar magnifier remains object-only. Switching the shared floaty
|
so the status-bar magnifier remains object-only. Switching the shared floaty
|
||||||
from a pending object appraisal to a spell releases exactly that appraisal
|
from a pending object appraisal to a spell releases exactly that appraisal
|
||||||
transaction before showing the SpellPanel.
|
transaction before showing the SpellPanel.
|
||||||
|
The formula-media correction now passes the component's DAT icon DID to
|
||||||
|
`GetSpellComponentIcon` instead of its WCID, so the authored cells render
|
||||||
|
their actual scarab/taper images. ACE characters with component enforcement
|
||||||
|
disabled show the modern scarab/prismatic formula rather than an inactive
|
||||||
|
legacy recipe (IA-21). A per-window authored-geometry revision resets the
|
||||||
|
obsolete saved 545-pixel examination height once to LayoutDesc's 310 x 400
|
||||||
|
extent, preserving its position and all future retail-style user resizing.
|
||||||
|
|
||||||
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
|
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
|
||||||
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
`src/AcDream.App/UI/RetailUiRuntime.cs`;
|
||||||
|
|
@ -196,6 +203,10 @@ dragging preserves that selection without activating the item.
|
||||||
Left-pressing a favorite spell likewise changes its selection immediately.
|
Left-pressing a favorite spell likewise changes its selection immediately.
|
||||||
Right-clicking it opens the spell examination subview; selecting it and using
|
Right-clicking it opens the spell examination subview; selecting it and using
|
||||||
the status-bar magnifier does not examine the spell.
|
the status-bar magnifier does not examine the spell.
|
||||||
|
The formula contains only its power scarab(s) and prismatic taper(s) in the
|
||||||
|
component-disabled ACE test profile, every component cell shows its DAT icon,
|
||||||
|
and the first corrected open is 310 x 400 even when the old profile saved a
|
||||||
|
545-pixel height.
|
||||||
The Black Phyntos Hive specifically reads `Value: ???`, `Burden: Unknown`,
|
The Black Phyntos Hive specifically reads `Value: ???`, `Burden: Unknown`,
|
||||||
then its description after one retail paragraph break, without the bogus
|
then its description after one retail paragraph break, without the bogus
|
||||||
255-item/255-container line.
|
255-item/255-container line.
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,12 @@ accepted-divergence entries (#96, #49, #50).
|
||||||
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms). Synthetic wrapper borders/whole-window drag regions use the exact DAT Type-2/Type-9 control cursors. `gmPanelUI` children are independently imported retained frames rather than one physical parent, but `RetailPanelUiController` owns their one canonical geometry and exclusive child lifecycle | `src/AcDream.App/UI/README.md:3`; `src/AcDream.App/UI/CursorFeedbackController.cs`; `src/AcDream.App/UI/Layout/RetailPanelUiController.cs` | keystone.dll has no PDB/decomp; semantics are reconstructed from retail UI deep-dives, named client methods, and production LayoutDesc media. Separate child wrappers preserve each LayoutDesc's content tree while typed move/resize synchronization gives all registered toolbar/detail children the same persistent parent rectangle | Edge-case low-level input semantics can differ silently even though outer geometry, visibility, and restore-previous ownership match | `UIElementManager::CheckCursor` 0x0045ABF0; `UIElement_Resizebar::StartMouseResizing` 0x0046B7E0; `UIElement_Dragbar::StartMouseMoving` 0x0046C760; `gmPanelUI::SetupChildren` 0x004BC9E0; docs/research/retail-ui/04-input-events.md |
|
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms). Synthetic wrapper borders/whole-window drag regions use the exact DAT Type-2/Type-9 control cursors. `gmPanelUI` children are independently imported retained frames rather than one physical parent, but `RetailPanelUiController` owns their one canonical geometry and exclusive child lifecycle | `src/AcDream.App/UI/README.md:3`; `src/AcDream.App/UI/CursorFeedbackController.cs`; `src/AcDream.App/UI/Layout/RetailPanelUiController.cs` | keystone.dll has no PDB/decomp; semantics are reconstructed from retail UI deep-dives, named client methods, and production LayoutDesc media. Separate child wrappers preserve each LayoutDesc's content tree while typed move/resize synchronization gives all registered toolbar/detail children the same persistent parent rectangle | Edge-case low-level input semantics can differ silently even though outer geometry, visibility, and restore-previous ownership match | `UIElementManager::CheckCursor` 0x0045ABF0; `UIElement_Resizebar::StartMouseResizing` 0x0046B7E0; `UIElement_Dragbar::StartMouseMoving` 0x0046C760; `gmPanelUI::SetupChildren` 0x004BC9E0; docs/research/retail-ui/04-input-events.md |
|
||||||
| IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
|
| IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
|
||||||
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` |
|
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` |
|
||||||
| IA-15 | D.2b gameplay UI is our own `UiHost`/`UiRoot` retained tree, not a byte-port of Keystone. `RetailUiRuntime` owns the production import/mount graph; `RetailWindowManager`/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, schema-v2 per-character/per-resolution automatic layouts, and portable named `saveui/loadui` profiles; `RetailWindowFrame` is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals `0x2100006C`, chat `0x21000006`, toolbar `0x21000016`, character `0x2100002E`, inventory `0x21000023` plus mounted `0x21000024/22/21`, dialog catalog `0x2100003C`, radar `0x21000074`, and external container `0x21000008` (shared bevel plus a user-directed compact 700-pixel initial content width instead of its authored 800-pixel root). The dialog context/queue/callback lifecycle is now a named-client port; only its retained rendering remains under this Keystone adaptation. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/RetailWindowManager.cs`; `src/AcDream.App/UI/RetainedPanelControllerGroup.cs`; `src/AcDream.App/UI/UiHost.cs`; `src/AcDream.App/UI/RetailWindowLayoutPersistence.cs`; `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/Layout/RetailDialogFactory.cs`; `src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs`; `src/AcDream.App/UI/Layout/ExternalContainerController.cs`; `src/AcDream.App/UI/Layout/LayoutImporter.cs`; binding supply in `GameWindow.cs` | Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle; the external strip's initial width follows the connected visual direction and remains horizontally resizable to its authored extent or the viewport edge. | Persistence and low-level widget rendering are behaviorally reconstructed from retail semantics rather than a Keystone byte-port; lifecycle edge cases remain constrained by conformance tests. The external strip opens 100 pixels narrower than the raw LayoutDesc before user/persistence resizing. | Production LayoutDesc objects; `DialogFactory @ 0x004773C0..0x00478470`; `gmExternalContainerUI @ 0x004CBAD0..0x004CBFE0`; `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`; Keystone behavior notes in `docs/research/retail-ui/` |
|
| IA-15 | D.2b gameplay UI is our own `UiHost`/`UiRoot` retained tree, not a byte-port of Keystone. `RetailUiRuntime` owns the production import/mount graph; `RetailWindowManager`/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, schema-v2 per-character/per-resolution automatic layouts with per-window authored-geometry revisions, and portable named `saveui/loadui` profiles; `RetailWindowFrame` is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals `0x2100006C`, chat `0x21000006`, toolbar `0x21000016`, character `0x2100002E`, inventory `0x21000023` plus mounted `0x21000024/22/21`, dialog catalog `0x2100003C`, radar `0x21000074`, and external container `0x21000008` (shared bevel plus a user-directed compact 700-pixel initial content width instead of its authored 800-pixel root). The dialog context/queue/callback lifecycle is now a named-client port; only its retained rendering remains under this Keystone adaptation. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/RetailWindowManager.cs`; `src/AcDream.App/UI/RetainedPanelControllerGroup.cs`; `src/AcDream.App/UI/UiHost.cs`; `src/AcDream.App/UI/RetailWindowLayoutPersistence.cs`; `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/Layout/RetailDialogFactory.cs`; `src/AcDream.App/UI/Layout/RetailConfirmationDialogView.cs`; `src/AcDream.App/UI/Layout/ExternalContainerController.cs`; `src/AcDream.App/UI/Layout/LayoutImporter.cs`; binding supply in `GameWindow.cs` | Keystone has no matching PDB/decomp, so we preserve its observable ElementDesc/state/input behavior from DAT, named client call sites, and live evidence while using modern retained ownership. Real RenderSurfaces and imported element geometry remain the visual oracle; the external strip's initial width follows the connected visual direction and remains horizontally resizable to its authored extent or the viewport edge. | Persistence and low-level widget rendering are behaviorally reconstructed from retail semantics rather than a Keystone byte-port; lifecycle edge cases remain constrained by conformance tests. The external strip opens 100 pixels narrower than the raw LayoutDesc before user/persistence resizing. | Production LayoutDesc objects; `DialogFactory @ 0x004773C0..0x00478470`; `gmExternalContainerUI @ 0x004CBAD0..0x004CBFE0`; `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`; Keystone behavior notes in `docs/research/retail-ui/` |
|
||||||
| IA-17 | Toolbar chrome is toolkit-supplied through the central `RetailWindowFrame` mount (`UiCollapsibleFrame` 8-piece bevel) because LayoutDesc `0x21000016` carries no baked frame. It also supports a toolkit-defined collapse-to-one-row (bottom-edge resize snapping between a row-1-only and a two-row height, row-2 visibility tied to the stop) — retail's real collapse is keystone.dll (no decomp) and the DAT stacks both rows always. | `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/UiCollapsibleFrame.cs`; toolbar policy in `GameWindow.cs`; spec: `docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md` | The central mount now owns wrapper geometry/registration uniformly; border-over-content prevents the row-2 right cap from poking through | The collapse stops remain a toolkit reconstruction rather than a byte-port of Keystone behavior | gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump) |
|
| IA-17 | Toolbar chrome is toolkit-supplied through the central `RetailWindowFrame` mount (`UiCollapsibleFrame` 8-piece bevel) because LayoutDesc `0x21000016` carries no baked frame. It also supports a toolkit-defined collapse-to-one-row (bottom-edge resize snapping between a row-1-only and a two-row height, row-2 visibility tied to the stop) — retail's real collapse is keystone.dll (no decomp) and the DAT stacks both rows always. | `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/UiCollapsibleFrame.cs`; toolbar policy in `GameWindow.cs`; spec: `docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md` | The central mount now owns wrapper geometry/registration uniformly; border-over-content prevents the row-2 right cap from poking through | The collapse stops remain a toolkit reconstruction rather than a byte-port of Keystone behavior | gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump) |
|
||||||
| IA-18 | Effect overlay tile (enum 0x10000005) is a `ReplaceColor` SURFACE SOURCE — pure-white pixels in the composited drag icon are replaced PER-PIXEL with the same (x,y) pixel of the effect tile (the SURFACE overload `SurfaceWindow::ReplaceColor` 0x004415b0), preserving the tile's texture/gradient; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior. **Anti-regression: do NOT re-implement this as a blit layer NOR as a flat-color replace (it is a per-pixel surface copy).** | `src/AcDream.App/UI/IconComposer.cs` (`ReplaceWhiteFromSurface`) | Faithful port of `IconData::RenderIcons` @407614 → the SURFACE overload `ReplaceColor` 0x004415b0 (`dst[x,y]=src[x,y]` where `dst==white`); confirmed via clean Ghidra decompile + named decomp + visual (the Energy Crystal's blue is a gradient, 2026-06-17). | A blit-layer or flat-color re-implementation would show the wrong effect look (no gradient) — the visual-verification regression that retired the mean-color approximation | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407524; `ReplaceColor` SURFACE overload 0x004415b0:71656; `docs/research/2026-06-17-stateful-icon-RESOLVED.md` |
|
| IA-18 | Effect overlay tile (enum 0x10000005) is a `ReplaceColor` SURFACE SOURCE — pure-white pixels in the composited drag icon are replaced PER-PIXEL with the same (x,y) pixel of the effect tile (the SURFACE overload `SurfaceWindow::ReplaceColor` 0x004415b0), preserving the tile's texture/gradient; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior. **Anti-regression: do NOT re-implement this as a blit layer NOR as a flat-color replace (it is a per-pixel surface copy).** | `src/AcDream.App/UI/IconComposer.cs` (`ReplaceWhiteFromSurface`) | Faithful port of `IconData::RenderIcons` @407614 → the SURFACE overload `ReplaceColor` 0x004415b0 (`dst[x,y]=src[x,y]` where `dst==white`); confirmed via clean Ghidra decompile + named decomp + visual (the Energy Crystal's blue is a gradient, 2026-06-17). | A blit-layer or flat-color re-implementation would show the wrong effect look (no gradient) — the visual-verification regression that retired the mean-color approximation | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407524; `ReplaceColor` SURFACE overload 0x004415b0:71656; `docs/research/2026-06-17-stateful-icon-RESOLVED.md` |
|
||||||
| IA-19 | Automatic combat acquisition is narrowed to attackable non-player monsters. Retail `AutoTarget` falls back to `SelectNext(SELECTION_TYPE_COMPASS_ITEM)`, whose combat filter can also admit attackable enemy players in compatible PK states. | `src/AcDream.Core/Combat/CombatTargetPolicy.cs`; consumers `src/AcDream.App/Interaction/WorldSelectionQuery.cs` (`IsHostileMonster`/`FindClosestHostileMonster`) and `SelectionInteractionController.cs` (`SelectClosestCombatTarget`) | Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available | In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually | `ClientCombatSystem::AutoTarget @ 0x0056BC80`; `CPlayerSystem::SelectNext @ 0x0055F9A0`; `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600` |
|
| IA-19 | Automatic combat acquisition is narrowed to attackable non-player monsters. Retail `AutoTarget` falls back to `SelectNext(SELECTION_TYPE_COMPASS_ITEM)`, whose combat filter can also admit attackable enemy players in compatible PK states. | `src/AcDream.Core/Combat/CombatTargetPolicy.cs`; consumers `src/AcDream.App/Interaction/WorldSelectionQuery.cs` (`IsHostileMonster`/`FindClosestHostileMonster`) and `SelectionInteractionController.cs` (`SelectClosestCombatTarget`) | Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available | In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually | `ClientCombatSystem::AutoTarget @ 0x0056BC80`; `CPlayerSystem::SelectNext @ 0x0055F9A0`; `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600` |
|
||||||
| IA-20 | The basic combat bar keeps dark-red media `0x0600715E` visible as the centered middle baseline. Retail skill-gates field `0x100005EF` to trained Recklessness; the separate bright child remains faithful live `SetPowerbarLevel` feedback from the absolute left edge. | `src/AcDream.App/UI/UiScrollbar.cs`; child-policy extraction in `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` | Explicit connected visual direction: the dark middle track remains present behind live attack charge; the exact skill-gated treatment remains tracked by AP-112 | Untrained characters retain the dark-red baseline where retail may leave only the gray track; trained/untrained Recklessness presentation is not distinguishable | `gmCombatUI::RecvNotice_SetPowerbarLevel @ 0x004CC0E0`; `gmCombatUI::ListenToElementMessage @ 0x004CC430`; LayoutDesc `0x21000073` |
|
| IA-20 | The basic combat bar keeps dark-red media `0x0600715E` visible as the centered middle baseline. Retail skill-gates field `0x100005EF` to trained Recklessness; the separate bright child remains faithful live `SetPowerbarLevel` feedback from the absolute left edge. | `src/AcDream.App/UI/UiScrollbar.cs`; child-policy extraction in `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` | Explicit connected visual direction: the dark middle track remains present behind live attack charge; the exact skill-gated treatment remains tracked by AP-112 | Untrained characters retain the dark-red baseline where retail may leave only the gray track; trained/untrained Recklessness presentation is not distinguishable | `gmCombatUI::RecvNotice_SetPowerbarLevel @ 0x004CC0E0`; `gmCombatUI::ListenToElementMessage @ 0x004CC430`; LayoutDesc `0x21000073` |
|
||||||
|
| IA-21 | When ACE sends player BoolProperty `68` (`SpellComponentsRequired`) false, acdream presents the retail scarab/prismatic-taper formula even without a directly carried school focus. With component enforcement enabled, retail's exact focus/infusion versus account-customized selection remains intact. | `src/AcDream.App/Spells/SpellComponentRequirementService.cs` | A component-disabled server has no actionable legacy recipe; explicit product direction is that this client/server mode uses the modern scarab/taper component presentation | A custom server could expect retail's legacy recipe to remain visible even though casting consumes no components | `ClientMagicSystem::AreSpellComponentsRequired @ 0x00567B90`; `ClientMagicSystem::GetAppropriateSpellFormula @ 0x00567D50`; `CSpellBase::InqScarabOnlyFormula @ 0x00597050` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,12 @@ selects without waiting for release; right-click opens the examination
|
||||||
floaty's authored SpellPanel locally; and spell IDs never enter global object
|
floaty's authored SpellPanel locally; and spell IDs never enter global object
|
||||||
selection or the status-bar magnifier path. The view projects retail's
|
selection or the status-bar magnifier path. The view projects retail's
|
||||||
name/school/mana/duration/range/description and authored appropriate-formula
|
name/school/mana/duration/range/description and authored appropriate-formula
|
||||||
component cells, including live missing-component state. Its connected visual
|
component cells, including live missing-component state. The follow-up resolves
|
||||||
gate is pending. Slice 3's first connected gate exposed
|
those cells through the component icon DID, shows the modern
|
||||||
|
scarab/prismatic formula for ACE characters whose component enforcement is
|
||||||
|
disabled (IA-21), and one-time migrates stale saved examination dimensions to
|
||||||
|
the authored 310 x 400 extent while preserving retail-style later resizing.
|
||||||
|
Its connected visual gate is pending. Slice 3's first connected gate exposed
|
||||||
three deeper defects, now corrected: the response enum mislabeled ACE's
|
three deeper defects, now corrected: the response enum mislabeled ACE's
|
||||||
creature bit `0x0100` and dropped monster packets while retaining the busy
|
creature bit `0x0100` and dropped monster packets while retaining the busy
|
||||||
cursor; the imported 310 x 400 examination layout was incorrectly hosted as
|
cursor; the imported 310 x 400 examination layout was incorrectly hosted as
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,12 @@ Favorite spells now use the parallel retail catalog contract: MouseDown
|
||||||
selects immediately, right-click opens the examination floaty's authored
|
selects immediately, right-click opens the examination floaty's authored
|
||||||
SpellPanel locally, and no spell ID enters global object selection or the
|
SpellPanel locally, and no spell ID enters global object selection or the
|
||||||
status-bar magnifier command. Exact spell fields and the current appropriate
|
status-bar magnifier command. Exact spell fields and the current appropriate
|
||||||
formula's authored component cells are automated; the connected visual gate
|
formula's authored component cells are automated. The correction resolves
|
||||||
is pending.
|
component icons by DAT DID, projects scarabs/prismatic tapers when ACE
|
||||||
|
disables component enforcement (IA-21), and resets obsolete saved dimensions
|
||||||
|
once to the authored 310 x 400 extent. The Release build with #228's 17
|
||||||
|
tracked test warnings, 3,555 App tests / 3 skips, and 7,986 complete-solution
|
||||||
|
tests / 5 skips pass; the connected visual gate is pending.
|
||||||
Slice 3's first connected gate found and corrected the real remaining defects:
|
Slice 3's first connected gate found and corrected the real remaining defects:
|
||||||
ACE creature flag `0x0100` was mislabeled as a weapon profile and left the
|
ACE creature flag `0x0100` was mislabeled as a weapon profile and left the
|
||||||
appraisal busy reference held, retail's examination UI had been mounted as a
|
appraisal busy reference held, retail's examination UI had been mounted as a
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ corrected and live-confirmed. The creature-page follow-up now has retail's
|
||||||
ordered stat rows and animated private preview. The follow-up item report and
|
ordered stat rows and animated private preview. The follow-up item report and
|
||||||
authored 310 x 400 layout correction are implemented; their connected visual
|
authored 310 x 400 layout correction are implemented; their connected visual
|
||||||
gate is pending. Favorite-spell press-time selection and right-click local
|
gate is pending. Favorite-spell press-time selection and right-click local
|
||||||
SpellPanel examination are implemented and await their connected visual gate.
|
SpellPanel examination are implemented. The follow-up now maps
|
||||||
|
component-disabled ACE characters to the modern scarab/prismatic formula,
|
||||||
|
resolves formula icons by their DAT icon DIDs, and migrates stale examination
|
||||||
|
dimensions once to the authored 310 x 400 extent. Their connected visual gate
|
||||||
|
is pending.
|
||||||
**Milestone:** M4 prerequisite/preamble.
|
**Milestone:** M4 prerequisite/preamble.
|
||||||
**Architecture:** retained gameplay UI over shared selection, object, and
|
**Architecture:** retained gameplay UI over shared selection, object, and
|
||||||
interaction state. `GameWindow` remains a composition/callback shell.
|
interaction state. `GameWindow` remains a composition/callback shell.
|
||||||
|
|
@ -257,9 +261,15 @@ on left press, while the spell branch of
|
||||||
SpellPanel locally on right-click. Spell IDs never enter `SelectionState` or
|
SpellPanel locally on right-click. Spell IDs never enter `SelectionState` or
|
||||||
the status-bar magnifier path, and no Appraise/busy transaction is invented.
|
the status-bar magnifier path, and no Appraise/busy transaction is invented.
|
||||||
The view projects exact spell fields and the current appropriate formula
|
The view projects exact spell fields and the current appropriate formula
|
||||||
through authored component template `0x1000032E`; its connected visual gate is
|
through authored component template `0x1000032E`. Its corrective pass uses
|
||||||
pending. The 61-test focused App gate, Release solution build, 3,553 App tests
|
each component descriptor's icon DID rather than its inventory WCID, applies
|
||||||
/ 3 skips, and 7,984 complete-solution tests / 5 skips pass.
|
the modern scarab/prismatic formula when ACE disables component enforcement
|
||||||
|
(IA-21), and introduces per-window authored-geometry revisions so an obsolete
|
||||||
|
saved examination height resets once without discarding position or future
|
||||||
|
user resizing. The connected visual gate is pending. The 61-test focused App
|
||||||
|
gate passed before this correction. The corrected focused tests, Release build
|
||||||
|
with #228's 17 tracked test warnings, 3,555 App tests / 3 skips, and 7,986
|
||||||
|
complete-solution tests / 5 skips pass.
|
||||||
|
|
||||||
## Slice 1 — spell-bar overflow arrows
|
## Slice 1 — spell-bar overflow arrows
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,37 @@ favorite-component events use WCIDs, resolved through DualEnumIDMap
|
||||||
`0x27000002`. The magic-school focus map is retail enum category `0x28`, key
|
`0x27000002`. The magic-school focus map is retail enum category `0x28`, key
|
||||||
`0x10000001`.
|
`0x10000001`.
|
||||||
|
|
||||||
|
ACE integration policy (IA-21): when player BoolProperty `68`
|
||||||
|
(`SpellComponentsRequired`) is false, casting already bypasses component
|
||||||
|
preflight. In that server mode acdream also projects
|
||||||
|
`InqScarabOnlyFormula` in Spell examination, so an inactive account-randomized
|
||||||
|
legacy recipe is not exposed. With enforcement enabled, the focus/infusion
|
||||||
|
branch above remains byte-for-byte retail.
|
||||||
|
|
||||||
|
### Spell-examination formula media and extent
|
||||||
|
|
||||||
|
Retail: `SpellExamineUI::ExamineSpell` `0x004B6210`,
|
||||||
|
`gmFloatyExaminationUI::UpdateFromPlayerModule` `0x004D4250`, and LayoutDesc
|
||||||
|
`0x2100006B`.
|
||||||
|
|
||||||
|
```text
|
||||||
|
for each SCID in GetAppropriateSpellFormula(spell):
|
||||||
|
component = SpellComponentTable[SCID]
|
||||||
|
texture = GetSpellComponentIcon(component.iconDID)
|
||||||
|
cell = AddItemFromTemplateList(0x1000032E)
|
||||||
|
cell.icon = texture
|
||||||
|
cell.missingOverlay = !ComponentTracker.ComponentIsOwned(component.WCID)
|
||||||
|
|
||||||
|
initial floaty extent = LayoutDesc root extent = 310 x 400 pixels
|
||||||
|
restore user position/size from player UI state
|
||||||
|
```
|
||||||
|
|
||||||
|
The component icon lookup consumes the component's icon DID, not its WCID.
|
||||||
|
acdream's retained-layout persistence carries an authored-geometry revision:
|
||||||
|
when a shipped LayoutDesc extent correction invalidates an older saved size,
|
||||||
|
only width/height reset once; position, visibility, and later user resizing
|
||||||
|
remain persistent like retail.
|
||||||
|
|
||||||
## Authoritative enchantment events
|
## Authoritative enchantment events
|
||||||
|
|
||||||
Retail dispatchers:
|
Retail dispatchers:
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ public sealed class SpellComponentRequirementService
|
||||||
/// Retail <c>ClientMagicSystem::GetAppropriateSpellFormula</c>
|
/// Retail <c>ClientMagicSystem::GetAppropriateSpellFormula</c>
|
||||||
/// (0x00567D50): an infused school or a directly carried school focus uses
|
/// (0x00567D50): an infused school or a directly carried school focus uses
|
||||||
/// the scarab-only formula; otherwise the account-customized formula wins.
|
/// the scarab-only formula; otherwise the account-customized formula wins.
|
||||||
|
/// ACE characters with component enforcement disabled use the same modern
|
||||||
|
/// scarab/taper presentation instead of exposing an unusable legacy recipe.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<uint> GetAppropriateFormula(uint spellId)
|
public IReadOnlyList<uint> GetAppropriateFormula(uint spellId)
|
||||||
{
|
{
|
||||||
|
|
@ -105,6 +107,9 @@ public sealed class SpellComponentRequirementService
|
||||||
ClientObject? player,
|
ClientObject? player,
|
||||||
uint playerGuid)
|
uint playerGuid)
|
||||||
{
|
{
|
||||||
|
bool componentsRequired =
|
||||||
|
player?.Properties.GetBool(SpellComponentsRequiredProperty, true)
|
||||||
|
?? true;
|
||||||
bool infused = SchoolInfusionProperty(formula.School) is uint propertyId
|
bool infused = SchoolInfusionProperty(formula.School) is uint propertyId
|
||||||
&& player?.Properties.GetInt(propertyId) > 0;
|
&& player?.Properties.GetInt(propertyId) > 0;
|
||||||
bool carriesMagicPack = _magicPackWcidBySchool.TryGetValue(
|
bool carriesMagicPack = _magicPackWcidBySchool.TryGetValue(
|
||||||
|
|
@ -112,7 +117,7 @@ public sealed class SpellComponentRequirementService
|
||||||
&& _objects.Objects.Any(item =>
|
&& _objects.Objects.Any(item =>
|
||||||
item.ContainerId == playerGuid
|
item.ContainerId == playerGuid
|
||||||
&& item.WeenieClassId == packWcid);
|
&& item.WeenieClassId == packWcid);
|
||||||
return infused || carriesMagicPack
|
return !componentsRequired || infused || carriesMagicPack
|
||||||
? RetailSpellFormula.InqScarabOnlyFormula(formula.ComponentIds)
|
? RetailSpellFormula.InqScarabOnlyFormula(formula.ComponentIds)
|
||||||
: RetailSpellFormula.CustomizeForAccount(
|
: RetailSpellFormula.CustomizeForAccount(
|
||||||
formula.ComponentIds, formula.FormulaVersion, _accountName());
|
formula.ComponentIds, formula.FormulaVersion, _accountName());
|
||||||
|
|
|
||||||
|
|
@ -783,7 +783,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
||||||
{
|
{
|
||||||
SpellExamineComponent component = components[i];
|
SpellExamineComponent component = components[i];
|
||||||
UiElement cell = _spellComponentTemplates.Create(
|
UiElement cell = _spellComponentTemplates.Create(
|
||||||
_resolveComponentIcon(component.Descriptor.WeenieClassId),
|
_resolveComponentIcon(component.Descriptor.IconId),
|
||||||
component.Owned);
|
component.Owned);
|
||||||
cell.LayoutPolicy = null;
|
cell.LayoutPolicy = null;
|
||||||
cell.Anchors = AnchorEdges.Left | AnchorEdges.Top;
|
cell.Anchors = AnchorEdges.Left | AnchorEdges.Top;
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,13 @@ public static class RetailWindowFrame
|
||||||
public bool DrawChromeCenter { get; init; } = true;
|
public bool DrawChromeCenter { get; init; } = true;
|
||||||
public bool Visible { get; init; } = true;
|
public bool Visible { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Increment when the authored default extent changes incompatibly.
|
||||||
|
/// Persistence preserves position and window state while resetting stale
|
||||||
|
/// saved dimensions to this mount's current DAT-authored extent.
|
||||||
|
/// </summary>
|
||||||
|
public int AuthoredGeometryRevision { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Top-level desktop anchors. Most movable windows are unanchored; fixed
|
/// Top-level desktop anchors. Most movable windows are unanchored; fixed
|
||||||
/// retail HUD roots can preserve their authored screen-edge margins.
|
/// retail HUD roots can preserve their authored screen-edge margins.
|
||||||
|
|
@ -173,7 +180,8 @@ public static class RetailWindowFrame
|
||||||
outerFrame,
|
outerFrame,
|
||||||
content,
|
content,
|
||||||
options.Controller,
|
options.Controller,
|
||||||
options.StateController);
|
options.StateController,
|
||||||
|
options.AuthoredGeometryRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float ResolveConstraint(
|
private static float ResolveConstraint(
|
||||||
|
|
|
||||||
|
|
@ -1045,6 +1045,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
Top = root.Top,
|
Top = root.Top,
|
||||||
ContentWidth = root.Width,
|
ContentWidth = root.Width,
|
||||||
ContentHeight = root.Height,
|
ContentHeight = root.Height,
|
||||||
|
AuthoredGeometryRevision = 1,
|
||||||
Visible = false,
|
Visible = false,
|
||||||
ResizeX = true,
|
ResizeX = true,
|
||||||
ResizeY = true,
|
ResizeY = true,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ public sealed class RetailWindowHandle
|
||||||
UiElement outerFrame,
|
UiElement outerFrame,
|
||||||
UiElement contentRoot,
|
UiElement contentRoot,
|
||||||
IRetainedPanelController? controller,
|
IRetainedPanelController? controller,
|
||||||
IRetainedWindowStateController? stateController)
|
IRetainedWindowStateController? stateController,
|
||||||
|
int authoredGeometryRevision)
|
||||||
{
|
{
|
||||||
_owner = owner;
|
_owner = owner;
|
||||||
Name = name;
|
Name = name;
|
||||||
|
|
@ -28,6 +29,9 @@ public sealed class RetailWindowHandle
|
||||||
ContentRoot = contentRoot;
|
ContentRoot = contentRoot;
|
||||||
Controller = controller;
|
Controller = controller;
|
||||||
StateController = stateController;
|
StateController = stateController;
|
||||||
|
AuthoredGeometryRevision = Math.Max(0, authoredGeometryRevision);
|
||||||
|
AuthoredWidth = outerFrame.Width;
|
||||||
|
AuthoredHeight = outerFrame.Height;
|
||||||
_notifiedVisible = outerFrame.Visible;
|
_notifiedVisible = outerFrame.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,6 +40,9 @@ public sealed class RetailWindowHandle
|
||||||
public UiElement ContentRoot { get; }
|
public UiElement ContentRoot { get; }
|
||||||
public IRetainedPanelController? Controller { get; private set; }
|
public IRetainedPanelController? Controller { get; private set; }
|
||||||
public IRetainedWindowStateController? StateController { get; }
|
public IRetainedWindowStateController? StateController { get; }
|
||||||
|
public int AuthoredGeometryRevision { get; }
|
||||||
|
public float AuthoredWidth { get; }
|
||||||
|
public float AuthoredHeight { get; }
|
||||||
public bool IsRegistered => !_disposed;
|
public bool IsRegistered => !_disposed;
|
||||||
public bool IsVisible => OuterFrame.Visible;
|
public bool IsVisible => OuterFrame.Visible;
|
||||||
public bool IsLocked => _owner.IsLocked;
|
public bool IsLocked => _owner.IsLocked;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,9 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||||
UiWindowLayout? saved = _store.LoadWindowLayout(
|
UiWindowLayout? saved = _store.LoadWindowLayout(
|
||||||
character, resolution, handle.Name, fallback);
|
character, resolution, handle.Name, fallback);
|
||||||
if (saved is not { } layout) continue;
|
if (saved is not { } layout) continue;
|
||||||
|
layout = MigrateAuthoredGeometry(
|
||||||
|
layout,
|
||||||
|
AuthoredGeometry(handle, fallback));
|
||||||
|
|
||||||
Apply(
|
Apply(
|
||||||
handle,
|
handle,
|
||||||
|
|
@ -110,6 +113,10 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||||
UiWindowLayout? saved = _store.LoadNamedWindowLayout(
|
UiWindowLayout? saved = _store.LoadNamedWindowLayout(
|
||||||
profileName, handle.Name, Capture(handle));
|
profileName, handle.Name, Capture(handle));
|
||||||
if (saved is not { } layout) continue;
|
if (saved is not { } layout) continue;
|
||||||
|
UiWindowLayout fallback = Capture(handle);
|
||||||
|
layout = MigrateAuthoredGeometry(
|
||||||
|
layout,
|
||||||
|
AuthoredGeometry(handle, fallback));
|
||||||
Apply(
|
Apply(
|
||||||
handle,
|
handle,
|
||||||
layout,
|
layout,
|
||||||
|
|
@ -166,9 +173,34 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||||
state.PersistedHeight ?? handle.Height,
|
state.PersistedHeight ?? handle.Height,
|
||||||
handle.IsVisible,
|
handle.IsVisible,
|
||||||
state.Collapsed,
|
state.Collapsed,
|
||||||
state.Maximized);
|
state.Maximized,
|
||||||
|
handle.AuthoredGeometryRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UiWindowLayout MigrateAuthoredGeometry(
|
||||||
|
UiWindowLayout saved,
|
||||||
|
UiWindowLayout authored)
|
||||||
|
{
|
||||||
|
if (saved.AuthoredGeometryRevision >= authored.AuthoredGeometryRevision)
|
||||||
|
return saved;
|
||||||
|
|
||||||
|
return saved with
|
||||||
|
{
|
||||||
|
Width = authored.Width,
|
||||||
|
Height = authored.Height,
|
||||||
|
AuthoredGeometryRevision = authored.AuthoredGeometryRevision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UiWindowLayout AuthoredGeometry(
|
||||||
|
RetailWindowHandle handle,
|
||||||
|
UiWindowLayout current) => current with
|
||||||
|
{
|
||||||
|
Width = handle.AuthoredWidth,
|
||||||
|
Height = handle.AuthoredHeight,
|
||||||
|
AuthoredGeometryRevision = handle.AuthoredGeometryRevision,
|
||||||
|
};
|
||||||
|
|
||||||
private static void Apply(
|
private static void Apply(
|
||||||
RetailWindowHandle handle,
|
RetailWindowHandle handle,
|
||||||
UiWindowLayout layout,
|
UiWindowLayout layout,
|
||||||
|
|
|
||||||
|
|
@ -38,11 +38,13 @@ public sealed class RetailWindowManager : IDisposable
|
||||||
UiElement outerFrame,
|
UiElement outerFrame,
|
||||||
UiElement? contentRoot = null,
|
UiElement? contentRoot = null,
|
||||||
IRetainedPanelController? controller = null,
|
IRetainedPanelController? controller = null,
|
||||||
IRetainedWindowStateController? stateController = null)
|
IRetainedWindowStateController? stateController = null,
|
||||||
|
int authoredGeometryRevision = 0)
|
||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||||
ArgumentNullException.ThrowIfNull(outerFrame);
|
ArgumentNullException.ThrowIfNull(outerFrame);
|
||||||
|
authoredGeometryRevision = Math.Max(0, authoredGeometryRevision);
|
||||||
|
|
||||||
if (!ReferenceEquals(outerFrame.Parent, _root))
|
if (!ReferenceEquals(outerFrame.Parent, _root))
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
|
@ -56,7 +58,8 @@ public sealed class RetailWindowManager : IDisposable
|
||||||
if (ReferenceEquals(sameName.OuterFrame, outerFrame)
|
if (ReferenceEquals(sameName.OuterFrame, outerFrame)
|
||||||
&& ReferenceEquals(sameName.ContentRoot, contentRoot ?? outerFrame)
|
&& ReferenceEquals(sameName.ContentRoot, contentRoot ?? outerFrame)
|
||||||
&& ReferenceEquals(sameName.Controller, controller)
|
&& ReferenceEquals(sameName.Controller, controller)
|
||||||
&& ReferenceEquals(sameName.StateController, resolvedStateController))
|
&& ReferenceEquals(sameName.StateController, resolvedStateController)
|
||||||
|
&& sameName.AuthoredGeometryRevision == authoredGeometryRevision)
|
||||||
return sameName;
|
return sameName;
|
||||||
|
|
||||||
Unregister(name);
|
Unregister(name);
|
||||||
|
|
@ -71,7 +74,8 @@ public sealed class RetailWindowManager : IDisposable
|
||||||
outerFrame,
|
outerFrame,
|
||||||
contentRoot ?? outerFrame,
|
contentRoot ?? outerFrame,
|
||||||
controller,
|
controller,
|
||||||
resolvedStateController);
|
resolvedStateController,
|
||||||
|
authoredGeometryRevision);
|
||||||
_byName.Add(name, handle);
|
_byName.Add(name, handle);
|
||||||
_byFrame.Add(outerFrame, handle);
|
_byFrame.Add(outerFrame, handle);
|
||||||
handle.NotifyInitialState();
|
handle.NotifyInitialState();
|
||||||
|
|
|
||||||
|
|
@ -191,8 +191,15 @@ public sealed class UiHost : System.IDisposable
|
||||||
UiElement window,
|
UiElement window,
|
||||||
UiElement? contentRoot = null,
|
UiElement? contentRoot = null,
|
||||||
IRetainedPanelController? controller = null,
|
IRetainedPanelController? controller = null,
|
||||||
IRetainedWindowStateController? stateController = null)
|
IRetainedWindowStateController? stateController = null,
|
||||||
=> Root.RegisterWindow(name, window, contentRoot, controller, stateController);
|
int authoredGeometryRevision = 0)
|
||||||
|
=> Root.RegisterWindow(
|
||||||
|
name,
|
||||||
|
window,
|
||||||
|
contentRoot,
|
||||||
|
controller,
|
||||||
|
stateController,
|
||||||
|
authoredGeometryRevision);
|
||||||
|
|
||||||
public bool UnregisterWindow(string name) => Root.UnregisterWindow(name);
|
public bool UnregisterWindow(string name) => Root.UnregisterWindow(name);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -739,8 +739,15 @@ public sealed class UiRoot : UiElement
|
||||||
UiElement window,
|
UiElement window,
|
||||||
UiElement? contentRoot = null,
|
UiElement? contentRoot = null,
|
||||||
IRetainedPanelController? controller = null,
|
IRetainedPanelController? controller = null,
|
||||||
IRetainedWindowStateController? stateController = null)
|
IRetainedWindowStateController? stateController = null,
|
||||||
=> WindowManager.Register(name, window, contentRoot, controller, stateController);
|
int authoredGeometryRevision = 0)
|
||||||
|
=> WindowManager.Register(
|
||||||
|
name,
|
||||||
|
window,
|
||||||
|
contentRoot,
|
||||||
|
controller,
|
||||||
|
stateController,
|
||||||
|
authoredGeometryRevision);
|
||||||
|
|
||||||
public bool UnregisterWindow(string name) => WindowManager.Unregister(name);
|
public bool UnregisterWindow(string name) => WindowManager.Unregister(name);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,8 @@ public sealed class SettingsStore
|
||||||
ReadNodeFloat(node, "height", fallback.Height),
|
ReadNodeFloat(node, "height", fallback.Height),
|
||||||
ReadNodeBool(node, "visible", fallback.Visible),
|
ReadNodeBool(node, "visible", fallback.Visible),
|
||||||
ReadNodeBool(node, "collapsed", fallback.Collapsed),
|
ReadNodeBool(node, "collapsed", fallback.Collapsed),
|
||||||
ReadNodeBool(node, "maximized", fallback.Maximized));
|
ReadNodeBool(node, "maximized", fallback.Maximized),
|
||||||
|
ReadNodeInt(node, "authoredGeometryRevision", 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version-1 migration: radar stored only X/Y and had no resolution key.
|
// Version-1 migration: radar stored only X/Y and had no resolution key.
|
||||||
|
|
@ -417,6 +418,7 @@ public sealed class SettingsStore
|
||||||
["visible"] = layout.Visible,
|
["visible"] = layout.Visible,
|
||||||
["collapsed"] = layout.Collapsed,
|
["collapsed"] = layout.Collapsed,
|
||||||
["maximized"] = layout.Maximized,
|
["maximized"] = layout.Maximized,
|
||||||
|
["authoredGeometryRevision"] = layout.AuthoredGeometryRevision,
|
||||||
};
|
};
|
||||||
root["version"] = CurrentSchemaVersion;
|
root["version"] = CurrentSchemaVersion;
|
||||||
WriteMutableRoot(root);
|
WriteMutableRoot(root);
|
||||||
|
|
@ -449,7 +451,8 @@ public sealed class SettingsStore
|
||||||
ReadNodeFloat(node, "height", fallback.Height),
|
ReadNodeFloat(node, "height", fallback.Height),
|
||||||
ReadNodeBool(node, "visible", fallback.Visible),
|
ReadNodeBool(node, "visible", fallback.Visible),
|
||||||
ReadNodeBool(node, "collapsed", fallback.Collapsed),
|
ReadNodeBool(node, "collapsed", fallback.Collapsed),
|
||||||
ReadNodeBool(node, "maximized", fallback.Maximized));
|
ReadNodeBool(node, "maximized", fallback.Maximized),
|
||||||
|
ReadNodeInt(node, "authoredGeometryRevision", 0));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -484,6 +487,7 @@ public sealed class SettingsStore
|
||||||
["visible"] = layout.Visible,
|
["visible"] = layout.Visible,
|
||||||
["collapsed"] = layout.Collapsed,
|
["collapsed"] = layout.Collapsed,
|
||||||
["maximized"] = layout.Maximized,
|
["maximized"] = layout.Maximized,
|
||||||
|
["authoredGeometryRevision"] = layout.AuthoredGeometryRevision,
|
||||||
};
|
};
|
||||||
|
|
||||||
private JsonObject LoadMutableRoot()
|
private JsonObject LoadMutableRoot()
|
||||||
|
|
@ -518,6 +522,12 @@ public sealed class SettingsStore
|
||||||
catch { return fallback; }
|
catch { return fallback; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ReadNodeInt(JsonObject node, string key, int fallback)
|
||||||
|
{
|
||||||
|
try { return node[key]?.GetValue<int>() ?? fallback; }
|
||||||
|
catch { return fallback; }
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryParseResolution(string key, out int width, out int height)
|
private static bool TryParseResolution(string key, out int width, out int height)
|
||||||
{
|
{
|
||||||
width = 0;
|
width = 0;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ namespace AcDream.UI.Abstractions.Panels.Settings;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Complete persisted state for one retained window at one screen resolution.
|
/// Complete persisted state for one retained window at one screen resolution.
|
||||||
/// Position and size are outer-frame pixels; panel flags retain the small amount
|
/// Position and size are outer-frame pixels; panel flags retain the small amount
|
||||||
/// of state that geometry alone cannot reproduce reliably.
|
/// of state that geometry alone cannot reproduce reliably. The authored revision
|
||||||
|
/// lets a window invalidate obsolete saved dimensions without discarding its
|
||||||
|
/// position, visibility, or later user resizing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct UiWindowLayout(
|
public readonly record struct UiWindowLayout(
|
||||||
float X,
|
float X,
|
||||||
|
|
@ -12,4 +14,5 @@ public readonly record struct UiWindowLayout(
|
||||||
float Height,
|
float Height,
|
||||||
bool Visible,
|
bool Visible,
|
||||||
bool Collapsed,
|
bool Collapsed,
|
||||||
bool Maximized);
|
bool Maximized,
|
||||||
|
int AuthoredGeometryRevision = 0);
|
||||||
|
|
|
||||||
|
|
@ -37,15 +37,18 @@ public sealed class SpellComponentRequirementServiceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ServerDisabledComponents_BypassesInventoryPreflight()
|
public void ServerDisabledComponents_BypassesPreflightAndUsesModernFormula()
|
||||||
{
|
{
|
||||||
var objects = new ClientObjectTable();
|
var objects = new ClientObjectTable();
|
||||||
var player = new ClientObject { ObjectId = 1u, Name = "Player" };
|
var player = new ClientObject { ObjectId = 1u, Name = "Player" };
|
||||||
player.Properties.Bools[SpellComponentRequirementService.SpellComponentsRequiredProperty] = false;
|
player.Properties.Bools[SpellComponentRequirementService.SpellComponentsRequiredProperty] = false;
|
||||||
objects.AddOrUpdate(player);
|
objects.AddOrUpdate(player);
|
||||||
|
|
||||||
Assert.True(Service(objects, [10u], new Dictionary<uint, uint>())
|
SpellComponentRequirementService service =
|
||||||
.HasRequiredComponents(50u));
|
Service(objects, [1u, 63u, 10u], new Dictionary<uint, uint>());
|
||||||
|
|
||||||
|
Assert.True(service.HasRequiredComponents(50u));
|
||||||
|
Assert.Equal(new uint[] { 1u, 0xBCu }, service.GetAppropriateFormula(50u));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -717,6 +717,17 @@ public sealed class AppraisalUiControllerTests
|
||||||
new SpellComponentDescriptor(101u, "Malar Herb", 1u, 0x06000011u),
|
new SpellComponentDescriptor(101u, "Malar Herb", 1u, 0x06000011u),
|
||||||
Owned: false),
|
Owned: false),
|
||||||
};
|
};
|
||||||
|
var componentTemplate = new SpellExamineComponentTemplateFactory(
|
||||||
|
new ElementInfo
|
||||||
|
{
|
||||||
|
Id = SpellExamineComponentTemplateFactory.TemplateId,
|
||||||
|
Type = 3,
|
||||||
|
Width = 20,
|
||||||
|
Height = 20,
|
||||||
|
},
|
||||||
|
NoTexture,
|
||||||
|
defaultFont: null);
|
||||||
|
var resolvedComponentIcons = new List<uint>();
|
||||||
int shown = 0;
|
int shown = 0;
|
||||||
using AppraisalUiController controller = Bind(
|
using AppraisalUiController controller = Bind(
|
||||||
layout,
|
layout,
|
||||||
|
|
@ -730,8 +741,14 @@ public sealed class AppraisalUiControllerTests
|
||||||
selection: selection,
|
selection: selection,
|
||||||
spellbook: spellbook,
|
spellbook: spellbook,
|
||||||
resolveSpellIcon: id => id + 1_000u,
|
resolveSpellIcon: id => id + 1_000u,
|
||||||
|
resolveComponentIcon: did =>
|
||||||
|
{
|
||||||
|
resolvedComponentIcons.Add(did);
|
||||||
|
return did + 2_000u;
|
||||||
|
},
|
||||||
spellComponents: _ => components,
|
spellComponents: _ => components,
|
||||||
magicSkill: _ => 200u)!;
|
magicSkill: _ => 200u,
|
||||||
|
spellComponentTemplates: componentTemplate)!;
|
||||||
|
|
||||||
Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId));
|
Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId));
|
||||||
Assert.Equal(1, interaction.BusyCount);
|
Assert.Equal(1, interaction.BusyCount);
|
||||||
|
|
@ -784,6 +801,17 @@ public sealed class AppraisalUiControllerTests
|
||||||
UiTextureElement icon = Assert.Single(
|
UiTextureElement icon = Assert.Single(
|
||||||
iconHost.Children.OfType<UiTextureElement>());
|
iconHost.Children.OfType<UiTextureElement>());
|
||||||
Assert.Equal(metadata.SpellId + 1_000u, icon.Texture);
|
Assert.Equal(metadata.SpellId + 1_000u, icon.Texture);
|
||||||
|
Assert.Equal(
|
||||||
|
new uint[] { 0x06000010u, 0x06000011u },
|
||||||
|
resolvedComponentIcons);
|
||||||
|
UiElement formula = layout.FindElement(
|
||||||
|
AppraisalUiController.SpellFormulaListId)!;
|
||||||
|
Assert.Equal(
|
||||||
|
new uint[] { 0x06000010u + 2_000u, 0x06000011u + 2_000u },
|
||||||
|
formula.Children
|
||||||
|
.SelectMany(cell => cell.Children)
|
||||||
|
.OfType<UiTextureElement>()
|
||||||
|
.Select(texture => texture.Texture));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AppraisalUiController? Bind(
|
private static AppraisalUiController? Bind(
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,87 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
||||||
Assert.True(state.Restored.Maximized);
|
Assert.True(state.Restored.Maximized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Restore_OlderAuthoredGeometryRevision_ResetsOnlySavedExtent()
|
||||||
|
{
|
||||||
|
var store = new SettingsStore(PathName);
|
||||||
|
store.SaveWindowLayout(
|
||||||
|
"Alice",
|
||||||
|
"800x600",
|
||||||
|
WindowNames.Examination,
|
||||||
|
new UiWindowLayout(
|
||||||
|
44f,
|
||||||
|
55f,
|
||||||
|
310f,
|
||||||
|
545f,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
AuthoredGeometryRevision: 0));
|
||||||
|
|
||||||
|
var root = new UiRoot { Width = 800, Height = 600 };
|
||||||
|
RetailWindowHandle handle = Mount(
|
||||||
|
root,
|
||||||
|
WindowNames.Examination,
|
||||||
|
authoredGeometryRevision: 1,
|
||||||
|
width: 310f,
|
||||||
|
height: 400f);
|
||||||
|
using var persistence = new RetailWindowLayoutPersistence(
|
||||||
|
root.WindowManager,
|
||||||
|
store,
|
||||||
|
() => "Alice",
|
||||||
|
() => (800, 600));
|
||||||
|
|
||||||
|
persistence.RestoreAll();
|
||||||
|
|
||||||
|
Assert.Equal((44f, 55f), (handle.Left, handle.Top));
|
||||||
|
Assert.Equal((310f, 400f), (handle.Width, handle.Height));
|
||||||
|
UiWindowLayout migrated = Assert.IsType<UiWindowLayout>(
|
||||||
|
store.LoadWindowLayout(
|
||||||
|
"Alice",
|
||||||
|
"800x600",
|
||||||
|
WindowNames.Examination,
|
||||||
|
default));
|
||||||
|
Assert.Equal(1, migrated.AuthoredGeometryRevision);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RestoreNamed_OlderRevisionUsesMountedAuthoredExtent_NotCurrentSize()
|
||||||
|
{
|
||||||
|
var store = new SettingsStore(PathName);
|
||||||
|
store.SaveNamedWindowLayout(
|
||||||
|
"legacy",
|
||||||
|
WindowNames.Examination,
|
||||||
|
new UiWindowLayout(
|
||||||
|
44f,
|
||||||
|
55f,
|
||||||
|
310f,
|
||||||
|
545f,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
AuthoredGeometryRevision: 0));
|
||||||
|
|
||||||
|
var root = new UiRoot { Width = 800, Height = 600 };
|
||||||
|
RetailWindowHandle handle = Mount(
|
||||||
|
root,
|
||||||
|
WindowNames.Examination,
|
||||||
|
authoredGeometryRevision: 1,
|
||||||
|
width: 310f,
|
||||||
|
height: 400f);
|
||||||
|
handle.ResizeTo(500f, 300f);
|
||||||
|
using var persistence = new RetailWindowLayoutPersistence(
|
||||||
|
root.WindowManager,
|
||||||
|
store,
|
||||||
|
() => "Alice",
|
||||||
|
() => (800, 600));
|
||||||
|
|
||||||
|
persistence.RestoreNamed("legacy");
|
||||||
|
|
||||||
|
Assert.Equal((44f, 55f), (handle.Left, handle.Top));
|
||||||
|
Assert.Equal((310f, 400f), (handle.Width, handle.Height));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultPreLoginKey_NeverOverwritesCharacterLayouts()
|
public void DefaultPreLoginKey_NeverOverwritesCharacterLayouts()
|
||||||
{
|
{
|
||||||
|
|
@ -169,14 +250,17 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
||||||
private static RetailWindowHandle Mount(
|
private static RetailWindowHandle Mount(
|
||||||
UiRoot root,
|
UiRoot root,
|
||||||
string name,
|
string name,
|
||||||
IRetainedWindowStateController? state = null)
|
IRetainedWindowStateController? state = null,
|
||||||
|
int authoredGeometryRevision = 0,
|
||||||
|
float width = 200f,
|
||||||
|
float height = 100f)
|
||||||
{
|
{
|
||||||
var frame = new UiPanel
|
var frame = new UiPanel
|
||||||
{
|
{
|
||||||
Left = 10f,
|
Left = 10f,
|
||||||
Top = 20f,
|
Top = 20f,
|
||||||
Width = 200f,
|
Width = width,
|
||||||
Height = 100f,
|
Height = height,
|
||||||
MinWidth = 100f,
|
MinWidth = 100f,
|
||||||
MinHeight = 80f,
|
MinHeight = 80f,
|
||||||
MaxWidth = 600f,
|
MaxWidth = 600f,
|
||||||
|
|
@ -185,7 +269,11 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
||||||
Resizable = true,
|
Resizable = true,
|
||||||
};
|
};
|
||||||
root.AddChild(frame);
|
root.AddChild(frame);
|
||||||
return root.RegisterWindow(name, frame, stateController: state);
|
return root.RegisterWindow(
|
||||||
|
name,
|
||||||
|
frame,
|
||||||
|
stateController: state,
|
||||||
|
authoredGeometryRevision: authoredGeometryRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue