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
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue