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:
parent
9d0d9b07e0
commit
dcb61efb5a
34 changed files with 2076 additions and 103 deletions
|
|
@ -15,6 +15,7 @@ using AcDream.Core.Net;
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Entities;
|
||||
|
|
@ -363,6 +364,157 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Assert.Equal(0, harness.EntityObjects.Events.SubscriberCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdapterBorrowsExactJ4ViewsAndCheckpointState()
|
||||
{
|
||||
using var harness = new Harness();
|
||||
harness.Character.Options.Replace(0x04000000u, 0x00948700u);
|
||||
harness.Character.MovementSkills.Update(200, 175);
|
||||
harness.Character.Spellbook.OnSpellLearned(42u);
|
||||
harness.InventoryState.Shortcuts.Replace(
|
||||
[
|
||||
new ShortcutEntry(1, 0x80000001u, 0u),
|
||||
]);
|
||||
harness.Communication.Friends.Apply(new FriendsUpdate(
|
||||
FriendsUpdateType.Full,
|
||||
[
|
||||
new FriendEntry(
|
||||
0x50000003u,
|
||||
"Borrowed",
|
||||
true,
|
||||
false,
|
||||
[],
|
||||
[]),
|
||||
]));
|
||||
|
||||
Assert.Same(harness.Character.View, harness.Runtime.Character);
|
||||
Assert.Same(
|
||||
harness.InventoryState.View,
|
||||
harness.Runtime.InventoryState);
|
||||
Assert.Same(
|
||||
harness.Communication.SocialView,
|
||||
harness.Runtime.Social);
|
||||
|
||||
RuntimeStateCheckpoint checkpoint =
|
||||
harness.Runtime.CaptureCheckpoint();
|
||||
Assert.Equal(1, checkpoint.Character.LearnedSpellCount);
|
||||
Assert.True(checkpoint.Character.MovementSkills.IsComplete);
|
||||
Assert.Equal(1, checkpoint.InventoryState.ShortcutCount);
|
||||
Assert.Equal(1, checkpoint.Social.FriendCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void J4StateCommandsAreGenerationGatedAndReachCurrentTransport()
|
||||
{
|
||||
using var harness = new Harness();
|
||||
var trace = new RuntimeTraceRecorder();
|
||||
using IDisposable subscription = harness.Runtime.Subscribe(trace);
|
||||
_ = harness.Runtime.Session.Start(harness.Runtime.Generation);
|
||||
RuntimeGenerationToken generation = harness.Runtime.Generation;
|
||||
IGameRuntimeCommands commands = harness.Runtime;
|
||||
|
||||
RuntimeCommandResult shortcut = commands.InventoryState.AddShortcut(
|
||||
generation,
|
||||
new RuntimeShortcutCommand(2, 0x80000001u, 0u));
|
||||
RuntimeCommandResult favorite = commands.Spellbook.AddFavorite(
|
||||
generation,
|
||||
tabIndex: 0,
|
||||
position: 0,
|
||||
spellId: 42u);
|
||||
RuntimeCommandResult filter = commands.Spellbook.SetFilter(
|
||||
generation,
|
||||
0x3FFEu);
|
||||
RuntimeCommandResult desired = commands.Spellbook.SetDesiredComponent(
|
||||
generation,
|
||||
0x68000001u,
|
||||
10u);
|
||||
RuntimeCommandResult advancement = commands.Character.Advance(
|
||||
generation,
|
||||
new RuntimeAdvancementCommand(
|
||||
RuntimeAdvancementKind.Skill,
|
||||
StatId: 6u,
|
||||
Cost: 500u));
|
||||
RuntimeCommandResult options = commands.Character.SetOptions1(
|
||||
generation,
|
||||
0x50C4A54Au);
|
||||
RuntimeCommandResult friend = commands.Social.Execute(
|
||||
generation,
|
||||
new RuntimeFriendCommand(
|
||||
RuntimeFriendCommandKind.Add,
|
||||
Name: "Runtime Friend"));
|
||||
RuntimeCommandResult squelch = commands.Social.Execute(
|
||||
generation,
|
||||
new RuntimeSquelchCommand(
|
||||
RuntimeSquelchScope.Global,
|
||||
Add: true,
|
||||
MessageType: 3u));
|
||||
|
||||
Assert.All(
|
||||
new[]
|
||||
{
|
||||
shortcut,
|
||||
favorite,
|
||||
filter,
|
||||
desired,
|
||||
advancement,
|
||||
options,
|
||||
friend,
|
||||
squelch,
|
||||
},
|
||||
static result => Assert.True(result.Accepted));
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is AddShortcutRuntimeCmd);
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is AddFavoriteRuntimeCmd);
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is SetSpellbookFilterRuntimeCmd);
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is SetDesiredComponentRuntimeCmd);
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is RaiseSkillRuntimeCmd);
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is SetCharacterOptionsRuntimeCmd);
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is AddFriendRuntimeCmd);
|
||||
Assert.Contains(
|
||||
harness.Commands.Published,
|
||||
static command => command is ModifyGlobalSquelchRuntimeCmd);
|
||||
Assert.Contains(
|
||||
trace.Entries,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||
&& (entry.Code >> 16)
|
||||
== (int)RuntimeCommandDomain.InventoryState);
|
||||
Assert.Contains(
|
||||
trace.Entries,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||
&& (entry.Code >> 16)
|
||||
== (int)RuntimeCommandDomain.Spellbook);
|
||||
Assert.Contains(
|
||||
trace.Entries,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||
&& (entry.Code >> 16)
|
||||
== (int)RuntimeCommandDomain.Character);
|
||||
Assert.Contains(
|
||||
trace.Entries,
|
||||
static entry => entry.Kind == RuntimeTraceKind.Command
|
||||
&& (entry.Code >> 16)
|
||||
== (int)RuntimeCommandDomain.Social);
|
||||
|
||||
int published = harness.Commands.Published.Count;
|
||||
RuntimeCommandResult stale = commands.Character.SetOptions1(
|
||||
new RuntimeGenerationToken(generation.Value - 1),
|
||||
0u);
|
||||
Assert.Equal(RuntimeCommandStatus.StaleGeneration, stale.Status);
|
||||
Assert.Equal(published, harness.Commands.Published.Count);
|
||||
}
|
||||
|
||||
private static ClientObject Object(WorldSession.EntitySpawn spawn) => new()
|
||||
{
|
||||
ObjectId = spawn.Guid,
|
||||
|
|
@ -407,6 +559,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
{
|
||||
Identity = new LocalPlayerIdentityState();
|
||||
EntityObjects = new RuntimeEntityObjectLifetime();
|
||||
InventoryState = new RuntimeInventoryState(EntityObjects);
|
||||
Character = new RuntimeCharacterState();
|
||||
Entities = new LiveEntityRuntime(
|
||||
new GpuWorldState(),
|
||||
new NoopEntityResources(),
|
||||
|
|
@ -448,7 +602,9 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
|
||||
Options = LiveOptions();
|
||||
Commands = new RecordingCommandRouting();
|
||||
_session = new LiveSessionController(new SessionOperations());
|
||||
Transport = new TestTransport();
|
||||
_session = new LiveSessionController(
|
||||
new SessionOperations(Transport));
|
||||
Host = CreateHost(_session, Commands, Identity);
|
||||
Runtime = new CurrentGameRuntimeAdapter(
|
||||
_session,
|
||||
|
|
@ -457,6 +613,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Identity,
|
||||
Entities,
|
||||
EntityObjects,
|
||||
InventoryState,
|
||||
Character,
|
||||
Communication,
|
||||
new LocalPlayerControllerSlot(),
|
||||
WorldReveal,
|
||||
|
|
@ -470,6 +628,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
public RuntimeOptions Options { get; }
|
||||
public LocalPlayerIdentityState Identity { get; }
|
||||
public RuntimeEntityObjectLifetime EntityObjects { get; }
|
||||
public RuntimeInventoryState InventoryState { get; }
|
||||
public RuntimeCharacterState Character { get; }
|
||||
public LiveEntityRuntime Entities { get; }
|
||||
public ClientObjectTable Objects { get; }
|
||||
public RuntimeCommunicationState Communication { get; }
|
||||
|
|
@ -481,12 +641,15 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
public UpdateFrameClock Clock { get; }
|
||||
public WorldRevealCoordinator WorldReveal { get; }
|
||||
public RecordingCommandRouting Commands { get; }
|
||||
public TestTransport Transport { get; }
|
||||
public LiveSessionHost Host { get; }
|
||||
public CurrentGameRuntimeAdapter Runtime { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Runtime.Dispose();
|
||||
Character.Dispose();
|
||||
InventoryState.Dispose();
|
||||
Communication.Dispose();
|
||||
_session.Dispose();
|
||||
_items.Dispose();
|
||||
|
|
@ -644,13 +807,14 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
Physics: physics);
|
||||
}
|
||||
|
||||
private sealed class SessionOperations : ILiveSessionOperations
|
||||
private sealed class SessionOperations(TestTransport transport)
|
||||
: ILiveSessionOperations
|
||||
{
|
||||
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
||||
new(IPAddress.Loopback, port);
|
||||
|
||||
public WorldSession CreateSession(IPEndPoint endpoint) =>
|
||||
new(endpoint, new TestTransport());
|
||||
new(endpoint, transport);
|
||||
|
||||
public void Connect(WorldSession session, string user, string password)
|
||||
{
|
||||
|
|
@ -686,14 +850,18 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
public void DisposeSession(WorldSession session) => session.Dispose();
|
||||
}
|
||||
|
||||
private sealed class TestTransport : IWorldSessionTransport
|
||||
internal sealed class TestTransport : IWorldSessionTransport
|
||||
{
|
||||
public List<byte[]> Sent { get; } = [];
|
||||
|
||||
public void Send(ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
Sent.Add(datagram.ToArray());
|
||||
}
|
||||
|
||||
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram)
|
||||
{
|
||||
Sent.Add(datagram.ToArray());
|
||||
}
|
||||
|
||||
public int Receive(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue