+204
@@ -0,0 +1,204 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using BSO.Net;
|
||||
|
||||
namespace BSO
|
||||
{
|
||||
/// <summary>
|
||||
/// Verbindung zum BSO-Server (wie MSCO per IP): F8 verbindet/trennt, Server und Name stehen in BSO.json. Laeuft im
|
||||
/// Hauptthread: Verbindungsaufbau wird nur abgefragt (nicht blockierend), empfangen wird ueber Connection.Poll().
|
||||
/// Schickt den eigenen Zustand 10x pro Sekunde, alle 2 s einen Ping.
|
||||
/// </summary>
|
||||
public static class Multiplayer
|
||||
{
|
||||
public enum NetStatus { Offline, Connecting, Connected }
|
||||
|
||||
public class Remote
|
||||
{
|
||||
public int Id;
|
||||
public string Name;
|
||||
public PlayerState State;
|
||||
public bool HasState;
|
||||
}
|
||||
|
||||
const int ConnectTimeoutMs = 5000;
|
||||
const int SendIntervalMs = 100, PingIntervalMs = 2000;
|
||||
|
||||
public static NetStatus Status { get; private set; }
|
||||
/// <summary>Warum offline (letzter Fehler / Trennung) - fuer die Anzeige.</summary>
|
||||
public static string LastError { get; private set; } = "";
|
||||
public static string ServerName { get; private set; } = "";
|
||||
public static int MyId { get; private set; }
|
||||
public static int PingMs { get; private set; } = -1;
|
||||
public static readonly Dictionary<int, Remote> Players = new();
|
||||
|
||||
static TcpClient tcp;
|
||||
static Task connectTask;
|
||||
static Connection conn;
|
||||
static readonly Stopwatch connectClock = new(), sendClock = new(), pingClock = new();
|
||||
static bool autoConnectDone;
|
||||
|
||||
public static void Update()
|
||||
{
|
||||
if (!autoConnectDone && LocalPlayer.InScene)
|
||||
{
|
||||
autoConnectDone = true;
|
||||
if (Config.Current.AutoConnect) Connect();
|
||||
}
|
||||
if (Input.Pressed(Input.F8))
|
||||
{
|
||||
if (Status == NetStatus.Offline) Connect();
|
||||
else Disconnect("Getrennt mit F8");
|
||||
}
|
||||
|
||||
if (Status == NetStatus.Connecting) PollConnect();
|
||||
if (Status != NetStatus.Connected) return;
|
||||
|
||||
foreach (Message m in conn.Poll())
|
||||
{
|
||||
try { Handle(m); }
|
||||
catch (Exception e) { Bootstrap.Log($"Nachricht {m.Type} fehlerhaft: {e.Message}"); }
|
||||
if (Status != NetStatus.Connected) return; // z.B. Reject
|
||||
}
|
||||
if (conn.IsClosed)
|
||||
{
|
||||
Disconnect(conn.CloseReason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sendClock.ElapsedMilliseconds >= SendIntervalMs)
|
||||
{
|
||||
sendClock.Restart();
|
||||
PlayerState s = LocalPlayer.State;
|
||||
conn.Send(MsgType.PlayerState, Wire.Build(w => s.Write(w)));
|
||||
}
|
||||
if (pingClock.ElapsedMilliseconds >= PingIntervalMs)
|
||||
{
|
||||
pingClock.Restart();
|
||||
conn.Send(MsgType.Ping, Wire.Build(w => w.Write(Stopwatch.GetTimestamp())));
|
||||
}
|
||||
}
|
||||
|
||||
public static string ServerAddress => Config.Current.Server;
|
||||
|
||||
static void Connect()
|
||||
{
|
||||
(string host, int port) = ParseAddress(ServerAddress);
|
||||
Bootstrap.Log($"Verbinde mit {host}:{port} ...");
|
||||
LastError = "";
|
||||
tcp = new TcpClient();
|
||||
connectTask = tcp.ConnectAsync(host, port);
|
||||
connectClock.Restart();
|
||||
Status = NetStatus.Connecting;
|
||||
}
|
||||
|
||||
static void PollConnect()
|
||||
{
|
||||
if (!connectTask.IsCompleted)
|
||||
{
|
||||
if (connectClock.ElapsedMilliseconds > ConnectTimeoutMs) Fail("Server antwortet nicht");
|
||||
return;
|
||||
}
|
||||
if (connectTask.IsFaulted || !tcp.Connected)
|
||||
{
|
||||
Fail(connectTask.Exception?.GetBaseException().Message ?? "Verbindung fehlgeschlagen");
|
||||
return;
|
||||
}
|
||||
|
||||
conn = new Connection(tcp);
|
||||
conn.Send(MsgType.Hello, Wire.Build(w =>
|
||||
{
|
||||
w.Write(Protocol.Version);
|
||||
w.Write(Bootstrap.ModVersion);
|
||||
w.Write(Config.Current.Name ?? "");
|
||||
}));
|
||||
// Erst nach Welcome gilt man als verbunden - bis dahin bleibt der Status "Verbinde"
|
||||
Status = NetStatus.Connected;
|
||||
sendClock.Restart();
|
||||
pingClock.Restart();
|
||||
ServerName = "";
|
||||
}
|
||||
|
||||
static void Fail(string reason)
|
||||
{
|
||||
try { tcp?.Close(); } catch { }
|
||||
Status = NetStatus.Offline;
|
||||
LastError = reason;
|
||||
Bootstrap.Log("Verbindung fehlgeschlagen: " + reason);
|
||||
}
|
||||
|
||||
static void Handle(Message m)
|
||||
{
|
||||
BinaryReader r = m.Reader();
|
||||
switch (m.Type)
|
||||
{
|
||||
case MsgType.Welcome:
|
||||
MyId = r.ReadInt32();
|
||||
ServerName = r.ReadString();
|
||||
Players.Clear();
|
||||
int n = r.ReadInt32();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var p = new Remote { Id = r.ReadInt32(), Name = r.ReadString() };
|
||||
Players[p.Id] = p;
|
||||
}
|
||||
Bootstrap.Log($"Verbunden mit \"{ServerName}\" als #{MyId}, {n} andere Spieler");
|
||||
break;
|
||||
|
||||
case MsgType.Reject:
|
||||
Disconnect("Abgelehnt: " + r.ReadString());
|
||||
break;
|
||||
|
||||
case MsgType.PlayerJoined:
|
||||
var joined = new Remote { Id = r.ReadInt32(), Name = r.ReadString() };
|
||||
Players[joined.Id] = joined;
|
||||
Bootstrap.Log($"{joined.Name} ist beigetreten");
|
||||
break;
|
||||
|
||||
case MsgType.PlayerLeft:
|
||||
int left = r.ReadInt32();
|
||||
if (Players.Remove(left, out Remote gone)) Bootstrap.Log($"{gone.Name} ist gegangen");
|
||||
break;
|
||||
|
||||
case MsgType.PlayerState:
|
||||
int id = r.ReadInt32();
|
||||
if (!Players.TryGetValue(id, out Remote p2)) return; // PlayerJoined kommt immer vorher
|
||||
p2.State = PlayerState.Read(r);
|
||||
p2.HasState = true;
|
||||
break;
|
||||
|
||||
case MsgType.Pong:
|
||||
PingMs = (int)Stopwatch.GetElapsedTime(r.ReadInt64()).TotalMilliseconds;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Disconnect(string reason)
|
||||
{
|
||||
if (Status == NetStatus.Offline) return;
|
||||
conn?.Close(reason);
|
||||
conn = null;
|
||||
try { tcp?.Close(); } catch { }
|
||||
Status = NetStatus.Offline;
|
||||
LastError = reason;
|
||||
Players.Clear();
|
||||
PingMs = -1;
|
||||
Bootstrap.Log("Getrennt: " + reason);
|
||||
}
|
||||
|
||||
/// <summary>Ist der Server schon angekommen (Welcome)?</summary>
|
||||
public static bool Welcomed => Status == NetStatus.Connected && ServerName.Length > 0;
|
||||
|
||||
static (string, int) ParseAddress(string address)
|
||||
{
|
||||
address = (address ?? "").Trim();
|
||||
int colon = address.LastIndexOf(':');
|
||||
if (colon > 0 && int.TryParse(address.Substring(colon + 1), out int port)) return (address.Substring(0, colon), port);
|
||||
return (address.Length > 0 ? address : "127.0.0.1", Protocol.DefaultPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user