434 lines
14 KiB
C#
434 lines
14 KiB
C#
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);
|
|
const uint selectedObject = 0x70000001u;
|
|
runtime.InventoryOwner.Objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = selectedObject,
|
|
Type = ItemType.Misc,
|
|
});
|
|
|
|
RuntimeCommandResult chat = adapter.Chat.Execute(
|
|
runtime.Generation,
|
|
new RuntimeChatCommand(
|
|
RuntimeChatChannel.Say,
|
|
"hello"));
|
|
RuntimeCommandResult portal = adapter.Portal.Execute(
|
|
runtime.Generation,
|
|
RuntimePortalCommand.RecallLifestone);
|
|
runtime.CommunicationOwner.TurbineChat.OnChannelsReceived(
|
|
allegianceRoom: 0x10u,
|
|
generalRoom: 0x11u,
|
|
tradeRoom: 0x12u,
|
|
lfgRoom: 0x13u,
|
|
roleplayRoom: 0x14u,
|
|
olthoiRoom: 0x15u,
|
|
societyRoom: 0x16u,
|
|
societyCelestialHandRoom: 0u,
|
|
societyEldrytchWebRoom: 0u,
|
|
societyRadiantBloodRoom: 0u);
|
|
RuntimeCommandResult[] stateAndWireCommands =
|
|
[
|
|
adapter.Selection.SelectObject(
|
|
runtime.Generation,
|
|
selectedObject),
|
|
adapter.Selection.Clear(runtime.Generation),
|
|
adapter.Movement.SetIntent(
|
|
runtime.Generation,
|
|
new MovementInput(Forward: true, Run: true)),
|
|
adapter.Movement.ClearIntent(runtime.Generation),
|
|
adapter.Movement.Execute(
|
|
runtime.Generation,
|
|
RuntimeMovementCommand.Stop),
|
|
adapter.Chat.Execute(
|
|
runtime.Generation,
|
|
new RuntimeChatCommand(
|
|
RuntimeChatChannel.Fellowship,
|
|
"group")),
|
|
adapter.Chat.Execute(
|
|
runtime.Generation,
|
|
new RuntimeChatCommand(
|
|
RuntimeChatChannel.General,
|
|
"global")),
|
|
adapter.InventoryState.AddShortcut(
|
|
runtime.Generation,
|
|
new RuntimeShortcutCommand(0, 0x70000001u, 0u)),
|
|
adapter.InventoryState.RemoveShortcut(
|
|
runtime.Generation,
|
|
0),
|
|
adapter.Spellbook.AddFavorite(
|
|
runtime.Generation,
|
|
tabIndex: 0,
|
|
position: 0,
|
|
spellId: 7u),
|
|
adapter.Spellbook.RemoveFavorite(
|
|
runtime.Generation,
|
|
tabIndex: 0,
|
|
spellId: 7u),
|
|
adapter.Spellbook.SetFilter(
|
|
runtime.Generation,
|
|
filters: 3u),
|
|
adapter.Spellbook.ForgetSpell(
|
|
runtime.Generation,
|
|
spellId: 7u),
|
|
adapter.Spellbook.SetDesiredComponent(
|
|
runtime.Generation,
|
|
componentId: 11u,
|
|
amount: 3u),
|
|
adapter.Spellbook.ClearDesiredComponents(
|
|
runtime.Generation),
|
|
adapter.Character.Advance(
|
|
runtime.Generation,
|
|
new RuntimeAdvancementCommand(
|
|
RuntimeAdvancementKind.Attribute,
|
|
StatId: 1u,
|
|
Cost: 10u)),
|
|
adapter.Character.Advance(
|
|
runtime.Generation,
|
|
new RuntimeAdvancementCommand(
|
|
RuntimeAdvancementKind.Vital,
|
|
StatId: 2u,
|
|
Cost: 11u)),
|
|
adapter.Character.Advance(
|
|
runtime.Generation,
|
|
new RuntimeAdvancementCommand(
|
|
RuntimeAdvancementKind.Skill,
|
|
StatId: 3u,
|
|
Cost: 12u)),
|
|
adapter.Character.Advance(
|
|
runtime.Generation,
|
|
new RuntimeAdvancementCommand(
|
|
RuntimeAdvancementKind.TrainSkill,
|
|
StatId: 4u,
|
|
Cost: 1u)),
|
|
adapter.Character.SetOptions1(
|
|
runtime.Generation,
|
|
options: 0x1234u),
|
|
adapter.Social.Execute(
|
|
runtime.Generation,
|
|
new RuntimeFriendCommand(
|
|
RuntimeFriendCommandKind.Add,
|
|
Name: "Friend")),
|
|
adapter.Social.Execute(
|
|
runtime.Generation,
|
|
new RuntimeFriendCommand(
|
|
RuntimeFriendCommandKind.Remove,
|
|
CharacterId: 0x50000003u)),
|
|
adapter.Social.Execute(
|
|
runtime.Generation,
|
|
new RuntimeFriendCommand(
|
|
RuntimeFriendCommandKind.Clear)),
|
|
adapter.Social.Execute(
|
|
runtime.Generation,
|
|
new RuntimeSquelchCommand(
|
|
RuntimeSquelchScope.Character,
|
|
Add: true,
|
|
CharacterId: 0x50000004u,
|
|
Name: "Muted")),
|
|
adapter.Social.Execute(
|
|
runtime.Generation,
|
|
new RuntimeSquelchCommand(
|
|
RuntimeSquelchScope.Account,
|
|
Add: true,
|
|
Name: "AccountMuted")),
|
|
adapter.Social.Execute(
|
|
runtime.Generation,
|
|
new RuntimeSquelchCommand(
|
|
RuntimeSquelchScope.Global,
|
|
Add: true,
|
|
MessageType: 2u)),
|
|
];
|
|
|
|
RuntimeSessionStartResult reconnected =
|
|
adapter.Session.Reconnect(runtime.Generation);
|
|
RuntimeCommandResult stale = adapter.Chat.Execute(
|
|
firstGeneration,
|
|
new RuntimeChatCommand(
|
|
RuntimeChatChannel.Say,
|
|
"stale"));
|
|
RuntimeCommandResult staleMovement =
|
|
adapter.Movement.SetIntent(
|
|
firstGeneration,
|
|
new MovementInput(Forward: true));
|
|
|
|
Assert.Equal(RuntimeSessionStartStatus.Connected, started.Status);
|
|
Assert.Equal(
|
|
RuntimeSessionStartStatus.Connected,
|
|
reconnected.Status);
|
|
Assert.True(chat.Accepted);
|
|
Assert.True(portal.Accepted);
|
|
Assert.All(
|
|
stateAndWireCommands,
|
|
result => Assert.Equal(
|
|
RuntimeCommandStatus.Accepted,
|
|
result.Status));
|
|
Assert.Equal(
|
|
RuntimeCommandStatus.StaleGeneration,
|
|
stale.Status);
|
|
Assert.Equal(
|
|
RuntimeCommandStatus.StaleGeneration,
|
|
staleMovement.Status);
|
|
Assert.False(runtime.MovementOwner.HasCommandInput);
|
|
Assert.True(gameActions.Count >= 20);
|
|
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);
|
|
Assert.Contains(
|
|
trace.Entries,
|
|
entry => entry.Kind == RuntimeTraceKind.Combat);
|
|
|
|
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()
|
|
{
|
|
}
|
|
}
|
|
}
|