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:
Erik 2026-07-24 07:08:47 +02:00
parent 3e31b0ac70
commit d4ce64f56b
22 changed files with 307 additions and 41 deletions

View file

@ -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());

View file

@ -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;

View file

@ -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(

View file

@ -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,

View file

@ -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;

View file

@ -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,

View file

@ -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();

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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);