Beim Start installiert sich ein neuer Launcher sofort selbst (Fortschrittsfenster, Neustart), neue BSO-Versionen kommen nach dem Anmelden - beides abschaltbar. Einstellungen: Nach Updates suchen / Installieren und neu starten. Updates wieder nur von der Website; dotnet publish laedt hoch (release.ps1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
470 lines
23 KiB
C#
470 lines
23 KiB
C#
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.Json;
|
|
using Microsoft.Web.WebView2.Core;
|
|
using Microsoft.Web.WebView2.WinForms;
|
|
|
|
namespace BSO.Launcher;
|
|
|
|
/// <summary>
|
|
/// Fenster mit WebView2 (wie beim MSCO Launcher): die Oberflaeche (ui/*.html/css/js, in der .exe eingebettet) laeuft unter
|
|
/// https://app.bso/ und spricht ueber Nachrichten { id, cmd, args } mit dem Launcher; Antworten { id, ok, result | error },
|
|
/// Ereignisse { event, ... } (z.B. Download-Fortschritt).
|
|
/// </summary>
|
|
public sealed class MainForm : Form
|
|
{
|
|
const string Host = "app.bso";
|
|
readonly WebView2 web = new() { Dock = DockStyle.Fill, DefaultBackgroundColor = Color.FromArgb(0x0b, 0x0d, 0x12) };
|
|
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", "playLobby", "installLauncherUpdate", "uninstall" };
|
|
readonly HashSet<string> running = new();
|
|
static readonly HashSet<string> QuietCommands = new() { "window", "status", "me", "init", "releases", "friends", "lobby", "checkLauncherUpdate" };
|
|
bool updateInstalled;
|
|
static readonly JsonSerializerOptions Json = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
|
|
|
public MainForm()
|
|
{
|
|
Text = "BSO Launcher";
|
|
BackColor = Color.FromArgb(0x0b, 0x0d, 0x12);
|
|
ClientSize = new Size(1120, 700);
|
|
MinimumSize = new Size(900, 600);
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
Lang.Set(settings.Language);
|
|
LauncherLog.Info("BSO Launcher " + SelfUpdate.CurrentVersion + " gestartet" + (Api.Dev ? " (-dev)" : "") + ", Website " + Api.Base);
|
|
if (Api.Base != Api.BaseUrl) Text += " (DEV)";
|
|
try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath ?? Application.ExecutablePath); } catch { }
|
|
Controls.Add(web);
|
|
Load += async (_, _) => await InitWeb();
|
|
Resize += (_, _) => Emit(new { @event = "window", maximized = WindowState == FormWindowState.Maximized });
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Ohne Windows-Titelleiste
|
|
// Die Titelleiste zeichnet die Oberflaeche selbst (ziehen, minimieren, maximieren, schliessen). Der Rahmen bleibt:
|
|
// das Fenster laesst sich weiter mit der Maus vergroessern, Aero Snap und Schatten bleiben.
|
|
|
|
const int WM_NCCALCSIZE = 0x0083, WM_NCLBUTTONDOWN = 0x00A1, HTCAPTION = 2;
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
struct RECT { public int Left, Top, Right, Bottom; }
|
|
|
|
[DllImport("user32.dll")] static extern bool ReleaseCapture();
|
|
[DllImport("user32.dll")] static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
|
[DllImport("user32.dll")] static extern int GetSystemMetricsForDpi(int index, uint dpi);
|
|
[DllImport("user32.dll")] static extern uint GetDpiForWindow(IntPtr hwnd);
|
|
[DllImport("user32.dll")] static extern bool SetWindowPos(IntPtr hWnd, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
|
[DllImport("dwmapi.dll")] static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int value, int size);
|
|
|
|
protected override void OnHandleCreated(EventArgs e)
|
|
{
|
|
base.OnHandleCreated(e);
|
|
try
|
|
{
|
|
int dark = 1, round = 2; // dunkler Rahmen, abgerundete Ecken (Windows 11)
|
|
DwmSetWindowAttribute(Handle, 20, ref dark, 4);
|
|
DwmSetWindowAttribute(Handle, 33, ref round, 4);
|
|
}
|
|
catch { }
|
|
SetWindowPos(Handle, IntPtr.Zero, 0, 0, 0, 0, 0x0020 | 0x0002 | 0x0001 | 0x0004); // FRAMECHANGED|NOMOVE|NOSIZE|NOZORDER
|
|
}
|
|
|
|
protected override void WndProc(ref Message m)
|
|
{
|
|
if (m.Msg == WM_NCCALCSIZE && m.WParam != IntPtr.Zero)
|
|
{
|
|
// Nur die Titelleiste wegnehmen: oben beginnt der Inhalt direkt am Fensterrand
|
|
var before = Marshal.PtrToStructure<RECT>(m.LParam);
|
|
base.WndProc(ref m);
|
|
var after = Marshal.PtrToStructure<RECT>(m.LParam);
|
|
after.Top = before.Top;
|
|
if (WindowState == FormWindowState.Maximized)
|
|
{
|
|
// Maximiert ragt das Fenster um den Rahmen ueber den Bildschirm hinaus
|
|
uint dpi = GetDpiForWindow(Handle);
|
|
after.Top += GetSystemMetricsForDpi(33, dpi) + GetSystemMetricsForDpi(92, dpi); // SM_CYFRAME + SM_CXPADDEDBORDER
|
|
}
|
|
Marshal.StructureToPtr(after, m.LParam, false);
|
|
m.Result = IntPtr.Zero;
|
|
return;
|
|
}
|
|
base.WndProc(ref m);
|
|
}
|
|
|
|
void WindowCommand(string action)
|
|
{
|
|
switch (action)
|
|
{
|
|
case "drag":
|
|
ReleaseCapture();
|
|
SendMessage(Handle, WM_NCLBUTTONDOWN, HTCAPTION, IntPtr.Zero);
|
|
break;
|
|
case "minimize":
|
|
WindowState = FormWindowState.Minimized;
|
|
break;
|
|
case "maximize":
|
|
WindowState = WindowState == FormWindowState.Maximized ? FormWindowState.Normal : FormWindowState.Maximized;
|
|
break;
|
|
case "close":
|
|
Close();
|
|
break;
|
|
}
|
|
}
|
|
|
|
async Task InitWeb()
|
|
{
|
|
try
|
|
{
|
|
var env = await CoreWebView2Environment.CreateAsync(null, Path.Combine(Settings.Folder, "WebView2"));
|
|
await web.EnsureCoreWebView2Async(env);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
var answer = MessageBox.Show(Lang.T("Die Microsoft Edge WebView2 Runtime fehlt (" + e.Message + ").\n\nDownload-Seite oeffnen?"),
|
|
Installer.AppName, MessageBoxButtons.YesNo, MessageBoxIcon.Error);
|
|
if (answer == DialogResult.Yes) OpenUrl("https://developer.microsoft.com/microsoft-edge/webview2/");
|
|
Close();
|
|
return;
|
|
}
|
|
var core = web.CoreWebView2;
|
|
core.Settings.AreDevToolsEnabled = Debugger.IsAttached;
|
|
core.Settings.AreDefaultContextMenusEnabled = Debugger.IsAttached;
|
|
core.Settings.IsZoomControlEnabled = false;
|
|
core.Settings.IsStatusBarEnabled = false;
|
|
core.Settings.AreBrowserAcceleratorKeysEnabled = Debugger.IsAttached;
|
|
try { core.Settings.IsNonClientRegionSupportEnabled = true; } catch { } // CSS "app-region: drag" = Titelleiste
|
|
core.AddWebResourceRequestedFilter($"https://{Host}/*", CoreWebView2WebResourceContext.All);
|
|
core.WebResourceRequested += ServeUi;
|
|
// Befehle nur von der eigenen Oberflaeche (nicht aus fremden Seiten oder eingebetteten Frames)
|
|
core.WebMessageReceived += async (_, e) =>
|
|
{
|
|
if (!e.Source.StartsWith($"https://{Host}/", StringComparison.Ordinal)) return;
|
|
await OnMessage(e.WebMessageAsJson);
|
|
};
|
|
// Links nach aussen im Browser oeffnen, nicht im Launcher
|
|
core.NewWindowRequested += (_, e) => { e.Handled = true; OpenUrl(e.Uri); };
|
|
core.NavigationStarting += (_, e) =>
|
|
{
|
|
if (!e.Uri.StartsWith($"https://{Host}/")) { e.Cancel = true; OpenUrl(e.Uri); }
|
|
};
|
|
core.Navigate($"https://{Host}/index.html");
|
|
}
|
|
|
|
static void OpenUrl(string url)
|
|
{
|
|
if (url.StartsWith("https://") || url.StartsWith("http://"))
|
|
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
|
}
|
|
|
|
void ServeUi(object? sender, CoreWebView2WebResourceRequestedEventArgs e)
|
|
{
|
|
string path = new Uri(e.Request.Uri).AbsolutePath.TrimStart('/');
|
|
if (path.Length == 0) path = "index.html";
|
|
var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("ui/" + path);
|
|
if (stream == null)
|
|
{
|
|
e.Response = web.CoreWebView2.Environment.CreateWebResourceResponse(null, 404, "Not found", "");
|
|
return;
|
|
}
|
|
string type = Path.GetExtension(path) switch
|
|
{
|
|
".html" => "text/html; charset=utf-8",
|
|
".js" => "text/javascript; charset=utf-8",
|
|
".css" => "text/css; charset=utf-8",
|
|
".png" => "image/png",
|
|
".svg" => "image/svg+xml",
|
|
_ => "application/octet-stream"
|
|
};
|
|
e.Response = web.CoreWebView2.Environment.CreateWebResourceResponse(stream, 200, "OK",
|
|
$"Content-Type: {type}\nCache-Control: no-store\nX-Content-Type-Options: nosniff\nContent-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'");
|
|
}
|
|
|
|
// ---------------------------------------------------------------- Nachrichten aus der Oberflaeche
|
|
|
|
void Emit(object message)
|
|
{
|
|
if (IsDisposed) return;
|
|
string json = JsonSerializer.Serialize(message, Json);
|
|
if (InvokeRequired) BeginInvoke(() => web.CoreWebView2?.PostWebMessageAsJson(json));
|
|
else web.CoreWebView2?.PostWebMessageAsJson(json);
|
|
}
|
|
|
|
void Progress(string task, double value, string text) => Emit(new { @event = "progress", task, value, text = Lang.T(text) });
|
|
|
|
async Task OnMessage(string raw)
|
|
{
|
|
int id = 0;
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(raw);
|
|
var msg = doc.RootElement;
|
|
id = msg.GetProperty("id").GetInt32();
|
|
string cmd = msg.GetProperty("cmd").GetString() ?? "";
|
|
JsonElement args = msg.TryGetProperty("args", out var a) ? a.Clone() : default;
|
|
if (!QuietCommands.Contains(cmd)) LauncherLog.Info("> " + cmd);
|
|
object? result;
|
|
if (Exclusive.Contains(cmd))
|
|
{
|
|
lock (running)
|
|
if (!running.Add(cmd)) throw new Exception("Laeuft schon - bitte warten");
|
|
try { result = await HandleCommand(cmd, args); }
|
|
finally
|
|
{
|
|
// Launcher-Update erfolgreich: der Launcher beendet sich gleich - nicht wieder freigeben
|
|
if (cmd != "installLauncherUpdate" || !updateInstalled) lock (running) running.Remove(cmd);
|
|
}
|
|
}
|
|
else result = await HandleCommand(cmd, args);
|
|
Emit(new { id, ok = true, result });
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
LauncherLog.Error(e.Message);
|
|
Emit(new { id, ok = false, error = Lang.T(e.Message) });
|
|
}
|
|
}
|
|
|
|
static string Arg(JsonElement args, string name) =>
|
|
args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : "";
|
|
|
|
static bool? BoolArg(JsonElement args, string name) =>
|
|
args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var v) && v.ValueKind is JsonValueKind.True or JsonValueKind.False
|
|
? v.GetBoolean() : null;
|
|
|
|
async Task<object?> HandleCommand(string cmd, JsonElement args)
|
|
{
|
|
switch (cmd)
|
|
{
|
|
case "window":
|
|
WindowCommand(Arg(args, "action"));
|
|
return null;
|
|
case "init":
|
|
return new
|
|
{
|
|
maximized = WindowState == FormWindowState.Maximized,
|
|
version = SelfUpdate.CurrentVersion,
|
|
username = settings.Username,
|
|
loggedIn = settings.Token != null,
|
|
master = Api.Base,
|
|
language = Lang.Current,
|
|
autoUpdateLauncher = settings.AutoUpdateLauncher,
|
|
dev = Api.Dev || Api.Base != Api.BaseUrl
|
|
};
|
|
|
|
// --- Konto
|
|
case "login":
|
|
case "register":
|
|
{
|
|
var r = cmd == "login"
|
|
? await Api.Login(Arg(args, "username"), Arg(args, "password"))
|
|
: await Api.Register(Arg(args, "username"), Arg(args, "password"), Arg(args, "email") is { Length: > 0 } mail ? mail : null);
|
|
if (!r.Ok) throw new Exception(r.Error ?? "Fehlgeschlagen");
|
|
settings.Token = r.Json.GetProperty("token").GetString();
|
|
settings.Username = r.Json.GetProperty("account").GetProperty("username").GetString();
|
|
settings.Save();
|
|
LauncherLog.Info((cmd == "login" ? "Angemeldet als " : "Registriert als ") + settings.Username);
|
|
return new { username = settings.Username };
|
|
}
|
|
case "me":
|
|
{
|
|
string? token = settings.Token;
|
|
if (token == null) return new { loggedIn = false };
|
|
var r = await Api.Me(token);
|
|
if (r.Status == 401) { settings.Token = null; settings.Save(); return new { loggedIn = false }; }
|
|
// Master gerade nicht erreichbar: angemeldet bleiben
|
|
return new { loggedIn = true, username = r.Ok ? r.Json.GetProperty("account").GetProperty("username").GetString() : settings.Username, offline = !r.Ok };
|
|
}
|
|
case "logout":
|
|
{
|
|
string? token = settings.Token;
|
|
if (token != null) await Api.Logout(token);
|
|
settings.Token = null;
|
|
settings.Save();
|
|
return null;
|
|
}
|
|
|
|
// --- Status, Installieren, Spielen
|
|
case "status":
|
|
return await Status();
|
|
case "installMod":
|
|
return await InstallMod();
|
|
case "launch":
|
|
return await Launch();
|
|
case "playLobby":
|
|
// Ersteller: SPIELEN schaltet den Lobby-Server mit ein (der Relay faehrt ihn beim ersten Spieler hoch)
|
|
if (BoolArg(args, "startServer") == true) await Site("POST", "/api/lobby/start");
|
|
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();
|
|
if (!r.Ok) throw new Exception(r.Error ?? "Neuigkeiten nicht verfuegbar");
|
|
return r.Json.GetProperty("releases");
|
|
}
|
|
case "checkLauncherUpdate":
|
|
{
|
|
var latest = await Api.Latest("launcher");
|
|
return new { current = SelfUpdate.CurrentVersion, latest = latest?.version,
|
|
available = latest != null && SelfUpdate.IsNewer(latest.version, SelfUpdate.CurrentVersion) };
|
|
}
|
|
case "installLauncherUpdate":
|
|
{
|
|
var latest = await Api.Latest("launcher") ?? throw new Exception("Kein Launcher-Update gefunden");
|
|
LauncherLog.Info("Launcher wird aktualisiert auf " + latest.version);
|
|
string version = await SelfUpdate.Install(latest, p => Progress("launcher", p, "Launcher-Update wird geladen …"));
|
|
updateInstalled = true;
|
|
LauncherLog.Info("Launcher " + version + " installiert - Neustart");
|
|
// Beenden, damit die neue Version starten kann. Haengt das Schliessen (WebView2 o.ae.), nach 5 s hart
|
|
// beenden - sonst bleibt die alte Version offen und die neue wartet vergeblich
|
|
BeginInvoke(() => Application.Exit());
|
|
_ = Task.Delay(5000).ContinueWith(_ => Environment.Exit(0));
|
|
return new { version, restarting = true };
|
|
}
|
|
|
|
// --- Einstellungen
|
|
case "setSettings":
|
|
if (args.ValueKind == JsonValueKind.Object && args.TryGetProperty("server", out var s) && s.ValueKind == JsonValueKind.String)
|
|
settings.Server = s.GetString()!.Trim();
|
|
settings.AutoConnect = BoolArg(args, "autoConnect") ?? settings.AutoConnect;
|
|
settings.KeepOpen = BoolArg(args, "keepOpen") ?? settings.KeepOpen;
|
|
settings.AutoUpdateMod = BoolArg(args, "autoUpdateMod") ?? settings.AutoUpdateMod;
|
|
settings.AutoUpdateLauncher = BoolArg(args, "autoUpdateLauncher") ?? settings.AutoUpdateLauncher;
|
|
if (Arg(args, "language") is "de" or "en") { settings.Language = Arg(args, "language"); Lang.Set(settings.Language); }
|
|
settings.Save();
|
|
return null;
|
|
case "chooseGameFolder":
|
|
{
|
|
using var dialog = new FolderBrowserDialog { Description = Lang.T("Ordner \"Bus-Simulator 2012\" waehlen"), UseDescriptionForTitle = true };
|
|
if (dialog.ShowDialog(this) != DialogResult.OK) return new { changed = false };
|
|
string folder = dialog.SelectedPath;
|
|
// Auch Bin_High_Win64 selbst gewaehlt? Dann den Ordner darueber nehmen
|
|
if (!Game.IsGameFolder(folder) && Game.IsGameFolder(Path.GetDirectoryName(folder))) folder = Path.GetDirectoryName(folder)!;
|
|
if (!Game.IsGameFolder(folder)) throw new Exception("In diesem Ordner ist kein Bus-Simulator 2012 (Bin_High_Win64\\BusSimulator2012.exe fehlt)");
|
|
settings.GameFolder = folder;
|
|
settings.Save();
|
|
return new { changed = true };
|
|
}
|
|
case "openDotnet":
|
|
OpenUrl(Game.DotnetDownload);
|
|
return null;
|
|
case "openLog":
|
|
Process.Start(new ProcessStartInfo("explorer.exe", $"\"{Settings.Folder}\"") { UseShellExecute = true });
|
|
return null;
|
|
case "uninstall":
|
|
if (Installer.Uninstall(false)) BeginInvoke(Close);
|
|
return null;
|
|
}
|
|
throw new Exception("Unbekannter Befehl: " + cmd);
|
|
}
|
|
|
|
async Task<object> Status()
|
|
{
|
|
string? folder = Game.FindFolder(settings);
|
|
var modTask = Api.Latest("mod");
|
|
var launcherTask = Api.Latest("launcher");
|
|
await Task.WhenAll(modTask, launcherTask);
|
|
Api.Release? mod = modTask.Result, launcher = launcherTask.Result;
|
|
string? installed = folder != null ? Game.ModVersion(folder) : null;
|
|
return new
|
|
{
|
|
game = new { found = folder != null, folder, running = Game.IsRunning() },
|
|
mod = new
|
|
{
|
|
installed,
|
|
latest = mod?.version,
|
|
changelog = mod?.changelog,
|
|
updateAvailable = mod != null && (installed == null || SelfUpdate.IsNewer(mod.version, installed))
|
|
},
|
|
dotnet = Game.DotnetInstalled(),
|
|
launcherUpdate = launcher != null && SelfUpdate.IsNewer(launcher.version, SelfUpdate.CurrentVersion) ? launcher.version : null,
|
|
launcherChangelog = launcher?.changelog,
|
|
launcherVersion = SelfUpdate.CurrentVersion,
|
|
masterReachable = mod != null || launcher != null,
|
|
settings = new { server = settings.Server, autoConnect = settings.AutoConnect, keepOpen = settings.KeepOpen, autoUpdateMod = settings.AutoUpdateMod }
|
|
};
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
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()
|
|
{
|
|
string folder = Game.FindFolder(settings) ?? throw new Exception("Bus-Simulator 2012 nicht gefunden - in den Einstellungen den Spielordner waehlen");
|
|
var latest = await Api.Latest("mod") ?? throw new Exception("Noch keine BSO-Version auf dem Server");
|
|
byte[] zip = await Api.Download(latest, p => Progress("mod", p, $"BSO {latest.version} wird geladen …"));
|
|
Progress("mod", 1, "Installieren …");
|
|
Game.InstallMod(folder, zip);
|
|
return new { version = latest.version };
|
|
}
|
|
|
|
/// <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");
|
|
if (Game.IsRunning()) throw new Exception("Bus-Simulator 2012 laeuft schon");
|
|
|
|
// Vorher aktualisieren (oder installieren, wenn BSO noch fehlt)
|
|
string? installed = Game.ModVersion(folder);
|
|
if (installed == null || settings.AutoUpdateMod)
|
|
{
|
|
var latest = await Api.Latest("mod");
|
|
if (latest != null && (installed == null || SelfUpdate.IsNewer(latest.version, installed))) await InstallMod();
|
|
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 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)
|
|
{
|
|
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
|
|
{
|
|
// Ohne Lobby: alleine fahren (Mehrspieler laeuft ueber Freunde & Lobby)
|
|
Game.WriteModConfig(folder, settings.Username, settings.Server, false, ticket);
|
|
LauncherLog.Info("Spiel gestartet (ohne Lobby)");
|
|
}
|
|
Game.Launch();
|
|
if (!settings.KeepOpen)
|
|
{
|
|
await Task.Delay(2500);
|
|
BeginInvoke(Close);
|
|
}
|
|
return new { started = true };
|
|
}
|
|
}
|