323 lines
13 KiB
C#
323 lines
13 KiB
C#
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 UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>Ein Eintrag der Serverliste (vom Master-Server, Spielerzahl/Ping direkt beim Server abgefragt).</summary>
|
|
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 string ServerVersion = ""; // Programmversion des Servers (z.B. 2.10.6), ältere melden sie nicht
|
|
public int Security; // Sicherheitsstufe des Servers (0 = alt, ohne Sicherheitsupdates)
|
|
/// <summary>Fehlen dem Server Sicherheitsupdates? Dann tritt die Mod nicht bei.</summary>
|
|
public bool Outdated { get { return Security < SiteStatus.RequiredServerSecurity; } }
|
|
public bool RequireSteam = true; // false = Non-Steam-Server
|
|
public string Owner; // Besitzer des Lizenzschlüssels
|
|
public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben)
|
|
public List<string> Tags = new List<string>(); // Admin-Tags (z.B. "official") und eigene Tags des Servers (Web-Panel)
|
|
public List<string> Mods = new List<string>(); // Mods des Servers ("Name Version") - bekommt man beim Beitreten
|
|
public bool Modded { get { return Mods.Count > 0 || Tags.Contains("modded"); } }
|
|
|
|
public int UptimeSeconds = -1;
|
|
public List<string> PlayerNames = new List<string>();
|
|
public int Ping = -1; // -1 = noch nicht abgefragt
|
|
public bool Offline;
|
|
public bool IsLocal; // Server auf diesem PC (localhost)
|
|
}
|
|
|
|
/// <summary>
|
|
/// Holt die Serverliste vom Master-Server (Keymaster) und fragt jeden Server im Hintergrund
|
|
/// nach aktuellen Infos und Ping (MsgType.Info).
|
|
/// </summary>
|
|
public class ServerBrowser
|
|
{
|
|
/// <summary>Offizieller Master (Serverliste) - fest eingebaut.</summary>
|
|
public const string MasterUrl = "https://msco.mbm.wtf";
|
|
const int QueryTimeoutMs = 3000;
|
|
|
|
// 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<ServerListEntry> servers = new List<ServerListEntry>();
|
|
readonly List<ServerListEntry> localServers = new List<ServerListEntry>();
|
|
|
|
public bool Loading { get; private set; }
|
|
public string Error { get; private set; }
|
|
/// <summary>Zeitpunkt der letzten Aktualisierung (Time.realtimeSinceStartup), für das Auto-Refresh.</summary>
|
|
public float LastRefresh { get; private set; }
|
|
|
|
/// <summary>Lokale Server zuerst, dann die öffentliche Liste.</summary>
|
|
public List<ServerListEntry> Servers
|
|
{
|
|
get
|
|
{
|
|
lock (gate)
|
|
{
|
|
var all = new List<ServerListEntry>(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<ServerListEntry> list;
|
|
try { list = ParseList(text); }
|
|
catch (Exception e)
|
|
{
|
|
Error = "Invalid server list: " + e.Message;
|
|
yield break;
|
|
}
|
|
|
|
lock (gate) servers = list;
|
|
foreach (ServerListEntry entry in list) QueryAsync(entry);
|
|
}
|
|
|
|
/// <summary>Antwort von /api/servers lesen (eigener JSON-Leser, siehe MiniJson).</summary>
|
|
static List<ServerListEntry> ParseList(string text)
|
|
{
|
|
var list = new List<ServerListEntry>();
|
|
var items = MiniJson.Parse(text) as List<object>;
|
|
if (items == null) throw new FormatException("Expected a list");
|
|
foreach (object item in items)
|
|
{
|
|
var o = item as Dictionary<string, object>;
|
|
if (o == null) continue;
|
|
list.Add(new ServerListEntry
|
|
{
|
|
Name = MiniJson.Str(o, "name", ""),
|
|
Description = MiniJson.Str(o, "description", ""),
|
|
Address = MiniJson.Str(o, "address", ""),
|
|
Port = MiniJson.Int(o, "port", 7777),
|
|
Players = MiniJson.Int(o, "players"),
|
|
MaxPlayers = MiniJson.Int(o, "maxPlayers"),
|
|
Version = MiniJson.Str(o, "version", ""),
|
|
ServerVersion = MiniJson.Str(o, "serverVersion", ""),
|
|
Security = MiniJson.Int(o, "security"),
|
|
RequireSteam = MiniJson.Bool(o, "requireSteam", true),
|
|
Owner = MiniJson.Str(o, "owner"),
|
|
Featured = MiniJson.Bool(o, "featured"),
|
|
Tags = MiniJson.Strings(o, "tags"),
|
|
Mods = ModNames(o),
|
|
});
|
|
}
|
|
return list;
|
|
}
|
|
|
|
static List<string> ModNames(Dictionary<string, object> o)
|
|
{
|
|
var names = new List<string>();
|
|
object v;
|
|
var list = o.TryGetValue("mods", out v) ? v as List<object> : null;
|
|
if (list == null) return names;
|
|
foreach (object item in list)
|
|
{
|
|
var m = item as Dictionary<string, object>;
|
|
if (m == null) continue;
|
|
string name = MiniJson.Str(m, "name", MiniJson.Str(m, "id", "?")), version = MiniJson.Str(m, "version", "");
|
|
names.Add(string.IsNullOrEmpty(version) ? name : name + " " + version);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
/// <summary>Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt.</summary>
|
|
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));
|
|
}
|
|
}
|
|
|
|
/// <summary>Info + Ping im Hintergrund abfragen (blockiert das Spiel nicht).</summary>
|
|
void QueryAsync(ServerListEntry entry, Action<ServerListEntry> 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;
|
|
// Direkt vom Server - gilt auch für lokale/direkt eingegebene Server ohne Keymaster
|
|
entry.Security = info.Security;
|
|
if (!string.IsNullOrEmpty(info.ServerVersion)) entry.ServerVersion = info.ServerVersion;
|
|
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();
|
|
}
|
|
|
|
/// <summary>Antwort eines Servers auf die Info-Abfrage.</summary>
|
|
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<string> PlayerNames;
|
|
// Ab Server 2.10.6: Programmversion und Sicherheitsstufe (ältere Server: "" und 0)
|
|
public string ServerVersion;
|
|
public int Security;
|
|
}
|
|
|
|
/// <summary>Server direkt nach Name, Spielerzahl und Version fragen (blockierend - im Hintergrund aufrufen).</summary>
|
|
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 PacketReader(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<string>();
|
|
for (int i = 0; i < count && i < 256; i++) info.PlayerNames.Add(r.ReadString());
|
|
if (r.BaseStream.Position < r.BaseStream.Length)
|
|
{
|
|
info.ServerVersion = r.ReadString();
|
|
info.Security = r.ReadInt32();
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
/// <summary>"1.2.3.4:7777" oder "host" (Standard-Port) zerlegen.</summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|