refactor(runtime): expose canonical gameplay state

Move character options and movement skills into the Runtime-owned character graph, expose borrowed inventory, character, and social views, and route retained UI state commands through generation-gated typed Runtime contracts. Preserve the existing synchronous wire path while deleting the App-owned option and skill mirrors and extending normalized parity checkpoints.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-26 09:12:30 +02:00
parent 9d0d9b07e0
commit dcb61efb5a
34 changed files with 2076 additions and 103 deletions

View file

@ -10,6 +10,7 @@ using AcDream.App.UI.Layout;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.UI.Abstractions;
namespace AcDream.App.Tests.Composition;
@ -67,6 +68,44 @@ public sealed class InteractionUiRuntimeSourcesTests
Assert.Throws<ObjectDisposedException>(() => source.Bind(query, interactions));
}
[Fact]
public void GameRuntimeCommandsCaptureTheExactCurrentGeneration()
{
var source = new DeferredGameRuntimeStateCommands();
var first = new RuntimeTarget(new RuntimeGenerationToken(7));
var second = new RuntimeTarget(new RuntimeGenerationToken(9));
Assert.False(source.IsInWorld);
Assert.Equal(
RuntimeCommandStatus.Inactive,
source.Advance(RuntimeAdvancementKind.Skill, 6u, 100u).Status);
IDisposable stale = source.Bind(first, first);
Assert.True(source.IsInWorld);
Assert.True(source.Advance(
RuntimeAdvancementKind.Skill,
6u,
100u).Accepted);
Assert.Equal(new RuntimeGenerationToken(7), first.LastGeneration);
Assert.Throws<InvalidOperationException>(() =>
source.Bind(second, second));
stale.Dispose();
using IDisposable current = source.Bind(second, second);
stale.Dispose();
Assert.True(source.AddFavorite(0, 2, 42u).Accepted);
Assert.Equal(new RuntimeGenerationToken(9), second.LastGeneration);
Assert.Equal(
RuntimeCommandStatus.Rejected,
source.RemoveShortcut(uint.MaxValue).Status);
source.Deactivate();
Assert.Equal(
RuntimeCommandStatus.Inactive,
source.RemoveShortcut(3u).Status);
Assert.Throws<ObjectDisposedException>(() => source.Bind(first, first));
}
[Fact]
public void RadarNeverCachesAnUnboundBootstrapSnapshot()
{
@ -172,6 +211,109 @@ public sealed class InteractionUiRuntimeSourcesTests
public ICommandBus Commands { get; } = new LiveCommandBus();
}
private sealed class RuntimeTarget
: IGameRuntimeView,
IGameRuntimeCommands,
IRuntimeInventoryStateCommands,
IRuntimeSpellbookCommands,
IRuntimeCharacterCommands
{
public RuntimeTarget(RuntimeGenerationToken generation)
{
Generation = generation;
}
public RuntimeGenerationToken LastGeneration { get; private set; }
public RuntimeGenerationToken Generation { get; }
public RuntimeLifecycleSnapshot Lifecycle =>
new(Generation, RuntimeLifecycleState.InWorld, 1u, true);
public IGameRuntimeClock Clock => null!;
public IRuntimeEntityView Entities => null!;
public IRuntimeInventoryView Inventory => null!;
public IRuntimeInventoryStateView InventoryState => null!;
public IRuntimeCharacterView Character => null!;
public IRuntimeSocialView Social => null!;
public IRuntimeChatView Chat => null!;
public IRuntimeMovementView Movement => null!;
public IRuntimePortalView Portal => null!;
public IRuntimeSessionCommands Session => null!;
public IRuntimeSelectionCommands Selection => null!;
public IRuntimeCombatCommands Combat => null!;
IRuntimeMovementCommands IGameRuntimeCommands.Movement => null!;
IRuntimeChatCommands IGameRuntimeCommands.Chat => null!;
IRuntimePortalCommands IGameRuntimeCommands.Portal => null!;
IRuntimeInventoryStateCommands IGameRuntimeCommands.InventoryState => this;
IRuntimeSpellbookCommands IGameRuntimeCommands.Spellbook => this;
IRuntimeCharacterCommands IGameRuntimeCommands.Character => this;
IRuntimeSocialCommands IGameRuntimeCommands.Social => null!;
public RuntimeStateCheckpoint CaptureCheckpoint() => default;
public RuntimeCommandResult AddShortcut(
RuntimeGenerationToken expectedGeneration,
in RuntimeShortcutCommand command) =>
Accepted(expectedGeneration, command.ObjectId);
public RuntimeCommandResult RemoveShortcut(
RuntimeGenerationToken expectedGeneration,
int index) =>
Accepted(expectedGeneration);
public RuntimeCommandResult AddFavorite(
RuntimeGenerationToken expectedGeneration,
int tabIndex,
int position,
uint spellId) =>
Accepted(expectedGeneration, spellId);
public RuntimeCommandResult RemoveFavorite(
RuntimeGenerationToken expectedGeneration,
int tabIndex,
uint spellId) =>
Accepted(expectedGeneration, spellId);
public RuntimeCommandResult SetFilter(
RuntimeGenerationToken expectedGeneration,
uint filters) =>
Accepted(expectedGeneration);
public RuntimeCommandResult ForgetSpell(
RuntimeGenerationToken expectedGeneration,
uint spellId) =>
Accepted(expectedGeneration, spellId);
public RuntimeCommandResult SetDesiredComponent(
RuntimeGenerationToken expectedGeneration,
uint componentId,
uint amount) =>
Accepted(expectedGeneration, componentId);
public RuntimeCommandResult ClearDesiredComponents(
RuntimeGenerationToken expectedGeneration) =>
Accepted(expectedGeneration);
public RuntimeCommandResult Advance(
RuntimeGenerationToken expectedGeneration,
in RuntimeAdvancementCommand command) =>
Accepted(expectedGeneration, command.StatId);
public RuntimeCommandResult SetOptions1(
RuntimeGenerationToken expectedGeneration,
uint options) =>
Accepted(expectedGeneration);
private RuntimeCommandResult Accepted(
RuntimeGenerationToken generation,
uint objectId = 0u)
{
LastGeneration = generation;
return new RuntimeCommandResult(
RuntimeCommandStatus.Accepted,
generation,
objectId);
}
}
private sealed class SelectionQuery : IRetainedUiSelectionQuery
{
public bool ShouldShowHealth(uint serverGuid) => true;