0.3.0: Serverliste und BSO-Konten

- Heartbeat an die Website, Abmelden beim Beenden (--private, --public-address, --description, --master)
- Spiel-Tickets pruefen (Signatur der Website), Name aus dem Konto, --no-auth fuer LAN
- IPv4 und IPv6, Protokoll 2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 18:10:51 +01:00
co-authored by Claude Opus 5.5
parent cda31cd070
commit 4e333c8250
7 changed files with 258 additions and 11 deletions
+36 -2
View File
@@ -11,7 +11,7 @@ namespace BSO.Server;
/// </summary>
public class GameServer
{
public const string Version = "0.2.0";
public const string Version = "0.3.0";
static readonly TimeSpan HelloTimeout = TimeSpan.FromSeconds(10);
static readonly TimeSpan SilenceTimeout = TimeSpan.FromSeconds(30); // Mod pingt alle 2 s
@@ -25,6 +25,7 @@ public class GameServer
public DateTime Connected, LastMessage;
public PlayerState State;
public bool HasState;
public string AccountId; // aus dem Spiel-Ticket (null mit --no-auth)
}
readonly TcpListener listener;
@@ -34,12 +35,21 @@ public class GameServer
public string Name { get; }
public int MaxPlayers { get; }
public TestBot Bot { get; private set; }
/// <summary>Fuer den Heartbeat (anderer Thread) - wird in Tick aktualisiert.</summary>
public int PlayerCount => playerCount;
volatile int playerCount;
/// <summary>Spiel-Tickets pruefen (Konto der BSO-Website)? Aus mit --no-auth (LAN, Tests).</summary>
public bool RequireAccount { get; set; } = true;
public MasterClient Master { get; set; }
public GameServer(int port, string name, int maxPlayers)
{
Name = name;
MaxPlayers = maxPlayers;
listener = new TcpListener(IPAddress.Any, port);
// IPv4 und IPv6 (die Website prueft die Erreichbarkeit ueber die Adresse, von der der Heartbeat kam - das kann IPv6 sein)
listener = new TcpListener(IPAddress.IPv6Any, port);
listener.Server.DualMode = true;
listener.Start();
Log($"BSO Server {Version} (Protokoll {Protocol.Version}) auf Port {port} - \"{name}\", max. {maxPlayers} Spieler");
}
@@ -76,6 +86,7 @@ public class GameServer
if (c.Conn.IsClosed) Remove(c);
}
playerCount = Players().Count();
if (Bot != null && Bot.Tick(Players().Where(p => p.HasState).Select(p => p.State)))
Broadcast(MsgType.PlayerState, StatePayload(Bot.Id, Bot.State), except: 0);
}
@@ -105,6 +116,29 @@ public class GameServer
Reject(c, $"Versionen passen nicht: Server Protokoll {Protocol.Version}, Mod {modVersion} hat Protokoll {protocol}");
return;
}
// Ab Protokoll 2: Spiel-Ticket des Launchers - der Name kommt dann aus dem Konto, nicht vom Spieler
string ticketText = r.ReadString();
if (RequireAccount)
{
if (Master?.TicketKey == null)
{
Reject(c, "Der Server kann gerade keine BSO-Konten pruefen (Website nicht erreichbar)");
return;
}
Ticket ticket = Ticket.Verify(ticketText, Master.TicketKey, out string ticketError);
if (ticket == null)
{
Reject(c, ticketError);
return;
}
if (Players().Any(p => p.AccountId == ticket.AccountId))
{
Reject(c, $"Das Konto {ticket.Username} ist schon auf diesem Server");
return;
}
c.AccountId = ticket.AccountId;
name = Clean(ticket.Username, c.Id);
}
if (Players().Count() >= MaxPlayers)
{
Reject(c, $"Server ist voll ({MaxPlayers} Spieler)");