From 57868ce17879f63c6bba992b0c6ecb41105326db Mon Sep 17 00:00:00 2001 From: Syntriax Date: Mon, 19 May 2025 22:24:06 +0300 Subject: [PATCH] feat: client & server packet listener interfaces added --- Engine | 2 +- Shared/Behaviours/PaddleBehaviour.cs | 35 ++-- Shared/GamePong.cs | 11 +- .../Network/Abstract/IPacketListenerClient.cs | 6 + .../Network/Abstract/IPacketListenerServer.cs | 6 + Shared/Network/LiteNetLib/LiteNetLibClient.cs | 2 +- .../LiteNetLib/LiteNetLibCommunicatorBase.cs | 10 +- Shared/Network/LiteNetLib/LiteNetLibServer.cs | 2 +- Shared/Network/NetworkManager.cs | 156 ++++++++++++++++++ 9 files changed, 199 insertions(+), 31 deletions(-) create mode 100644 Shared/Network/Abstract/IPacketListenerClient.cs create mode 100644 Shared/Network/Abstract/IPacketListenerServer.cs create mode 100644 Shared/Network/NetworkManager.cs diff --git a/Engine b/Engine index 0bf3823..3b6a93d 160000 --- a/Engine +++ b/Engine @@ -1 +1 @@ -Subproject commit 0bf38234c6dbef8824045c81c4bd79c440a70abc +Subproject commit 3b6a93d37a7453376ad97d52709af750304fe89a diff --git a/Shared/Behaviours/PaddleBehaviour.cs b/Shared/Behaviours/PaddleBehaviour.cs index d76282f..26638ed 100644 --- a/Shared/Behaviours/PaddleBehaviour.cs +++ b/Shared/Behaviours/PaddleBehaviour.cs @@ -8,7 +8,8 @@ using Syntriax.Engine.Systems.Input; namespace Pong.Behaviours; -public class PaddleBehaviour(Keys Up, Keys Down, float High, float Low, float Speed) : Behaviour2D, INetworkEntity +public class PaddleBehaviour(Keys Up, Keys Down, float High, float Low, float Speed) : Behaviour2D, + IPacketListenerServer, IPacketListenerClient { private Keys Up { get; } = Up; private Keys Down { get; } = Down; @@ -42,10 +43,8 @@ public class PaddleBehaviour(Keys Up, Keys Down, float High, float Low, float Sp protected override void OnFirstActiveFrame() { inputs = Universe.FindRequiredBehaviour>(); - networkClient = Universe.GetRequiredUniverseObject(); - networkServer = Universe.GetUniverseObject(); - networkClient.SubscribeToPackets(ReceiveDataClient); - networkServer?.SubscribeToPackets(ReceiveDataServer); + networkClient = Universe.FindRequiredBehaviour(); + networkServer = Universe.FindBehaviour(); inputs.RegisterOnPress(Up, OnUpPressed); inputs.RegisterOnRelease(Up, OnUpReleased); @@ -68,9 +67,20 @@ public class PaddleBehaviour(Keys Up, Keys Down, float High, float Low, float Sp private void OnDownPressed(IButtonInputs inputs, Keys keys) { isDownPressed = true; networkClient.SendToServer(new EntityPaddlePositionPacket(this)); } private void OnDownReleased(IButtonInputs inputs, Keys keys) { isDownPressed = false; networkClient.SendToServer(new EntityPaddlePositionPacket(this)); } - public void ReceiveDataClient(T data, string _) + public void OnServerPacketArrived(EntityPaddlePositionPacket packet, string from) { - if (data is not EntityPaddlePositionPacket position) + if (packet is not EntityPaddlePositionPacket position) + return; + + if (position.EntityId.CompareTo(Id) != 0) + return; + + networkServer?.SendToClient("*", position); + } + + public void OnClientPacketArrived(EntityPaddlePositionPacket packet) + { + if (packet is not EntityPaddlePositionPacket position) return; if (position.EntityId.CompareTo(Id) != 0) @@ -80,17 +90,6 @@ public class PaddleBehaviour(Keys Up, Keys Down, float High, float Low, float Sp isMovingDown = position.IsDownPressed; } - public void ReceiveDataServer(T data, string fromClientId) - { - if (data is not EntityPaddlePositionPacket position) - return; - - if (position.EntityId.CompareTo(Id) != 0) - return; - - networkServer?.SendToClient("*", position); - } - public class EntityPaddlePositionPacket : IEntityNetworkPacket { public string EntityId { get; set; } = default!; diff --git a/Shared/GamePong.cs b/Shared/GamePong.cs index 210bc2a..2821093 100644 --- a/Shared/GamePong.cs +++ b/Shared/GamePong.cs @@ -80,10 +80,15 @@ public class GamePong : Game if (Environment.GetCommandLineArgs().FirstOrDefault(x => x.CompareTo("-server") == 0) is not null) { - universe.InstantiateUniverseObject().SetUniverseObject("Server").Start(8888, 2); + LiteNetLibServer server = universe.InstantiateUniverseObject().SetUniverseObject("Server").BehaviourController.AddBehaviour(); + UniverseObjectFactory.Instantiate().SetUniverseObject("NetworkManager", server.BehaviourController.UniverseObject); + server.Start(8888, 2); Window.Title = $"{Window.Title} - Server"; } - universe.InstantiateUniverseObject().SetUniverseObject("Client").Connect("localhost", 8888); + LiteNetLibClient client = universe.InstantiateUniverseObject().SetUniverseObject("Client").BehaviourController.AddBehaviour(); + UniverseObjectFactory.Instantiate().SetUniverseObject("NetworkManager", client.BehaviourController.UniverseObject); + client.Connect("localhost", 8888); + universe.InstantiateUniverseObject().SetUniverseObject("Physics Engine 2D"); //////////////////////////////////////////////////////////////////////////////////// @@ -169,7 +174,7 @@ public class GamePong : Game universe.Update(gameTime.ToUniverseTime()); if (Keyboard.GetState().IsKeyDown(Keys.S)) - universe.GetRequiredUniverseObject().SendToServer(new TestMessagePacket($"Hola ({gameTime.TotalGameTime.TotalSeconds})")); + universe.FindRequiredBehaviour().SendToServer(new TestMessagePacket($"Hola ({gameTime.TotalGameTime.TotalSeconds})")); base.Update(gameTime); } diff --git a/Shared/Network/Abstract/IPacketListenerClient.cs b/Shared/Network/Abstract/IPacketListenerClient.cs new file mode 100644 index 0000000..dacef44 --- /dev/null +++ b/Shared/Network/Abstract/IPacketListenerClient.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Network; + +public interface IPacketListenerClient : INetworkEntity +{ + void OnClientPacketArrived(T packet); +} diff --git a/Shared/Network/Abstract/IPacketListenerServer.cs b/Shared/Network/Abstract/IPacketListenerServer.cs new file mode 100644 index 0000000..e83cbbc --- /dev/null +++ b/Shared/Network/Abstract/IPacketListenerServer.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Network; + +public interface IPacketListenerServer : INetworkEntity +{ + void OnServerPacketArrived(T packet, string from); +} diff --git a/Shared/Network/LiteNetLib/LiteNetLibClient.cs b/Shared/Network/LiteNetLib/LiteNetLibClient.cs index 19a5d32..f18ddde 100644 --- a/Shared/Network/LiteNetLib/LiteNetLibClient.cs +++ b/Shared/Network/LiteNetLib/LiteNetLibClient.cs @@ -8,7 +8,7 @@ public class LiteNetLibClient : LiteNetLibCommunicatorBase, INetworkCommunicator public INetworkCommunicatorClient Connect(string address, int port, string? password = null) { - if (!IsInUniverse) + if (!UniverseObject.IsInUniverse) throw new($"{nameof(LiteNetLibClient)} must be in an universe to connect"); Manager.Start(); diff --git a/Shared/Network/LiteNetLib/LiteNetLibCommunicatorBase.cs b/Shared/Network/LiteNetLib/LiteNetLibCommunicatorBase.cs index ca7fdfc..01928be 100644 --- a/Shared/Network/LiteNetLib/LiteNetLibCommunicatorBase.cs +++ b/Shared/Network/LiteNetLib/LiteNetLibCommunicatorBase.cs @@ -10,7 +10,7 @@ using Syntriax.Engine.Core; namespace Syntriax.Engine.Network; -public abstract class LiteNetLibCommunicatorBase : UniverseObject, INetworkCommunicator +public abstract class LiteNetLibCommunicatorBase : Behaviour, INetworkCommunicator { protected readonly NetPacketProcessor netPacketProcessor = new(); @@ -25,12 +25,12 @@ public abstract class LiteNetLibCommunicatorBase : UniverseObject, INetworkCommu return this; } - protected override void OnEnteringUniverse(IUniverse universe) + protected override void OnEnteredUniverse(IUniverse universe) { universe.OnPreUpdate += PollEvents; } - protected override void OnExitingUniverse(IUniverse universe) + protected override void OnExitedUniverse(IUniverse universe) { universe.OnPreUpdate -= PollEvents; @@ -82,13 +82,11 @@ public abstract class LiteNetLibCommunicatorBase : UniverseObject, INetworkCommu { if (!packetType.IsClass) { - Console.WriteLine($"Registering Nested: {packetType.FullName}"); MethodInfo genericRegisterNestedTypeMethod = registerNestedTypeMethod.MakeGenericMethod(packetType); genericRegisterNestedTypeMethod.Invoke(netPacketProcessor, []); continue; } - Console.WriteLine($"Registering Reusable: {packetType.FullName}"); MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(packetType); Type delegateType = typeof(Action<,>).MakeGenericType(packetType, typeof(NetPeer)); @@ -131,7 +129,6 @@ public abstract class LiteNetLibCommunicatorBase : UniverseObject, INetworkCommu public INetworkCommunicator SubscribeToPackets(Action callback) { Type type = typeof(T); - Console.WriteLine($"Subscribing to: {type.FullName} on {GetType().Name}"); if (!listeners.TryGetValue(type, out List? delegates)) { delegates = []; @@ -145,7 +142,6 @@ public abstract class LiteNetLibCommunicatorBase : UniverseObject, INetworkCommu public INetworkCommunicator UnsubscribeFromPackets(Action callback) { Type type = typeof(T); - Console.WriteLine($"Unsubscribing from: {type.FullName} on {GetType().Name}"); if (!listeners.TryGetValue(type, out List? delegates)) return this; diff --git a/Shared/Network/LiteNetLib/LiteNetLibServer.cs b/Shared/Network/LiteNetLib/LiteNetLibServer.cs index 6efe0e7..13740dd 100644 --- a/Shared/Network/LiteNetLib/LiteNetLibServer.cs +++ b/Shared/Network/LiteNetLib/LiteNetLibServer.cs @@ -30,7 +30,7 @@ public class LiteNetLibServer : LiteNetLibCommunicatorBase, INetworkCommunicator public INetworkCommunicatorServer Start(int port, int maxConnectionCount, string? password = null) { - if (!IsInUniverse) + if (!UniverseObject.IsInUniverse) throw new($"{nameof(LiteNetLibServer)} must be in an universe to start"); Password = password ?? string.Empty; diff --git a/Shared/Network/NetworkManager.cs b/Shared/Network/NetworkManager.cs new file mode 100644 index 0000000..3d1ac1f --- /dev/null +++ b/Shared/Network/NetworkManager.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Network; + +public class NetworkManager : UniverseObject, INetworkManager +{ + private INetworkCommunicator networkCommunicator = null!; + private readonly List<(Type packetType, Delegate callback)> delegates = []; + + public INetworkCommunicator NetworkCommunicator + { + get => networkCommunicator; + set + { + if (networkCommunicator == value) + return; + + var previousCommunicator = networkCommunicator; + networkCommunicator = value; + + if (previousCommunicator is not null) UnsubscribeDelegates(networkCommunicator); + if (networkCommunicator is not null) SubscribeDelegates(networkCommunicator); + } + } + + private readonly Dictionary> clientPacketListeners = []; + private readonly Dictionary> serverPacketListeners = []; + + private readonly Dictionary _networkEntities = []; + public IReadOnlyDictionary NetworkEntities => _networkEntities; + + private readonly BehaviourCollector _networkEntityCollector = new(); + public IBehaviourCollector NetworkEntityCollector => _networkEntityCollector; + + public NetworkManager() + { + CacheDelegates(); + + _networkEntityCollector.OnCollected += OnCollected; + _networkEntityCollector.OnRemoved += OnRemoved; + } + + private void CacheDelegates() + { + // Find network packets implementing EntityDataPacket<> + IEnumerable packetTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => typeof(IEntityNetworkPacket).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract && !t.IsGenericType); + + MethodInfo onPacketArrivedMethod = GetType() + .GetMethod(nameof(OnPacketReceived), BindingFlags.NonPublic | BindingFlags.Instance)!; + + foreach (Type packetType in packetTypes) + { + MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(packetType); + + Type genericDelegateType = typeof(Action<,>).MakeGenericType(packetType, typeof(string)); + Delegate genericPacketReceivedDelegate = Delegate.CreateDelegate(genericDelegateType, this, genericOnPacketArrivedMethod); + + delegates.Add((packetType, genericPacketReceivedDelegate)); + } + } + + private void SubscribeDelegates(INetworkCommunicator networkCommunicator) + { + MethodInfo subscribeToPacketsMethod = typeof(INetworkCommunicator) + .GetMethod(nameof(INetworkCommunicator.SubscribeToPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; + + foreach ((Type packetType, Delegate callback) in delegates) + { + MethodInfo genericSubscribeMethod = subscribeToPacketsMethod.MakeGenericMethod(packetType); + + genericSubscribeMethod.Invoke(networkCommunicator, [callback]); + } + } + + private void UnsubscribeDelegates(INetworkCommunicator networkCommunicator) + { + MethodInfo unsubscribeFromPacketsMethod = typeof(INetworkCommunicator) + .GetMethod(nameof(INetworkCommunicator.UnsubscribeFromPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; + + foreach ((Type packetType, Delegate callback) in delegates) + { + MethodInfo genericUnsubscribeMethod = unsubscribeFromPacketsMethod.MakeGenericMethod(packetType); + + genericUnsubscribeMethod.Invoke(networkCommunicator, [callback]); + } + } + + private void OnPacketReceived(T entityDataPacket, string fromClientId) + { + Type packetType = typeof(T); + + if (networkCommunicator is INetworkCommunicatorClient) + if (clientPacketListeners.TryGetValue(packetType, out List? clientListeners)) + foreach (object clientListener in clientListeners) + { + MethodInfo clientListenerReceiveMethod = typeof(IPacketListenerClient<>).MakeGenericType(packetType).GetMethods().First(m => m.Name == nameof(IPacketListenerClient.OnClientPacketArrived)); + clientListenerReceiveMethod.Invoke(clientListener, [entityDataPacket]); + } + if (networkCommunicator is INetworkCommunicatorServer) + if (serverPacketListeners.TryGetValue(packetType, out List? serverListeners)) + foreach (object serverListener in serverListeners) + { + MethodInfo serverListenerReceiveMethod = typeof(IPacketListenerServer<>).MakeGenericType(packetType).GetMethods().First(m => m.Name == nameof(IPacketListenerServer.OnServerPacketArrived)); + serverListenerReceiveMethod.Invoke(serverListener, [entityDataPacket, fromClientId]); + } + } + + private void OnCollected(IBehaviourCollector sender, INetworkEntity behaviourCollected) + { + if (!_networkEntities.TryAdd(behaviourCollected.Id, behaviourCollected)) + throw new($"Unable to add {behaviourCollected.Id} to {nameof(NetworkManager)}"); + + foreach (Type clientListenerType in behaviourCollected.GetType().GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPacketListenerClient<>))) + { + Type clientListenerParameterType = clientListenerType.GetGenericArguments().First(); + + if (!clientPacketListeners.TryGetValue(clientListenerParameterType, out List? clientListeners)) + { + clientListeners = []; + clientPacketListeners.Add(clientListenerParameterType, clientListeners); + } + + clientListeners.Add(behaviourCollected); + } + + foreach (Type serverListenerType in behaviourCollected.GetType().GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPacketListenerServer<>))) + { + Type serverListenerParameterType = serverListenerType.GetGenericArguments().First(); + + if (!serverPacketListeners.TryGetValue(serverListenerParameterType, out List? serverListeners)) + { + serverListeners = []; + serverPacketListeners.Add(serverListenerParameterType, serverListeners); + } + + serverListeners.Add(behaviourCollected); + } + } + + private void OnRemoved(IBehaviourCollector sender, INetworkEntity behaviourRemoved) + { + _networkEntities.Remove(behaviourRemoved.Id); + } + + protected override void OnExitingUniverse(IUniverse universe) => _networkEntityCollector.Unassign(); + protected override void OnEnteringUniverse(IUniverse universe) + { + _networkEntityCollector.Assign(universe); + NetworkCommunicator = BehaviourController.GetRequiredBehaviourInParent(); + } +}