refactor(runtime): own magic and player state
Move the coupled Spellbook and LocalPlayerState into one Runtime-owned character graph, route content, live-session, retained UI, reset, and shutdown through that exact owner, and delete the duplicate desired-component snapshot from inventory state. Preserve synchronous retail update and reset ordering while adding independent-instance and retryable-failure coverage. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
d362172620
commit
d02a12ceac
16 changed files with 418 additions and 96 deletions
85
src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs
Normal file
85
src/AcDream.Runtime/Gameplay/RuntimeCharacterState.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical presentation-independent owner for the local character's magic
|
||||
/// and player-sheet state. The two objects form one lifetime group because
|
||||
/// vital maxima read active enchantments from this exact spellbook.
|
||||
/// </summary>
|
||||
public sealed class RuntimeCharacterState : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeCharacterState(SpellTable? spellTable = null)
|
||||
{
|
||||
Spellbook = new Spellbook(spellTable);
|
||||
LocalPlayer = new LocalPlayerState(Spellbook);
|
||||
}
|
||||
|
||||
public Spellbook Spellbook { get; }
|
||||
public LocalPlayerState LocalPlayer { get; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Installs immutable DAT metadata without transferring its ownership to
|
||||
/// Runtime. The content host may install one table after portal.dat opens.
|
||||
/// </summary>
|
||||
public void InstallSpellMetadata(SpellTable spellTable)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
Spellbook.InstallMetadata(spellTable);
|
||||
}
|
||||
|
||||
public void ResetSpellbook()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
Spellbook.Clear();
|
||||
}
|
||||
|
||||
public void ResetLocalPlayer()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
LocalPlayer.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears both coupled owners while retaining every failed suffix for a
|
||||
/// retry. State mutation happens before the existing synchronous
|
||||
/// invalidation callbacks, so retrying is safe and convergent.
|
||||
/// </summary>
|
||||
public void ResetSession()
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
List<Exception>? failures = null;
|
||||
Try(Spellbook.Clear, ref failures);
|
||||
Try(LocalPlayer.Clear, ref failures);
|
||||
if (failures is not null)
|
||||
{
|
||||
throw new AggregateException(
|
||||
"Runtime character state did not converge during reset.",
|
||||
failures);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
ResetSession();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private static void Try(Action action, ref List<Exception>? failures)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
(failures ??= []).Add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
ExternalContainers = new ExternalContainerState();
|
||||
ItemMana = new ItemManaState();
|
||||
Shortcuts = new ShortcutState();
|
||||
DesiredComponents = new DesiredComponentState();
|
||||
Transactions = new InventoryTransactionState(_entityObjects.Objects);
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +27,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
public ExternalContainerState ExternalContainers { get; }
|
||||
public ItemManaState ItemMana { get; }
|
||||
public ShortcutState Shortcuts { get; }
|
||||
public DesiredComponentState DesiredComponents { get; }
|
||||
public InventoryTransactionState Transactions { get; }
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
|
|
@ -39,7 +37,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
public void ResetPlayerSnapshots()
|
||||
{
|
||||
Shortcuts.Clear();
|
||||
DesiredComponents.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -50,7 +47,6 @@ public sealed class RuntimeInventoryState : IDisposable
|
|||
Try(() => ExternalContainers.Reset(), ref failures);
|
||||
Try(ItemMana.Clear, ref failures);
|
||||
Try(Shortcuts.Clear, ref failures);
|
||||
Try(DesiredComponents.Clear, ref failures);
|
||||
Try(Transactions.Dispose, ref failures);
|
||||
if (failures is not null)
|
||||
throw new AggregateException(
|
||||
|
|
@ -90,25 +86,3 @@ public sealed class ShortcutState
|
|||
|
||||
public void Clear() => Replace(Array.Empty<ShortcutEntry>());
|
||||
}
|
||||
|
||||
public sealed class DesiredComponentState
|
||||
{
|
||||
private IReadOnlyList<(uint Id, uint Amount)> _items =
|
||||
Array.Empty<(uint Id, uint Amount)>();
|
||||
private long _revision;
|
||||
|
||||
public IReadOnlyList<(uint Id, uint Amount)> Items =>
|
||||
Volatile.Read(ref _items);
|
||||
public long Revision => Interlocked.Read(ref _revision);
|
||||
|
||||
public void Replace(IReadOnlyList<(uint Id, uint Amount)>? items)
|
||||
{
|
||||
Volatile.Write(
|
||||
ref _items,
|
||||
items ?? Array.Empty<(uint Id, uint Amount)>());
|
||||
Interlocked.Increment(ref _revision);
|
||||
}
|
||||
|
||||
public void Clear() =>
|
||||
Replace(Array.Empty<(uint Id, uint Amount)>());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue