feat(headless): complete portable single-session host
This commit is contained in:
parent
fbebb91848
commit
f8cb840fb1
30 changed files with 3571 additions and 32 deletions
|
|
@ -0,0 +1,296 @@
|
|||
using System.Net;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Session;
|
||||
|
||||
public sealed class DirectGameRuntimeCommandAdapterTests
|
||||
{
|
||||
[Fact]
|
||||
public void DirectRouteSendsTypedChatAndPortalAndRejectsOldGeneration()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
var gameplay = new FixtureGameplayOperations();
|
||||
using var runtime = new GameRuntime(new GameRuntimeDependencies(
|
||||
gameplay,
|
||||
gameplay,
|
||||
gameplay,
|
||||
gameplay,
|
||||
SessionOperations: operations));
|
||||
gameplay.Bind(runtime);
|
||||
var resetHost = new FixtureResetHost();
|
||||
DirectGameRuntimeCommandAdapter? adapter = null;
|
||||
LiveSessionConnectOptions options = new(
|
||||
true,
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
"account",
|
||||
"password");
|
||||
var live = new LiveSessionHost(
|
||||
runtime.Session,
|
||||
new LiveSessionHostBindings(
|
||||
new LiveSessionRoutingFactories(
|
||||
_ => new FixtureEventRoute(),
|
||||
session => adapter!.CreateRoute(session)),
|
||||
generation => runtime.ResetGeneration(
|
||||
generation,
|
||||
resetHost),
|
||||
new LiveSessionSelectionBindings(
|
||||
id => runtime.PlayerIdentity.ServerGuid = id,
|
||||
_ => { },
|
||||
runtime.CommunicationOwner.Chat.SetLocalPlayerGuid,
|
||||
_ => { },
|
||||
_ => { },
|
||||
runtime.ActionOwner.Combat.Clear),
|
||||
new LiveSessionEnteredWorldBindings(
|
||||
_ => { },
|
||||
() => { },
|
||||
() => { },
|
||||
_ => { },
|
||||
() => { }),
|
||||
(_, _, _) => { },
|
||||
() => { }),
|
||||
options);
|
||||
adapter = new DirectGameRuntimeCommandAdapter(runtime, live);
|
||||
var trace = new RuntimeTraceRecorder();
|
||||
using IDisposable subscription = runtime.Subscribe(trace);
|
||||
|
||||
RuntimeSessionStartResult started =
|
||||
adapter.Session.Start(runtime.Generation);
|
||||
RuntimeGenerationToken firstGeneration = runtime.Generation;
|
||||
var gameActions = new List<byte[]>();
|
||||
operations.Sessions[^1].GameActionCapture =
|
||||
body => gameActions.Add(body);
|
||||
|
||||
RuntimeCommandResult chat = adapter.Chat.Execute(
|
||||
runtime.Generation,
|
||||
new RuntimeChatCommand(
|
||||
RuntimeChatChannel.Say,
|
||||
"hello"));
|
||||
RuntimeCommandResult portal = adapter.Portal.Execute(
|
||||
runtime.Generation,
|
||||
RuntimePortalCommand.RecallLifestone);
|
||||
RuntimeCommandResult unsupported = adapter.Movement.Execute(
|
||||
runtime.Generation,
|
||||
RuntimeMovementCommand.Stop);
|
||||
|
||||
RuntimeSessionStartResult reconnected =
|
||||
adapter.Session.Reconnect(runtime.Generation);
|
||||
RuntimeCommandResult stale = adapter.Chat.Execute(
|
||||
firstGeneration,
|
||||
new RuntimeChatCommand(
|
||||
RuntimeChatChannel.Say,
|
||||
"stale"));
|
||||
|
||||
Assert.Equal(RuntimeSessionStartStatus.Connected, started.Status);
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
reconnected.Status);
|
||||
Assert.True(chat.Accepted);
|
||||
Assert.True(portal.Accepted);
|
||||
Assert.Equal(
|
||||
RuntimeCommandStatus.Unsupported,
|
||||
unsupported.Status);
|
||||
Assert.Equal(
|
||||
RuntimeCommandStatus.StaleGeneration,
|
||||
stale.Status);
|
||||
Assert.Equal(2, gameActions.Count);
|
||||
Assert.Contains(
|
||||
trace.Entries,
|
||||
entry => entry.Kind == RuntimeTraceKind.Command
|
||||
&& (entry.Code >> 16)
|
||||
== (int)RuntimeCommandDomain.Chat
|
||||
&& entry.Text == "hello");
|
||||
Assert.Contains(
|
||||
trace.Entries,
|
||||
entry => entry.Kind == RuntimeTraceKind.Command
|
||||
&& (entry.Code >> 16)
|
||||
== (int)RuntimeCommandDomain.Portal);
|
||||
|
||||
RuntimeTeardownAcknowledgement stopped =
|
||||
adapter.Session.Stop(runtime.Generation);
|
||||
Assert.True(stopped.IsComplete);
|
||||
Assert.False(runtime.Session.IsInWorld);
|
||||
}
|
||||
|
||||
private sealed class FixtureSessionOperations : ILiveSessionOperations
|
||||
{
|
||||
public List<WorldSession> Sessions { get; } = [];
|
||||
|
||||
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
||||
new(IPAddress.Loopback, port);
|
||||
|
||||
public WorldSession CreateSession(IPEndPoint endpoint)
|
||||
{
|
||||
var session = new WorldSession(
|
||||
endpoint,
|
||||
new FixtureTransport());
|
||||
Sessions.Add(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
public void Connect(
|
||||
WorldSession session,
|
||||
string user,
|
||||
string password)
|
||||
{
|
||||
}
|
||||
|
||||
public CharacterList.Parsed GetCharacters(WorldSession session) =>
|
||||
new(
|
||||
0u,
|
||||
[
|
||||
new CharacterList.Character(
|
||||
0x50000001u,
|
||||
"Direct",
|
||||
0u),
|
||||
],
|
||||
[],
|
||||
11,
|
||||
"account",
|
||||
true,
|
||||
true);
|
||||
|
||||
public void EnterWorld(
|
||||
WorldSession session,
|
||||
int activeCharacterIndex)
|
||||
{
|
||||
}
|
||||
|
||||
public void Tick(WorldSession session)
|
||||
{
|
||||
}
|
||||
|
||||
public void DisposeSession(WorldSession session) =>
|
||||
session.Dispose();
|
||||
}
|
||||
|
||||
private sealed class FixtureEventRoute : ILiveSessionEventRouting
|
||||
{
|
||||
public void Attach()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixtureResetHost : IRuntimeGenerationResetHost
|
||||
{
|
||||
public void RetireEntityProjection(RuntimeEntityRecord entity)
|
||||
{
|
||||
}
|
||||
|
||||
public void DrainEntityProjectionBoundary()
|
||||
{
|
||||
}
|
||||
|
||||
public void CompleteEntityProjectionRetirement()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixtureTransport : 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) =>
|
||||
ValueTask.FromException<NetReceiveResult>(
|
||||
new OperationCanceledException(cancellationToken));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixtureGameplayOperations
|
||||
: IRuntimeCombatAttackOperations,
|
||||
IRuntimeCombatTargetOperations,
|
||||
IRuntimeCombatModeOperations,
|
||||
IRuntimeSpellCastOperations
|
||||
{
|
||||
private GameRuntime? _runtime;
|
||||
|
||||
public void Bind(GameRuntime runtime) => _runtime = runtime;
|
||||
public bool CanStartAttack() => false;
|
||||
public void PrepareAttackRequest()
|
||||
{
|
||||
}
|
||||
|
||||
public bool SendAttack(AttackHeight height, float power) => false;
|
||||
public void SendCancelAttack()
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsDualWield => false;
|
||||
public bool PlayerReadyForAttack => false;
|
||||
public bool AutoRepeatAttack => false;
|
||||
public bool AutoTarget => false;
|
||||
public uint? SelectClosestTarget() => null;
|
||||
public bool IsInWorld => _runtime?.Session.IsInWorld == true;
|
||||
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
|
||||
public void NotifyExplicitCombatModeRequest()
|
||||
{
|
||||
}
|
||||
|
||||
public void SendChangeCombatMode(CombatMode mode)
|
||||
{
|
||||
}
|
||||
|
||||
public uint LocalPlayerId =>
|
||||
_runtime?.PlayerIdentity.ServerGuid ?? 0u;
|
||||
public bool CanSend => false;
|
||||
public bool HasRequiredComponents(uint spellId) => false;
|
||||
|
||||
public bool IsTargetCompatible(
|
||||
uint targetId,
|
||||
SpellMetadata spell,
|
||||
bool showMessage) => false;
|
||||
|
||||
public void StopCompletely()
|
||||
{
|
||||
}
|
||||
|
||||
public void SendUntargeted(uint spellId)
|
||||
{
|
||||
}
|
||||
|
||||
public void SendTargeted(uint targetId, uint spellId)
|
||||
{
|
||||
}
|
||||
|
||||
public void DisplayMessage(string message)
|
||||
{
|
||||
}
|
||||
|
||||
public void IncrementBusy()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue