refactor(net): own live session routing
This commit is contained in:
parent
707e606e35
commit
961bdd07b7
12 changed files with 1645 additions and 543 deletions
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