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
|
|
@ -199,7 +199,6 @@ public sealed class InteractionRetainedUiCompositionTests
|
|||
PlayerIdentity: null!,
|
||||
PlayerController: null!,
|
||||
PlayerMode: null!,
|
||||
CharacterOptions: null!,
|
||||
SelectionCameraFactory: static _ => Stub<SelectionCameraSource>(),
|
||||
FrameDiagnostics: new DeferredRenderFrameDiagnosticsSource(),
|
||||
ExistingVitals: null,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -193,6 +193,11 @@ public sealed class GameplayInputCommandControllerTests
|
|||
public IGameRuntimeClock Clock => throw new NotSupportedException();
|
||||
public IRuntimeEntityView Entities => throw new NotSupportedException();
|
||||
public IRuntimeInventoryView Inventory => throw new NotSupportedException();
|
||||
public IRuntimeInventoryStateView InventoryState =>
|
||||
throw new NotSupportedException();
|
||||
public IRuntimeCharacterView Character =>
|
||||
throw new NotSupportedException();
|
||||
public IRuntimeSocialView Social => throw new NotSupportedException();
|
||||
public IRuntimeChatView Chat => throw new NotSupportedException();
|
||||
public IRuntimeMovementView Movement => throw new NotSupportedException();
|
||||
public IRuntimePortalView Portal => throw new NotSupportedException();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Runtime.CompilerServices;
|
|||
using AcDream.App.Net;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
||||
|
|
@ -220,6 +221,35 @@ public sealed class LiveSessionCommandRouterTests
|
|||
GC.KeepAlive(router);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimeStateCommandsUseActiveGenerationRouteAndBecomeInert()
|
||||
{
|
||||
var shortcuts = new List<ShortcutEntry>();
|
||||
var options = new List<uint>();
|
||||
LiveSessionCommandRouter router = NewRouter(
|
||||
addShortcut: shortcuts.Add,
|
||||
setCharacterOptions: options.Add);
|
||||
|
||||
router.Publish(new AddShortcutRuntimeCmd(
|
||||
new ShortcutEntry(1, 0x80000001u, 0u)));
|
||||
router.Activate();
|
||||
router.Publish(new AddShortcutRuntimeCmd(
|
||||
new ShortcutEntry(2, 0x80000002u, 0u)));
|
||||
router.Publish(new SetCharacterOptionsRuntimeCmd(0x50C4A54Au));
|
||||
router.Dispose();
|
||||
router.Publish(new AddShortcutRuntimeCmd(
|
||||
new ShortcutEntry(3, 0x80000003u, 0u)));
|
||||
|
||||
Assert.Collection(
|
||||
shortcuts,
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal(2, entry.Index);
|
||||
Assert.Equal(0x80000002u, entry.ObjectId);
|
||||
});
|
||||
Assert.Equal([0x50C4A54Au], options);
|
||||
}
|
||||
|
||||
private static LiveSessionCommandRouter NewRouter(
|
||||
ChatLog? chat = null,
|
||||
TurbineChatState? turbine = null,
|
||||
|
|
@ -228,6 +258,8 @@ public sealed class LiveSessionCommandRouterTests
|
|||
Action<string, string>? sendTell = null,
|
||||
Action<uint, string>? sendChannel = null,
|
||||
Action<uint, uint, uint, uint, string, uint>? sendTurbine = null,
|
||||
Action<ShortcutEntry>? addShortcut = null,
|
||||
Action<uint>? setCharacterOptions = null,
|
||||
Action<string>? log = null,
|
||||
ClientCommandController.Bindings? clientBindings = null) => new(
|
||||
new LiveSessionCommandBindings(
|
||||
|
|
@ -239,7 +271,27 @@ public sealed class LiveSessionCommandRouterTests
|
|||
sendTell ?? ((_, _) => { }),
|
||||
sendChannel ?? ((_, _) => { }),
|
||||
sendTurbine ?? ((_, _, _, _, _, _) => { }),
|
||||
log));
|
||||
AddShortcut: addShortcut ?? (_ => { }),
|
||||
RemoveShortcut: _ => { },
|
||||
AddFavorite: (_, _, _) => { },
|
||||
RemoveFavorite: (_, _) => { },
|
||||
SetSpellbookFilter: _ => { },
|
||||
ForgetSpell: _ => { },
|
||||
SetDesiredComponent: (_, _) => { },
|
||||
ClearDesiredComponents: () => { },
|
||||
RaiseAttribute: (_, _) => { },
|
||||
RaiseVital: (_, _) => { },
|
||||
RaiseSkill: (_, _) => { },
|
||||
TrainSkill: (_, _) => { },
|
||||
SetCharacterOptions: setCharacterOptions ?? (_ => { }),
|
||||
AddFriend: _ => { },
|
||||
RemoveFriend: _ => { },
|
||||
ClearFriends: () => { },
|
||||
RequestLegacyFriends: () => { },
|
||||
ModifyCharacterSquelch: (_, _, _, _) => { },
|
||||
ModifyAccountSquelch: (_, _) => { },
|
||||
ModifyGlobalSquelch: (_, _) => { },
|
||||
Log: log));
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static (LiveSessionCommandRouter, WeakReference<object>)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -81,6 +81,37 @@ public sealed class RuntimeCharacterOwnershipTests
|
|||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppCharacterOptionAndMovementSkillOwnersWereDeleted()
|
||||
{
|
||||
string gameWindow = ReadSource("Rendering", "GameWindow.cs");
|
||||
string sources = ReadSource(
|
||||
"Composition",
|
||||
"InteractionUiRuntimeSources.cs");
|
||||
string playerState = ReadSource("Input", "LocalPlayerRuntimeState.cs");
|
||||
string session = ReadSource("Net", "LiveSessionRuntimeFactory.cs");
|
||||
string router = ReadRuntimeSource(
|
||||
"Session",
|
||||
"LiveSessionEventRouter.cs");
|
||||
|
||||
Assert.DoesNotContain(
|
||||
"PlayerCharacterOptionsState",
|
||||
gameWindow + sources + session,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"LocalPlayerSkillState",
|
||||
gameWindow + playerState + session,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"character.Character.Options.Replace",
|
||||
router,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"character.Character.MovementSkills.Update",
|
||||
router,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string ReadSource(params string[] relative) =>
|
||||
File.ReadAllText(Path.Combine(
|
||||
[FindRepositoryRoot(), "src", "AcDream.App", .. relative]));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
using System.Reflection;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests;
|
||||
|
||||
public sealed class GameRuntimeContractTests
|
||||
|
|
@ -137,4 +140,68 @@ public sealed class GameRuntimeContractTests
|
|||
Assert.Equal("General|Grey|hello", entry.Text);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TraceRecorderIncludesJ4GameplayStateInCheckpoint()
|
||||
{
|
||||
var recorder = new RuntimeTraceRecorder();
|
||||
var stamp = new RuntimeEventStamp(new(3), 1, 8);
|
||||
var checkpoint = new RuntimeStateCheckpoint(
|
||||
new RuntimeGenerationToken(3),
|
||||
RuntimeLifecycleState.InWorld,
|
||||
FrameNumber: 8,
|
||||
EntityCount: 2,
|
||||
MaterializedEntityCount: 2,
|
||||
InventoryObjectCount: 1,
|
||||
InventoryContainerCount: 1,
|
||||
new RuntimeInventoryStateSnapshot(
|
||||
0u, 0u, 0, true, null, 2, 4, 1, 3),
|
||||
new RuntimeCharacterSnapshot(
|
||||
CharacterRevision: 5,
|
||||
SpellbookRevision: 6,
|
||||
default,
|
||||
default,
|
||||
LearnedSpellCount: 7,
|
||||
ActiveEnchantmentCount: 0,
|
||||
DesiredComponentCount: 0,
|
||||
SkillCount: 8,
|
||||
SpellbookFilters: 0x3FFFu),
|
||||
new RuntimeSocialSnapshot(9, 10, 11, 0, 0, 0, 0),
|
||||
ChatRevision: 12,
|
||||
ChatCount: 13,
|
||||
default,
|
||||
default);
|
||||
|
||||
recorder.AddCheckpoint(stamp, checkpoint);
|
||||
|
||||
RuntimeTraceEntry entry = Assert.Single(recorder.Entries);
|
||||
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
|
||||
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
|
||||
Assert.Contains("social=9:10:11", entry.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void J4GameplayOwnersHaveNoStaticMutableSessionState()
|
||||
{
|
||||
Type[] owners =
|
||||
[
|
||||
typeof(RuntimeCommunicationState),
|
||||
typeof(RuntimeInventoryState),
|
||||
typeof(RuntimeCharacterState),
|
||||
typeof(RuntimeCharacterOptionsState),
|
||||
typeof(RuntimeMovementSkillState),
|
||||
];
|
||||
|
||||
foreach (Type owner in owners)
|
||||
{
|
||||
FieldInfo[] mutable = owner
|
||||
.GetFields(
|
||||
BindingFlags.Static
|
||||
| BindingFlags.Public
|
||||
| BindingFlags.NonPublic)
|
||||
.Where(static field => !field.IsLiteral)
|
||||
.ToArray();
|
||||
Assert.Empty(mutable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
|
@ -152,4 +153,105 @@ public sealed class RuntimeCharacterStateTests
|
|||
|
||||
Assert.True(state.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnsRetailCharacterOptionsAndMovementSkillProjection()
|
||||
{
|
||||
using var state = new RuntimeCharacterState();
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions1,
|
||||
state.Options.Options1);
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions2,
|
||||
state.Options.Options2);
|
||||
Assert.False(state.MovementSkills.IsComplete);
|
||||
|
||||
state.Options.Replace(0x04000000u, 0x12345678u);
|
||||
state.MovementSkills.Update(runSkill: 210, jumpSkill: -1);
|
||||
state.MovementSkills.Update(runSkill: -1, jumpSkill: 165);
|
||||
|
||||
Assert.True(state.Options.DragItemOnPlayerOpensSecureTrade);
|
||||
Assert.Equal(0x12345678u, state.Options.Options2);
|
||||
Assert.Equal(
|
||||
new RuntimeMovementSkillSnapshot(
|
||||
210,
|
||||
165,
|
||||
state.MovementSkills.Revision),
|
||||
state.MovementSkills.Snapshot);
|
||||
Assert.True(state.MovementSkills.IsComplete);
|
||||
|
||||
state.ResetSession();
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions1,
|
||||
state.Options.Options1);
|
||||
Assert.Equal(
|
||||
RuntimeCharacterOptionsState.DefaultOptions2,
|
||||
state.Options.Options2);
|
||||
Assert.Equal(-1, state.MovementSkills.RunSkill);
|
||||
Assert.Equal(-1, state.MovementSkills.JumpSkill);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CharacterViewBorrowsExactOwnersWithoutReconstructedState()
|
||||
{
|
||||
using var state = new RuntimeCharacterState();
|
||||
state.LocalPlayer.OnAttributeUpdate(1u, 40u, 10u, 500u);
|
||||
state.LocalPlayer.OnVitalUpdate(7u, 60u, 20u, 700u, 75u);
|
||||
state.LocalPlayer.OnSkillUpdate(
|
||||
6u,
|
||||
30u,
|
||||
2u,
|
||||
800u,
|
||||
10u,
|
||||
0u,
|
||||
5d,
|
||||
12u);
|
||||
state.Spellbook.OnSpellLearned(42u);
|
||||
state.Spellbook.SetFavorite(0, 0, 42u);
|
||||
state.Spellbook.SetDesiredComponent(0x68000001u, 11u);
|
||||
|
||||
RuntimeCharacterSnapshot summary = state.View.Snapshot;
|
||||
|
||||
Assert.Equal(1, summary.LearnedSpellCount);
|
||||
Assert.Equal(1, summary.DesiredComponentCount);
|
||||
Assert.Equal(1, summary.SkillCount);
|
||||
Assert.True(state.View.KnowsSpell(42u));
|
||||
Assert.True(state.View.TryGetFavorite(0, 0, out uint favorite));
|
||||
Assert.Equal(42u, favorite);
|
||||
Assert.True(state.View.TryGetDesiredComponent(
|
||||
0x68000001u,
|
||||
out uint desired));
|
||||
Assert.Equal(11u, desired);
|
||||
Assert.True(state.View.TryGetAttribute(
|
||||
(int)LocalPlayerState.AttributeKind.Strength,
|
||||
out RuntimeAttributeSnapshot attribute));
|
||||
Assert.Equal(50u, attribute.Current);
|
||||
Assert.True(state.View.TryGetVital(
|
||||
(int)LocalPlayerState.VitalKind.Health,
|
||||
out RuntimeVitalSnapshot vital));
|
||||
Assert.Equal(75u, vital.Current);
|
||||
Assert.True(state.View.TryGetSkill(6u, out RuntimeSkillSnapshot skill));
|
||||
Assert.Equal(52u, skill.CurrentLevel);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TwoRuntimeInstancesIsolateOptionsSkillsAndViewRevisions()
|
||||
{
|
||||
using var first = new RuntimeCharacterState();
|
||||
using var second = new RuntimeCharacterState();
|
||||
|
||||
first.Options.Replace(1u, 2u);
|
||||
first.MovementSkills.Update(100, 200);
|
||||
first.Spellbook.OnSpellLearned(9u);
|
||||
|
||||
Assert.NotEqual(
|
||||
first.View.Snapshot.Options,
|
||||
second.View.Snapshot.Options);
|
||||
Assert.True(first.View.Snapshot.MovementSkills.IsComplete);
|
||||
Assert.False(second.View.Snapshot.MovementSkills.IsComplete);
|
||||
Assert.True(first.View.Snapshot.SpellbookRevision > 0);
|
||||
Assert.Equal(0, second.View.Snapshot.SpellbookRevision);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,44 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
|||
|
||||
public sealed class RuntimeCommunicationStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void SocialViewBorrowsFriendsAndSquelchOwners()
|
||||
{
|
||||
using var state = new RuntimeCommunicationState();
|
||||
state.Friends.Apply(new AcDream.Core.Social.FriendsUpdate(
|
||||
AcDream.Core.Social.FriendsUpdateType.Full,
|
||||
[
|
||||
new AcDream.Core.Social.FriendEntry(
|
||||
0x50000001u,
|
||||
"Friend",
|
||||
Online: true,
|
||||
AppearOffline: false,
|
||||
Friends: [],
|
||||
FriendOf: []),
|
||||
]));
|
||||
state.Squelch.Replace(new AcDream.Core.Social.SquelchDatabase(
|
||||
new Dictionary<string, uint>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["account"] = 1u,
|
||||
},
|
||||
new Dictionary<uint, AcDream.Core.Social.SquelchInfo>(),
|
||||
new AcDream.Core.Social.SquelchInfo(
|
||||
string.Empty,
|
||||
false,
|
||||
new HashSet<uint> { 3u })));
|
||||
|
||||
RuntimeSocialSnapshot snapshot = state.SocialView.Snapshot;
|
||||
|
||||
Assert.Equal(1, snapshot.FriendCount);
|
||||
Assert.Equal(1, snapshot.SquelchedAccountCount);
|
||||
Assert.Equal(1, snapshot.GlobalSquelchTypeCount);
|
||||
Assert.True(state.SocialView.TryGetFriend(
|
||||
0x50000001u,
|
||||
out RuntimeFriendSnapshot friend));
|
||||
Assert.Equal("Friend", friend.Name);
|
||||
Assert.True(friend.Online);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnsOneExactCommunicationGraphAndPublishesCommittedEntries()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,44 @@ namespace AcDream.Runtime.Tests.Gameplay;
|
|||
|
||||
public sealed class RuntimeInventoryStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void ViewBorrowsTransactionShortcutAndManaOwners()
|
||||
{
|
||||
using var entities = new RuntimeEntityObjectLifetime();
|
||||
using var inventory = new RuntimeInventoryState(entities);
|
||||
inventory.Shortcuts.Replace(
|
||||
[
|
||||
new AcDream.Core.Items.ShortcutEntry(3, 0x80000001u, 0u),
|
||||
new AcDream.Core.Items.ShortcutEntry(4, 0u, 42u),
|
||||
]);
|
||||
inventory.ItemMana.OnQueryItemManaResponse(
|
||||
0x80000001u,
|
||||
0.75f,
|
||||
valid: true);
|
||||
inventory.Transactions.IncrementBusyCount();
|
||||
|
||||
RuntimeInventoryStateSnapshot snapshot = inventory.View.Snapshot;
|
||||
|
||||
Assert.Equal(2, snapshot.ShortcutCount);
|
||||
Assert.Equal(1, snapshot.ItemManaCount);
|
||||
Assert.Equal(1, snapshot.BusyCount);
|
||||
Assert.False(snapshot.CanBeginRequest);
|
||||
Assert.True(inventory.View.TryGetShortcut(
|
||||
3,
|
||||
out RuntimeShortcutSnapshot itemShortcut));
|
||||
Assert.Equal(
|
||||
new RuntimeShortcutSnapshot(3, 0x80000001u, 0u),
|
||||
itemShortcut);
|
||||
Assert.True(inventory.View.TryGetShortcut(
|
||||
4,
|
||||
out RuntimeShortcutSnapshot spellShortcut));
|
||||
Assert.Equal(new RuntimeShortcutSnapshot(4, 0u, 42u), spellShortcut);
|
||||
Assert.True(inventory.View.TryGetItemMana(
|
||||
itemShortcut.ObjectId,
|
||||
out float fraction));
|
||||
Assert.Equal(0.75f, fraction);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OwnsOneGameplayGraphOverExactEntityObjectTable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -242,7 +242,6 @@ public sealed class LiveSessionEventRouterTests
|
|||
OnSkillsUpdated: null,
|
||||
OnConfirmationRequest: null,
|
||||
OnConfirmationDone: null,
|
||||
OnCharacterOptions: null,
|
||||
ClientTime: () => 0d);
|
||||
|
||||
private static LiveSocialSessionBindings NewSocialBindings(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue