0.3.0: Freunde und Lobby, Server-Browser entfernt

- Seite Freunde: hinzufuegen, annehmen, online-Anzeige, Lobby erstellen/einladen/beitreten, Server starten
- SPIELEN in der Lobby: Ticket fuer die Lobby + Relay-Adresse in BSO.json
- Knopf 'Mod herunterladen' holt jederzeit die neueste Mod

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 19:30:22 +01:00
co-authored by Claude Opus 5.5
parent bcc85a51de
commit 3281468bee
10 changed files with 219 additions and 165 deletions
+48 -40
View File
@@ -19,9 +19,9 @@ public sealed class MainForm : Form
readonly Settings settings = Settings.Load();
/// <summary>Befehle, die nur einmal gleichzeitig laufen duerfen (Downloads/Installationen/Start).</summary>
static readonly HashSet<string> Exclusive = new() { "installMod", "launch", "join", "installLauncherUpdate", "uninstall" };
static readonly HashSet<string> Exclusive = new() { "installMod", "launch", "playLobby", "installLauncherUpdate", "uninstall" };
readonly HashSet<string> running = new();
static readonly HashSet<string> QuietCommands = new() { "window", "status", "me", "init", "releases", "servers" };
static readonly HashSet<string> QuietCommands = new() { "window", "status", "me", "init", "releases", "friends", "lobby" };
bool updateInstalled;
static readonly JsonSerializerOptions Json = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
@@ -288,18 +288,23 @@ public sealed class MainForm : Form
return await InstallMod();
case "launch":
return await Launch();
case "servers":
return await ServerList();
case "join":
{
// Beitreten: Server merken, beim Start automatisch verbinden, Spiel starten
string address = Arg(args, "address").Trim();
if (address.Length == 0 || address.Length > 260) throw new Exception("Ungueltige Serveradresse");
settings.Server = address;
settings.AutoConnect = true;
settings.Save();
return await Launch();
}
case "playLobby":
return await Launch(lobby: true);
// --- Freunde und Lobby (Website, mit dem Konto-Token)
case "friends": return await Site("GET", "/api/friends");
case "friendRequest": return await Site("POST", "/api/friends/request", new { username = Arg(args, "username").Trim() });
case "friendAccept": return await Site("POST", "/api/friends/accept", new { id = Arg(args, "id") });
case "friendRemove": return await Site("POST", "/api/friends/remove", new { id = Arg(args, "id") });
case "lobby": return await Site("GET", "/api/lobby");
case "lobbyCreate": return await Site("POST", "/api/lobby/create");
case "lobbyInvite": return await Site("POST", "/api/lobby/invite", new { accountId = Arg(args, "accountId") });
case "lobbyJoin": return await Site("POST", "/api/lobby/join", new { lobbyId = Arg(args, "lobbyId") });
case "lobbyDecline": return await Site("POST", "/api/lobby/decline", new { lobbyId = Arg(args, "lobbyId") });
case "lobbyLeave": return await Site("POST", "/api/lobby/leave");
case "lobbyKick": return await Site("POST", "/api/lobby/kick", new { accountId = Arg(args, "accountId") });
case "lobbyStart": return await Site("POST", "/api/lobby/start");
case "lobbyStop": return await Site("POST", "/api/lobby/stop");
case "releases":
{
var r = await Api.Releases();
@@ -375,19 +380,21 @@ public sealed class MainForm : Form
};
}
/// <summary>Serverliste der Website, jeder Server direkt angefragt (Ping, aktuelle Spielerzahl). Nicht erreichbar = ohne Ping.</summary>
static async Task<object> ServerList()
/// <summary>Anfrage an die Website mit dem Konto-Token. Abgelaufene Anmeldung -> zurueck zum Anmelden.</summary>
async Task<JsonElement> Site(string method, string path, object? body = null)
{
var servers = await Api.Servers();
var pings = await Task.WhenAll(servers.Select(s => ServerPing.Query(s.host, s.port)));
return servers.Select((s, i) => new
{
s.name, s.description, s.address, s.version,
players = pings[i]?.Players ?? s.players,
maxPlayers = pings[i]?.MaxPlayers ?? s.maxPlayers,
ping = pings[i]?.Ping,
compatible = s.protocol == 0 || s.protocol == 2 // Protokoll der Mod, die der Launcher installiert
}).OrderBy(s => s.ping == null).ThenByDescending(s => s.players).ToList();
string token = settings.Token ?? throw new Exception("Bitte anmelden");
var r = method == "GET" ? await Api.Get(path, token) : await Api.Post(path, token, body);
if (r.Status == 401) { LoggedOut(); throw new Exception("Anmeldung abgelaufen - bitte neu anmelden"); }
if (!r.Ok) throw new Exception(r.Error ?? "Fehlgeschlagen");
return r.Json;
}
void LoggedOut()
{
settings.Token = null;
settings.Save();
Emit(new { @event = "loggedOut" });
}
async Task<object> InstallMod()
@@ -400,7 +407,8 @@ public sealed class MainForm : Form
return new { version = latest.version };
}
async Task<object> Launch()
/// <summary>Spiel starten. lobby: auf den Server der eigenen Lobby (laeuft im BSO-Relay), sonst auf settings.Server.</summary>
async Task<object> Launch(bool lobby = false)
{
string folder = Game.FindFolder(settings) ?? throw new Exception("Bus-Simulator 2012 nicht gefunden - in den Einstellungen den Spielordner waehlen");
if (!Game.DotnetInstalled()) throw new Exception("Fuer BSO fehlt die .NET 10 Runtime (x64) - unter \"Status\" herunterladen");
@@ -415,22 +423,22 @@ public sealed class MainForm : Form
else if (installed == null) throw new Exception("BSO ist nicht installiert und der BSO-Server ist nicht erreichbar");
}
// Spiel-Ticket: ohne gueltiges Konto kein Multiplayer (Mod und Server pruefen es)
string token = settings.Token ?? throw new Exception("Bitte anmelden");
var t = await Api.Ticket(token);
if (t.Status == 401)
// Spiel-Ticket: ohne gueltiges Konto kein Multiplayer (Mod und Relay pruefen es). Fuer die Lobby gilt es fuer deren Server.
var t = await Site("POST", "/api/auth/ticket", lobby ? new { lobby = true } : null);
string ticket = t.GetProperty("ticket").GetString()!;
if (lobby)
{
settings.Token = null;
settings.Save();
Emit(new { @event = "loggedOut" });
throw new Exception("Anmeldung abgelaufen - bitte neu anmelden");
string lobbyId = t.GetProperty("lobby").GetString()!, label = t.GetProperty("lobbyName").GetString() ?? "Lobby";
string relay = (await Site("GET", "/api/config")).GetProperty("relay").GetString()!;
Game.WriteModConfig(folder, settings.Username, "relay:" + lobbyId, true, ticket, label, relay);
LauncherLog.Info($"Spiel gestartet ({label} ueber {relay})");
}
else
{
Game.WriteModConfig(folder, settings.Username, settings.Server, settings.AutoConnect, ticket);
LauncherLog.Info($"Spiel gestartet (Server {settings.Server}, automatisch verbinden {settings.AutoConnect})");
}
if (!t.Ok) throw new Exception("Konto konnte nicht geprueft werden: " + t.Error);
string ticket = t.Json.GetProperty("ticket").GetString()!;
Game.WriteModConfig(folder, settings.Username, settings.Server, settings.AutoConnect, ticket);
Game.Launch();
LauncherLog.Info($"Spiel gestartet (Server {settings.Server}, automatisch verbinden {settings.AutoConnect})");
if (!settings.KeepOpen)
{
await Task.Delay(2500);