refactor(runtime): move session lifetime and ordered transport
Move the canonical WorldSession generation, connect/enter/tick/stop transaction, inbound subscription owner, and retryable teardown acknowledgements into AcDream.Runtime. Keep App as a borrowing graphical host with a single inertable command projection and no mirrored session state. Validated by 79 Runtime tests, 3,776 App tests with three existing skips, the Release solution build, and 8,428 complete Release tests with five existing skips. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
ecc4816c5a
commit
7593078774
37 changed files with 884 additions and 355 deletions
|
|
@ -116,7 +116,7 @@ public sealed class SessionPlayerCompositionTests
|
|||
"new PlayerModeController(",
|
||||
"d.PortalTunnelFallback.Transfer(",
|
||||
"d.TeleportSink.BindOwned(localTeleport)",
|
||||
"sessionRuntimeFactory.Create(liveSession)",
|
||||
"LiveSessionHost sessionHost = sessionRuntimeFactory.Create(",
|
||||
"d.CombatModeCommands.BindOwned(combatCommand)",
|
||||
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
|
||||
"GameplayInputActionRouter.Create(",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.Update;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests;
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ internal sealed class TestLiveObjectFramePhase(Action<float> tick)
|
|||
}
|
||||
|
||||
internal sealed class TestLiveSessionFramePhase(Action tick)
|
||||
: ILiveSessionFramePhase
|
||||
: IRuntimeLiveSessionFramePhase
|
||||
{
|
||||
public void Tick() => tick();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ using System.Reflection;
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
|
||||
|
|
@ -30,6 +32,28 @@ public sealed class GameWindowLiveSessionOwnershipTests
|
|||
Assert.DoesNotContain(fields, field => field.Name == "_liveSessionCommands");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalSessionSourceBorrowsCanonicalRuntimeState()
|
||||
{
|
||||
FieldInfo[] fields = typeof(LiveSessionAppSource).GetFields(PrivateInstance);
|
||||
|
||||
Assert.Equal(2, fields.Length);
|
||||
Assert.Contains(
|
||||
fields,
|
||||
field => field.Name == "_session"
|
||||
&& field.FieldType == typeof(LiveSessionController));
|
||||
Assert.Contains(
|
||||
fields,
|
||||
field => field.Name == "_commands"
|
||||
&& field.FieldType == typeof(LiveSessionCommandSurface));
|
||||
Assert.DoesNotContain(fields, field => field.FieldType == typeof(WorldSession));
|
||||
Assert.DoesNotContain(fields, field => field.FieldType == typeof(bool));
|
||||
Assert.DoesNotContain(
|
||||
fields,
|
||||
field => field.FieldType == typeof(RuntimeGenerationToken)
|
||||
|| field.FieldType == typeof(ulong));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("TryStartLiveSession")]
|
||||
[InlineData("ClearInboundEntityState")]
|
||||
|
|
|
|||
|
|
@ -27,6 +27,31 @@ public sealed class LiveSessionCommandRouterTests
|
|||
Assert.False(router.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommandSurfacePublishesToOneBorrowedRouteAndRetiresStaleRoute()
|
||||
{
|
||||
var sent = new List<string>();
|
||||
var surface = new LiveSessionCommandSurface();
|
||||
var first = NewRouter(sendTalk: text => sent.Add($"first:{text}"));
|
||||
var firstLease = surface.Attach(first);
|
||||
firstLease.Activate();
|
||||
|
||||
surface.Publish(new SendServerCommandCmd("@one"));
|
||||
var second = NewRouter(sendTalk: text => sent.Add($"second:{text}"));
|
||||
Assert.Throws<InvalidOperationException>(() => surface.Attach(second));
|
||||
|
||||
firstLease.Dispose();
|
||||
first.Publish(new SendServerCommandCmd("@stale"));
|
||||
var secondLease = surface.Attach(second);
|
||||
secondLease.Activate();
|
||||
surface.Publish(new SendServerCommandCmd("@two"));
|
||||
secondLease.Dispose();
|
||||
|
||||
Assert.Equal(["first:@one", "second:@two"], sent);
|
||||
Assert.False(first.IsActive);
|
||||
Assert.False(second.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TellAndLegacyChannel_PreserveOutboundAndEchoPolicy()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
using System.Net;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
|
||||
public sealed class LiveSessionShutdownIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReentrantShutdownRetainsDependentsUntilRuntimeDisposeCompletes()
|
||||
{
|
||||
var operations = new TestOperations();
|
||||
var host = new TestHost();
|
||||
var controller = new LiveSessionController(operations);
|
||||
controller.Start(
|
||||
new LiveSessionConnectOptions(
|
||||
true,
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
"user",
|
||||
"password"),
|
||||
host);
|
||||
bool dependentDisposed = false;
|
||||
var shutdown = new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage("session lifetime",
|
||||
[
|
||||
new("live session", () =>
|
||||
{
|
||||
controller.Dispose();
|
||||
if (!controller.IsDisposalComplete)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"live-session disposal is still deferred");
|
||||
}
|
||||
}),
|
||||
]),
|
||||
new ResourceShutdownStage("session dependents",
|
||||
[
|
||||
new("streamer", () => dependentDisposed = true),
|
||||
]));
|
||||
operations.OnTick = () =>
|
||||
{
|
||||
Assert.Throws<AggregateException>(shutdown.CompleteOrThrow);
|
||||
Assert.Equal(0, shutdown.CurrentStage);
|
||||
Assert.False(dependentDisposed);
|
||||
};
|
||||
|
||||
controller.Tick();
|
||||
|
||||
Assert.True(controller.IsDisposalComplete);
|
||||
Assert.False(dependentDisposed);
|
||||
shutdown.CompleteOrThrow();
|
||||
Assert.True(shutdown.IsComplete);
|
||||
Assert.True(dependentDisposed);
|
||||
}
|
||||
|
||||
private sealed class TestOperations : ILiveSessionOperations
|
||||
{
|
||||
public Action? OnTick { get; set; }
|
||||
|
||||
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
||||
new(IPAddress.Loopback, port);
|
||||
|
||||
public WorldSession CreateSession(IPEndPoint endpoint) =>
|
||||
new(endpoint, new TestTransport());
|
||||
|
||||
public void Connect(WorldSession session, string user, string password) { }
|
||||
|
||||
public CharacterList.Parsed GetCharacters(WorldSession session) =>
|
||||
new(
|
||||
0u,
|
||||
[new CharacterList.Character(0x50000001u, "Ready", 0u)],
|
||||
[],
|
||||
11,
|
||||
"Runtime",
|
||||
true,
|
||||
true);
|
||||
|
||||
public void EnterWorld(WorldSession session, int activeCharacterIndex) { }
|
||||
public void Tick(WorldSession session) => OnTick?.Invoke();
|
||||
public void DisposeSession(WorldSession session) => session.Dispose();
|
||||
}
|
||||
|
||||
private sealed class TestHost : ILiveSessionLifecycleHost
|
||||
{
|
||||
public LiveSessionBinding BindSession(WorldSession session) =>
|
||||
new(
|
||||
session,
|
||||
activateCommands: static () => { },
|
||||
deactivateCommands: static () => { },
|
||||
detachEvents: static () => { });
|
||||
|
||||
public void ResetSessionState() { }
|
||||
public void ReportConnecting(string host, int port, string user) { }
|
||||
public void ReportConnected() { }
|
||||
public void ApplySelectedCharacter(LiveSessionCharacterSelection selection) { }
|
||||
public void ApplyEnteredWorld(LiveSessionCharacterSelection selection) { }
|
||||
public void DetachSession(WorldSession session) { }
|
||||
}
|
||||
|
||||
private sealed class TestTransport : IWorldSessionTransport
|
||||
{
|
||||
public void Send(ReadOnlySpan<byte> datagram) { }
|
||||
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram) { }
|
||||
|
||||
public int Receive(
|
||||
Span<byte> destination,
|
||||
TimeSpan timeout,
|
||||
out IPEndPoint? from)
|
||||
{
|
||||
from = null;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public ValueTask<NetReceiveResult> ReceiveAsync(
|
||||
Memory<byte> destination,
|
||||
CancellationToken cancellationToken) =>
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ public sealed class DevToolsFramePresenterTests
|
|||
public void CommandBusOwnedBindingReleasesExactlyAndAllowsRebind()
|
||||
{
|
||||
var source = new DevToolsCommandBusSource();
|
||||
using var first = new LiveSessionController();
|
||||
using var second = new LiveSessionController();
|
||||
var first = new TestUiSessionTarget();
|
||||
var second = new TestUiSessionTarget();
|
||||
|
||||
IDisposable firstBinding = source.BindOwned(first);
|
||||
Assert.Same(first.Commands, source.Current);
|
||||
|
|
@ -27,6 +27,13 @@ public sealed class DevToolsFramePresenterTests
|
|||
Assert.Same(second.Commands, source.Current);
|
||||
}
|
||||
|
||||
private sealed class TestUiSessionTarget : ILiveUiSessionTarget
|
||||
{
|
||||
public bool IsInWorld => false;
|
||||
public AcDream.Core.Net.WorldSession? CurrentSession => null;
|
||||
public ICommandBus Commands { get; } = new RecordingCommandBus();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Frame_PreservesBeginThenMenuPanelsAndDrawDataOrder()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ public sealed class GameWindowSlice8BoundaryTests
|
|||
"SessionPlayerComposition.cs"));
|
||||
AssertAppearsInOrder(
|
||||
sessionPhase,
|
||||
"sessionRuntimeFactory.Create(liveSession)",
|
||||
"LiveSessionHost sessionHost = sessionRuntimeFactory.Create(",
|
||||
"d.CombatModeCommands.BindOwned(combatCommand)",
|
||||
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
|
||||
"GameplayInputActionRouter.Create(",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ using AcDream.Core.Physics;
|
|||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Session;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
|
|
@ -227,9 +228,9 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
_session = new LiveSessionController(new SessionOperations());
|
||||
Host = CreateHost(_session, Commands, Identity);
|
||||
Runtime = new CurrentGameRuntimeAdapter(
|
||||
Options,
|
||||
_session,
|
||||
Host,
|
||||
Commands,
|
||||
Identity,
|
||||
Entities,
|
||||
Objects,
|
||||
|
|
@ -313,7 +314,7 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
new LiveSessionRoutingFactories(
|
||||
_ => new NoopEventRouting(),
|
||||
_ => commands),
|
||||
reset,
|
||||
LiveSessionResetManifest.Create(reset).Execute,
|
||||
new LiveSessionSelectionBindings(
|
||||
id => identity.ServerGuid = id,
|
||||
_ => { },
|
||||
|
|
@ -328,7 +329,13 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
_ => { },
|
||||
() => { }),
|
||||
(_, _, _) => { },
|
||||
() => { }));
|
||||
() => { }),
|
||||
new LiveSessionConnectOptions(
|
||||
true,
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
"user",
|
||||
"password"));
|
||||
}
|
||||
|
||||
private static RuntimeOptions LiveOptions()
|
||||
|
|
@ -493,7 +500,8 @@ public sealed class CurrentGameRuntimeAdapterTests
|
|||
}
|
||||
|
||||
internal sealed class RecordingCommandRouting
|
||||
: ILiveSessionCommandRouting
|
||||
: ILiveSessionCommandRouting,
|
||||
ICommandBus
|
||||
{
|
||||
private bool _active;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using System.Net;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
public sealed class LiveSessionControllerTests
|
||||
{
|
||||
|
|
@ -32,7 +31,7 @@ public sealed class LiveSessionControllerTests
|
|||
public void Dispose() { }
|
||||
}
|
||||
|
||||
private sealed class TestCommandBus : ICommandBus
|
||||
private sealed class TestCommandBus
|
||||
{
|
||||
public bool Active { get; set; }
|
||||
public int PublishCount { get; private set; }
|
||||
|
|
@ -169,7 +168,6 @@ public sealed class LiveSessionControllerTests
|
|||
CommandBuses.Add(commands);
|
||||
return new LiveSessionBinding(
|
||||
BindingSessionOverride ?? session,
|
||||
commands,
|
||||
activateCommands: () =>
|
||||
{
|
||||
calls.Add("activate");
|
||||
|
|
@ -298,7 +296,6 @@ public sealed class LiveSessionControllerTests
|
|||
result.Selection);
|
||||
Assert.True(controller.IsInWorld);
|
||||
Assert.Same(operations.Sessions[0], controller.CurrentSession);
|
||||
Assert.Same(host.CommandBuses[0], controller.Commands);
|
||||
Assert.True(host.CommandBuses[0].Active);
|
||||
}
|
||||
|
||||
|
|
@ -319,7 +316,6 @@ public sealed class LiveSessionControllerTests
|
|||
|
||||
Assert.Equal(2, host.ResetCount);
|
||||
Assert.Empty(operations.Sessions);
|
||||
Assert.Equal(NullCommandBus.Instance, controller.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -573,7 +569,6 @@ public sealed class LiveSessionControllerTests
|
|||
Assert.Equal(LiveSessionStartStatus.Deferred, result.Status);
|
||||
Assert.False(controller.IsInWorld);
|
||||
Assert.Null(controller.CurrentSession);
|
||||
Assert.Same(NullCommandBus.Instance, controller.Commands);
|
||||
if (operations.Sessions.Count == 0)
|
||||
{
|
||||
Assert.Contains(
|
||||
|
|
@ -695,7 +690,6 @@ public sealed class LiveSessionControllerTests
|
|||
Assert.False(controller.IsInWorld);
|
||||
Assert.Null(controller.CurrentSession);
|
||||
Assert.Equal(1, operations.DisposeCounts[operations.Sessions[0]]);
|
||||
Assert.Same(NullCommandBus.Instance, controller.Commands);
|
||||
controller.Dispose();
|
||||
}
|
||||
|
||||
|
|
@ -843,69 +837,72 @@ public sealed class LiveSessionControllerTests
|
|||
Assert.Equal(1, operations.DisposeCounts[session]);
|
||||
Assert.Equal(0, commands.PublishCount);
|
||||
Assert.False(controller.IsInWorld);
|
||||
Assert.Same(NullCommandBus.Instance, controller.Commands);
|
||||
Assert.Throws<ObjectDisposedException>(() => controller.Start(LiveOptions(), host));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantShutdownRetainsControllerAndDependentsUntilDeferredDisposeCompletes()
|
||||
public void GenerationScopedStopAcknowledgesTheCompleteTeardownTransaction()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var operations = new TestOperations(calls);
|
||||
var host = new TestHost(calls);
|
||||
var controller = new LiveSessionController(operations);
|
||||
controller.Start(LiveOptions(), host);
|
||||
bool dependentDisposed = false;
|
||||
var shutdown = new ResourceShutdownTransaction(
|
||||
new ResourceShutdownStage("session lifetime",
|
||||
[
|
||||
new("live session", () =>
|
||||
{
|
||||
controller.Dispose();
|
||||
if (!controller.IsDisposalComplete)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"live-session disposal is still deferred");
|
||||
}
|
||||
}),
|
||||
]),
|
||||
new ResourceShutdownStage("session dependents",
|
||||
[
|
||||
new("streamer", () => dependentDisposed = true),
|
||||
]));
|
||||
operations.OnTick = () =>
|
||||
{
|
||||
Assert.Throws<AggregateException>(shutdown.CompleteOrThrow);
|
||||
Assert.Equal(0, shutdown.CurrentStage);
|
||||
Assert.False(dependentDisposed);
|
||||
};
|
||||
RuntimeGenerationToken retired = controller.Generation;
|
||||
|
||||
controller.Tick();
|
||||
RuntimeTeardownAcknowledgement acknowledgement =
|
||||
controller.Stop(retired);
|
||||
|
||||
Assert.True(controller.IsDisposalComplete);
|
||||
Assert.False(dependentDisposed);
|
||||
shutdown.CompleteOrThrow();
|
||||
Assert.True(shutdown.IsComplete);
|
||||
Assert.True(dependentDisposed);
|
||||
Assert.True(acknowledgement.IsComplete);
|
||||
Assert.Equal(retired, acknowledgement.RetiredGeneration);
|
||||
Assert.Equal(controller.Generation, acknowledgement.CurrentGeneration);
|
||||
Assert.Equal(
|
||||
RuntimeTeardownStage.Complete,
|
||||
acknowledgement.CompletedStages);
|
||||
}
|
||||
|
||||
private static RuntimeOptions LiveOptions(
|
||||
bool live = true,
|
||||
string? user = "user")
|
||||
[Fact]
|
||||
public void FailedStopAcknowledgesOnlyCompletedPrefixAndRetryDrainsSuffix()
|
||||
{
|
||||
var environment = new Dictionary<string, string?>
|
||||
var calls = new List<string>();
|
||||
var operations = new TestOperations(calls);
|
||||
var host = new TestHost(calls)
|
||||
{
|
||||
["ACDREAM_LIVE"] = live ? "1" : "0",
|
||||
["ACDREAM_TEST_HOST"] = "127.0.0.1",
|
||||
["ACDREAM_TEST_PORT"] = "9000",
|
||||
["ACDREAM_TEST_USER"] = user,
|
||||
["ACDREAM_TEST_PASS"] = "password",
|
||||
FailEventDetachOnce = true,
|
||||
};
|
||||
return RuntimeOptions.Parse(
|
||||
"dat",
|
||||
name => environment.GetValueOrDefault(name));
|
||||
var controller = new LiveSessionController(operations);
|
||||
controller.Start(LiveOptions(), host);
|
||||
|
||||
RuntimeTeardownAcknowledgement failed =
|
||||
controller.Stop(controller.Generation);
|
||||
|
||||
Assert.Equal(RuntimeCommandStatus.Rejected, failed.Status);
|
||||
Assert.True(
|
||||
(failed.CompletedStages & RuntimeTeardownStage.CommandsInert) != 0);
|
||||
Assert.True(
|
||||
(failed.CompletedStages & RuntimeTeardownStage.InboundDetached) == 0);
|
||||
Assert.True(
|
||||
(failed.CompletedStages & RuntimeTeardownStage.TransportDisposed) == 0);
|
||||
|
||||
RuntimeTeardownAcknowledgement retry =
|
||||
controller.Stop(failed.CurrentGeneration);
|
||||
|
||||
Assert.True(retry.IsComplete);
|
||||
Assert.Equal(1, host.DeactivateCount);
|
||||
Assert.Equal(2, host.EventDetachCount);
|
||||
Assert.Single(operations.DisposeCounts);
|
||||
}
|
||||
|
||||
private static LiveSessionConnectOptions LiveOptions(
|
||||
bool live = true,
|
||||
string? user = "user") =>
|
||||
new(
|
||||
live,
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
user ?? string.Empty,
|
||||
"password");
|
||||
|
||||
private static CharacterList.Parsed AvailableCharacters() => new(
|
||||
0u,
|
||||
[
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using System.Net;
|
||||
using System.Reflection;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -8,8 +7,9 @@ using AcDream.Core.Net;
|
|||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
public sealed class LiveSessionEventRouterTests
|
||||
{
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
using System.Net;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
public sealed class LiveSessionHostTests
|
||||
{
|
||||
|
|
@ -45,7 +44,6 @@ public sealed class LiveSessionHostTests
|
|||
],
|
||||
calls);
|
||||
Assert.Same(controller.CurrentSession, host.CurrentSession);
|
||||
Assert.Same(commands, host.Commands);
|
||||
Assert.True(host.IsInWorld);
|
||||
|
||||
controller.Dispose();
|
||||
|
|
@ -223,7 +221,7 @@ public sealed class LiveSessionHostTests
|
|||
Func<WorldSession, ILiveSessionCommandRouting> createCommands) =>
|
||||
new(controller, new LiveSessionHostBindings(
|
||||
Routing: new(createEvents, createCommands),
|
||||
Reset: ResetBindings(() => calls.Add("reset")),
|
||||
Reset: () => calls.Add("reset"),
|
||||
Selection: new(
|
||||
id => calls.Add($"player:{id}"),
|
||||
id => calls.Add($"vitals:{id}"),
|
||||
|
|
@ -240,57 +238,15 @@ public sealed class LiveSessionHostTests
|
|||
Connecting: (_, _, _) => calls.Add("connecting"),
|
||||
Connected: () => calls.Add("connected")));
|
||||
|
||||
private static LiveSessionResetBindings ResetBindings(Action reset)
|
||||
{
|
||||
Action noop = static () => { };
|
||||
return new LiveSessionResetBindings
|
||||
{
|
||||
MouseCapture = reset,
|
||||
PlayerPresentation = noop,
|
||||
TeleportTransit = noop,
|
||||
SessionDialogs = noop,
|
||||
ChatCommandTargets = noop,
|
||||
SettingsCharacterContext = noop,
|
||||
EquippedChildren = noop,
|
||||
ExternalContainer = noop,
|
||||
InteractionAndSelection = noop,
|
||||
SelectionPresentation = noop,
|
||||
ObjectTable = noop,
|
||||
Spellbook = noop,
|
||||
MagicRuntime = noop,
|
||||
CombatAttack = noop,
|
||||
CombatState = noop,
|
||||
ItemMana = noop,
|
||||
LocalPlayer = noop,
|
||||
Friends = noop,
|
||||
Squelch = noop,
|
||||
TurbineChat = noop,
|
||||
ParticleVisibility = noop,
|
||||
InboundEventFifo = noop,
|
||||
LiveLiveness = noop,
|
||||
LiveRuntime = noop,
|
||||
RenderSceneProjection = noop,
|
||||
SessionIdentity = noop,
|
||||
RemoteTeleport = noop,
|
||||
NetworkEffects = noop,
|
||||
AnimationHookFrames = noop,
|
||||
LivePresentation = noop,
|
||||
RemoteMovementDiagnostics = noop,
|
||||
};
|
||||
}
|
||||
|
||||
private static RuntimeOptions LiveOptions(bool live = true, string? user = "user")
|
||||
{
|
||||
var environment = new Dictionary<string, string?>
|
||||
{
|
||||
["ACDREAM_LIVE"] = live ? "1" : "0",
|
||||
["ACDREAM_TEST_HOST"] = "127.0.0.1",
|
||||
["ACDREAM_TEST_PORT"] = "9000",
|
||||
["ACDREAM_TEST_USER"] = user,
|
||||
["ACDREAM_TEST_PASS"] = "password",
|
||||
};
|
||||
return RuntimeOptions.Parse("dat", environment.GetValueOrDefault);
|
||||
}
|
||||
private static LiveSessionConnectOptions LiveOptions(
|
||||
bool live = true,
|
||||
string? user = "user") =>
|
||||
new(
|
||||
live,
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
user ?? string.Empty,
|
||||
"password");
|
||||
|
||||
private sealed class TestEventRouting(List<string> calls)
|
||||
: ILiveSessionEventRouting
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
using System.Net;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
public sealed class LiveSessionLifecycleHostTests
|
||||
{
|
||||
|
|
@ -82,7 +81,6 @@ public sealed class LiveSessionLifecycleHostTests
|
|||
calls.Add("bind");
|
||||
return new LiveSessionBinding(
|
||||
session,
|
||||
NullCommandBus.Instance,
|
||||
activateCommands: () => calls.Add("activate"),
|
||||
deactivateCommands: () => calls.Add("deactivate"),
|
||||
detachEvents: () => calls.Add("detach-events"));
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using AcDream.App.Net;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
public sealed class LiveSessionSubscriptionSetTests
|
||||
{
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
using System.Net;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
public sealed class RuntimeLiveSessionNoWindowTests
|
||||
{
|
||||
[Fact]
|
||||
public void RuntimeOnlyHostEntersAndTearsDownWithoutPresentationAssemblies()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var operations = new TestOperations(calls);
|
||||
using var controller = new LiveSessionController(operations);
|
||||
var host = new LiveSessionHost(
|
||||
controller,
|
||||
new LiveSessionHostBindings(
|
||||
new LiveSessionRoutingFactories(
|
||||
_ => new EventRoute(calls),
|
||||
_ => new CommandRoute(calls)),
|
||||
() => calls.Add("reset"),
|
||||
new LiveSessionSelectionBindings(
|
||||
id => calls.Add($"player:{id}"),
|
||||
_ => { },
|
||||
_ => { },
|
||||
_ => { },
|
||||
_ => { },
|
||||
() => { }),
|
||||
new LiveSessionEnteredWorldBindings(
|
||||
name => calls.Add($"entered:{name}"),
|
||||
() => { },
|
||||
() => { },
|
||||
_ => { },
|
||||
() => { }),
|
||||
(_, _, _) => calls.Add("connecting"),
|
||||
() => calls.Add("connected")),
|
||||
new LiveSessionConnectOptions(
|
||||
true,
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
"test",
|
||||
"password"));
|
||||
|
||||
RuntimeSessionStartResult start =
|
||||
host.Start(RuntimeGenerationToken.Initial);
|
||||
RuntimeTeardownAcknowledgement stop = host.Stop(start.Generation);
|
||||
|
||||
Assert.Equal(RuntimeSessionStartStatus.Connected, start.Status);
|
||||
Assert.Equal(0x50000001u, start.CharacterId);
|
||||
Assert.True(stop.IsComplete);
|
||||
Assert.Null(controller.CurrentSession);
|
||||
Assert.False(controller.IsInWorld);
|
||||
Assert.Equal(
|
||||
[
|
||||
"reset",
|
||||
"resolve",
|
||||
"create",
|
||||
"attach-events",
|
||||
"connecting",
|
||||
"connect",
|
||||
"connected",
|
||||
"characters",
|
||||
"player:1342177281",
|
||||
"enter:0",
|
||||
"activate-commands",
|
||||
"entered:Runtime",
|
||||
"deactivate-commands",
|
||||
"detach-events",
|
||||
"dispose-session",
|
||||
"reset",
|
||||
],
|
||||
calls);
|
||||
|
||||
string[] loaded = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(static assembly => assembly.GetName().Name ?? string.Empty)
|
||||
.ToArray();
|
||||
Assert.DoesNotContain(loaded, static name =>
|
||||
name.StartsWith("AcDream.App", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith("AcDream.UI.", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith("Silk.NET", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith("OpenAL", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith("Arch", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.StartsWith("ImGui", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private sealed class TestOperations(List<string> calls)
|
||||
: ILiveSessionOperations
|
||||
{
|
||||
public IPEndPoint ResolveEndpoint(string host, int port)
|
||||
{
|
||||
calls.Add("resolve");
|
||||
return new IPEndPoint(IPAddress.Loopback, port);
|
||||
}
|
||||
|
||||
public WorldSession CreateSession(IPEndPoint endpoint)
|
||||
{
|
||||
calls.Add("create");
|
||||
return new WorldSession(endpoint, new TestTransport());
|
||||
}
|
||||
|
||||
public void Connect(WorldSession session, string user, string password) =>
|
||||
calls.Add("connect");
|
||||
|
||||
public CharacterList.Parsed GetCharacters(WorldSession session)
|
||||
{
|
||||
calls.Add("characters");
|
||||
return new CharacterList.Parsed(
|
||||
0u,
|
||||
[new CharacterList.Character(0x50000001u, "Runtime", 0u)],
|
||||
[],
|
||||
11,
|
||||
"NoWindow",
|
||||
true,
|
||||
true);
|
||||
}
|
||||
|
||||
public void EnterWorld(WorldSession session, int activeCharacterIndex) =>
|
||||
calls.Add($"enter:{activeCharacterIndex}");
|
||||
|
||||
public void Tick(WorldSession session) { }
|
||||
|
||||
public void DisposeSession(WorldSession session)
|
||||
{
|
||||
calls.Add("dispose-session");
|
||||
session.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class EventRoute(List<string> calls)
|
||||
: ILiveSessionEventRouting
|
||||
{
|
||||
public void Attach() => calls.Add("attach-events");
|
||||
public void Dispose() => calls.Add("detach-events");
|
||||
}
|
||||
|
||||
private sealed class CommandRoute(List<string> calls)
|
||||
: ILiveSessionCommandRouting
|
||||
{
|
||||
public void Activate() => calls.Add("activate-commands");
|
||||
public void Dispose() => calls.Add("deactivate-commands");
|
||||
}
|
||||
|
||||
private sealed class TestTransport : IWorldSessionTransport
|
||||
{
|
||||
public void Send(ReadOnlySpan<byte> datagram) { }
|
||||
public void Send(IPEndPoint remote, ReadOnlySpan<byte> datagram) { }
|
||||
|
||||
public int Receive(
|
||||
Span<byte> destination,
|
||||
TimeSpan timeout,
|
||||
out IPEndPoint? from)
|
||||
{
|
||||
from = null;
|
||||
return -1;
|
||||
}
|
||||
|
||||
public ValueTask<NetReceiveResult> ReceiveAsync(
|
||||
Memory<byte> destination,
|
||||
CancellationToken cancellationToken) =>
|
||||
throw new OperationCanceledException(cancellationToken);
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue