/* * MIT License * * Copyright (c) 2020 Tom Weiland * Copyright (c) 2022 Vincent Dowling * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ using System; using System.Collections.Generic; using System.Threading; using JetBrains.Annotations; using Scripts.CustomServer; using UnityEngine; using Client = Scripts.CustomServer.Client; // ReSharper disable once CheckNamespace namespace Scripts.Matchmaker { public interface IMatchmaker { /************************************************************************************************************** * Matchmaker Programming Interface - External **************************************************************************************************************/ // Global Variables -------------------------------------------------------------------------------------------- public static Client listClient; public static Client lobbyClient; public static int port = Config.Port; public static string ip = Config.Address; public static string GameId = Config.GameId; public static readonly Client.ClientPackets apiPackets = new Client.ClientPackets( new Dictionary() { { 10, API.APIPacketHandlers.ReceiveLobbyId }, { 11, API.APIPacketHandlers.GetLobbyAttributesReceived }, { 12, API.APIPacketHandlers.FinishedSendingLobbyIds }, }); // API Functions - Connection Management ----------------------------------------------------------------------- /// /// MatchmakerNode_Connect the List Client. /// public static bool Connect() { if (listClient != null) { listClient.ip = ip; listClient.port = port; listClient.welcomeReceived = false; if (!listClient.ConnectToServer(apiPackets)) return false; var startTime = DateTime.Now.Ticks; while (listClient.welcomeReceived == false && ((DateTime.Now.Ticks - startTime) < 30000000)) { } if ((DateTime.Now.Ticks - startTime) < 30000000) return true; Debug.LogError("Failed to connect to List Server: Connection timed out!"); } else { Debug.LogError( "Attempted to connect to the List Server while listClient is null. Please set Matchmaker.listClient first!"); } return false; } /// /// MatchmakerNode_Connect the Lobby Client. /// public static bool ConnectLobby() { lobbyClient.ip = ip; lobbyClient.port = port + 2; lobbyClient.welcomeReceived = false; if (lobbyClient.ConnectToServer(new Client.ClientPackets(new Dictionary()))) { var startTime = DateTime.Now.Ticks; while (lobbyClient.welcomeReceived == false && ((DateTime.Now.Ticks - startTime) < 30000000)) { } if ((DateTime.Now.Ticks - startTime) < 30000000) return true; Debug.LogError("Failed to connect to to Lobby Server: Connection timed out!"); return false; } return false; } /// /// Disconnect the List Client. /// public static void Disconnect() { if (listClient != null) { listClient.Disconnect(); } else { Debug.LogWarning( "Attempted to disconnect a List Client while listClient is null. Please set Matchmaker.listClient first!"); } } /// /// Disconnect the Lobby Client. /// public static void DisconnectLobby() { Debug.Log("Disconnecting from Lobby server..."); // SetLobbyAttribute("_auto_uuid", "DISCONNECTED_LOBBY"); // SetLobbyAttribute("_auto_ip", "DISCONNECTED_LOBBY"); // SetLobbyAttribute("_auto_port", "DISCONNECTED_LOBBY"); lobbyClient.Disconnect(); } // API Functions - Client Attributes --------------------------------------------------------------------------- /// /// Set an Attribute on the List Client. /// /// Name of the Attribute to set /// Value of the Attribute to set to public static bool SetClientAttribute(string name, string value) { if (listClient != null) { if (listClient.IsConnected) { if (ClientSend.SetClientAttribute(listClient, name, value)) { return true; } } } else { Debug.LogWarning( "Attempted to set a List Client Attribute while listClient is null. Please set Matchmaker.listClient first!"); } return false; } /// /// Get a List Client's Attribute. /// /// What List Client to find the Attribute in /// Name of the Attribute to get. /// The value of the Attribute. Returns null on failure. public static string GetClientAttribute(int clientId, string name) { if (listClient != null) { if (listClient.IsConnected) { var foo = ClientSend.GetClientAttribute(listClient, clientId, name); // Debug check - Is the received Attributes Name not the same as the requested one? if (foo.name == name) return foo.value; Debug.LogError("Requested Attribute Name and Received Attribute Name do not match!"); return null; } } else { Debug.LogWarning( "Attempted to get a List Client Attribute while listClient is null. Please set Matchmaker.listClient first!"); } return null; } // API Functions - Lobby Attributes ---------------------------------------------------------------------------- /// /// Get a Lobby Client's Attribute straight from the Lobby Server. /// /// What Lobby Client to find the Attribute in /// Name of the Attribute to get. /// The value of the Attribute. Returns null on failure. public static string GetLobbyAttributeDirect(string name) { if (lobbyClient != null) { if (lobbyClient.IsConnected) { var foo = ClientSend.GetClientAttribute(lobbyClient, lobbyClient.myId, name); // Debug check - Is the received Attributes Name not the same as the requested one? if (foo.name == name) return foo.value; Debug.LogError("Requested Attribute Name and Received Attribute Name do not match!"); return null; } } else { Debug.LogWarning( "Attempted to get a Lobby Client's Attribute *directly* while lobbyClient is null. Please set Matchmaker.lobbyClient first!"); } return null; } /// /// Set a Attribute on the Lobby Client. /// /// Name of the Attribute to set. /// What value to set the Attribute to. public static bool SetLobbyAttribute(string name, string value) { if (lobbyClient != null) { if (lobbyClient.IsConnected) { return ClientSend.SetClientAttribute(lobbyClient, name, value); } } else { Debug.LogWarning( "Attempted to set a Lobby Client Attribute while lobbyClient is null. Please set Matchmaker.lobbyClient first!"); } return false; } /// /// Get a Lobby Client Attribute by passing through the List Server. /// /// What Lobby Client to find the Attribute in /// Name of the Attribute to get. /// The value of the Attribute. Returns null on failure. public static string GetLobbyAttribute(int clientId, string name) { if (listClient != null) { if (listClient.IsConnected) { var foo = API.APIClientPackets.GetLobbyAttribute(listClient, clientId, name); if (foo == null) { Debug.LogWarning("Received Lobby Attribute is null!"); return null; } // Debug check - Is the received Attributes Name not the same as the requested one? if (foo.name == name) return foo.value; Debug.LogError( $"Requested Attribute Name {foo.name} and Received Attribute Name {name} do not match!"); return null; } } else { Debug.LogWarning( "Attempted to get a Lobby Client's Attribute while listClient is null. Please set Matchmaker.listClient first!"); } return null; } // API Functions - Lobby Discovery ----------------------------------------------------------------------------- /// /// Gets a Lobby Client's ID by its UUID. /// /// The UUID to find a Lobby Client with. /// A Lobby Client's ID with matching UUID. public static int GetLobbyIdByUuid(string uuid) { if (listClient != null) { if (listClient.IsConnected) { API.APIPacketHandlers.lobbyIds.Clear(); var foo = GetAllLobbyIdsByAttribute("_auto_uuid", uuid); if(foo.Count > 1) { Debug.LogWarning("Found more than one lobby with UUID!"); } if (foo.Count <= 0) { Debug.LogWarning( "Failed to find Lobby IDs with matching UUID!"); return 0; } else { return foo[0]; } } } else { Debug.LogWarning( "Attempted to get a Lobby Client's ID through a UUID while listClient is null. Please set Matchmaker.listClient first!"); } return 0; } /// /// Gets all Lobby IDs by matching Attribute /// /// The name of the Attribute /// The value of the Attribute public static List GetAllLobbyIdsByAttribute(string attribName, string attribValue) { API.APIPacketHandlers.lobbyIds.Clear(); if (listClient != null) { if (!listClient.IsConnected) return API.APIPacketHandlers.lobbyIds; var foo = API.APIClientPackets.RequestLobbyIdsWithMatchingAttribute(listClient, attribName, attribValue); if (!foo) { Debug.LogWarning( "Failed to find Lobby IDs with matching Attrbute!"); } return API.APIPacketHandlers.lobbyIds; } else { Debug.LogWarning( "Attempted to get all lobby IDs with matching Attribute while listClient is null. Please set Matchmaker.listClient first!"); } return API.APIPacketHandlers.lobbyIds; } /// /// Gets a list of the IDs of all connected Lobby Clients. Also calls a callback at Matchmaker.API.lobbyCallback for every ID. /// /// List of Lobby Client IDs [CanBeNull] public static List GetAllLobbyIds() { if (listClient != null) { API.APIPacketHandlers.lobbyIds.Clear(); Debug.Log("GetAllLobbyIds() called."); if (listClient.IsConnected) { API.APIClientPackets.RequestAllLobbyIds(listClient); } Debug.LogWarning("Attempted to request all Lobby IDs while Client is disconnected!"); } else { Debug.LogWarning( "Attempted to get a list of all Lobby IDs while listClient is null. Please set Matchmaker.listClient first!"); } Debug.Log($"Lobby ID list length: {API.APIPacketHandlers.lobbyIds.Count}"); return API.APIPacketHandlers.lobbyIds; } /************************************************************************************************************** * Matchmaker Programming Interface - Internal **************************************************************************************************************/ /// /// Contains the internal functionality for the Matchmaker API /// public static class API { // API Internals - Global Variables ------------------------------------------------------------------------ public delegate void UserCallbackDelegate(int userId); /// /// Gets called back whenever a Lobby Client ID is received by GetAllLobbyIds(). /// public static UserCallbackDelegate lobbyCallback; private static readonly List ExecuteOnApiThread = new(); private static readonly List ExecuteCopiedOnApiThread = new(); private static bool _actionToExecuteOnApiThread; private const int TicksPerSec = 30; private const float MsPerTick = 1000f / TicksPerSec; private static bool _isRunning = true; private static Thread _apiThread; // API Internals - API Thread Manager ---------------------------------------------------------------------- private static void APIThread(object data) { Debug.Log($"MatchmakerBehaviour thread started. Running at {TicksPerSec} ticks per second."); var nextLoop = DateTime.Now; while (_isRunning) { while (nextLoop < DateTime.Now) { UpdateAPI(); nextLoop = nextLoop.AddMilliseconds(MsPerTick); if (nextLoop > DateTime.Now) { Thread.Sleep(nextLoop - DateTime.Now); } } } } public static void StartThreads() { // if (apiThread != null || _isRunning != false) return; _isRunning = true; _apiThread = new Thread(APIThread); _apiThread.Start(0); } public static void StopThreads() { _isRunning = false; } /// Sets an action to be executed on the main thread. /// The action to be executed on the main thread. public static void ExecuteOnAPIThread(Action action) { if (action == null) { Debug.Log("No action to execute on MatchmakerBehaviour thread!"); return; } lock (ExecuteOnApiThread) { ExecuteOnApiThread.Add(action); _actionToExecuteOnApiThread = true; } } /// /// Executes all code meant to run on the main thread. NOTE: Call this ONLY from the main thread. /// private static void UpdateAPI() { if (_actionToExecuteOnApiThread) { ExecuteCopiedOnApiThread.Clear(); lock (ExecuteOnApiThread) { ExecuteCopiedOnApiThread.AddRange(ExecuteOnApiThread); ExecuteOnApiThread.Clear(); _actionToExecuteOnApiThread = false; } foreach (var t in ExecuteCopiedOnApiThread) { t(); } } } // API Internals - Packet Handling ------------------------------------------------------------------------- public static class APIPacketHandlers { public static readonly List lobbyIds = new(); public static bool finishedSendingIds; public static bool receivedId; public static void ReceiveLobbyId(Client client, Packet packet) { receivedId = true; int userId = packet.ReadInt(); int mod = packet.ReadInt(); Debug.Log("Received lobby id: " + userId.ToString()); lobbyIds.Add(userId); switch (mod) { case 0: { if (API.lobbyCallback != null) { API.lobbyCallback.Invoke(userId); } break; } default: client.lastReceivedInt = userId; break; } } public static void GetLobbyAttributesReceived(Client client, Packet packet) { int requestedId = packet.ReadInt(); string name = packet.ReadString(); string value = packet.ReadString(); Debug.Log("Received lobby attributes (Obj. ID: " + requestedId.ToString() + "): '" + name + "', '" + value + "'."); client.lastReceivedAttrib = new ClientHandle.ClientAttrib(requestedId, name, value); } public static void FinishedSendingLobbyIds(Client client, Packet packet) { Debug.Log("Finished receiving Lobby IDs."); finishedSendingIds = true; } } // API Internals - Packet Sending -------------------------------------------------------------------------- public static class APIClientPackets { // ReSharper disable once FieldCanBeMadeReadOnly.Global // ReSharper disable once ConvertToConstant.Global // ReSharper disable once MemberCanBePrivate.Global public static long timeOutTime = 300000000; public static bool RequestAllLobbyIds(Client client) { if (client.IsConnected) { APIPacketHandlers.finishedSendingIds = false; Debug.Log("Getting all the connected lobby IDs..."); using var packet = new Packet(10); packet.Write(client.myId); ClientSend.SendTcpData(client, packet); var startTime = DateTime.Now.Ticks; while (!APIPacketHandlers.finishedSendingIds && ((DateTime.Now.Ticks - startTime) < timeOutTime)) { if (!APIPacketHandlers.receivedId) continue; startTime = DateTime.Now.Ticks; APIPacketHandlers.receivedId = false; } if (!((DateTime.Now.Ticks - startTime) < timeOutTime)) { Debug.LogError( $"RequestAllLobbyIds: Request timed out! Returning {client.lastReceivedInt}"); return false; } return true; } else { Debug.LogWarning("Tried to request all Lobby IDs while the Client is disconnected!"); return false; } } public static bool RequestLobbyIdsWithMatchingAttribute(Client client, string attribName, string attribValue) { Debug.Log("Getting a lobby ID by UUID..."); using var packet = new Packet(11); APIPacketHandlers.finishedSendingIds = false; packet.Write(client.myId); packet.Write(attribName); packet.Write(attribValue); ClientSend.SendTcpData(client, packet); var startTime = DateTime.Now.Ticks; while (!APIPacketHandlers.finishedSendingIds && ((DateTime.Now.Ticks - startTime) < timeOutTime)) { // if (!APIPacketHandlers.receivedId) continue; // startTime = DateTime.Now.Ticks; // APIPacketHandlers.receivedId = false; } if (!((DateTime.Now.Ticks - startTime) < timeOutTime)) { Debug.LogError( $"RequestLobbyIdsWithMatchingAttribute: Request timed out!"); return false; } return true; } public static ClientHandle.ClientAttrib GetLobbyAttribute(Client client, int requestedId, string name) { Debug.Log("Getting a lobby attribute..."); using var packet = new Packet(12); packet.Write(client.myId); packet.Write(requestedId); packet.Write(name); ClientSend.SendTcpData(client, packet); var startTime = DateTime.Now.Ticks; client.lastReceivedAttrib = null; while (client.lastReceivedAttrib == null && ((DateTime.Now.Ticks - startTime) < timeOutTime)) { } if (!((DateTime.Now.Ticks - startTime) < timeOutTime)) { Debug.LogError($"GetLobbyAttribute: Request timed out!."); } return client.lastReceivedAttrib; } } } } }