refactor(runtime): close canonical gameplay ownership

Unify the toolbar shortcut manager with Runtime inventory state, route retail-ordered shortcut and spellbook command effects through the canonical owners, and make retained controllers borrow those exact instances. Remove the item-interaction transaction fallback and add graphical/no-window parity plus failure-safe terminal ownership-ledger coverage.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 09:48:51 +02:00
parent ce6fae7b38
commit 89e6b207f8
36 changed files with 1433 additions and 251 deletions

View file

@ -115,7 +115,7 @@ public sealed class RuntimeCharacterStateTests
}
[Fact]
public void DisposalRetriesObserverFailuresAndClearsBothOwners()
public void DisposalReportsObserverFailuresAfterTerminalConvergence()
{
var state = new RuntimeCharacterState();
state.Spellbook.OnSpellLearned(7u);
@ -143,13 +143,13 @@ public sealed class RuntimeCharacterStateTests
state.Dispose);
Assert.Equal(2, error.InnerExceptions.Count);
Assert.False(state.IsDisposed);
Assert.True(state.IsDisposed);
Assert.False(state.Spellbook.Knows(7u));
Assert.Null(state.LocalPlayer.Get(
AcDream.Core.Player.LocalPlayerState.VitalKind.Health));
Assert.True(state.CaptureOwnership().IsConverged);
state.Dispose();
state.Dispose();
Assert.True(state.IsDisposed);
}
@ -254,4 +254,77 @@ public sealed class RuntimeCharacterStateTests
Assert.True(first.View.Snapshot.SpellbookRevision > 0);
Assert.Equal(0, second.View.Snapshot.SpellbookRevision);
}
[Fact]
public void SpellbookCommandsFollowRetailLocalAndOutboundOrder()
{
using var state = new RuntimeCharacterState();
var order = new List<string>();
state.Spellbook.SpellbookChanged += () => order.Add("local");
state.Spellbook.DesiredComponentsChanged += () => order.Add("local");
Assert.True(state.TryAddFavorite(
0,
0,
42u,
() => order.Add("send")));
Assert.Equal(["local", "send"], order);
Assert.Equal([42u], state.Spellbook.GetFavorites(0));
order.Clear();
state.SetSpellbookFilter(0x3FFEu, () => order.Add("send"));
Assert.Equal(["local", "send"], order);
Assert.Equal(0x3FFEu, state.Spellbook.SpellbookFilters);
order.Clear();
Assert.True(state.TrySetDesiredComponent(
0x68000001u,
0u,
() => order.Add("send")));
Assert.Equal(["send", "local"], order);
Assert.True(state.Spellbook.DesiredComponents.ContainsKey(
0x68000001u));
Assert.Equal(0u, state.Spellbook.DesiredComponents[0x68000001u]);
order.Clear();
state.ClearDesiredComponents(() => order.Add("send"));
Assert.Equal(["send", "local"], order);
Assert.Empty(state.Spellbook.DesiredComponents);
Assert.Throws<InvalidOperationException>(
() => state.TrySetDesiredComponent(
0x68000002u,
10u,
() => throw new InvalidOperationException("transport")));
Assert.Equal(10u, state.Spellbook.DesiredComponents[0x68000002u]);
}
[Fact]
public void InvalidSpellbookCommandsDoNotPublishOrMutate()
{
using var state = new RuntimeCharacterState();
int sends = 0;
Assert.False(state.TryAddFavorite(
8,
0,
42u,
() => sends++));
Assert.False(state.TryRemoveFavorite(
-1,
42u,
() => sends++));
Assert.False(state.TrySetDesiredComponent(
0u,
1u,
() => sends++));
Assert.False(state.TrySetDesiredComponent(
1u,
5001u,
() => sends++));
Assert.Equal(0, sends);
Assert.Empty(state.Spellbook.GetFavorites(0));
Assert.Empty(state.Spellbook.DesiredComponents);
}
}