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 copiedTime = EventDelegate>( session, nameof(session.ServerTimeUpdated)); Action copiedTeleport = EventDelegate>( session, nameof(session.TeleportStarted)); Action copiedPlayerInt = EventDelegate>( session, nameof(session.PlayerIntPropertyUpdated)); Action copiedDamage = EventDelegate>( 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>(session, nameof(session.ServerTimeUpdated))(1d); Assert.Equal(1, first.ServerTime); Assert.Equal(1, second.ServerTime); routerA.Dispose(); AssertSessionHandlerCounts(session, multiplier: 1); EventDelegate>(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>(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(() => EventDelegate>( 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(() => 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? 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( WorldSession session, string eventName) where TDelegate : Delegate => Assert.IsType(EventField(session, eventName).GetValue(session)); private static TDelegate EventDelegate( TOwner owner, string eventName) where TOwner : class where TDelegate : Delegate => Assert.IsType( 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; } }