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