refactor(net): own live session routing

This commit is contained in:
Erik 2026-07-21 11:17:09 +02:00
parent 707e606e35
commit 961bdd07b7
12 changed files with 1645 additions and 543 deletions

View 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;
}
}