0.5.0: Lobby ueber das BSO-Relay (ohne Portfreigabe)

- Server relay:<Lobby> verbindet per WebSocket ueber eine lokale Bruecke (RelayClient)
- Ticket mit Lobby, Anzeige 'Lobby von ...'
- Nach Szenenwechsel Mitspieler-Modelle erst nach 3 s neu anlegen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 19:30:20 +01:00
co-authored by Claude Opus 5.5
parent 1dca505e3c
commit 8a1329f5fa
9 changed files with 170 additions and 21 deletions
+116
View File
@@ -0,0 +1,116 @@
using System;
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace BSO.Net
{
/// <summary>
/// BSO-Relay (Lobby-Server ohne Portfreigabe): eine TCP-Verbindung wird ueber einen WebSocket zum Relay
/// durchgereicht. Die Mod spricht weiter ganz normal TCP (Connection) - nur eben mit 127.0.0.1, die Bruecke
/// schickt alles ueber das Relay. Angemeldet wird mit dem Spiel-Ticket der Lobby (Kopfzeile X-BSO-Ticket).
/// </summary>
public static class RelayClient
{
/// <summary>https://site/relay/... -> wss://site/relay/...</summary>
public static Uri WsUri(string master, string pathAndQuery)
{
string b = master.TrimEnd('/');
if (b.StartsWith("https://")) b = "wss://" + b.Substring(8);
else if (b.StartsWith("http://")) b = "ws://" + b.Substring(7);
return new Uri(b + pathAndQuery);
}
public static async Task<ClientWebSocket> ConnectAsync(string master, string pathAndQuery, string ticket, CancellationToken ct)
{
var ws = new ClientWebSocket();
ws.Options.SetRequestHeader("X-BSO-Ticket", ticket ?? "");
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(20);
try
{
await ws.ConnectAsync(WsUri(master, pathAndQuery), ct);
return ws;
}
catch (WebSocketException e)
{
ws.Dispose();
string msg = e.Message ?? "";
if (msg.Contains("404")) throw new Exception("Der Server der Lobby laeuft gerade nicht");
if (msg.Contains("403")) throw new Exception("Kein Zugang zu dieser Lobby - im Launcher neu auf SPIELEN druecken");
throw new Exception("BSO-Relay nicht erreichbar: " + msg);
}
}
/// <summary>
/// Mitspieler: mit dem Lobby-Server verbinden und eine lokale Bruecke oeffnen. Gibt den Port auf 127.0.0.1 zurueck,
/// mit dem man sich dann normal per TCP verbindet. Die Bruecke nimmt genau eine Verbindung an.
/// </summary>
public static async Task<int> JoinAsync(string master, string lobbyId, string ticket, CancellationToken ct)
{
ClientWebSocket ws = await ConnectAsync(master, "/join?lobby=" + Uri.EscapeDataString(lobbyId), ticket, ct);
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
_ = Task.Run(async () =>
{
try
{
using var accept = new CancellationTokenSource(TimeSpan.FromSeconds(15));
using TcpClient tcp = await listener.AcceptTcpClientAsync(accept.Token);
listener.Stop();
await BridgeAsync(tcp, ws);
}
catch { }
finally
{
try { listener.Stop(); } catch { }
ws.Dispose();
}
});
return port;
}
/// <summary>Daten zwischen TCP und WebSocket in beide Richtungen, bis eine Seite trennt.</summary>
public static async Task BridgeAsync(TcpClient tcp, WebSocket ws)
{
tcp.NoDelay = true;
NetworkStream stream = tcp.GetStream();
using var cts = new CancellationTokenSource();
async Task TcpToWs()
{
var buffer = new byte[64 * 1024];
try
{
int n;
while ((n = await stream.ReadAsync(buffer, cts.Token)) > 0)
await ws.SendAsync(new ArraySegment<byte>(buffer, 0, n), WebSocketMessageType.Binary, true, cts.Token);
}
catch { }
cts.Cancel();
}
async Task WsToTcp()
{
var buffer = new byte[64 * 1024];
try
{
while (!cts.IsCancellationRequested)
{
var r = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), cts.Token);
if (r.MessageType == WebSocketMessageType.Close) break;
await stream.WriteAsync(buffer, 0, r.Count, cts.Token);
}
}
catch { }
cts.Cancel();
}
await Task.WhenAll(TcpToWs(), WsToTcp());
try { tcp.Close(); } catch { }
try { if (ws.State == WebSocketState.Open) await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); } catch { }
}
}
}