using Unity.Netcode.Transports.UTP; using Unity.Netcode; using NobleConnect.Ice; using System; using System.Collections.Generic; using System.Net; using UnityEngine; using System.Reflection; using Unity.Networking.Transport; using NetworkEvent = Unity.Netcode.NetworkEvent; using System.Collections; namespace NobleConnect.NetCodeForGameObjects { /// Extends UnityTransport to use Noble Connect for punchthrough and relays public class NobleUnityTransport : UnityTransport { /// Some useful configuration settings like geographic region and timeouts. [Header("Noble Connect Settings")] public Config Config; /// You can enable this to force relay connections to be used for testing purposes. /// /// Disables punchthrough and direct connections. Forces connections to use the relays. /// This is useful if you want to test your game with the unavoidable latency that is /// introduced when the relay servers are used. /// public bool ForceRelayOnly { get => Config.ForceRelayOnly; set => Config.ForceRelayOnly = value; } /// This is the address that clients should connect to. It is assigned by the relay server. /// /// Note that this is not the host's actual IP address, but one assigned to the host by the relay server. /// When clients connect to this address, Noble Connect will find the best possible connection and use it. /// This means that the client may actually end up connecting to an address on the local network, or an address /// on the router, or an address on the relay. But you don't need to worry about any of that, it is all /// handled for you internally. /// public IPEndPoint HostRelayEndPoint; /// You can check this on the client after they connect, it will either be Direct, Punchthrough, or Relay. public ConnectionType LatestConnectionType { get { if (Peer != null) return Peer.latestConnectionType; else return ConnectionType.NONE; } } /// Use this callback to be informed when something goes horribly wrong. /// /// You should see an error in your console with more info any time this is called. Generally /// it will either mean you've completely lost connection to the relay server or you /// have exceeded your CCU or bandwidth limit. /// public event Action OnFatalErrorCallback; /// Use this callback to know when a Server has received their HostRelayEndPoint and is ready to receive incoming connections. /// /// If you are using some sort matchmaking this is a good time to create a match now that you have the HostRelayEndPoint that clients will need to connect to. /// /// The address of the HostRelayEndPoint the clients should use when connecting to the host. /// The port of the HostRelayEndPoint that clients should use when connecting to the host public event Action OnServerPreparedCallback; public ConnectionType ConnectionType => Peer.latestConnectionType; /// Keeps track of which end point each connection belongs to so that when they disconnect we can clean up. Dictionary EndPointByConnection = new Dictionary(); /// Represents a peer (client or server) in Noble Connect. Handles creating and destroying connection routes. /// /// This is the interface to the relay and punchthrough services. /// It is used to find the best route to connect and to clean up when a client disconnects. /// Peer Peer; /// This delegate allows us to call the private base.Update method Action BaseUpdateDelegate; /// This delegate allows us to call the private ParseClientId method Func ParseClientIdDelegate; /// Used to get the remote address of connecting clients FieldInfo DriverField; private void Awake() { // Set up logging using the LogLevel from the NetworkManager Logger.logger = Debug.Log; Logger.warnLogger = Debug.LogWarning; Logger.errorLogger = Debug.LogError; switch (NetworkManager.Singleton.LogLevel) { case LogLevel.Developer: Logger.logLevel = Logger.Level.Developer; break; case LogLevel.Error: Logger.logLevel = Logger.Level.Error; break; case LogLevel.Normal: Logger.logLevel = Logger.Level.Info; break; case LogLevel.Nothing: Logger.logLevel = Logger.Level.Fatal; break; } // The base update method is inaccessible but we need it to be called, so use reflection var baseUpdateMethod = typeof(UnityTransport).GetMethod("Update", BindingFlags.NonPublic | BindingFlags.Instance); // Creating a delegate allows for faster method calls than Invoke, and we call Update a lot, so let's do that BaseUpdateDelegate = (Action)Delegate.CreateDelegate(typeof(Action), this, baseUpdateMethod); // We need access to the private m_Driver field in order to get the remote address of clients DriverField = typeof(UnityTransport).GetField("m_Driver", BindingFlags.NonPublic | BindingFlags.Instance); // We need this private method to convert the ulong network id into a NetworkConnection MethodInfo ParseClientIdMethod = typeof(UnityTransport).GetMethod("ParseClientId", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(ulong) }, null); ParseClientIdDelegate = (Func)Delegate.CreateDelegate(typeof(Func), ParseClientIdMethod); // Set up the callbacks we need OnFatalErrorCallback += OnFatalError; OnServerPreparedCallback += OnServerPrepared; Config.OnFatalError = OnFatalErrorCallback; // The Unity Transport apparently does not support ipv6, so disable it Config.EnableIPv6 = false; // Hook in to the transport level events so we can know when a client connects / disconnects OnTransportEvent += OnReceivedTransportEvent; } public override void Initialize(NetworkManager netMan) { // Initialize the Peer Peer = new Peer(Config.AsIceConfig()); base.Initialize(netMan); } /// Start a server and allocate a relay. /// /// When the server has received a relay address the OnServerPreparedCallback will be triggered. /// This callback is a good place to do things like create a match in your matchmaking system of choice, /// or anything else that you want to do as soon as you have the host's address. /// public override bool StartServer() { bool success = base.StartServer(); Peer.InitializeHosting(ConnectionData.Port, OnServerPreparedCallback); return success; } /// Start a client and connect to ConnectionData.Address at ConnectionData.Port /// /// ConnectionData.Address and ConnectionData.Port should be set to a host's HostRelayEndPoint before calling this method. /// public override bool StartClient() { Peer.InitializeClient(new IPEndPoint(IPAddress.Parse(ConnectionData.Address), ConnectionData.Port), OnClientPrepared); return true; } /// Shut down, disconnect, and clean up public override void Shutdown() { base.Shutdown(); if (Peer != null) { Peer.CleanUpEverything(); Peer.Dispose(); Peer = null; } } /// Calls the Peer's Update() method to process messages. Also calls the base Update method via reflection void Update() { if (Peer != null) { Peer.Update(); } // Equivalent to base.Update() BaseUpdateDelegate(); } /// Transport level events are received here. Used to handle client connect / disconnect /// The type of NetworkEvent that occurred /// The network id of the client that instigated the event /// Any payload related to the event /// The time that the event was triggered private void OnReceivedTransportEvent(NetworkEvent eventType, ulong clientId, ArraySegment payload, float receiveTime) { if (NetworkManager.Singleton.IsServer) { if (eventType == NetworkEvent.Connect) { OnIncomingClientConnection(clientId); } else if (eventType == NetworkEvent.Disconnect) { OnServerLostClient(clientId); } } } /// Keep track of the incoming client and their associated end point /// /// We need to know the disconnecting client's EndPoint in order to clean up properly when they disconnect. /// Ideally we would just look this up via the transport when the disconnect happens, but by the time we get the event the transport has already /// purged that info, so instead we store it ourselves on connect so we can look it up on disconnect. /// /// The network Id of the disconnecting client void OnIncomingClientConnection(ulong clientId) { var clientEndPoint = GetClientEndPoint(clientId); EndPointByConnection[clientId] = clientEndPoint; } /// Clean up resources associated with the disconnecting client /// /// This uses the end point that was associated with the clientId in OnIncomingClientConnection /// /// The network Id of the disconnecting client void OnServerLostClient(ulong clientId) { if (EndPointByConnection.ContainsKey(clientId)) { IPEndPoint endPoint = EndPointByConnection[clientId]; Peer.EndSession(endPoint); EndPointByConnection.Remove(clientId); } } /// Called when the client has been allocated a relay. This is where the Transport level connection starts. /// The IPv4 EndPoint to connect to /// The IPv6 EndPoint to connect to. Not used here. void OnClientPrepared(IPEndPoint bridgeEndPoint, IPEndPoint bridgeEndPointIPv6) { ConnectionData.Address = bridgeEndPoint.Address.ToString(); ConnectionData.Port = (ushort)bridgeEndPoint.Port; StartCoroutine(ConnectEventually()); } IEnumerator ConnectEventually() { yield return new WaitForSeconds(1); base.StartClient(); } /// Called when the server has been allocated a relay and is ready to receive incoming connections /// The host relay address that clients should connect to /// The host relay port that clients should connect to void OnServerPrepared(string address, ushort port) { HostRelayEndPoint = new IPEndPoint(IPAddress.Parse(address), port); } /// If anythin goes horribly wrong, stop hosting / disconnect. /// The error message from Noble Connect void OnFatalError(string errorMessage) { NetworkManager.Singleton.Shutdown(); } /// Get a client's end point from their network id /// The network id of the client /// The IPEndPoint of the client IPEndPoint GetClientEndPoint(ulong clientId) { var driver = (NetworkDriver)DriverField.GetValue(this); var clientNetworkConnection = ParseClientIdDelegate(clientId); var remoteEndPoint = driver.RemoteEndPoint(clientNetworkConnection); var ip = new IPAddress(remoteEndPoint.GetRawAddressBytes().ToArray()); var port = remoteEndPoint.Port; return new IPEndPoint(ip, port); } } }