Beta Release
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
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
|
||||
{
|
||||
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<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>Adresse des Master-Servers (änderbar mit "mp master <url>", z.B. zum lokalen Testen).</summary>
|
||||
public static string MasterUrl
|
||||
{
|
||||
get { return PlayerPrefs.GetString(PrefMasterUrl, DefaultMasterUrl).TrimEnd('/'); }
|
||||
set { PlayerPrefs.SetString(PrefMasterUrl, string.IsNullOrEmpty(value) ? DefaultMasterUrl : value.TrimEnd('/')); }
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user