diff --git a/Engine.Systems/Network/Abstract/IConnection.cs b/Engine.Systems/Network/Abstract/IConnection.cs new file mode 100644 index 0000000..30aeecf --- /dev/null +++ b/Engine.Systems/Network/Abstract/IConnection.cs @@ -0,0 +1,12 @@ +namespace Syntriax.Engine.Systems.Network; + +public interface IConnection +{ + string Id { get; } + + float Ping { get; } + float RoundTrip { get; } + + int PingMs { get; } + int RoundTripMs { get; } +} diff --git a/Engine.Systems/Network/Abstract/IEntityNetworkPacket.cs b/Engine.Systems/Network/Abstract/IEntityNetworkPacket.cs new file mode 100644 index 0000000..7d1ee2a --- /dev/null +++ b/Engine.Systems/Network/Abstract/IEntityNetworkPacket.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Systems.Network; + +public interface IEntityNetworkPacket : INetworkPacket +{ + string EntityId { get; } +} diff --git a/Engine.Systems/Network/Abstract/INetworkCommunicator.cs b/Engine.Systems/Network/Abstract/INetworkCommunicator.cs new file mode 100644 index 0000000..95e28a1 --- /dev/null +++ b/Engine.Systems/Network/Abstract/INetworkCommunicator.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Systems.Network; + +public interface INetworkCommunicator +{ + Event OnConnectionEstablished { get; } + Event OnConnectionAbolished { get; } + + IReadOnlyDictionary Connections { get; } + + INetworkCommunicator Stop(); + + INetworkCommunicator SubscribeToPackets(Event.EventHandler callback); + INetworkCommunicator UnsubscribeFromPackets(Event.EventHandler callback); +} + +public interface INetworkCommunicatorClient : INetworkCommunicator +{ + INetworkCommunicatorClient Connect(string address, int port, string? password = null); + + INetworkCommunicatorClient SendToServer(T packet, PacketDelivery packetDelivery = PacketDelivery.ReliableInOrder) where T : class, new(); +} + +public interface INetworkCommunicatorServer : INetworkCommunicator +{ + string Password { get; } + int MaxConnectionCount { get; } + int Port { get; } + + INetworkCommunicatorServer Start(int port, int maxConnectionCount, string? password = null); + + INetworkCommunicatorServer SendToClient(IConnection connection, T packet, PacketDelivery packetDelivery = PacketDelivery.ReliableInOrder) where T : class, new(); + INetworkCommunicatorServer SendToAll(T packet, PacketDelivery packetDelivery = PacketDelivery.ReliableInOrder) where T : class, new(); +} diff --git a/Engine.Systems/Network/Abstract/INetworkEntity.cs b/Engine.Systems/Network/Abstract/INetworkEntity.cs new file mode 100644 index 0000000..1e6a1f6 --- /dev/null +++ b/Engine.Systems/Network/Abstract/INetworkEntity.cs @@ -0,0 +1,5 @@ +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Systems.Network; + +public interface INetworkEntity : IEntity; diff --git a/Engine.Systems/Network/Abstract/INetworkManager.cs b/Engine.Systems/Network/Abstract/INetworkManager.cs new file mode 100644 index 0000000..3f9c950 --- /dev/null +++ b/Engine.Systems/Network/Abstract/INetworkManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Systems.Network; + +public interface INetworkManager +{ + IReadOnlyDictionary NetworkEntities { get; } + IBehaviourCollector NetworkEntityCollector { get; } +} diff --git a/Engine.Systems/Network/Abstract/INetworkPacket.cs b/Engine.Systems/Network/Abstract/INetworkPacket.cs new file mode 100644 index 0000000..042f14d --- /dev/null +++ b/Engine.Systems/Network/Abstract/INetworkPacket.cs @@ -0,0 +1,3 @@ +namespace Syntriax.Engine.Systems.Network; + +public interface INetworkPacket; diff --git a/Engine.Systems/Network/Abstract/IPacketListenerClient.cs b/Engine.Systems/Network/Abstract/IPacketListenerClient.cs new file mode 100644 index 0000000..bfe39d7 --- /dev/null +++ b/Engine.Systems/Network/Abstract/IPacketListenerClient.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Systems.Network; + +public interface IPacketListenerClient : INetworkEntity +{ + void OnClientPacketArrived(IConnection sender, T packet); +} diff --git a/Engine.Systems/Network/Abstract/IPacketListenerServer.cs b/Engine.Systems/Network/Abstract/IPacketListenerServer.cs new file mode 100644 index 0000000..b084949 --- /dev/null +++ b/Engine.Systems/Network/Abstract/IPacketListenerServer.cs @@ -0,0 +1,6 @@ +namespace Syntriax.Engine.Systems.Network; + +public interface IPacketListenerServer : INetworkEntity +{ + void OnServerPacketArrived(IConnection sender, T packet); +} diff --git a/Engine.Systems/Network/Abstract/PacketDelivery.cs b/Engine.Systems/Network/Abstract/PacketDelivery.cs new file mode 100644 index 0000000..2fce6d0 --- /dev/null +++ b/Engine.Systems/Network/Abstract/PacketDelivery.cs @@ -0,0 +1,9 @@ +namespace Syntriax.Engine.Systems.Network; + +public enum PacketDelivery +{ + ReliableInOrder, + ReliableOutOfOrder, + UnreliableInOrder, + UnreliableOutOfOrder, +}; diff --git a/Engine.Systems/Network/NetworkManager.cs b/Engine.Systems/Network/NetworkManager.cs new file mode 100644 index 0000000..915782b --- /dev/null +++ b/Engine.Systems/Network/NetworkManager.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +using Syntriax.Engine.Core; + +namespace Syntriax.Engine.Systems.Network; + +/// +/// Intermediary manager that looks up in it's hierarchy for a to route/broadcast it's received packets to their destinations. +/// +public class NetworkManager : Behaviour, INetworkManager +{ + private readonly Dictionary>> clientPacketArrivalMethods = []; + private readonly Dictionary>> serverPacketArrivalMethods = []; + + private readonly Dictionary> clientPacketRouters = []; + private readonly Dictionary> serverPacketRouters = []; + + private readonly List<(Type packetType, Delegate @delegate)> packetRetrievalDelegates = []; + + private readonly Dictionary clearRoutesMethods = []; + private readonly Dictionary registerPacketListenersMethods = []; + + private readonly Dictionary _networkEntities = []; + public IReadOnlyDictionary NetworkEntities => _networkEntities; + + private readonly BehaviourCollector _networkEntityCollector = new(); + public IBehaviourCollector NetworkEntityCollector => _networkEntityCollector; + + private INetworkCommunicator _networkCommunicator = null!; + public INetworkCommunicator NetworkCommunicator + { + get => _networkCommunicator; + set + { + if (_networkCommunicator == value) + return; + + INetworkCommunicator? previousCommunicator = _networkCommunicator; + _networkCommunicator = value; + + if (previousCommunicator is not null) UnsubscribeCommunicatorMethods(previousCommunicator); + if (_networkCommunicator is not null) SubscribeCommunicatorMethods(_networkCommunicator); + } + } + + #region Communicator Subscriptions + private void SubscribeCommunicatorMethods(INetworkCommunicator networkCommunicator) + { + MethodInfo subscribeToPacketsMethod = typeof(INetworkCommunicator) + .GetMethod(nameof(INetworkCommunicator.SubscribeToPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; + + foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates) + { + MethodInfo genericSubscribeMethod = subscribeToPacketsMethod.MakeGenericMethod(packetType); + genericSubscribeMethod.Invoke(networkCommunicator, [@delegate]); + } + } + + private void UnsubscribeCommunicatorMethods(INetworkCommunicator networkCommunicator) + { + MethodInfo unsubscribeFromPacketsMethod = typeof(INetworkCommunicator) + .GetMethod(nameof(INetworkCommunicator.UnsubscribeFromPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!; + + foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates) + { + MethodInfo genericUnsubscribeMethod = unsubscribeFromPacketsMethod.MakeGenericMethod(packetType); + genericUnsubscribeMethod.Invoke(networkCommunicator, [@delegate]); + } + } + #endregion + + #region Packet Routing + private void OnPacketReceived(IConnection sender, T entityDataPacket) + { + if (entityDataPacket is IEntityNetworkPacket entityPacket) + RoutePacket(sender, entityDataPacket, entityPacket); + else + BroadcastPacket(sender, entityDataPacket); + } + + private void RoutePacket(IConnection sender, T entityDataPacket, IEntityNetworkPacket entityPacket) + { + if (NetworkCommunicator is INetworkCommunicatorClient) + RoutePacket(clientPacketRouters, entityPacket.EntityId, sender, entityDataPacket); + if (NetworkCommunicator is INetworkCommunicatorServer) + RoutePacket(serverPacketRouters, entityPacket.EntityId, sender, entityDataPacket); + } + + private void BroadcastPacket(IConnection sender, T entityDataPacket) + { + if (NetworkCommunicator is INetworkCommunicatorClient) + BroadcastPacket(clientPacketRouters, sender, entityDataPacket); + if (NetworkCommunicator is INetworkCommunicatorServer) + BroadcastPacket(serverPacketRouters, sender, entityDataPacket); + } + + private static void BroadcastPacket( + Dictionary> packetRouters, + IConnection sender, + T entityDataPacket) + { + if (!packetRouters.TryGetValue(entityDataPacket!.GetType(), out Dictionary? routers)) + return; + + foreach ((string behaviourId, object routerEventReference) in routers) + { + Event routerEvent = (Event)routerEventReference; + routerEvent.Invoke(sender, entityDataPacket!); + } + } + + private static void RoutePacket( + Dictionary> packetRouters, + string entityId, + IConnection sender, + T entityDataPacket) + { + if (!packetRouters.TryGetValue(entityDataPacket!.GetType(), out Dictionary? routers)) + return; + + if (!routers.TryGetValue(entityId, out object? routerEventReference)) + return; + + Event routerEvent = (Event)routerEventReference; + routerEvent.Invoke(sender, entityDataPacket!); + } + #endregion + + #region Packet Routers + private void RegisterPacketRoutersFor( + INetworkEntity behaviour, + Dictionary> packetRouters, + Dictionary>> packetArrivalMethods, + NetworkType networkType) + { + if (!packetArrivalMethods.TryGetValue(behaviour.GetType(), out Dictionary>? arrivalMethods)) + return; + + foreach ((Type packetType, List methods) in arrivalMethods) + foreach (MethodInfo receiveMethod in methods) + { + if (!packetRouters.TryGetValue(packetType, out Dictionary? routers)) + { + routers = []; + packetRouters.Add(packetType, routers); + } + + object packetListenerEvent = CreateEventAndRegister(packetType, behaviour, networkType); + routers.Add(behaviour.Id, packetListenerEvent); + } + } + + private object CreateEventAndRegister(Type packetType, INetworkEntity behaviour, NetworkType networkType) + { + Type genericEventType = typeof(Event<,>).MakeGenericType(typeof(IConnection), packetType); + object packetListenerEvent = Activator.CreateInstance(genericEventType)!; + + if (!registerPacketListenersMethods.TryGetValue(packetType, out MethodInfo? registerPacketListenerMethod)) + throw new($"{nameof(RegisterPacketListenerEvent)} for {packetType.Name} has not been cached."); + + registerPacketListenerMethod.Invoke(this, [behaviour, packetListenerEvent, networkType]); + return packetListenerEvent; + } + + private static void RegisterPacketListenerEvent( + INetworkEntity behaviour, + Event packetListenerEvent, + NetworkType networkType) + { + switch (networkType) + { + case NetworkType.Client: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerClient)behaviour).OnClientPacketArrived(sender, packet)); break; + case NetworkType.Server: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerServer)behaviour).OnServerPacketArrived(sender, packet)); break; + } + } + + private void UnregisterPacketRoutersFor( + INetworkEntity behaviour, + Dictionary> packetRouters, + Dictionary>> packetArrivalMethods) + { + if (!packetArrivalMethods.TryGetValue(behaviour.GetType(), out Dictionary>? arrivalMethods)) + return; + + foreach ((Type packetType, List methods) in arrivalMethods) + { + if (!packetRouters.TryGetValue(packetType, out Dictionary? routers)) + continue; + + if (!routers.TryGetValue(behaviour.Id, out object? routerEventReference)) + continue; + + if (!clearRoutesMethods.TryGetValue(packetType, out MethodInfo? clearRouterMethod)) + continue; + + clearRouterMethod.Invoke(this, [routerEventReference]); + } + } + + private static void ClearRouter(object routerEventReference) + { + Event routerEvent = (Event)routerEventReference; + routerEvent.Clear(); + } + + #endregion + + #region Engine Callbacks + private void OnCollected(IBehaviourCollector sender, IBehaviourCollector.BehaviourCollectedArguments args) + { + INetworkEntity collectedBehaviour = args.BehaviourCollected; + + if (!_networkEntities.TryAdd(collectedBehaviour.Id, collectedBehaviour)) + throw new($"Unable to add {collectedBehaviour.Id} to {nameof(NetworkManager)}"); + + RegisterPacketRoutersFor(collectedBehaviour, clientPacketRouters, clientPacketArrivalMethods, NetworkType.Client); + RegisterPacketRoutersFor(collectedBehaviour, serverPacketRouters, serverPacketArrivalMethods, NetworkType.Server); + } + + private void OnRemoved(IBehaviourCollector sender, IBehaviourCollector.BehaviourRemovedArguments args) + { + INetworkEntity removedBehaviour = args.BehaviourRemoved; + if (!_networkEntities.Remove(args.BehaviourRemoved.Id)) + return; + + UnregisterPacketRoutersFor(removedBehaviour, clientPacketRouters, clientPacketArrivalMethods); + UnregisterPacketRoutersFor(removedBehaviour, serverPacketRouters, serverPacketArrivalMethods); + } + + protected override void OnExitedUniverse(IUniverse universe) => _networkEntityCollector.Unassign(); + protected override void OnEnteredUniverse(IUniverse universe) + { + _networkEntityCollector.Assign(universe); + NetworkCommunicator = BehaviourController.GetRequiredBehaviourInParent(); + } + #endregion + + #region Initialization + public NetworkManager() + { + CachePacketRetrievalDelegates(); + CacheRegistrationMethods(); + CachePacketArrivalMethods(); + + _networkEntityCollector.OnCollected.AddListener(OnCollected); + _networkEntityCollector.OnRemoved.AddListener(OnRemoved); + } + + private void CachePacketRetrievalDelegates() + { + IEnumerable packetTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()) + .Where(t => typeof(INetworkPacket).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(Event<,>.EventHandler).MakeGenericType(typeof(IConnection), packetType); + Delegate genericPacketReceivedDelegate = Delegate.CreateDelegate(genericDelegateType, this, genericOnPacketArrivedMethod); + + packetRetrievalDelegates.Add((packetType, genericPacketReceivedDelegate)); + } + } + + private void CacheRegistrationMethods() + { + CacheRegistrationMethods(registerPacketListenersMethods, nameof(RegisterPacketListenerEvent)); + CacheRegistrationMethods(clearRoutesMethods, nameof(ClearRouter)); + } + + private void CacheRegistrationMethods(Dictionary registrationMethods, string methodName) + { + MethodInfo registerPacketMethod = typeof(NetworkManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static)!; + foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates) + { + MethodInfo genericMethod = registerPacketMethod.MakeGenericMethod(packetType); + registrationMethods.Add(packetType, genericMethod); + } + } + + private void CachePacketArrivalMethods() + { + CachePacketArrivalMethods(clientPacketArrivalMethods, typeof(IPacketListenerClient<>), nameof(IPacketListenerClient.OnClientPacketArrived)); + CachePacketArrivalMethods(serverPacketArrivalMethods, typeof(IPacketListenerServer<>), nameof(IPacketListenerServer.OnServerPacketArrived)); + } + + private static void CachePacketArrivalMethods(Dictionary>> packetArrivalMethods, Type listenerType, string packetArrivalMethodName) + { + foreach (Type listenerClass in GetGenericsWith(listenerType)) + { + Dictionary> packetRouters = []; + packetArrivalMethods.Add(listenerClass, packetRouters); + + foreach (Type packetListener in GetGenericInterfacesWith(listenerType, listenerClass)) + { + Type packetType = packetListener.GetGenericArguments().First(); + + List arrivalMethods = packetListener + .GetMethods() + .Where(m => m.Name == packetArrivalMethodName) + .ToList(); + + packetRouters.Add(packetType, arrivalMethods); + } + } + } + + private static IEnumerable GetGenericsWith(Type type) + => AppDomain.CurrentDomain + .GetAssemblies() + .SelectMany(a => + a.GetTypes().Where( + t => t.GetInterfaces().Any( + i => i.IsGenericType && i.GetGenericTypeDefinition() == type + ) + ) + ); + + private static IEnumerable GetGenericInterfacesWith(Type interfaceType, Type type) + => type.GetInterfaces().Where( + i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType + ); + #endregion + + private enum NetworkType { Client, Server } +}