using System; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; using UnityEngine; // Based on https://github.com/EnlightenedOne/MirrorNetworkDiscovery // forked from https://github.com/in0finite/MirrorNetworkDiscovery // Both are MIT Licensed namespace Mirror.Discovery { /// /// Base implementation for Network Discovery. Extend this component /// to provide custom discovery with game specific data /// NetworkDiscovery for a sample implementation /// [DisallowMultipleComponent] [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-discovery")] public abstract class NetworkDiscoveryBase : MonoBehaviour where Request : NetworkMessage where Response : NetworkMessage { public static bool SupportedOnThisPlatform { get { return Application.platform != RuntimePlatform.WebGLPlayer; } } // each game should have a random unique handshake, this way you can tell if this is the same game or not [HideInInspector] public long secretHandshake; [SerializeField] [Tooltip("The UDP port the server will listen for multi-cast messages")] protected int serverBroadcastListenPort = 47777; [SerializeField] [Tooltip("If true, broadcasts a discovery request every ActiveDiscoveryInterval seconds")] public bool enableActiveDiscovery = true; [SerializeField] [Tooltip("Time in seconds between multi-cast messages")] [Range(1, 60)] float ActiveDiscoveryInterval = 3; protected UdpClient serverUdpClient; protected UdpClient clientUdpClient; #if UNITY_EDITOR void OnValidate() { if (secretHandshake == 0) { secretHandshake = RandomLong(); UnityEditor.Undo.RecordObject(this, "Set secret handshake"); } } #endif public static long RandomLong() { int value1 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); int value2 = UnityEngine.Random.Range(int.MinValue, int.MaxValue); return value1 + ((long)value2 << 32); } /// /// virtual so that inheriting classes' Start() can call base.Start() too /// public virtual void Start() { // Server mode? then start advertising #if UNITY_SERVER AdvertiseServer(); #endif } // Ensure the ports are cleared no matter when Game/Unity UI exits void OnApplicationQuit() { //Debug.Log("NetworkDiscoveryBase OnApplicationQuit"); Shutdown(); } void OnDisable() { //Debug.Log("NetworkDiscoveryBase OnDisable"); Shutdown(); } void OnDestroy() { //Debug.Log("NetworkDiscoveryBase OnDestroy"); Shutdown(); } void Shutdown() { EndpMulticastLock(); if (serverUdpClient != null) { try { serverUdpClient.Close(); } catch (Exception) { // it is just close, swallow the error } serverUdpClient = null; } if (clientUdpClient != null) { try { clientUdpClient.Close(); } catch (Exception) { // it is just close, swallow the error } clientUdpClient = null; } CancelInvoke(); } #region Server /// /// Advertise this server in the local network /// public void AdvertiseServer() { if (!SupportedOnThisPlatform) throw new PlatformNotSupportedException("Network discovery not supported in this platform"); StopDiscovery(); // Setup port -- may throw exception serverUdpClient = new UdpClient(serverBroadcastListenPort) { EnableBroadcast = true, MulticastLoopback = false }; // listen for client pings _ = ServerListenAsync(); } public async Task ServerListenAsync() { BeginMulticastLock(); while (true) { try { await ReceiveRequestAsync(serverUdpClient); } catch (ObjectDisposedException) { // socket has been closed break; } catch (Exception) { } } } async Task ReceiveRequestAsync(UdpClient udpClient) { // only proceed if there is available data in network buffer, or otherwise Receive() will block // average time for UdpClient.Available : 10 us UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync(); using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer)) { long handshake = networkReader.ReadLong(); if (handshake != secretHandshake) { // message is not for us throw new ProtocolViolationException("Invalid handshake"); } Request request = networkReader.Read(); ProcessClientRequest(request, udpReceiveResult.RemoteEndPoint); } } /// /// Reply to the client to inform it of this server /// /// /// Override if you wish to ignore server requests based on /// custom criteria such as language, full server game mode or difficulty /// /// Request coming from client /// Address of the client that sent the request protected virtual void ProcessClientRequest(Request request, IPEndPoint endpoint) { Response info = ProcessRequest(request, endpoint); if (info == null) return; using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter()) { try { writer.WriteLong(secretHandshake); writer.Write(info); ArraySegment data = writer.ToArraySegment(); // signature matches // send response serverUdpClient.Send(data.Array, data.Count, endpoint); } catch (Exception ex) { Debug.LogException(ex, this); } } } /// /// Process the request from a client /// /// /// Override if you wish to provide more information to the clients /// such as the name of the host player /// /// Request coming from client /// Address of the client that sent the request /// The message to be sent back to the client or null protected abstract Response ProcessRequest(Request request, IPEndPoint endpoint); // Android Multicast fix: https://github.com/vis2k/Mirror/pull/2887 #if UNITY_ANDROID AndroidJavaObject multicastLock; bool hasMulticastLock; #endif void BeginMulticastLock() { #if UNITY_ANDROID if (hasMulticastLock) return; if (Application.platform == RuntimePlatform.Android) { using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic("currentActivity")) { using (var wifiManager = activity.Call("getSystemService", "wifi")) { multicastLock = wifiManager.Call("createMulticastLock", "lock"); multicastLock.Call("acquire"); hasMulticastLock = true; } } } #endif } void EndpMulticastLock() { #if UNITY_ANDROID if (!hasMulticastLock) return; multicastLock?.Call("release"); hasMulticastLock = false; #endif } #endregion #region Client /// /// Start Active Discovery /// public void StartDiscovery() { if (!SupportedOnThisPlatform) throw new PlatformNotSupportedException("Network discovery not supported in this platform"); StopDiscovery(); try { // Setup port clientUdpClient = new UdpClient(0) { EnableBroadcast = true, MulticastLoopback = false }; } catch (Exception) { // Free the port if we took it //Debug.LogError("NetworkDiscoveryBase StartDiscovery Exception"); Shutdown(); throw; } _ = ClientListenAsync(); if (enableActiveDiscovery) InvokeRepeating(nameof(BroadcastDiscoveryRequest), 0, ActiveDiscoveryInterval); } /// /// Stop Active Discovery /// public void StopDiscovery() { //Debug.Log("NetworkDiscoveryBase StopDiscovery"); Shutdown(); } /// /// Awaits for server response /// /// ClientListenAsync Task public async Task ClientListenAsync() { // while clientUpdClient to fix: // https://github.com/vis2k/Mirror/pull/2908 // // If, you cancel discovery the clientUdpClient is set to null. // However, nothing cancels ClientListenAsync. If we change the if(true) // to check if the client is null. You can properly cancel the discovery, // and kill the listen thread. // // Prior to this fix, if you cancel the discovery search. It crashes the // thread, and is super noisy in the output. As well as causes issues on // the quest. while (clientUdpClient != null) { try { await ReceiveGameBroadcastAsync(clientUdpClient); } catch (ObjectDisposedException) { // socket was closed, no problem return; } catch (Exception ex) { Debug.LogException(ex); } } } /// /// Sends discovery request from client /// public void BroadcastDiscoveryRequest() { if (clientUdpClient == null) return; if (NetworkClient.isConnected) { StopDiscovery(); return; } IPEndPoint endPoint = new IPEndPoint(IPAddress.Broadcast, serverBroadcastListenPort); using (PooledNetworkWriter writer = NetworkWriterPool.GetWriter()) { writer.WriteLong(secretHandshake); try { Request request = GetRequest(); writer.Write(request); ArraySegment data = writer.ToArraySegment(); clientUdpClient.SendAsync(data.Array, data.Count, endPoint); } catch (Exception) { // It is ok if we can't broadcast to one of the addresses } } } /// /// Create a message that will be broadcasted on the network to discover servers /// /// /// Override if you wish to include additional data in the discovery message /// such as desired game mode, language, difficulty, etc... /// An instance of ServerRequest with data to be broadcasted protected virtual Request GetRequest() => default; async Task ReceiveGameBroadcastAsync(UdpClient udpClient) { // only proceed if there is available data in network buffer, or otherwise Receive() will block // average time for UdpClient.Available : 10 us UdpReceiveResult udpReceiveResult = await udpClient.ReceiveAsync(); using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(udpReceiveResult.Buffer)) { if (networkReader.ReadLong() != secretHandshake) return; Response response = networkReader.Read(); ProcessResponse(response, udpReceiveResult.RemoteEndPoint); } } /// /// Process the answer from a server /// /// /// A client receives a reply from a server, this method processes the /// reply and raises an event /// /// Response that came from the server /// Address of the server that replied protected abstract void ProcessResponse(Response response, IPEndPoint endpoint); #endregion } }