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,
|
||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||
Release builds; the appraisal and retained catalog-input corrections pass
|
||||
3,553 App tests / 3 skips and 7,984 complete-solution tests / 5 skips.
|
||||
Release builds; the appraisal formula-media/geometry correction passes
|
||||
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
|
||||
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
|
||||
monster-response flag mismatch, non-retail shared-panel mounting, and missing
|
||||
inscription transaction. The creature and common-item report corrections are
|
||||
user-accepted; favorite-spell press-time selection and right-click local
|
||||
examination are the current visual gate.
|
||||
user-accepted. Favorite-spell press-time selection and right-click local
|
||||
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
|
||||
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,
|
||||
outdoor/world-edge/dungeon travel, same-location revisit, exact graceful
|
||||
disconnect, and uncapped fresh-process reconnect with JSON/PNG artifacts.
|
||||
Release builds; the appraisal and retained catalog-input corrections pass
|
||||
3,553 App tests / 3 skips and 7,984 complete-solution tests / 5 skips.
|
||||
Release builds; the appraisal formula-media/geometry correction passes
|
||||
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
|
||||
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
|
||||
monster-response flag mismatch, non-retail shared-panel mounting, and missing
|
||||
inscription transaction. The creature and common-item report corrections are
|
||||
user-accepted; favorite-spell press-time selection and right-click local
|
||||
examination are the current visual gate.
|
||||
user-accepted. Favorite-spell press-time selection and right-click local
|
||||
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
|
||||
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
|
||||
from a pending object appraisal to a spell releases exactly that appraisal
|
||||
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`;
|
||||
`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.
|
||||
Right-clicking it opens the spell examination subview; selecting it and using
|
||||
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`,
|
||||
then its description after one retail paragraph break, without the bogus
|
||||
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-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
|
||||
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` |
|
||||
| IA-15 | D.2b gameplay UI is our own `UiHost`/`UiRoot` retained tree, not a byte-port of Keystone. `RetailUiRuntime` owns the production import/mount graph; `RetailWindowManager`/typed handles centralize registry, raise, focus/capture cleanup, lifecycle events, reverse-order grouped controller teardown, removable Silk input subscriptions, schema-v2 per-character/per-resolution automatic layouts, and portable named `saveui/loadui` profiles; `RetailWindowFrame` is the single production/Studio mount contract for imported-chrome and shared-wrapper windows. Production LayoutDesc imports include vitals `0x2100006C`, chat `0x21000006`, toolbar `0x21000016`, character `0x2100002E`, inventory `0x21000023` plus mounted `0x21000024/22/21`, dialog catalog `0x2100003C`, 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-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-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
|
||||
selection or the status-bar magnifier path. The view projects retail's
|
||||
name/school/mana/duration/range/description and authored appropriate-formula
|
||||
component cells, including live missing-component state. Its connected visual
|
||||
gate is pending. Slice 3's first connected gate exposed
|
||||
component cells, including live missing-component state. The follow-up resolves
|
||||
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
|
||||
creature bit `0x0100` and dropped monster packets while retaining the busy
|
||||
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
|
||||
SpellPanel locally, and no spell ID enters global object selection or the
|
||||
status-bar magnifier command. Exact spell fields and the current appropriate
|
||||
formula's authored component cells are automated; the connected visual gate
|
||||
is pending.
|
||||
formula's authored component cells are automated. The correction resolves
|
||||
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:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
authored 310 x 400 layout correction are implemented; their connected visual
|
||||
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.
|
||||
**Architecture:** retained gameplay UI over shared selection, object, and
|
||||
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
|
||||
the status-bar magnifier path, and no Appraise/busy transaction is invented.
|
||||
The view projects exact spell fields and the current appropriate formula
|
||||
through authored component template `0x1000032E`; its connected visual gate is
|
||||
pending. The 61-test focused App gate, Release solution build, 3,553 App tests
|
||||
/ 3 skips, and 7,984 complete-solution tests / 5 skips pass.
|
||||
through authored component template `0x1000032E`. Its corrective pass uses
|
||||
each component descriptor's icon DID rather than its inventory WCID, applies
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -87,6 +87,37 @@ favorite-component events use WCIDs, resolved through DualEnumIDMap
|
|||
`0x27000002`. The magic-school focus map is retail enum category `0x28`, key
|
||||
`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
|
||||
|
||||
Retail dispatchers:
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ public sealed class SpellComponentRequirementService
|
|||
/// Retail <c>ClientMagicSystem::GetAppropriateSpellFormula</c>
|
||||
/// (0x00567D50): an infused school or a directly carried school focus uses
|
||||
/// 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>
|
||||
public IReadOnlyList<uint> GetAppropriateFormula(uint spellId)
|
||||
{
|
||||
|
|
@ -105,6 +107,9 @@ public sealed class SpellComponentRequirementService
|
|||
ClientObject? player,
|
||||
uint playerGuid)
|
||||
{
|
||||
bool componentsRequired =
|
||||
player?.Properties.GetBool(SpellComponentsRequiredProperty, true)
|
||||
?? true;
|
||||
bool infused = SchoolInfusionProperty(formula.School) is uint propertyId
|
||||
&& player?.Properties.GetInt(propertyId) > 0;
|
||||
bool carriesMagicPack = _magicPackWcidBySchool.TryGetValue(
|
||||
|
|
@ -112,7 +117,7 @@ public sealed class SpellComponentRequirementService
|
|||
&& _objects.Objects.Any(item =>
|
||||
item.ContainerId == playerGuid
|
||||
&& item.WeenieClassId == packWcid);
|
||||
return infused || carriesMagicPack
|
||||
return !componentsRequired || infused || carriesMagicPack
|
||||
? RetailSpellFormula.InqScarabOnlyFormula(formula.ComponentIds)
|
||||
: RetailSpellFormula.CustomizeForAccount(
|
||||
formula.ComponentIds, formula.FormulaVersion, _accountName());
|
||||
|
|
|
|||
|
|
@ -783,7 +783,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
{
|
||||
SpellExamineComponent component = components[i];
|
||||
UiElement cell = _spellComponentTemplates.Create(
|
||||
_resolveComponentIcon(component.Descriptor.WeenieClassId),
|
||||
_resolveComponentIcon(component.Descriptor.IconId),
|
||||
component.Owned);
|
||||
cell.LayoutPolicy = null;
|
||||
cell.Anchors = AnchorEdges.Left | AnchorEdges.Top;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,13 @@ public static class RetailWindowFrame
|
|||
public bool DrawChromeCenter { 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>
|
||||
/// Top-level desktop anchors. Most movable windows are unanchored; fixed
|
||||
/// retail HUD roots can preserve their authored screen-edge margins.
|
||||
|
|
@ -173,7 +180,8 @@ public static class RetailWindowFrame
|
|||
outerFrame,
|
||||
content,
|
||||
options.Controller,
|
||||
options.StateController);
|
||||
options.StateController,
|
||||
options.AuthoredGeometryRevision);
|
||||
}
|
||||
|
||||
private static float ResolveConstraint(
|
||||
|
|
|
|||
|
|
@ -1045,6 +1045,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Top = root.Top,
|
||||
ContentWidth = root.Width,
|
||||
ContentHeight = root.Height,
|
||||
AuthoredGeometryRevision = 1,
|
||||
Visible = false,
|
||||
ResizeX = true,
|
||||
ResizeY = true,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ public sealed class RetailWindowHandle
|
|||
UiElement outerFrame,
|
||||
UiElement contentRoot,
|
||||
IRetainedPanelController? controller,
|
||||
IRetainedWindowStateController? stateController)
|
||||
IRetainedWindowStateController? stateController,
|
||||
int authoredGeometryRevision)
|
||||
{
|
||||
_owner = owner;
|
||||
Name = name;
|
||||
|
|
@ -28,6 +29,9 @@ public sealed class RetailWindowHandle
|
|||
ContentRoot = contentRoot;
|
||||
Controller = controller;
|
||||
StateController = stateController;
|
||||
AuthoredGeometryRevision = Math.Max(0, authoredGeometryRevision);
|
||||
AuthoredWidth = outerFrame.Width;
|
||||
AuthoredHeight = outerFrame.Height;
|
||||
_notifiedVisible = outerFrame.Visible;
|
||||
}
|
||||
|
||||
|
|
@ -36,6 +40,9 @@ public sealed class RetailWindowHandle
|
|||
public UiElement ContentRoot { get; }
|
||||
public IRetainedPanelController? Controller { get; private set; }
|
||||
public IRetainedWindowStateController? StateController { get; }
|
||||
public int AuthoredGeometryRevision { get; }
|
||||
public float AuthoredWidth { get; }
|
||||
public float AuthoredHeight { get; }
|
||||
public bool IsRegistered => !_disposed;
|
||||
public bool IsVisible => OuterFrame.Visible;
|
||||
public bool IsLocked => _owner.IsLocked;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
UiWindowLayout? saved = _store.LoadWindowLayout(
|
||||
character, resolution, handle.Name, fallback);
|
||||
if (saved is not { } layout) continue;
|
||||
layout = MigrateAuthoredGeometry(
|
||||
layout,
|
||||
AuthoredGeometry(handle, fallback));
|
||||
|
||||
Apply(
|
||||
handle,
|
||||
|
|
@ -110,6 +113,10 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
UiWindowLayout? saved = _store.LoadNamedWindowLayout(
|
||||
profileName, handle.Name, Capture(handle));
|
||||
if (saved is not { } layout) continue;
|
||||
UiWindowLayout fallback = Capture(handle);
|
||||
layout = MigrateAuthoredGeometry(
|
||||
layout,
|
||||
AuthoredGeometry(handle, fallback));
|
||||
Apply(
|
||||
handle,
|
||||
layout,
|
||||
|
|
@ -166,9 +173,34 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
state.PersistedHeight ?? handle.Height,
|
||||
handle.IsVisible,
|
||||
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(
|
||||
RetailWindowHandle handle,
|
||||
UiWindowLayout layout,
|
||||
|
|
|
|||
|
|
@ -38,11 +38,13 @@ public sealed class RetailWindowManager : IDisposable
|
|||
UiElement outerFrame,
|
||||
UiElement? contentRoot = null,
|
||||
IRetainedPanelController? controller = null,
|
||||
IRetainedWindowStateController? stateController = null)
|
||||
IRetainedWindowStateController? stateController = null,
|
||||
int authoredGeometryRevision = 0)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
ArgumentNullException.ThrowIfNull(outerFrame);
|
||||
authoredGeometryRevision = Math.Max(0, authoredGeometryRevision);
|
||||
|
||||
if (!ReferenceEquals(outerFrame.Parent, _root))
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -56,7 +58,8 @@ public sealed class RetailWindowManager : IDisposable
|
|||
if (ReferenceEquals(sameName.OuterFrame, outerFrame)
|
||||
&& ReferenceEquals(sameName.ContentRoot, contentRoot ?? outerFrame)
|
||||
&& ReferenceEquals(sameName.Controller, controller)
|
||||
&& ReferenceEquals(sameName.StateController, resolvedStateController))
|
||||
&& ReferenceEquals(sameName.StateController, resolvedStateController)
|
||||
&& sameName.AuthoredGeometryRevision == authoredGeometryRevision)
|
||||
return sameName;
|
||||
|
||||
Unregister(name);
|
||||
|
|
@ -71,7 +74,8 @@ public sealed class RetailWindowManager : IDisposable
|
|||
outerFrame,
|
||||
contentRoot ?? outerFrame,
|
||||
controller,
|
||||
resolvedStateController);
|
||||
resolvedStateController,
|
||||
authoredGeometryRevision);
|
||||
_byName.Add(name, handle);
|
||||
_byFrame.Add(outerFrame, handle);
|
||||
handle.NotifyInitialState();
|
||||
|
|
|
|||
|
|
@ -191,8 +191,15 @@ public sealed class UiHost : System.IDisposable
|
|||
UiElement window,
|
||||
UiElement? contentRoot = null,
|
||||
IRetainedPanelController? controller = null,
|
||||
IRetainedWindowStateController? stateController = null)
|
||||
=> Root.RegisterWindow(name, window, contentRoot, controller, stateController);
|
||||
IRetainedWindowStateController? stateController = null,
|
||||
int authoredGeometryRevision = 0)
|
||||
=> Root.RegisterWindow(
|
||||
name,
|
||||
window,
|
||||
contentRoot,
|
||||
controller,
|
||||
stateController,
|
||||
authoredGeometryRevision);
|
||||
|
||||
public bool UnregisterWindow(string name) => Root.UnregisterWindow(name);
|
||||
|
||||
|
|
|
|||
|
|
@ -739,8 +739,15 @@ public sealed class UiRoot : UiElement
|
|||
UiElement window,
|
||||
UiElement? contentRoot = null,
|
||||
IRetainedPanelController? controller = null,
|
||||
IRetainedWindowStateController? stateController = null)
|
||||
=> WindowManager.Register(name, window, contentRoot, controller, stateController);
|
||||
IRetainedWindowStateController? stateController = null,
|
||||
int authoredGeometryRevision = 0)
|
||||
=> WindowManager.Register(
|
||||
name,
|
||||
window,
|
||||
contentRoot,
|
||||
controller,
|
||||
stateController,
|
||||
authoredGeometryRevision);
|
||||
|
||||
public bool UnregisterWindow(string name) => WindowManager.Unregister(name);
|
||||
|
||||
|
|
|
|||
|
|
@ -367,7 +367,8 @@ public sealed class SettingsStore
|
|||
ReadNodeFloat(node, "height", fallback.Height),
|
||||
ReadNodeBool(node, "visible", fallback.Visible),
|
||||
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.
|
||||
|
|
@ -417,6 +418,7 @@ public sealed class SettingsStore
|
|||
["visible"] = layout.Visible,
|
||||
["collapsed"] = layout.Collapsed,
|
||||
["maximized"] = layout.Maximized,
|
||||
["authoredGeometryRevision"] = layout.AuthoredGeometryRevision,
|
||||
};
|
||||
root["version"] = CurrentSchemaVersion;
|
||||
WriteMutableRoot(root);
|
||||
|
|
@ -449,7 +451,8 @@ public sealed class SettingsStore
|
|||
ReadNodeFloat(node, "height", fallback.Height),
|
||||
ReadNodeBool(node, "visible", fallback.Visible),
|
||||
ReadNodeBool(node, "collapsed", fallback.Collapsed),
|
||||
ReadNodeBool(node, "maximized", fallback.Maximized));
|
||||
ReadNodeBool(node, "maximized", fallback.Maximized),
|
||||
ReadNodeInt(node, "authoredGeometryRevision", 0));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -484,6 +487,7 @@ public sealed class SettingsStore
|
|||
["visible"] = layout.Visible,
|
||||
["collapsed"] = layout.Collapsed,
|
||||
["maximized"] = layout.Maximized,
|
||||
["authoredGeometryRevision"] = layout.AuthoredGeometryRevision,
|
||||
};
|
||||
|
||||
private JsonObject LoadMutableRoot()
|
||||
|
|
@ -518,6 +522,12 @@ public sealed class SettingsStore
|
|||
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)
|
||||
{
|
||||
width = 0;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ namespace AcDream.UI.Abstractions.Panels.Settings;
|
|||
/// <summary>
|
||||
/// Complete persisted state for one retained window at one screen resolution.
|
||||
/// 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>
|
||||
public readonly record struct UiWindowLayout(
|
||||
float X,
|
||||
|
|
@ -12,4 +14,5 @@ public readonly record struct UiWindowLayout(
|
|||
float Height,
|
||||
bool Visible,
|
||||
bool Collapsed,
|
||||
bool Maximized);
|
||||
bool Maximized,
|
||||
int AuthoredGeometryRevision = 0);
|
||||
|
|
|
|||
|
|
@ -37,15 +37,18 @@ public sealed class SpellComponentRequirementServiceTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerDisabledComponents_BypassesInventoryPreflight()
|
||||
public void ServerDisabledComponents_BypassesPreflightAndUsesModernFormula()
|
||||
{
|
||||
var objects = new ClientObjectTable();
|
||||
var player = new ClientObject { ObjectId = 1u, Name = "Player" };
|
||||
player.Properties.Bools[SpellComponentRequirementService.SpellComponentsRequiredProperty] = false;
|
||||
objects.AddOrUpdate(player);
|
||||
|
||||
Assert.True(Service(objects, [10u], new Dictionary<uint, uint>())
|
||||
.HasRequiredComponents(50u));
|
||||
SpellComponentRequirementService service =
|
||||
Service(objects, [1u, 63u, 10u], new Dictionary<uint, uint>());
|
||||
|
||||
Assert.True(service.HasRequiredComponents(50u));
|
||||
Assert.Equal(new uint[] { 1u, 0xBCu }, service.GetAppropriateFormula(50u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -717,6 +717,17 @@ public sealed class AppraisalUiControllerTests
|
|||
new SpellComponentDescriptor(101u, "Malar Herb", 1u, 0x06000011u),
|
||||
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;
|
||||
using AppraisalUiController controller = Bind(
|
||||
layout,
|
||||
|
|
@ -730,8 +741,14 @@ public sealed class AppraisalUiControllerTests
|
|||
selection: selection,
|
||||
spellbook: spellbook,
|
||||
resolveSpellIcon: id => id + 1_000u,
|
||||
resolveComponentIcon: did =>
|
||||
{
|
||||
resolvedComponentIcons.Add(did);
|
||||
return did + 2_000u;
|
||||
},
|
||||
spellComponents: _ => components,
|
||||
magicSkill: _ => 200u)!;
|
||||
magicSkill: _ => 200u,
|
||||
spellComponentTemplates: componentTemplate)!;
|
||||
|
||||
Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId));
|
||||
Assert.Equal(1, interaction.BusyCount);
|
||||
|
|
@ -784,6 +801,17 @@ public sealed class AppraisalUiControllerTests
|
|||
UiTextureElement icon = Assert.Single(
|
||||
iconHost.Children.OfType<UiTextureElement>());
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -94,6 +94,87 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
|||
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]
|
||||
public void DefaultPreLoginKey_NeverOverwritesCharacterLayouts()
|
||||
{
|
||||
|
|
@ -169,14 +250,17 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
|||
private static RetailWindowHandle Mount(
|
||||
UiRoot root,
|
||||
string name,
|
||||
IRetainedWindowStateController? state = null)
|
||||
IRetainedWindowStateController? state = null,
|
||||
int authoredGeometryRevision = 0,
|
||||
float width = 200f,
|
||||
float height = 100f)
|
||||
{
|
||||
var frame = new UiPanel
|
||||
{
|
||||
Left = 10f,
|
||||
Top = 20f,
|
||||
Width = 200f,
|
||||
Height = 100f,
|
||||
Width = width,
|
||||
Height = height,
|
||||
MinWidth = 100f,
|
||||
MinHeight = 80f,
|
||||
MaxWidth = 600f,
|
||||
|
|
@ -185,7 +269,11 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
|||
Resizable = true,
|
||||
};
|
||||
root.AddChild(frame);
|
||||
return root.RegisterWindow(name, frame, stateController: state);
|
||||
return root.RegisterWindow(
|
||||
name,
|
||||
frame,
|
||||
stateController: state,
|
||||
authoredGeometryRevision: authoredGeometryRevision);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue