merge: fix #490 part 2 — plugin windows adopt an authored size change (geometry-hash revision)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
3d7065ebc0
7 changed files with 301 additions and 29 deletions
|
|
@ -74,32 +74,32 @@ after each deliberate `Top` write for the imported-layout element. Precedent:
|
|||
`MapPageController.cs:235-249` (the same fix already landed for other
|
||||
runtime-repositioned imported/programmatic elements).
|
||||
|
||||
## #491 — MossTank extra/blacklisted buff lists are shown but not consumed by BuffPlan.Build
|
||||
|
||||
**Status:** OPEN — found 2026-09-07 at the slice 7 architecture review.
|
||||
**Severity:** LOW (honest UI after fix round B; behavior owed)
|
||||
**Component:** `src/AcDream.Plugins.MossTank/BuffPlan.cs` (`Build`), `BuffSettings.ExtraBuffSpellNames`/`BlacklistedBuffFamilyNames`
|
||||
|
||||
**Description.** Slice 7 added VTank's "Extra Buff Spells" and "Blacklisted Buff Families" lists to the Buffs tab. They are stored (persisted after fix round B) but `BuffPlan.Build` never reads them: extra exemplars are not cast, blacklisted families are not skipped. Belongs to VT2 slice 4 (buffs); wire `Build` to add the "best similar" spell per extra exemplar and to skip blacklisted families, with plan tests.
|
||||
|
||||
## #490 — Plugin panel host: `StartVisible=false` + `ShowInSidePanel=false` is permanently unshowable; layout persistence has no revision bump
|
||||
|
||||
**Status:** OPEN — found live 2026-09-07 at slice 7 fix round A (`78b42a519`), workaround in the plugin.
|
||||
**Severity:** LOW/MEDIUM (host defect; silent)
|
||||
**Component:** `src/AcDream.App/UI/RetailUiRuntime.cs` (~5735-5757, `PluginWindowVisibilityController`), `RetailWindowLayoutPersistence`
|
||||
|
||||
**Description.** (1) `_requestedVisible = startVisible` and only `OnShown()` ever sets it true; a window registered with `ShowInSidePanel=false` has no shelf entry to raise `OnShown`, so `StartVisible=false` can never become visible, with no error. MossTank works around it by registering its popups `StartVisible=true` and gating on its own visibility bindings. Fix shape: validate the combination at `AddPanel` (throw, or coerce). (2) Plugin windows restore a stale persisted position over a changed authored default with no revision bump — the slice 7 screenshots needed an isolated `ACDREAM_CONFIG_DIR` to open at the authored 28,42. Fix shape: key the persisted layout by the panel's authored geometry hash (or a plugin-declared layout revision) so a redesign resets the stored position once.
|
||||
|
||||
## #489 — Headless: SpewBox pending queue grows unbounded when no console ticks it; console polish
|
||||
|
||||
**Status:** OPEN — found 2026-09-07 by the Opus re-check of the headless console (`738111239`).
|
||||
**Severity:** LOW/MEDIUM (leak in long-lived bots)
|
||||
**Component:** `src/AcDream.Runtime/.../SpewBoxState.cs` (`Enqueue` ~:110, `_pending`), `src/AcDream.Headless/Hosting/HeadlessConsoleSpewBoxPump.cs`
|
||||
|
||||
**Description.** `RuntimeCommunicationState.AddText` routes every `ClientLocal` (0x1A) line into `SpewBoxState.Enqueue`; the only `Tick` caller in the headless host is the console pump, so with the console disabled (every scripted/CI bot) `_pending` grows for the life of the session. Pre-existing before the console; the console merely made it visible. Fix shape: tick the SpewBox from the session tick regardless of the console (or drop `ClientLocal` text when nothing observes it), with a pin that a 10,000-line burst without a console does not grow the queue.
|
||||
|
||||
**Polish carried from the same re-check:** in `--console` mode the JSON diagnostics/resources stream still interleaves with the chat lines on stdout — quiet it or send it to stderr when the console is on; `--console` missing from `--help`; `HeadlessConsoleOptions.cs:51` re-types the env-var literal (the LaunchOptions regex needs it — a const rename would split the two reads); the `/quit`/`/status`/"not handled" writes and `Pump()` sit outside the S4 try/catch (a broken stdout pipe would fault the session); the SpewBox's 4-entry visible cap can drop interface-text lines produced between two pumps.
|
||||
|
||||
## #491 — MossTank extra/blacklisted buff lists are shown but not consumed by BuffPlan.Build
|
||||
|
||||
**Status:** OPEN — found 2026-09-07 at the slice 7 architecture review.
|
||||
**Severity:** LOW (honest UI after fix round B; behavior owed)
|
||||
**Component:** `src/AcDream.Plugins.MossTank/BuffPlan.cs` (`Build`), `BuffSettings.ExtraBuffSpellNames`/`BlacklistedBuffFamilyNames`
|
||||
|
||||
**Description.** Slice 7 added VTank's "Extra Buff Spells" and "Blacklisted Buff Families" lists to the Buffs tab. They are stored (persisted after fix round B) but `BuffPlan.Build` never reads them: extra exemplars are not cast, blacklisted families are not skipped. Belongs to VT2 slice 4 (buffs); wire `Build` to add the "best similar" spell per extra exemplar and to skip blacklisted families, with plan tests.
|
||||
|
||||
## #490 — Plugin panel host: `StartVisible=false` + `ShowInSidePanel=false` is permanently unshowable; layout persistence has no revision bump
|
||||
|
||||
**Status:** OPEN — found live 2026-09-07 at slice 7 fix round A (`78b42a519`), workaround in the plugin.
|
||||
**Severity:** LOW/MEDIUM (host defect; silent)
|
||||
**Component:** `src/AcDream.App/UI/RetailUiRuntime.cs` (~5735-5757, `PluginWindowVisibilityController`), `RetailWindowLayoutPersistence`
|
||||
|
||||
**Description.** (1) `_requestedVisible = startVisible` and only `OnShown()` ever sets it true; a window registered with `ShowInSidePanel=false` has no shelf entry to raise `OnShown`, so `StartVisible=false` can never become visible, with no error. MossTank works around it by registering its popups `StartVisible=true` and gating on its own visibility bindings. Fix shape: validate the combination at `AddPanel` (throw, or coerce). Still OPEN. (2) **FIXED at `05f22d46f`.** Plugin windows restored a stale persisted SIZE over a changed authored default with no revision bump (MossTank's panel went 856x236 -> 984x271 and every stored layout stayed at 856x236 forever — not a position bug, a size bug; the earlier "28,42" framing above was about part (1)'s workaround, not this). `RetailWindowManager.ComputeAuthoredGeometryRevision` now derives the revision from the panel's own authored `(w, h, minw, minh, resizable)` tuple via a process-stable hash (deliberately not `System.HashCode`, which reseeds per process), and `MountPlugins` passes it instead of the hard-coded `0`. `RetailWindowLayoutPersistence.MigrateAuthoredGeometry` now compares revisions for inequality rather than "newer only", since a hash is not an ordered counter. Position is kept (not reset) and still clamps to the live screen; a genuinely unchanged authored size keeps the user's own resize. See `RetailWindowLayoutPersistence`'s class doc for the full contract.
|
||||
|
||||
## #489 — Headless: SpewBox pending queue grows unbounded when no console ticks it; console polish
|
||||
|
||||
**Status:** OPEN — found 2026-09-07 by the Opus re-check of the headless console (`738111239`).
|
||||
**Severity:** LOW/MEDIUM (leak in long-lived bots)
|
||||
**Component:** `src/AcDream.Runtime/.../SpewBoxState.cs` (`Enqueue` ~:110, `_pending`), `src/AcDream.Headless/Hosting/HeadlessConsoleSpewBoxPump.cs`
|
||||
|
||||
**Description.** `RuntimeCommunicationState.AddText` routes every `ClientLocal` (0x1A) line into `SpewBoxState.Enqueue`; the only `Tick` caller in the headless host is the console pump, so with the console disabled (every scripted/CI bot) `_pending` grows for the life of the session. Pre-existing before the console; the console merely made it visible. Fix shape: tick the SpewBox from the session tick regardless of the console (or drop `ClientLocal` text when nothing observes it), with a pin that a 10,000-line burst without a console does not grow the queue.
|
||||
|
||||
**Polish carried from the same re-check:** in `--console` mode the JSON diagnostics/resources stream still interleaves with the chat lines on stdout — quiet it or send it to stderr when the console is on; `--console` missing from `--help`; `HeadlessConsoleOptions.cs:51` re-types the env-var literal (the LaunchOptions regex needs it — a const rename would split the two reads); the `/quit`/`/status`/"not handled" writes and `Pump()` sit outside the S4 try/catch (a broken stdout pipe would fault the session); the SpewBox's 4-entry visible cap can drop interface-text lines produced between two pumps.
|
||||
|
||||
## #488 — MossTank `.utl` expression block: length prefix measured before newline normalization
|
||||
|
||||
**Status:** OPEN — found 2026-09-07 by the final Opus re-check of Campaign VT
|
||||
|
|
|
|||
|
|
@ -210,7 +210,12 @@ No other markup or host wiring is needed to make a panel resizable: once
|
|||
`resizable="true"` sets the window's `Resizable`/`ResizeX`/`ResizeY`/
|
||||
`MinWidth`/`MinHeight`, the SAME drag-resize, persistence (save/restore
|
||||
across sessions, clamped to `minw`/`minh`), and UI-lock behavior every other
|
||||
retained window already has just applies.
|
||||
retained window already has just applies. Changing a panel's authored `w`/
|
||||
`h`/`minw`/`minh`/`resizable` in a later plugin update resets every user's
|
||||
stored SIZE to the new authored default exactly once (their saved position
|
||||
is kept and re-clamped to the screen) — the host derives a stable revision
|
||||
from that tuple automatically, so a plugin author never needs to bump one
|
||||
by hand (#490 part 2).
|
||||
|
||||
```xml
|
||||
<panel x="0" y="0" w="420" h="320" title="MossTank" resizable="true" minw="360" minh="260">
|
||||
|
|
|
|||
|
|
@ -4767,11 +4767,20 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
// later registration/sidepanel failure then rolls the mounted
|
||||
// subtree back through FailMount instead of leaking it.
|
||||
_bindings.Plugins.CompleteMount(panel, Host.Root, element);
|
||||
// #490 part 2: derive the authored-geometry revision from
|
||||
// the panel's own authored extent instead of a hard-coded 0
|
||||
// — see RetailWindowLayoutPersistence's class doc and
|
||||
// RetailWindowManager.ComputeAuthoredGeometryRevision's own
|
||||
// doc for why a plugin window can't use the built-in
|
||||
// windows' manual-literal scheme.
|
||||
int authoredGeometryRevision = RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
element.Width, element.Height, element.MinWidth, element.MinHeight, element.Resizable);
|
||||
RetailWindowHandle handle = Host.WindowManager.Register(
|
||||
panel.WindowName,
|
||||
element,
|
||||
element,
|
||||
visibility);
|
||||
visibility,
|
||||
authoredGeometryRevision: authoredGeometryRevision);
|
||||
_bindings.Plugins.CompleteWindowMount(
|
||||
panel,
|
||||
() => Host.WindowManager.Unregister(panel.WindowName));
|
||||
|
|
|
|||
|
|
@ -9,6 +9,31 @@ namespace AcDream.App.UI;
|
|||
/// per-resolution settings. It deliberately ignores the temporary pre-login
|
||||
/// <c>default</c> character key so startup layout cannot overwrite a real
|
||||
/// character's state.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Authored-geometry revision (#490 part 2).</b> Every registered window
|
||||
/// carries an <c>authoredGeometryRevision</c> (see
|
||||
/// <see cref="RetailWindowHandle.AuthoredGeometryRevision"/>); a restore
|
||||
/// whose saved revision differs from the handle's current one replaces only
|
||||
/// the saved WIDTH/HEIGHT with the current authored size
|
||||
/// (<see cref="MigrateAuthoredGeometry"/>) — position, visibility, and
|
||||
/// collapsed/maximized state are untouched, and the clamp in
|
||||
/// <see cref="Apply"/> still re-fits the kept position to the live screen.
|
||||
/// Built-in retail-imported windows hand-pick that revision as a small
|
||||
/// incrementing literal at their <c>Register</c> call site (chat windows:
|
||||
/// <c>authoredGeometryRevision = 1</c>) — a deliberate author decision each
|
||||
/// time their authored size changes. Plugin windows have no such call site
|
||||
/// an author remembers to touch, so <c>MountPlugins</c> instead derives the
|
||||
/// revision automatically from the authored geometry tuple itself via
|
||||
/// <see cref="RetailWindowManager.ComputeAuthoredGeometryRevision"/>
|
||||
/// (width, height, min width, min height, resizable): unchanged authored
|
||||
/// geometry hashes to the same revision (a user's own resize survives
|
||||
/// restore), and ANY authored geometry change hashes to a different one
|
||||
/// (the stored size resets to the new default exactly once). Because a hash
|
||||
/// is not an ordered counter, the comparison is for INEQUALITY — see
|
||||
/// <see cref="MigrateAuthoredGeometry"/>'s own doc for why the original
|
||||
/// "newer revision only" read was wrong for this case.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||
{
|
||||
|
|
@ -295,11 +320,26 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
handle.AuthoredGeometryRevision);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #490 part 2: compares revisions for INEQUALITY, not ordering. Built-in
|
||||
/// retail-imported windows hand-pick a small incrementing literal
|
||||
/// (0, 1, 2…) that only ever grows, so the original "migrate only if
|
||||
/// saved < authored" read fine for them. Plugin windows instead derive
|
||||
/// their revision from a hash of the authored geometry itself
|
||||
/// (<see cref="RetailWindowManager.ComputeAuthoredGeometryRevision"/>) so
|
||||
/// their author never has to remember to bump a literal — but a hash is
|
||||
/// not a counter, and two different authored sizes can hash in either
|
||||
/// order. "The authored size changed" therefore means "the value
|
||||
/// differs", not "the value went up"; treating it as ordered silently
|
||||
/// dropped every size-decreasing (by hash value, not by pixels) plugin
|
||||
/// update, which is exactly how MossTank's 856x236 -> 984x271 bump got
|
||||
/// stuck at the old size for every user with a stored layout.
|
||||
/// </summary>
|
||||
private static UiWindowLayout MigrateAuthoredGeometry(
|
||||
UiWindowLayout saved,
|
||||
UiWindowLayout authored)
|
||||
{
|
||||
if (saved.AuthoredGeometryRevision >= authored.AuthoredGeometryRevision)
|
||||
if (saved.AuthoredGeometryRevision == authored.AuthoredGeometryRevision)
|
||||
return saved;
|
||||
|
||||
return saved with
|
||||
|
|
|
|||
|
|
@ -111,6 +111,45 @@ public sealed class RetailWindowManager : IDisposable
|
|||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Derives a stable authored-geometry revision from a window's own
|
||||
/// authored extent (width, height, min width, min height, resizable), so
|
||||
/// a plugin window's <see cref="Register"/> call can invalidate an
|
||||
/// obsolete saved size across an authored-size change WITHOUT the plugin
|
||||
/// author remembering to bump an explicit revision literal the way
|
||||
/// built-in retail-imported windows do (#490 part 2 — MossTank shipped
|
||||
/// 856x236 -> 984x271 and every stored layout stayed at 856x236 forever).
|
||||
/// Deliberately NOT <see cref="HashCode"/>: that type reseeds its
|
||||
/// internal state once per process specifically to defeat hash-flooding
|
||||
/// attacks, so the SAME geometry would hash to a DIFFERENT value on
|
||||
/// every relaunch — every login would look like a fresh authored-geometry
|
||||
/// revision and reset every plugin window's saved size, every time. This
|
||||
/// instead combines the exact IEEE-754 bit patterns with a fixed FNV-1a-
|
||||
/// style multiplier, which is stable across processes, machines, and
|
||||
/// .NET versions. <see cref="RetailWindowLayoutPersistence.MigrateAuthoredGeometry"/>
|
||||
/// compares revisions for INEQUALITY, not ordering — a hash is not a
|
||||
/// counter, so "authored size changed" means "the value differs",
|
||||
/// whichever direction it moved. The sign bit is masked off the result:
|
||||
/// <see cref="Register"/> clamps a negative <c>authoredGeometryRevision</c>
|
||||
/// up to 0 (its "no explicit revision" sentinel), and a hash landing
|
||||
/// there would be indistinguishable from an old, pre-hash saved layout
|
||||
/// that never had a revision at all.
|
||||
/// </summary>
|
||||
public static int ComputeAuthoredGeometryRevision(
|
||||
float width, float height, float minWidth, float minHeight, bool resizable)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hash = 17;
|
||||
hash = (hash * 31) + BitConverter.SingleToInt32Bits(width);
|
||||
hash = (hash * 31) + BitConverter.SingleToInt32Bits(height);
|
||||
hash = (hash * 31) + BitConverter.SingleToInt32Bits(minWidth);
|
||||
hash = (hash * 31) + BitConverter.SingleToInt32Bits(minHeight);
|
||||
hash = (hash * 31) + (resizable ? 1 : 0);
|
||||
return hash & 0x7FFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGet(string name, out RetailWindowHandle handle)
|
||||
=> _byName.TryGetValue(name, out handle!);
|
||||
|
||||
|
|
|
|||
|
|
@ -499,6 +499,143 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
|||
Assert.Equal((200f, 150f), (panel.Width, panel.Height));
|
||||
}
|
||||
|
||||
// ── #490 part 2: a plugin's authored panel size changes across a plugin
|
||||
// update (MossTank went 856x236 -> 984x271) and every user's stored
|
||||
// layout must adopt the new authored size rather than keep the old one
|
||||
// forever. MountPlugins derives authoredGeometryRevision from the
|
||||
// authored geometry tuple via RetailWindowManager.ComputeAuthoredGeometryRevision
|
||||
// (#490 part 2) instead of a manual literal, so these tests register the
|
||||
// way MountPlugins does: pass the SAME computed hash to RegisterWindow. ──
|
||||
|
||||
[Fact]
|
||||
public void PluginMarkupPanel_AuthoredSizeChanged_ReplacesStoredSizeButKeepsPosition()
|
||||
{
|
||||
const string oldXml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"856\" h=\"236\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
|
||||
const string newXml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"984\" h=\"271\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
|
||||
var store = new SettingsStore(PathName);
|
||||
|
||||
// "Old session": the plugin's previous authored size registers and
|
||||
// the user drags the window.
|
||||
var oldPanel = MarkupDocument.Build(oldXml, new object(), _ => (1u, 32, 32));
|
||||
var oldRoot = new UiRoot { Width = 1280, Height = 720 };
|
||||
oldRoot.AddChild(oldPanel);
|
||||
RetailWindowHandle oldHandle = oldRoot.RegisterWindow(
|
||||
"moss-tank",
|
||||
oldPanel,
|
||||
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
oldPanel.Width, oldPanel.Height, oldPanel.MinWidth, oldPanel.MinHeight, oldPanel.Resizable));
|
||||
using (var oldPersistence = new RetailWindowLayoutPersistence(
|
||||
oldRoot.WindowManager, store, () => "Alice", () => (1280, 720)))
|
||||
{
|
||||
oldHandle.MoveTo(120f, 90f);
|
||||
}
|
||||
|
||||
// "New session": the plugin ships its new authored 984x271 size.
|
||||
var newPanel = MarkupDocument.Build(newXml, new object(), _ => (1u, 32, 32));
|
||||
var newRoot = new UiRoot { Width = 1280, Height = 720 };
|
||||
newRoot.AddChild(newPanel);
|
||||
newRoot.RegisterWindow(
|
||||
"moss-tank",
|
||||
newPanel,
|
||||
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
newPanel.Width, newPanel.Height, newPanel.MinWidth, newPanel.MinHeight, newPanel.Resizable));
|
||||
using var persistence = new RetailWindowLayoutPersistence(
|
||||
newRoot.WindowManager, store, () => "Alice", () => (1280, 720));
|
||||
|
||||
persistence.RestoreAll();
|
||||
|
||||
Assert.Equal((984f, 271f), (newPanel.Width, newPanel.Height));
|
||||
Assert.Equal((120f, 90f), (newPanel.Left, newPanel.Top));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PluginMarkupPanel_AuthoredSizeUnchanged_KeepsUserResizedSize()
|
||||
{
|
||||
const string xml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"856\" h=\"236\" resizable=\"true\" minw=\"400\" minh=\"150\"></panel>";
|
||||
var store = new SettingsStore(PathName);
|
||||
|
||||
var panel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||
var root = new UiRoot { Width = 1280, Height = 720 };
|
||||
root.AddChild(panel);
|
||||
RetailWindowHandle handle = root.RegisterWindow(
|
||||
"moss-tank",
|
||||
panel,
|
||||
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
panel.Width, panel.Height, panel.MinWidth, panel.MinHeight, panel.Resizable));
|
||||
using (var persistence = new RetailWindowLayoutPersistence(
|
||||
root.WindowManager, store, () => "Alice", () => (1280, 720)))
|
||||
{
|
||||
handle.MoveTo(50f, 50f);
|
||||
handle.ResizeTo(900f, 300f);
|
||||
}
|
||||
|
||||
// Fresh session: the SAME authored geometry (same markup) registers
|
||||
// again — the derived revision is unchanged, so the user's own
|
||||
// resize must survive.
|
||||
var freshPanel = MarkupDocument.Build(xml, new object(), _ => (1u, 32, 32));
|
||||
var freshRoot = new UiRoot { Width = 1280, Height = 720 };
|
||||
freshRoot.AddChild(freshPanel);
|
||||
freshRoot.RegisterWindow(
|
||||
"moss-tank",
|
||||
freshPanel,
|
||||
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
freshPanel.Width, freshPanel.Height, freshPanel.MinWidth, freshPanel.MinHeight, freshPanel.Resizable));
|
||||
using var freshPersistence = new RetailWindowLayoutPersistence(
|
||||
freshRoot.WindowManager, store, () => "Alice", () => (1280, 720));
|
||||
|
||||
freshPersistence.RestoreAll();
|
||||
|
||||
Assert.Equal((900f, 300f), (freshPanel.Width, freshPanel.Height));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PluginMarkupPanel_AuthoredSizeChanged_ClampsPositionToNewScreenBounds()
|
||||
{
|
||||
// The authored-size migration keeps the saved POSITION, but that
|
||||
// position still runs through the ordinary restore clamp (Apply's
|
||||
// MoveTo clamp) — a larger authored size can push a near-edge saved
|
||||
// X/Y off the live screen.
|
||||
const string oldXml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"200\" h=\"100\" resizable=\"true\" minw=\"100\" minh=\"80\"></panel>";
|
||||
const string newXml =
|
||||
"<panel x=\"0\" y=\"0\" w=\"220\" h=\"110\" resizable=\"true\" minw=\"100\" minh=\"80\"></panel>";
|
||||
var store = new SettingsStore(PathName);
|
||||
|
||||
var oldPanel = MarkupDocument.Build(oldXml, new object(), _ => (1u, 32, 32));
|
||||
var oldRoot = new UiRoot { Width = 800, Height = 600 };
|
||||
oldRoot.AddChild(oldPanel);
|
||||
RetailWindowHandle oldHandle = oldRoot.RegisterWindow(
|
||||
"moss-tank",
|
||||
oldPanel,
|
||||
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
oldPanel.Width, oldPanel.Height, oldPanel.MinWidth, oldPanel.MinHeight, oldPanel.Resizable));
|
||||
using (var oldPersistence = new RetailWindowLayoutPersistence(
|
||||
oldRoot.WindowManager, store, () => "Alice", () => (800, 600)))
|
||||
{
|
||||
oldHandle.MoveTo(590f, 490f); // fits the OLD 200x100 size exactly
|
||||
}
|
||||
|
||||
var newPanel = MarkupDocument.Build(newXml, new object(), _ => (1u, 32, 32));
|
||||
var newRoot = new UiRoot { Width = 800, Height = 600 };
|
||||
newRoot.AddChild(newPanel);
|
||||
newRoot.RegisterWindow(
|
||||
"moss-tank",
|
||||
newPanel,
|
||||
authoredGeometryRevision: RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
newPanel.Width, newPanel.Height, newPanel.MinWidth, newPanel.MinHeight, newPanel.Resizable));
|
||||
using var persistence = new RetailWindowLayoutPersistence(
|
||||
newRoot.WindowManager, store, () => "Alice", () => (800, 600));
|
||||
|
||||
persistence.RestoreAll();
|
||||
|
||||
Assert.Equal((220f, 110f), (newPanel.Width, newPanel.Height));
|
||||
// maxX = 800 - 220 = 580 (590 clamps down); maxY = 600 - 110 = 490 (already in bounds).
|
||||
Assert.Equal((580f, 490f), (newPanel.Left, newPanel.Top));
|
||||
}
|
||||
|
||||
private static RetailWindowHandle Mount(
|
||||
UiRoot root,
|
||||
string name,
|
||||
|
|
|
|||
|
|
@ -291,6 +291,48 @@ public sealed class RetailWindowManagerTests
|
|||
Assert.Equal(0, resized);
|
||||
}
|
||||
|
||||
// ── #490 part 2: plugin windows derive their authored-geometry revision
|
||||
// from the authored geometry itself, so a plugin author who ships a new
|
||||
// panel size does not also have to remember to bump an explicit revision
|
||||
// literal the way built-in retail-imported windows do. ────────────────
|
||||
|
||||
[Fact]
|
||||
public void ComputeAuthoredGeometryRevision_SameGeometry_IsStable()
|
||||
{
|
||||
int a = RetailWindowManager.ComputeAuthoredGeometryRevision(856f, 236f, 400f, 150f, true);
|
||||
int b = RetailWindowManager.ComputeAuthoredGeometryRevision(856f, 236f, 400f, 150f, true);
|
||||
|
||||
Assert.Equal(a, b);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(984f, 236f, 400f, 150f, true)] // width changed (the #490 MossTank case)
|
||||
[InlineData(856f, 271f, 400f, 150f, true)] // height changed
|
||||
[InlineData(856f, 236f, 420f, 150f, true)] // minw changed
|
||||
[InlineData(856f, 236f, 400f, 160f, true)] // minh changed
|
||||
[InlineData(856f, 236f, 400f, 150f, false)] // resizable changed
|
||||
public void ComputeAuthoredGeometryRevision_AnyFieldDiffers_ChangesTheValue(
|
||||
float width, float height, float minWidth, float minHeight, bool resizable)
|
||||
{
|
||||
int baseline = RetailWindowManager.ComputeAuthoredGeometryRevision(856f, 236f, 400f, 150f, true);
|
||||
int changed = RetailWindowManager.ComputeAuthoredGeometryRevision(
|
||||
width, height, minWidth, minHeight, resizable);
|
||||
|
||||
Assert.NotEqual(baseline, changed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeAuthoredGeometryRevision_IsNeverNegative()
|
||||
{
|
||||
// Register clamps a negative authoredGeometryRevision up to 0 (its
|
||||
// "no explicit revision" sentinel) — a hash landing there would be
|
||||
// indistinguishable from an old, pre-hash saved layout that never
|
||||
// had a revision at all, so the function must never produce one.
|
||||
Assert.True(RetailWindowManager.ComputeAuthoredGeometryRevision(856f, 236f, 400f, 150f, true) >= 0);
|
||||
Assert.True(RetailWindowManager.ComputeAuthoredGeometryRevision(0f, 0f, 0f, 0f, false) >= 0);
|
||||
Assert.True(RetailWindowManager.ComputeAuthoredGeometryRevision(-1f, -1f, -1f, -1f, true) >= 0);
|
||||
}
|
||||
|
||||
private sealed class RecordingController : IRetainedPanelController
|
||||
{
|
||||
public int ShownCount { get; private set; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue