258 lines
9.9 KiB
C#
258 lines
9.9 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 Newtonsoft.Json;
|
|
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 bool RequireSteam = true; // false = Non-Steam-Server
|
|
public string Owner; // Besitzer des Lizenzschlüssels
|
|
|
|
[JsonIgnore] public int UptimeSeconds = -1;
|
|
[JsonIgnore] public List<string> PlayerNames = new List<string>();
|
|
[JsonIgnore] public int Ping = -1; // -1 = noch nicht abgefragt
|
|
[JsonIgnore] public bool Offline;
|
|
[JsonIgnore] 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 = JsonConvert.DeserializeObject<List<ServerListEntry>>(text) ?? new List<ServerListEntry>(); }
|
|
catch (Exception e)
|
|
{
|
|
Error = "Invalid server list: " + e.Message;
|
|
yield break;
|
|
}
|
|
|
|
lock (gate) servers = list;
|
|
foreach (ServerListEntry entry in list) QueryAsync(entry);
|
|
}
|
|
|
|
/// <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;
|
|
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;
|
|
}
|
|
|
|
/// <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 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<string>();
|
|
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;
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
}
|