using System; using System.Collections; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; public static class Cupid { private static string serverAddress; public static string ServerAddress => serverAddress; private static string cupidBaseUri; public static string CupidURI => cupidBaseUri; private static int port; public static int Port => port; private static string bearerToken; public static string BearerToken => bearerToken; private static string settingsPassword = ""; public static string SettingsPassword => settingsPassword; private static CupidSettings settings = null; public static CupidSettings Settings => settings; public static bool isInitialized => settings != null; /// True once has built a matchmaker base URI and stored a bearer token (required for /settings). public static bool IsEndpointReady => !string.IsNullOrEmpty(cupidBaseUri) && !string.IsNullOrEmpty(bearerToken); public static int RoomPort = -1; /// Clears auth/matchmaker session fields. Server address is left so the next login can again. public static void ClearSession() { bearerToken = ""; RoomPort = -1; } public static async Task Init(string _serverAddress, int _port, string _bearerToken, string _settingsPassword) { if (!TryBuildEndpoints(_serverAddress, _port, out serverAddress, out cupidBaseUri, out port)) { Logger.Log("Cupid: invalid or empty server address."); bearerToken = _bearerToken ?? ""; settings = null; return; } bearerToken = _bearerToken ?? ""; if (string.IsNullOrEmpty(bearerToken)) { Logger.Log("Cupid: no bearer token; configure auth before matchmaking."); settings = null; return; } settingsPassword = _settingsPassword ?? ""; string settingsUrl = CupidURI + "/settings?password=" + UnityWebRequest.EscapeURL(settingsPassword); using (UnityWebRequest www = UnityWebRequest.Get(settingsUrl)) { www.SetRequestHeader("Authorization", "Bearer " + bearerToken); var operation = www.SendWebRequest(); while (!operation.isDone) { await Task.Yield(); } try { if (www.result != UnityWebRequest.Result.Success) { Logger.Log("Cupid settings request failed: " + www.error); Logger.Log(www.downloadHandler != null ? www.downloadHandler.text : ""); settings = null; return; } string body = www.downloadHandler != null ? www.downloadHandler.text : ""; if (string.IsNullOrWhiteSpace(body)) { settings = null; return; } ApplySettingsJsonBody(body, logSuccess: true); } catch (Exception e) { Logger.Log("Error retreiving settings from server " + e.Message); Logger.Log(www.downloadHandler != null ? www.downloadHandler.text : ""); settings = null; } } } /// /// Re-fetches GET /settings (e.g. each time Main Menu is shown). Safe to call from a coroutine; no-op if not . /// public static IEnumerator CoRefreshSettings() { if (!IsEndpointReady) yield break; string settingsUrl = CupidURI + "/settings?password=" + UnityWebRequest.EscapeURL(settingsPassword ?? ""); using (UnityWebRequest www = UnityWebRequest.Get(settingsUrl)) { www.SetRequestHeader("Authorization", "Bearer " + bearerToken); yield return www.SendWebRequest(); if (www.result != UnityWebRequest.Result.Success) { Logger.Log("Cupid settings refresh failed: " + www.error); Logger.Log(www.downloadHandler != null ? www.downloadHandler.text : ""); yield break; } string body = www.downloadHandler != null ? www.downloadHandler.text : ""; ApplySettingsJsonBody(body, logSuccess: true); } } static void ApplySettingsJsonBody(string body, bool logSuccess) { settings = null; if (string.IsNullOrWhiteSpace(body)) return; if (body.Trim() == "403 Unauthorized") { Logger.Log("Cupid settings: wrong or missing password query (server rejected /settings)."); return; } try { var parsed = JsonUtility.FromJson(body); if (parsed == null) throw new NullReferenceException(nameof(CupidSettings)); settings = parsed; if (logSuccess) Logger.Log("Cupid settings updated"); } catch (Exception e) { Logger.Log("Cupid settings JSON error: " + e.Message + " body=" + body); settings = null; } } /// /// Plain host/IP + HTTP port, or an absolute URL (https://host with no port uses 443). /// Sets for game client transport; has no trailing slash. /// static bool TryBuildEndpoints(string input, int fallbackHttpPort, out string hostOnly, out string baseUri, out int resolvedPort) { hostOnly = ""; baseUri = ""; resolvedPort = fallbackHttpPort; input = (input ?? "").Trim(); if (string.IsNullOrEmpty(input)) return false; if (input.IndexOf("://", StringComparison.Ordinal) >= 0 && Uri.TryCreate(input, UriKind.Absolute, out Uri uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)) { hostOnly = uri.Host; string authority = uri.GetLeftPart(UriPartial.Authority); string path = uri.AbsolutePath; if (!string.IsNullOrEmpty(path) && path != "/") baseUri = authority.TrimEnd('/') + path.TrimEnd('/'); else baseUri = authority; resolvedPort = uri.Port; return true; } hostOnly = input; baseUri = "http://" + input + ":" + fallbackHttpPort; resolvedPort = fallbackHttpPort; return true; } public static string RandomUsername{ get{ string pool="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; string output =""; for(int i=0; i < 5; i++){ output += pool.ToCharArray()[UnityEngine.Random.Range(0,pool.Length)]; } return output; } } /// Matchmaker GET / body: "0" while waiting; otherwise a JSON room object. public static CupidRoom? ParseRoom(string data) => ParseRoom(data, 0, null); /// /// Used to pick for_red vs for_blue when both list the same players. /// Optional hint (e.g. ) when the roster is duplicated on both sides. public static CupidRoom? ParseRoom(string data, int currentUserId, Team? teamDisambiguation) { if (string.IsNullOrWhiteSpace(data)) return null; string t = data.Trim(); if (t == "0") return null; if (t.IndexOf("\"for_red\"", StringComparison.Ordinal) >= 0) { MatchmadeResponse env = JsonUtility.FromJson(t); if (env == null || !env.ok || !MatchmadeResponse.IsMatchReady(env)) return null; MatchmadeTeamPayload branch = MatchmadeResponse.SelectTeamPayload(env, currentUserId, teamDisambiguation); if (branch == null || branch.Port <= 0) return null; return ToCupidRoom(branch); } try { CupidRoom room = JsonUtility.FromJson(t); if (room.Port <= 0) return null; return room; } catch { return null; } } static CupidRoom ToCupidRoom(MatchmadeTeamPayload p) { var room = new CupidRoom { GameName = p.GameName ?? "", Port = p.Port, InitTime = p.InitTime, match_id = p.match_id, your_team = p.your_team ?? "" }; if (p.Players != null) { room.Players = new CupidQueueEntry[p.Players.Length]; for (int i = 0; i < p.Players.Length; i++) { room.Players[i] = new CupidQueueEntry { Name = p.Players[i].Name, LastSeen = p.Players[i].LastSeen, UserId = p.Players[i].UserId }; } } return room; } } [System.Serializable] public class CupidSettings { public int minimum_players; public int maximum_players; public int waiting_time; public int port_range_min; public int port_range_max; public CupidGameEntry[] games; /// RC stake per player for the default match (/settings), in coin units (same as matchmaker entry_fee). public int entry_fee; /// Optional side-bet or table minimum from matchmaker settings; same units as . public int bet_fee; /// Winner net RC prize from settings, coin units (same as room rc_prize). public int rc_prize; public CupidTableSettings table_settings; } [System.Serializable] public class CupidGameEntry { public string name; public string exe; } [System.Serializable] public class CupidTableSettings { public string entry_fee; public string bet_fee; /// Minimum client version (e.g. 0.99, 1.21). Compared to . public string version; public string error_msg; public string warning_msg; } [System.Serializable] public struct CupidRoom { public CupidQueueEntry[] Players; public string GameName; public int Port; public long InitTime; public int match_id; public string your_team; } [System.Serializable] public struct CupidQueueEntry { public string Name; public long LastSeen; public int UserId; }