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]));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue