refactor(net): own live session routing
This commit is contained in:
parent
707e606e35
commit
961bdd07b7
12 changed files with 1645 additions and 543 deletions
301
tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs
Normal file
301
tests/AcDream.App.Tests/Net/LiveSessionCommandRouterTests.cs
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
|
||||
public sealed class LiveSessionCommandRouterTests
|
||||
{
|
||||
[Fact]
|
||||
public void InactiveAndDisposedRouter_CannotReachTransport()
|
||||
{
|
||||
var sent = new List<string>();
|
||||
var router = NewRouter(sendTalk: sent.Add);
|
||||
|
||||
router.Publish(new SendServerCommandCmd("@before"));
|
||||
router.Activate();
|
||||
router.Activate();
|
||||
router.Publish(new SendServerCommandCmd("@active"));
|
||||
router.Dispose();
|
||||
router.Dispose();
|
||||
router.Publish(new SendServerCommandCmd("@after"));
|
||||
|
||||
Assert.Equal(["@active"], sent);
|
||||
Assert.False(router.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TellAndLegacyChannel_PreserveOutboundAndEchoPolicy()
|
||||
{
|
||||
var tells = new List<(string Target, string Text)>();
|
||||
var channels = new List<(uint Id, string Text)>();
|
||||
var chat = new ChatLog();
|
||||
var router = NewRouter(
|
||||
chat: chat,
|
||||
sendTell: (target, text) => tells.Add((target, text)),
|
||||
sendChannel: (id, text) => channels.Add((id, text)));
|
||||
router.Activate();
|
||||
|
||||
router.Publish(new SendChatCmd(ChatChannelKind.Tell, "Friend", "hello"));
|
||||
router.Publish(new SendChatCmd(ChatChannelKind.Fellowship, null, "group"));
|
||||
|
||||
Assert.Equal([("Friend", "hello")], tells);
|
||||
Assert.Equal([(0x00000800u, "group")], channels);
|
||||
Assert.Collection(
|
||||
chat.Snapshot(),
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal(ChatKind.Tell, entry.Kind);
|
||||
Assert.Equal("Friend", entry.Sender);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal(ChatKind.Channel, entry.Kind);
|
||||
Assert.Equal("Fellowship", entry.ChannelName);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TurbineChannel_UsesRuntimeRoomCookieAndNoOptimisticEcho()
|
||||
{
|
||||
var turbine = new TurbineChatState();
|
||||
turbine.OnChannelsReceived(
|
||||
allegianceRoom: 0u,
|
||||
generalRoom: 0x70000001u,
|
||||
tradeRoom: 0u,
|
||||
lfgRoom: 0u,
|
||||
roleplayRoom: 0u,
|
||||
olthoiRoom: 0u,
|
||||
societyRoom: 0u,
|
||||
societyCelestialHandRoom: 0u,
|
||||
societyEldrytchWebRoom: 0u,
|
||||
societyRadiantBloodRoom: 0u);
|
||||
var sent = new List<(uint Room, uint Type, uint Dispatch, uint Sender, string Text, uint Cookie)>();
|
||||
var chat = new ChatLog();
|
||||
var router = NewRouter(
|
||||
chat: chat,
|
||||
turbine: turbine,
|
||||
playerGuid: () => 0x50000001u,
|
||||
sendTurbine: (room, type, dispatch, sender, text, cookie) =>
|
||||
sent.Add((room, type, dispatch, sender, text, cookie)));
|
||||
router.Activate();
|
||||
|
||||
router.Publish(new SendChatCmd(ChatChannelKind.General, null, "world"));
|
||||
|
||||
Assert.Collection(sent, message =>
|
||||
{
|
||||
Assert.Equal(0x70000001u, message.Room);
|
||||
Assert.Equal(0x02u, message.Type);
|
||||
Assert.Equal(0x02u, message.Dispatch);
|
||||
Assert.Equal(0x50000001u, message.Sender);
|
||||
Assert.Equal("world", message.Text);
|
||||
Assert.Equal(1u, message.Cookie);
|
||||
});
|
||||
Assert.Equal(0, chat.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActivateAfterDispose_IsRejected()
|
||||
{
|
||||
var router = NewRouter();
|
||||
router.Dispose();
|
||||
|
||||
Assert.Throws<ObjectDisposedException>(router.Activate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ConcurrentDispose_WaitsForInFlightTransportThenMakesRouterInert()
|
||||
{
|
||||
using var sendEntered = new ManualResetEventSlim();
|
||||
using var releaseSend = new ManualResetEventSlim();
|
||||
var sent = new List<string>();
|
||||
var router = NewRouter(sendTalk: text =>
|
||||
{
|
||||
sendEntered.Set();
|
||||
releaseSend.Wait();
|
||||
sent.Add(text);
|
||||
});
|
||||
router.Activate();
|
||||
|
||||
Task publish = Task.Run(() =>
|
||||
router.Publish(new SendServerCommandCmd("@active")));
|
||||
Assert.True(sendEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
using var disposeStarted = new ManualResetEventSlim();
|
||||
Task dispose = Task.Run(() =>
|
||||
{
|
||||
disposeStarted.Set();
|
||||
router.Dispose();
|
||||
});
|
||||
Assert.True(disposeStarted.Wait(TimeSpan.FromSeconds(5)));
|
||||
Task firstCompletion = await Task.WhenAny(
|
||||
dispose,
|
||||
Task.Delay(TimeSpan.FromMilliseconds(100)));
|
||||
Assert.NotSame(dispose, firstCompletion);
|
||||
|
||||
releaseSend.Set();
|
||||
await Task.WhenAll(publish, dispose);
|
||||
router.Publish(new SendServerCommandCmd("@after"));
|
||||
|
||||
Assert.Equal(["@active"], sent);
|
||||
Assert.False(router.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReentrantDisposeFromLog_PreventsLaterTransportAndEcho()
|
||||
{
|
||||
var turbine = new TurbineChatState();
|
||||
turbine.OnChannelsReceived(
|
||||
0u, 0x70000001u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u);
|
||||
var sent = new List<string>();
|
||||
var chat = new ChatLog();
|
||||
LiveSessionCommandRouter? router = null;
|
||||
router = NewRouter(
|
||||
chat: chat,
|
||||
turbine: turbine,
|
||||
sendTurbine: (_, _, _, _, text, _) => sent.Add(text),
|
||||
log: _ => router!.Dispose());
|
||||
router.Activate();
|
||||
|
||||
router.Publish(new SendChatCmd(ChatChannelKind.General, null, "world"));
|
||||
|
||||
Assert.Empty(sent);
|
||||
Assert.Equal(0, chat.Count);
|
||||
Assert.False(router.IsActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetainedDisposedRouter_ReleasesCapturedTransportGraph()
|
||||
{
|
||||
(LiveSessionCommandRouter router, WeakReference<object> transport) =
|
||||
CreateRouterWithCapturedTransport();
|
||||
|
||||
router.Dispose();
|
||||
ForceFullCollection();
|
||||
|
||||
Assert.False(transport.TryGetTarget(out _));
|
||||
GC.KeepAlive(router);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DelayedConfirmationCallback_ReleasesTransportAndBecomesInert()
|
||||
{
|
||||
(LiveSessionCommandRouter router, WeakReference<object> transport, Action<bool> callback) =
|
||||
CreateRouterWithDelayedConfirmation();
|
||||
|
||||
router.Dispose();
|
||||
ForceFullCollection();
|
||||
|
||||
Assert.False(transport.TryGetTarget(out _));
|
||||
callback(true);
|
||||
Assert.False(router.IsActive);
|
||||
GC.KeepAlive(callback);
|
||||
GC.KeepAlive(router);
|
||||
}
|
||||
|
||||
private static LiveSessionCommandRouter NewRouter(
|
||||
ChatLog? chat = null,
|
||||
TurbineChatState? turbine = null,
|
||||
Func<uint>? playerGuid = null,
|
||||
Action<string>? sendTalk = null,
|
||||
Action<string, string>? sendTell = null,
|
||||
Action<uint, string>? sendChannel = null,
|
||||
Action<uint, uint, uint, uint, string, uint>? sendTurbine = null,
|
||||
Action<string>? log = null,
|
||||
ClientCommandController.Bindings? clientBindings = null) => new(
|
||||
new LiveSessionCommandBindings(
|
||||
clientBindings ?? NewClientBindings(),
|
||||
chat ?? new ChatLog(),
|
||||
turbine ?? new TurbineChatState(),
|
||||
playerGuid ?? (() => 0u),
|
||||
sendTalk ?? (_ => { }),
|
||||
sendTell ?? ((_, _) => { }),
|
||||
sendChannel ?? ((_, _) => { }),
|
||||
sendTurbine ?? ((_, _, _, _, _, _) => { }),
|
||||
log));
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static (LiveSessionCommandRouter, WeakReference<object>)
|
||||
CreateRouterWithCapturedTransport()
|
||||
{
|
||||
object transport = new object();
|
||||
var weak = new WeakReference<object>(transport);
|
||||
var router = NewRouter(sendTalk: _ => GC.KeepAlive(transport));
|
||||
return (router, weak);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static (LiveSessionCommandRouter, WeakReference<object>, Action<bool>)
|
||||
CreateRouterWithDelayedConfirmation()
|
||||
{
|
||||
object transport = new object();
|
||||
var weak = new WeakReference<object>(transport);
|
||||
Action<bool>? callback = null;
|
||||
ClientCommandController.Bindings bindings = NewClientBindings() with
|
||||
{
|
||||
ShowConfirmation = (_, completion) => callback = completion,
|
||||
Suicide = () => GC.KeepAlive(transport),
|
||||
};
|
||||
var router = NewRouter(clientBindings: bindings);
|
||||
router.Activate();
|
||||
router.Publish(new ExecuteClientCommandCmd(ClientCommandId.Die, string.Empty));
|
||||
return (router, weak, Assert.IsType<Action<bool>>(callback));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ForceFullCollection()
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.Collect();
|
||||
}
|
||||
|
||||
private static ClientCommandController.Bindings NewClientBindings() => new(
|
||||
TeleportToLifestone: () => { },
|
||||
TeleportToMarketplace: () => { },
|
||||
TeleportToPkArena: () => { },
|
||||
TeleportToPkLiteArena: () => { },
|
||||
TeleportToHouse: () => { },
|
||||
TeleportToMansion: () => { },
|
||||
QueryAge: () => { },
|
||||
QueryBirth: () => { },
|
||||
ToggleFrameRate: () => { },
|
||||
ToggleUiLock: () => { },
|
||||
ShowSystemMessage: _ => { },
|
||||
ShowWeenieError: _ => { },
|
||||
PlayerPublicWeenieBitfield: () => null,
|
||||
ClientVersion: () => "test",
|
||||
CurrentPosition: () => null,
|
||||
LastOutsideCorpsePosition: () => null,
|
||||
ShowConfirmation: (_, _) => { },
|
||||
Suicide: () => { },
|
||||
ClearChat: _ => { },
|
||||
SaveUi: _ => { },
|
||||
LoadUi: _ => { },
|
||||
SaveAutoUi: () => { },
|
||||
LoadAutoUi: () => { },
|
||||
IsAway: () => false,
|
||||
SetAway: _ => { },
|
||||
SetAwayMessage: _ => { },
|
||||
AcceptLootPermits: () => false,
|
||||
SetAcceptLootPermits: _ => { },
|
||||
DisplayConsent: () => { },
|
||||
ClearConsent: () => { },
|
||||
RemoveConsent: _ => { },
|
||||
SendEmote: _ => { },
|
||||
Friends: new FriendsState(),
|
||||
AddFriend: _ => { },
|
||||
RemoveFriend: _ => { },
|
||||
ClearFriends: () => { },
|
||||
RequestLegacyFriends: () => { },
|
||||
Squelch: new SquelchState(),
|
||||
ModifyCharacterSquelch: (_, _, _, _) => { },
|
||||
ModifyAccountSquelch: (_, _) => { },
|
||||
ModifyGlobalSquelch: (_, _) => { },
|
||||
LastTeller: () => null,
|
||||
ClearDesiredComponents: () => { },
|
||||
HasOpenVendor: () => false,
|
||||
FillComponentBuyList: (_, _) => { });
|
||||
}
|
||||
291
tests/AcDream.App.Tests/Net/LiveSessionEventRouterTests.cs
Normal file
291
tests/AcDream.App.Tests/Net/LiveSessionEventRouterTests.cs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
using System.Net;
|
||||
using System.Reflection;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Core.Chat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Player;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Tests.Net;
|
||||
|
||||
public sealed class LiveSessionEventRouterTests
|
||||
{
|
||||
[Fact]
|
||||
public void Dispose_DetachesExactHandlersAndSilencesCopiedDelegates()
|
||||
{
|
||||
using var session = NewSession();
|
||||
int baselineGameEvents = session.GameEvents.RegisteredHandlerCount;
|
||||
var counters = new Counters();
|
||||
var combat = new CombatState();
|
||||
var chat = new ChatLog();
|
||||
var router = NewRouter(session, counters, combat: combat, chat: chat);
|
||||
|
||||
AssertSessionHandlerCounts(session, multiplier: 1);
|
||||
Assert.True(session.GameEvents.RegisteredHandlerCount > baselineGameEvents);
|
||||
Action<double> copiedTime = EventDelegate<Action<double>>(
|
||||
session,
|
||||
nameof(session.ServerTimeUpdated));
|
||||
Action<uint> copiedTeleport = EventDelegate<Action<uint>>(
|
||||
session,
|
||||
nameof(session.TeleportStarted));
|
||||
Action<WorldSession.PlayerIntPropertyUpdate> copiedPlayerInt =
|
||||
EventDelegate<Action<WorldSession.PlayerIntPropertyUpdate>>(
|
||||
session,
|
||||
nameof(session.PlayerIntPropertyUpdated));
|
||||
Action<CombatState.DamageDealt> copiedDamage =
|
||||
EventDelegate<CombatState, Action<CombatState.DamageDealt>>(
|
||||
combat,
|
||||
nameof(combat.DamageDealtAccepted));
|
||||
copiedTime(123d);
|
||||
copiedTeleport(0x50000001u);
|
||||
copiedPlayerInt(new WorldSession.PlayerIntPropertyUpdate(
|
||||
CombatStateWiring.CombatModePropertyId,
|
||||
(int)CombatMode.Melee));
|
||||
copiedDamage(new CombatState.DamageDealt("Drudge", 1u, 5u, 0.1f));
|
||||
Assert.Equal(1, counters.ServerTime);
|
||||
Assert.Equal(1, counters.Teleport);
|
||||
Assert.Equal(CombatMode.Melee, combat.CurrentMode);
|
||||
Assert.Equal(1, chat.Count);
|
||||
|
||||
router.Dispose();
|
||||
router.Dispose();
|
||||
AssertSessionHandlerCounts(session, multiplier: 0);
|
||||
Assert.Equal(baselineGameEvents, session.GameEvents.RegisteredHandlerCount);
|
||||
|
||||
copiedTime(456d);
|
||||
copiedTeleport(0x50000002u);
|
||||
copiedPlayerInt(new WorldSession.PlayerIntPropertyUpdate(
|
||||
CombatStateWiring.CombatModePropertyId,
|
||||
(int)CombatMode.Magic));
|
||||
copiedDamage(new CombatState.DamageDealt("Drudge", 1u, 7u, 0.2f));
|
||||
Assert.Equal(1, counters.ServerTime);
|
||||
Assert.Equal(1, counters.Teleport);
|
||||
Assert.Equal(CombatMode.Melee, combat.CurrentMode);
|
||||
Assert.Equal(1, chat.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedRouters_DisposeOlderFirstLeavesOnlyNewerRouter()
|
||||
{
|
||||
using var session = NewSession();
|
||||
int baselineGameEvents = session.GameEvents.RegisteredHandlerCount;
|
||||
var first = new Counters();
|
||||
var second = new Counters();
|
||||
var routerA = NewRouter(session, first);
|
||||
var routerB = NewRouter(session, second);
|
||||
|
||||
AssertSessionHandlerCounts(session, multiplier: 2);
|
||||
EventDelegate<Action<double>>(session, nameof(session.ServerTimeUpdated))(1d);
|
||||
Assert.Equal(1, first.ServerTime);
|
||||
Assert.Equal(1, second.ServerTime);
|
||||
|
||||
routerA.Dispose();
|
||||
AssertSessionHandlerCounts(session, multiplier: 1);
|
||||
EventDelegate<Action<double>>(session, nameof(session.ServerTimeUpdated))(2d);
|
||||
Assert.Equal(1, first.ServerTime);
|
||||
Assert.Equal(2, second.ServerTime);
|
||||
|
||||
routerB.Dispose();
|
||||
AssertSessionHandlerCounts(session, multiplier: 0);
|
||||
Assert.Equal(baselineGameEvents, session.GameEvents.RegisteredHandlerCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedRouters_DisposeNewerFirstLeavesOnlyOlderRouter()
|
||||
{
|
||||
using var session = NewSession();
|
||||
int baselineGameEvents = session.GameEvents.RegisteredHandlerCount;
|
||||
var first = new Counters();
|
||||
var second = new Counters();
|
||||
var routerA = NewRouter(session, first);
|
||||
var routerB = NewRouter(session, second);
|
||||
|
||||
routerB.Dispose();
|
||||
AssertSessionHandlerCounts(session, multiplier: 1);
|
||||
EventDelegate<Action<double>>(session, nameof(session.ServerTimeUpdated))(1d);
|
||||
Assert.Equal(1, first.ServerTime);
|
||||
Assert.Equal(0, second.ServerTime);
|
||||
|
||||
routerA.Dispose();
|
||||
AssertSessionHandlerCounts(session, multiplier: 0);
|
||||
Assert.Equal(baselineGameEvents, session.GameEvents.RegisteredHandlerCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThrowingSink_DoesNotCorruptLaterTeardown()
|
||||
{
|
||||
using var session = NewSession();
|
||||
int baselineGameEvents = session.GameEvents.RegisteredHandlerCount;
|
||||
var router = NewRouter(
|
||||
session,
|
||||
new Counters { ThrowOnServerTime = true });
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
EventDelegate<Action<double>>(
|
||||
session,
|
||||
nameof(session.ServerTimeUpdated))(1d));
|
||||
|
||||
router.Dispose();
|
||||
AssertSessionHandlerCounts(session, multiplier: 0);
|
||||
Assert.Equal(baselineGameEvents, session.GameEvents.RegisteredHandlerCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructionFailure_UnwindsEveryPriorRegistration()
|
||||
{
|
||||
using var session = NewSession();
|
||||
int baselineGameEvents = session.GameEvents.RegisteredHandlerCount;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => NewRouter(
|
||||
session,
|
||||
new Counters(),
|
||||
step =>
|
||||
{
|
||||
if (step == 20)
|
||||
throw new InvalidOperationException("injected construction failure");
|
||||
}));
|
||||
|
||||
AssertSessionHandlerCounts(session, multiplier: 0);
|
||||
Assert.Equal(baselineGameEvents, session.GameEvents.RegisteredHandlerCount);
|
||||
}
|
||||
|
||||
private static LiveSessionEventRouter NewRouter(
|
||||
WorldSession session,
|
||||
Counters counters,
|
||||
Action<int>? constructionCheckpoint = null,
|
||||
CombatState? combat = null,
|
||||
ChatLog? chat = null) => new(
|
||||
session,
|
||||
new LiveEntitySessionSink(
|
||||
Spawned: _ => { },
|
||||
Deleted: _ => { },
|
||||
PickedUp: _ => { },
|
||||
MotionUpdated: _ => { },
|
||||
PositionUpdated: _ => { },
|
||||
VectorUpdated: _ => { },
|
||||
StateUpdated: _ => { },
|
||||
ParentUpdated: _ => { },
|
||||
TeleportStarted: _ => counters.Teleport++,
|
||||
AppearanceUpdated: _ => { },
|
||||
PlayPhysicsScript: _ => { },
|
||||
PlayPhysicsScriptType: _ => { }),
|
||||
new LiveEnvironmentSessionSink(
|
||||
EnvironChanged: _ => { },
|
||||
ServerTimeUpdated: _ =>
|
||||
{
|
||||
if (counters.ThrowOnServerTime)
|
||||
throw new InvalidOperationException("injected sink failure");
|
||||
counters.ServerTime++;
|
||||
}),
|
||||
NewInventoryBindings(),
|
||||
NewCharacterBindings(combat),
|
||||
NewSocialBindings(chat),
|
||||
constructionCheckpoint);
|
||||
|
||||
private static LiveInventorySessionBindings NewInventoryBindings() => new(
|
||||
new ClientObjectTable(),
|
||||
new LocalPlayerState(),
|
||||
PlayerGuid: () => 0x50000001u,
|
||||
OnShortcuts: null,
|
||||
OnUseDone: null,
|
||||
ItemMana: new ItemManaState(),
|
||||
OnDesiredComponents: null,
|
||||
ExternalContainers: new ExternalContainerState());
|
||||
|
||||
private static LiveCharacterSessionBindings NewCharacterBindings(
|
||||
CombatState? combat = null) => new(
|
||||
combat ?? new CombatState(),
|
||||
new Spellbook(),
|
||||
ResolveSkillFormulaBonus: null,
|
||||
OnSkillsUpdated: null,
|
||||
OnConfirmationRequest: null,
|
||||
OnConfirmationDone: null,
|
||||
OnCharacterOptions: null,
|
||||
ClientTime: () => 0d);
|
||||
|
||||
private static LiveSocialSessionBindings NewSocialBindings(
|
||||
ChatLog? chat = null) => new(
|
||||
chat ?? new ChatLog(),
|
||||
new TurbineChatState(),
|
||||
new FriendsState(),
|
||||
new SquelchState());
|
||||
|
||||
private static WorldSession NewSession() =>
|
||||
new(new IPEndPoint(IPAddress.Loopback, 9));
|
||||
|
||||
private static void AssertSessionHandlerCounts(
|
||||
WorldSession session,
|
||||
int multiplier)
|
||||
{
|
||||
string[] directEvents =
|
||||
[
|
||||
nameof(session.EntitySpawned),
|
||||
nameof(session.EntityDeleted),
|
||||
nameof(session.EntityPickedUp),
|
||||
nameof(session.MotionUpdated),
|
||||
nameof(session.PositionUpdated),
|
||||
nameof(session.VectorUpdated),
|
||||
nameof(session.StateUpdated),
|
||||
nameof(session.ParentUpdated),
|
||||
nameof(session.TeleportStarted),
|
||||
nameof(session.AppearanceUpdated),
|
||||
nameof(session.PlayPhysicsScriptReceived),
|
||||
nameof(session.PlayPhysicsScriptTypeReceived),
|
||||
nameof(session.EnvironChanged),
|
||||
nameof(session.ServerTimeUpdated),
|
||||
nameof(session.SpeechHeard),
|
||||
nameof(session.ServerMessageReceived),
|
||||
nameof(session.EmoteHeard),
|
||||
nameof(session.SoulEmoteHeard),
|
||||
nameof(session.PlayerKilledReceived),
|
||||
nameof(session.TurbineChatReceived),
|
||||
nameof(session.VitalUpdated),
|
||||
nameof(session.VitalCurrentUpdated),
|
||||
];
|
||||
foreach (string eventName in directEvents)
|
||||
Assert.Equal(multiplier, HandlerCount(session, eventName));
|
||||
|
||||
Assert.Equal(multiplier, HandlerCount(session, nameof(session.ObjectIntPropertyUpdated)));
|
||||
Assert.Equal(multiplier * 2, HandlerCount(session, nameof(session.PlayerIntPropertyUpdated)));
|
||||
Assert.Equal(multiplier, HandlerCount(session, nameof(session.PlayerInt64PropertyUpdated)));
|
||||
Assert.Equal(multiplier, HandlerCount(session, nameof(session.StackSizeUpdated)));
|
||||
Assert.Equal(multiplier, HandlerCount(session, nameof(session.InventoryObjectRemoved)));
|
||||
}
|
||||
|
||||
private static int HandlerCount(WorldSession session, string eventName) =>
|
||||
(EventField(session, eventName).GetValue(session) as MulticastDelegate)?
|
||||
.GetInvocationList()
|
||||
.Length ?? 0;
|
||||
|
||||
private static TDelegate EventDelegate<TDelegate>(
|
||||
WorldSession session,
|
||||
string eventName)
|
||||
where TDelegate : Delegate =>
|
||||
Assert.IsType<TDelegate>(EventField(session, eventName).GetValue(session));
|
||||
|
||||
private static TDelegate EventDelegate<TOwner, TDelegate>(
|
||||
TOwner owner,
|
||||
string eventName)
|
||||
where TOwner : class
|
||||
where TDelegate : Delegate =>
|
||||
Assert.IsType<TDelegate>(
|
||||
typeof(TOwner).GetField(
|
||||
eventName,
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(owner));
|
||||
|
||||
private static FieldInfo EventField(WorldSession session, string eventName) =>
|
||||
typeof(WorldSession).GetField(
|
||||
eventName,
|
||||
BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?? throw new InvalidOperationException($"event field {eventName} not found");
|
||||
|
||||
private sealed class Counters
|
||||
{
|
||||
public int ServerTime;
|
||||
public int Teleport;
|
||||
public bool ThrowOnServerTime;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue