using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Sockets; using System.Threading; // (Unitys WWW wird nicht mehr benutzt - siehe WinHttp) using Newtonsoft.Json; using UnityEngine; namespace MSCO { /// Ein Eintrag der Serverliste (vom Master-Server, Spielerzahl/Ping direkt beim Server abgefragt). public class ServerListEntry { public string Name; public string Description; public string Address; public int Port; public int Players; public int MaxPlayers; public string Version; public bool RequireSteam = true; // false = Non-Steam-Server public string Owner; // Besitzer des Lizenzschlüssels [JsonIgnore] public int UptimeSeconds = -1; [JsonIgnore] public List PlayerNames = new List(); [JsonIgnore] public int Ping = -1; // -1 = noch nicht abgefragt [JsonIgnore] public bool Offline; [JsonIgnore] public bool IsLocal; // Server auf diesem PC (localhost) } /// /// Holt die Serverliste vom Master-Server (Keymaster) und fragt jeden Server im Hintergrund /// nach aktuellen Infos und Ping (MsgType.Info). /// public class ServerBrowser { public const string DefaultMasterUrl = "https://msco.mbm.wtf"; const string PrefMasterUrl = "MSCO_MasterUrl"; const int QueryTimeoutMs = 3000; // Wie bei FiveM: ein Server auf diesem PC erscheint automatisch oben in der Liste static readonly int[] LocalPorts = { 7777, 7778, 7779, 7780 }; public const float AutoRefreshSeconds = 15f; readonly object gate = new object(); List servers = new List(); readonly List localServers = new List(); public bool Loading { get; private set; } public string Error { get; private set; } /// Zeitpunkt der letzten Aktualisierung (Time.realtimeSinceStartup), für das Auto-Refresh. public float LastRefresh { get; private set; } /// Adresse des Master-Servers (änderbar mit "mp master <url>", z.B. zum lokalen Testen). public static string MasterUrl { get { return PlayerPrefs.GetString(PrefMasterUrl, DefaultMasterUrl).TrimEnd('/'); } set { PlayerPrefs.SetString(PrefMasterUrl, string.IsNullOrEmpty(value) ? DefaultMasterUrl : value.TrimEnd('/')); } } /// Lokale Server zuerst, dann die öffentliche Liste. public List Servers { get { lock (gate) { var all = new List(localServers); all.AddRange(servers); return all; } } } public void Refresh(MonoBehaviour host) { if (Loading) return; host.StartCoroutine(Fetch()); } IEnumerator Fetch() { Loading = true; Error = null; LastRefresh = Time.realtimeSinceStartup; ProbeLocalServers(); // unabhängig vom Master: auch ohne Internet sichtbar // Über WinHTTP (TLS 1.2/1.3) im Hintergrund - Unitys WWW kann nur TLS 1.0 string text = null, error = null; bool done = false; string url = MasterUrl + "/api/servers"; var thread = new Thread(() => { text = WinHttp.Get(url, 8000, out error); done = true; }); thread.IsBackground = true; thread.Start(); while (!done) yield return null; Loading = false; if (text == null) { Error = "Server list not reachable (" + error + ")"; yield break; } List list; try { list = JsonConvert.DeserializeObject>(text) ?? new List(); } catch (Exception e) { Error = "Invalid server list: " + e.Message; yield break; } lock (gate) servers = list; foreach (ServerListEntry entry in list) QueryAsync(entry); } /// Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt. void ProbeLocalServers() { lock (gate) localServers.Clear(); foreach (int port in LocalPorts) { var entry = new ServerListEntry { Name = "Local server", Address = "127.0.0.1", Port = port, IsLocal = true, Version = "" }; QueryAsync(entry, found => localServers.Add(found)); } } /// Info + Ping im Hintergrund abfragen (blockiert das Spiel nicht). void QueryAsync(ServerListEntry entry, Action onOnline = null) { var thread = new Thread(() => { ServerInfo info; bool ok = QueryInfo(entry.Address, entry.Port, QueryTimeoutMs, out info); lock (gate) { if (!ok) { entry.Offline = true; return; } entry.Name = info.Name; entry.Description = info.Description; entry.Players = info.Players; entry.MaxPlayers = info.MaxPlayers; entry.Version = info.Version; entry.Ping = info.Ping; if (info.HasProfile) { entry.RequireSteam = info.RequireSteam; if (!string.IsNullOrEmpty(info.Owner)) entry.Owner = info.Owner; entry.UptimeSeconds = info.UptimeSeconds; entry.PlayerNames = info.PlayerNames; } entry.Offline = false; if (onOnline != null) onOnline(entry); } }); thread.IsBackground = true; thread.Start(); } /// Antwort eines Servers auf die Info-Abfrage. public struct ServerInfo { public string Name, Description, Version; public int Players, MaxPlayers, Ping; // Server-Profil (ab MSCO 2.4; ältere Server liefern das nicht) public bool HasProfile; public bool RequireSteam; public string Owner; public int UptimeSeconds; public List PlayerNames; } /// Server direkt nach Name, Spielerzahl und Version fragen (blockierend - im Hintergrund aufrufen). public static bool QueryInfo(string host, int port, int timeoutMs, out ServerInfo info) { info = new ServerInfo(); try { var watch = Stopwatch.StartNew(); using (var client = new TcpClient()) { IAsyncResult connect = client.BeginConnect(host, port, null, null); if (!connect.AsyncWaitHandle.WaitOne(timeoutMs)) return false; client.EndConnect(connect); client.ReceiveTimeout = timeoutMs; NetworkStream stream = client.GetStream(); byte[] request = FrameParser.Frame(MsgType.Info, null); stream.Write(request, 0, request.Length); byte[] header = ReadExactly(stream, 5); int length = BitConverter.ToInt32(header, 0); if (header[4] != (byte)MsgType.InfoReply || length < 1 || length > 64 * 1024) return false; byte[] payload = ReadExactly(stream, length - 1); info.Ping = (int)watch.ElapsedMilliseconds; using (var r = new BinaryReader(new MemoryStream(payload))) { info.Name = r.ReadString(); info.Description = r.ReadString(); info.Players = r.ReadInt32(); info.MaxPlayers = r.ReadInt32(); info.Version = r.ReadString(); info.RequireSteam = true; if (r.BaseStream.Position < r.BaseStream.Length) { info.HasProfile = true; info.RequireSteam = r.ReadBoolean(); info.Owner = r.ReadString(); info.UptimeSeconds = r.ReadInt32(); int count = r.ReadInt32(); info.PlayerNames = new List(); for (int i = 0; i < count && i < 256; i++) info.PlayerNames.Add(r.ReadString()); } } return true; } } catch { return false; } } static byte[] ReadExactly(Stream stream, int count) { var data = new byte[count]; int read = 0; while (read < count) { int n = stream.Read(data, read, count - read); if (n <= 0) throw new IOException("Connection closed"); read += n; } return data; } /// "1.2.3.4:7777" oder "host" (Standard-Port) zerlegen. public static bool TryParseAddress(string text, out string host, out int port) { host = null; port = MultiplayerManager.DefaultPort; if (string.IsNullOrEmpty(text)) return false; text = text.Trim(); int colon = text.LastIndexOf(':'); if (colon > 0 && text.IndexOf(':') == colon) { if (!int.TryParse(text.Substring(colon + 1), out port) || port < 1 || port > 65535) return false; text = text.Substring(0, colon); } host = text; return host.Length > 0; } } }