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