Auto-Update wie beim MSCO Launcher

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>
This commit is contained in:
2026-10-28 21:19:48 +01:00
co-authored by Claude Opus 5.5
parent 9cd5f2b153
commit c7a30e7fa3
11 changed files with 284 additions and 225 deletions
+4 -4
View File
@@ -32,10 +32,10 @@
<EmbeddedResource Include="ui\**\*" LogicalName="ui/%(RecursiveDir)%(Filename)%(Extension)" /> <EmbeddedResource Include="ui\**\*" LogicalName="ui/%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup> </ItemGroup>
<!-- dotnet publish -c Release -o publish -p:BsoRelease=true: die neue Version signieren und als Gitea-Release veroeffentlichen <!-- dotnet publish: neue Version automatisch auf die Website laden (wie MSCO, nur mit Release-Token, siehe release.ps1).
(release.ps1, Changelog aus CHANGELOG.md). Ohne -p:BsoRelease=true wird nur gebaut. --> Gibt es die Version schon, passiert nichts. BSO_NO_RELEASE=1 baut, ohne hochzuladen. -->
<Target Name="BsoRelease" AfterTargets="Publish" Condition="'$(BsoRelease)' == 'true'"> <Target Name="BsoRelease" AfterTargets="Publish">
<Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -File &quot;$(MSBuildProjectDirectory)\release.ps1&quot; -Product launcher -Version $(Version) -File &quot;$(PublishDir)BSO-Launcher.exe&quot;" /> <Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -File &quot;$(MSBuildProjectDirectory)\release.ps1&quot; -Product launcher -Version $(Version) -File &quot;$([System.IO.Path]::GetFullPath('$(PublishDir)'))BSO-Launcher.exe&quot;" ContinueOnError="true" />
</Target> </Target>
</Project> </Project>
+4 -2
View File
@@ -1,10 +1,12 @@
# Changelog – BSO Launcher # Changelog – BSO Launcher
Neue Version: hier einen Abschnitt `## <Version>` anlegen, `<Version>` in `BSO.Launcher.csproj` erhöhen, dann Neue Version: hier einen Abschnitt `## <Version>` anlegen, `<Version>` in `BSO.Launcher.csproj` erhöhen, dann
`dotnet publish -c Release -o publish -p:BsoRelease=true` – der Abschnitt wird der Changelog auf der Website und im Launcher. `dotnet publish -c Release -o publish` – lädt die Version hoch; der Abschnitt wird der Changelog auf der Website und im Launcher.
## 0.3.2 ## 0.3.2
- Updates kommen direkt aus den Releases auf git.mbm.wtf (Website nur noch als Reserve) - Auto-Update wie beim MSCO Launcher: ein neuer Launcher installiert sich beim Start sofort selbst und startet neu,
neue BSO-Versionen kommen beim Start (beides in den Einstellungen abschaltbar)
- Einstellungen: Nach Updates suchen
- Startseite: Server-Karte entfernt – zusammen gespielt wird über Freunde & Lobby - Startseite: Server-Karte entfernt – zusammen gespielt wird über Freunde & Lobby
## 0.3.1 ## 0.3.1
+16 -5
View File
@@ -21,7 +21,7 @@ public sealed class MainForm : Form
/// <summary>Befehle, die nur einmal gleichzeitig laufen duerfen (Downloads/Installationen/Start).</summary> /// <summary>Befehle, die nur einmal gleichzeitig laufen duerfen (Downloads/Installationen/Start).</summary>
static readonly HashSet<string> Exclusive = new() { "installMod", "launch", "playLobby", "installLauncherUpdate", "uninstall" }; static readonly HashSet<string> Exclusive = new() { "installMod", "launch", "playLobby", "installLauncherUpdate", "uninstall" };
readonly HashSet<string> running = new(); readonly HashSet<string> running = new();
static readonly HashSet<string> QuietCommands = new() { "window", "status", "me", "init", "releases", "friends", "lobby" }; static readonly HashSet<string> QuietCommands = new() { "window", "status", "me", "init", "releases", "friends", "lobby", "checkLauncherUpdate" };
bool updateInstalled; bool updateInstalled;
static readonly JsonSerializerOptions Json = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; static readonly JsonSerializerOptions Json = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
@@ -248,6 +248,7 @@ public sealed class MainForm : Form
loggedIn = settings.Token != null, loggedIn = settings.Token != null,
master = Api.Base, master = Api.Base,
language = Lang.Current, language = Lang.Current,
autoUpdateLauncher = settings.AutoUpdateLauncher,
dev = Api.Dev || Api.Base != Api.BaseUrl dev = Api.Dev || Api.Base != Api.BaseUrl
}; };
@@ -311,19 +312,28 @@ public sealed class MainForm : Form
case "lobbyStop": return await Site("POST", "/api/lobby/stop"); case "lobbyStop": return await Site("POST", "/api/lobby/stop");
case "releases": case "releases":
{ {
if (!Api.TestWebsite && await GiteaReleases.List() is { Count: > 0 } fromGit) return fromGit;
var r = await Api.Releases(); var r = await Api.Releases();
if (!r.Ok) throw new Exception(r.Error ?? "Neuigkeiten nicht verfuegbar"); if (!r.Ok) throw new Exception(r.Error ?? "Neuigkeiten nicht verfuegbar");
return r.Json.GetProperty("releases"); 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": case "installLauncherUpdate":
{ {
var latest = await Api.Latest("launcher") ?? throw new Exception("Kein Launcher-Update gefunden"); 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 …")); string version = await SelfUpdate.Install(latest, p => Progress("launcher", p, "Launcher-Update wird geladen …"));
updateInstalled = true; updateInstalled = true;
LauncherLog.Info("Launcher aktualisiert auf " + version); LauncherLog.Info("Launcher " + version + " installiert - Neustart");
BeginInvoke(Close); // die neue Version startet gerade // Beenden, damit die neue Version starten kann. Haengt das Schliessen (WebView2 o.ae.), nach 5 s hart
return new { version }; // 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 // --- Einstellungen
@@ -333,6 +343,7 @@ public sealed class MainForm : Form
settings.AutoConnect = BoolArg(args, "autoConnect") ?? settings.AutoConnect; settings.AutoConnect = BoolArg(args, "autoConnect") ?? settings.AutoConnect;
settings.KeepOpen = BoolArg(args, "keepOpen") ?? settings.KeepOpen; settings.KeepOpen = BoolArg(args, "keepOpen") ?? settings.KeepOpen;
settings.AutoUpdateMod = BoolArg(args, "autoUpdateMod") ?? settings.AutoUpdateMod; 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); } if (Arg(args, "language") is "de" or "en") { settings.Language = Arg(args, "language"); Lang.Set(settings.Language); }
settings.Save(); settings.Save();
return null; return null;
+15 -12
View File
@@ -15,8 +15,8 @@ WinForms-Fenster mit WebView2, die Oberfläche (`ui\`) ist in der .exe eingebett
oder eine neuere Version installiert, bietet es Starten an. `--portable` startet ohne Installation. oder eine neuere Version installiert, bietet es Starten an. `--portable` startet ohne Installation.
- **BSO-Konto:** Registrieren und Anmelden bei der BSO-Website. Die Anmeldung bleibt 30 Tage (Token DPAPI-verschlüsselt). - **BSO-Konto:** Registrieren und Anmelden bei der BSO-Website. Die Anmeldung bleibt 30 Tage (Token DPAPI-verschlüsselt).
Der Kontoname ist der Name im Spiel. Der Kontoname ist der Name im Spiel.
- **Auto-Update:** Bei jedem Start fragt der Launcher, ob ein neues Update (Launcher oder Mod) installiert werden soll; - **Auto-Update (wie MSCO):** Beim Start installiert sich ein neuer Launcher sofort selbst und startet neu; neue Mod-
dazu neueste Mod-Version vor jedem Spielstart (abschaltbar). Versionen kommen beim Start und vor jedem Spielstart (beides abschaltbar). Einstellungen: *Nach Updates suchen*.
Downloads sind fortsetzbar, SHA-256-geprüft und müssen mit dem **BSO-Release-Schlüssel signiert** sein. Downloads sind fortsetzbar, SHA-256-geprüft und müssen mit dem **BSO-Release-Schlüssel signiert** sein.
- **Prüft alles:** Spielordner (alle Steam-Bibliotheken), BSO-Version, .NET 10 Runtime (braucht die Mod). - **Prüft alles:** Spielordner (alle Steam-Bibliotheken), BSO-Version, .NET 10 Runtime (braucht die Mod).
- **Freunde & Lobby:** Freunde per Benutzername hinzufügen (online-Anzeige), Lobby erstellen, einladen - SPIELEN des Erstellers startet den Lobby-Server. - **Freunde & Lobby:** Freunde per Benutzername hinzufügen (online-Anzeige), Lobby erstellen, einladen - SPIELEN des Erstellers startet den Lobby-Server.
@@ -29,13 +29,12 @@ WinForms-Fenster mit WebView2, die Oberfläche (`ui\`) ist in der .exe eingebett
## Bauen und veröffentlichen ## Bauen und veröffentlichen
```powershell ```powershell
dotnet publish -c Release -o publish -p:BsoRelease=true # bauen + als Gitea-Release veroeffentlichen dotnet publish -c Release -o publish # bauen - und die neue Version gleich auf https://bso.mbm.wtf laden
.\release.ps1 -Product launcher -Version 0.3.2 -File publish\BSO-Launcher.exe # oder von Hand
``` ```
Updates kommen aus den **Gitea-Releases** auf git.mbm.wtf (Mod: `MrMaho/BSO-Mod`, Launcher: `MrMaho/BSO-Launcher`, beide Wie bei MSCO: `dotnet publish` ruft `release.ps1` auf – signiert die .exe und lädt sie mit Changelog (Abschnitt
öffentlich). Jedes Release hat die Datei und `bso-release.json` (Version, SHA-256, Signatur). Ist Gitea nicht erreichbar, `## <Version>` aus `CHANGELOG.md`) auf die Website. Gibt es die Version schon, passiert nichts; also Version in der
fragt der Launcher die Website; mit `-dev` umgekehrt. `.csproj` erhöhen und bauen. `BSO_NO_RELEASE=1` baut nur. Der Launcher holt seine Updates von der Website.
## Gegen die lokale Test-Website (`-dev`) ## Gegen die lokale Test-Website (`-dev`)
@@ -53,11 +52,15 @@ Lokale Website starten: im BSO-Website-Repo `dotnet run --urls http://localhost:
## Release-Schlüssel ## Release-Schlüssel
`new-release-key.ps1` legt einmalig den Schlüssel an (RSA 3072, privater Teil DPAPI-verschlüsselt unter Geheimnisse liegen DPAPI-verschlüsselt unter `%USERPROFILE%\.bso\` (wie `.msco` bei MSCO):
`%APPDATA%\BSO\release-key.dpapi`) und trägt den öffentlichen Teil in `Services\ReleaseSignature.cs` ein.
`release.ps1` (in BSO-Mod und BSO-Launcher) signiert damit `BSO-RELEASE|produkt|version|sha256` und legt das | | Datei | Einrichten |
Gitea-Release an – braucht `BSO_GITEA_TOKEN` (Gitea: Einstellungen → Anwendungen → Token mit *repository: Lesen und |---|---|---|
Schreiben*), gern als Windows-Benutzervariable. | Release-Token (`RELEASE_TOKEN` der Website) | `release-token.dpapi` | `release.ps1 -SaveToken` (oder `BSO_RELEASE_TOKEN`) |
| Signier-Schlüssel (RSA 3072) | `release-signing-key.dpapi` | `release.ps1 -ImportSigningKey <sicherung>` |
Sicherung: `release.ps1 -ExportSigningKey <datei>` (AES-256, eigenes Passwort). Ein ganz neues Paar nur im Notfall:
`new-release-key.ps1` (trägt den öffentlichen Teil in `Services\ReleaseSignature.cs` ein).
**Den Schlüssel sichern:** Mit einem neuen Schlüssel erkennen bereits verteilte Launcher keine Updates mehr. **Den Schlüssel sichern:** Mit einem neuen Schlüssel erkennen bereits verteilte Launcher keine Updates mehr.
+2 -10
View File
@@ -75,16 +75,8 @@ public static class Api
// --- Freunde und Lobby (alles mit dem Konto-Token) // --- Freunde und Lobby (alles mit dem Konto-Token)
public static Task<Result> Get(string path, string token) => Send(HttpMethod.Get, path, null, token); public static Task<Result> Get(string path, string token) => Send(HttpMethod.Get, path, null, token);
public static Task<Result> Post(string path, string token, object? body = null) => Send(HttpMethod.Post, path, body ?? new { }, token); public static Task<Result> Post(string path, string token, object? body = null) => Send(HttpMethod.Post, path, body ?? new { }, token);
/// <summary>Gegen eine Test-Website (-dev, BSO_LAUNCHER_API)? Dann kommen Releases zuerst von dort.</summary> /// <summary>Neueste Version von mod/launcher auf der Website (wie beim MSCO Keymaster) - oder null.</summary>
public static bool TestWebsite => Base != BaseUrl.TrimEnd('/'); public static async Task<Release?> Latest(string product)
/// <summary>Neueste Version: vom Git-Server (Gitea-Releases), sonst von der Website - mit -dev umgekehrt.</summary>
public static async Task<Release?> Latest(string product) =>
TestWebsite
? await WebsiteLatest(product) ?? await GiteaReleases.Latest(product)
: await GiteaReleases.Latest(product) ?? await WebsiteLatest(product);
static async Task<Release?> WebsiteLatest(string product)
{ {
var r = await Send(HttpMethod.Get, "/api/releases/latest?product=" + product, null, null); var r = await Send(HttpMethod.Get, "/api/releases/latest?product=" + product, null, null);
if (!r.Ok) return null; if (!r.Ok) return null;
-85
View File
@@ -1,85 +0,0 @@
using System.Text.Json;
namespace BSO.Launcher;
/// <summary>
/// Releases direkt vom Git-Server (git.mbm.wtf, Gitea): Mod aus BSO-Mod, Launcher aus BSO-Launcher (beide oeffentlich).
/// Jedes Release hat die Datei und bso-release.json (Version, SHA-256, Signatur aus release.ps1) - installiert wird weiter
/// nur, was mit dem BSO-Release-Schluessel signiert ist, egal woher die Datei kommt.
/// </summary>
public static class GiteaReleases
{
public const string ApiBase = "https://git.mbm.wtf/api/v1/repos/MrMaho/";
public const string ManifestName = "bso-release.json";
static string Repo(string product) => product == "launcher" ? "BSO-Launcher" : "BSO-Mod";
sealed record Manifest(string product, string version, string sha256, long size, string file, string signature);
/// <summary>Version aus dem Tag ("v0.5.0" -> "0.5.0") oder null.</summary>
static string? TagVersion(JsonElement release)
{
string tag = release.GetProperty("tag_name").GetString() ?? "";
string v = tag.TrimStart('v', 'V');
return Version.TryParse(v, out _) ? v : null;
}
static bool Published(JsonElement release) =>
!release.GetProperty("draft").GetBoolean() && !release.GetProperty("prerelease").GetBoolean();
static async Task<JsonElement[]> Fetch(string product)
{
using var doc = JsonDocument.Parse(await Api.Http.GetStringAsync(ApiBase + Repo(product) + "/releases?limit=20"));
return doc.RootElement.EnumerateArray().Where(Published).Select(r => r.Clone()).ToArray();
}
/// <summary>Neueste Version von mod/launcher - null, wenn es keine gibt oder Gitea nicht erreichbar ist.</summary>
public static async Task<Api.Release?> Latest(string product)
{
try
{
var newest = (await Fetch(product))
.Select(r => (release: r, version: TagVersion(r)))
.Where(x => x.version != null)
.OrderByDescending(x => Version.Parse(x.version!))
.FirstOrDefault();
if (newest.version == null) return null;
var assets = newest.release.GetProperty("assets").EnumerateArray()
.ToDictionary(a => a.GetProperty("name").GetString() ?? "", a => a.GetProperty("browser_download_url").GetString() ?? "");
if (!assets.TryGetValue(ManifestName, out string? manifestUrl)) return null;
var m = JsonSerializer.Deserialize<Manifest>(await Api.Http.GetStringAsync(manifestUrl));
if (m == null || m.product != product || !assets.TryGetValue(m.file, out string? fileUrl)) return null;
return new Api.Release(product, m.version, m.sha256, m.size, newest.release.GetProperty("body").GetString() ?? "",
newest.release.GetProperty("published_at").GetString() ?? "", m.file, fileUrl, m.signature);
}
catch (Exception e)
{
LauncherLog.Error($"Gitea-Release {product}: {e.Message}");
return null;
}
}
/// <summary>Neuigkeiten: Releases von Mod und Launcher, neueste zuerst. null = Gitea nicht erreichbar.</summary>
public static async Task<List<object>?> List()
{
var items = new List<(DateTimeOffset date, object item)>();
int reached = 0;
foreach (string product in new[] { "mod", "launcher" })
{
try
{
foreach (var r in await Fetch(product))
{
if (TagVersion(r) is not { } version) continue;
string date = r.GetProperty("published_at").GetString() ?? "";
items.Add((DateTimeOffset.TryParse(date, out var d) ? d : default,
new { product, version, changelog = r.GetProperty("body").GetString() ?? "", date }));
}
reached++;
}
catch (Exception e) { LauncherLog.Error($"Gitea-Releases {product}: {e.Message}"); }
}
return reached == 0 ? null : items.OrderByDescending(x => x.date).Select(x => x.item).ToList();
}
}
+2
View File
@@ -23,6 +23,8 @@ public sealed class Settings
public bool AutoConnect { get; set; } = true; public bool AutoConnect { get; set; } = true;
public bool KeepOpen { get; set; } public bool KeepOpen { get; set; }
public bool AutoUpdateMod { get; set; } = true; public bool AutoUpdateMod { get; set; } = true;
/// <summary>Beim Start eine neue Launcher-Version gleich installieren und neu starten (wie beim MSCO Launcher).</summary>
public bool AutoUpdateLauncher { get; set; } = true;
/// <summary>"de" oder "en" - null = nach der Windows-Sprache (Lang.Default).</summary> /// <summary>"de" oder "en" - null = nach der Windows-Sprache (Lang.Default).</summary>
public string? Language { get; set; } public string? Language { get; set; }
+160 -62
View File
@@ -1,41 +1,143 @@
# Signiert ein Release und veroeffentlicht es als Gitea-Release auf git.mbm.wtf (Launcher und Website holen es von dort). # Neue Version automatisch auf die BSO-Website hochladen (wie release.ps1 bei MSCO -> Keymaster).
# .\release.ps1 -Product mod|launcher -Version 0.5.0 -File dist\BSO-Mod.zip [-Changelog "..."] # Laeuft nach dem Release-Build von selbst (Launcher: dotnet publish -c Release, Mod: build.ps1) - oder von Hand:
# Laeuft auch automatisch bei "dotnet publish -p:BsoRelease=true" (Launcher) bzw. "build.ps1 -Release" (Mod). # powershell -ExecutionPolicy Bypass -File release.ps1 -Product mod|launcher -File <datei> [-Version 0.5.1] [-Changelog "..."]
# - Repo: mod -> MrMaho/BSO-Mod, launcher -> MrMaho/BSO-Launcher; Tag v<Version> (wird auf dem Standard-Branch angelegt). #
# - Ohne -Changelog: der Abschnitt "## <Version>" aus CHANGELOG.md neben diesem Skript. # Braucht den Release-Token der Website (RELEASE_TOKEN in deren .env) und den privaten Signier-Schluessel. Beide liegen
# - Dateien im Release: die Datei (Launcher als BSO-Launcher-Setup.exe) und bso-release.json mit Version, SHA-256 und # verschluesselt beim Entwickler unter %USERPROFILE%\.bso\*.dpapi (Windows DPAPI - nur dein Windows-Konto kann sie lesen).
# Signatur. Signiert wird "BSO-RELEASE|produkt|version|sha256" - der Launcher prueft das mit dem eingebauten # Einrichten: "-SaveToken" (Token), Schluessel auf einem weiteren Rechner mit "-ImportSigningKey <backup>" aus der
# oeffentlichen Schluessel, egal woher die Datei kommt. # Sicherung ("-ExportSigningKey <datei>", mit eigenem Passwort) uebernehmen. Neues Paar: BSO-Launcher\new-release-key.ps1.
# Braucht: Release-Schluessel (BSO-Launcher\new-release-key.ps1) und BSO_GITEA_TOKEN (Gitea: Einstellungen -> Anwendungen # BSO_RELEASE_TOKEN (Umgebungs- oder Benutzervariable) ersetzt den gespeicherten Token, BSO_MASTER_URL die Website
# -> Token mit "repository: Lesen und Schreiben"), auch als Windows-Benutzervariable. Optional BSO_GITEA_URL. # (Standard https://bso.mbm.wtf), BSO_NO_RELEASE=1 schaltet Hochladen ab.
#
# Signatur: jede Datei wird signiert (RSA-3072, SHA-256, "BSO-RELEASE|produkt|version|sha256") - der Launcher
# installiert nur, was zu seinem eingebauten oeffentlichen Schluessel passt.
# Gibt es die Version auf der Website schon, passiert nichts - also einfach die Version erhoehen und bauen.
# Fehler brechen den Build nicht ab (Meldung, Exit 0).
param( param(
[Parameter(Mandatory)][ValidateSet("mod", "launcher")][string]$Product, [ValidateSet("mod", "launcher")][string]$Product,
[Parameter(Mandatory)][string]$Version, [string]$File,
[Parameter(Mandatory)][string]$File, [string]$Version = "",
[string]$Changelog = "" [string]$Changelog = "",
[switch]$SaveToken,
[string]$ExportSigningKey = "",
[string]$ImportSigningKey = ""
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
Add-Type -AssemblyName System.Security Add-Type -AssemblyName System.Security
Add-Type -AssemblyName System.Net.Http Add-Type -AssemblyName System.Net.Http
# Aus der Umgebung - sonst die Windows-Benutzervariable (z.B. wenn Visual Studio vor dem Setzen gestartet wurde) # ---------------------------------------------------------------- Ablage der Geheimnisse
function Setting([string]$name) { # Nicht in AppData: das virtualisieren App-Pakete (z.B. die Claude-App) - ein normales Terminal saehe die Dateien nicht
$v = [Environment]::GetEnvironmentVariable($name) $secretDir = Join-Path $env:USERPROFILE ".bso"
if (-not $v) { $v = [Environment]::GetEnvironmentVariable($name, "User") } $tokenFile = Join-Path $secretDir "release-token.dpapi"
return $v $keyFile = Join-Path $secretDir "release-signing-key.dpapi"
} $oldKeyFile = Join-Path $env:APPDATA "BSO\release-key.dpapi" # fruehere Ablage (new-release-key.ps1)
$gitea = ((Setting "BSO_GITEA_URL"), "https://git.mbm.wtf" | Where-Object { $_ } | Select-Object -First 1).TrimEnd('/')
$token = Setting "BSO_GITEA_TOKEN"
if (-not $token) { Write-Host "BSO_GITEA_TOKEN nicht gesetzt - kein Release angelegt"; return }
$keyFile = Join-Path $env:APPDATA "BSO\release-key.dpapi"
if (-not (Test-Path $keyFile)) { throw "Kein Release-Schluessel ($keyFile) - erst new-release-key.ps1 im BSO-Launcher-Repo" }
$Version = $Version.Split('+')[0]
$repo = @{ mod = "BSO-Mod"; launcher = "BSO-Launcher" }[$Product]
$api = "$gitea/api/v1/repos/MrMaho/$repo"
# Changelog aus CHANGELOG.md: alles unter "## <Version>" bis zur naechsten "## "-Zeile function Read-Secret([string]$file) {
if (-not (Test-Path -LiteralPath $file)) { return $null }
return [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($file), $null, [Security.Cryptography.DataProtectionScope]::CurrentUser)
}
function Write-Secret([string]$file, [byte[]]$data) {
New-Item -ItemType Directory -Force -Path $secretDir | Out-Null
[IO.File]::WriteAllBytes($file, [Security.Cryptography.ProtectedData]::Protect($data, $null, [Security.Cryptography.DataProtectionScope]::CurrentUser))
}
function Read-SigningKey {
$k = Read-Secret $keyFile
if ($null -eq $k) { $k = Read-Secret $oldKeyFile }
return $k
}
function Get-Pbkdf2Key([string]$password, [byte[]]$salt) {
$kdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($password, $salt, 300000, [Security.Cryptography.HashAlgorithmName]::SHA256)
return $kdf.GetBytes(32)
}
function Get-BackupPassword([string]$prompt) {
if ($env:BSO_BACKUP_PASSWORD) { return $env:BSO_BACKUP_PASSWORD }
$secure = Read-Host $prompt -AsSecureString
return (New-Object System.Net.NetworkCredential('', $secure)).Password
}
# ---------------------------------------------------------------- Einrichten / Sichern
if ($SaveToken) {
$plain = $env:BSO_RELEASE_TOKEN
if (-not $plain) {
$secure = Read-Host "Release-Token" -AsSecureString
$plain = (New-Object System.Net.NetworkCredential('', $secure)).Password
}
if (-not $plain -or $plain.Length -lt 24) { Write-Host "Token zu kurz - nichts gespeichert"; exit 1 }
Write-Secret $tokenFile ([Text.Encoding]::UTF8.GetBytes($plain))
Write-Host "Release-Token verschluesselt gespeichert: $tokenFile"
exit 0
}
# Sicherung des privaten Schluessels mit eigenem Passwort (AES-256, PBKDF2) - z.B. auf dem NAS / USB-Stick
if ($ExportSigningKey) {
$private = Read-SigningKey
if ($null -eq $private) { Write-Host "Kein Signier-Schluessel ($keyFile)"; exit 1 }
$pw = Get-BackupPassword "Passwort fuer die Sicherung"
if ($pw.Length -lt 12) { Write-Host "Mindestens 12 Zeichen"; exit 1 }
if ((Get-BackupPassword "Passwort wiederholen") -ne $pw) { Write-Host "Die Passwoerter sind nicht gleich - nichts geschrieben"; exit 1 }
$salt = New-Object byte[] 16; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($salt)
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = Get-Pbkdf2Key $pw $salt; $aes.GenerateIV()
$enc = $aes.CreateEncryptor().TransformFinalBlock($private, 0, $private.Length)
$out = "BSO-SIGNING-KEY-BACKUP v1`n" + [Convert]::ToBase64String($salt) + "`n" + [Convert]::ToBase64String($aes.IV) + "`n" + [Convert]::ToBase64String($enc)
[IO.File]::WriteAllText($ExportSigningKey, $out)
# Gegenprobe: Datei lesen und mit dem Passwort wieder entschluesseln
$lines = [IO.File]::ReadAllText($ExportSigningKey).Split("`n")
$aes2 = [Security.Cryptography.Aes]::Create(); $aes2.Key = Get-Pbkdf2Key $pw ([Convert]::FromBase64String($lines[1])); $aes2.IV = [Convert]::FromBase64String($lines[2])
$check = [Convert]::FromBase64String($lines[3]); $check = $aes2.CreateDecryptor().TransformFinalBlock($check, 0, $check.Length)
if ([Convert]::ToBase64String($check) -ne [Convert]::ToBase64String($private)) { Write-Host "Gegenprobe FEHLGESCHLAGEN - die Sicherung ist unbrauchbar"; exit 1 }
Write-Host "Sicherung geschrieben und geprueft: $ExportSigningKey - Datei und Passwort gut aufheben"
exit 0
}
if ($ImportSigningKey) {
if (-not (Test-Path -LiteralPath $ImportSigningKey)) { Write-Host "Nicht gefunden: $ImportSigningKey"; exit 1 }
$lines = [IO.File]::ReadAllText($ImportSigningKey).Split("`n")
if ($lines.Count -lt 4 -or $lines[0].Trim() -ne "BSO-SIGNING-KEY-BACKUP v1") { Write-Host "Das ist keine BSO-Schluessel-Sicherung"; exit 1 }
$pw = Get-BackupPassword "Passwort der Sicherung"
try {
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = Get-Pbkdf2Key $pw ([Convert]::FromBase64String($lines[1])); $aes.IV = [Convert]::FromBase64String($lines[2])
$enc = [Convert]::FromBase64String($lines[3].Trim())
$private = $aes.CreateDecryptor().TransformFinalBlock($enc, 0, $enc.Length)
[Security.Cryptography.RSA]::Create().FromXmlString([Text.Encoding]::UTF8.GetString($private))
} catch { Write-Host "Falsches Passwort oder beschaedigte Sicherung"; exit 1 }
if ($null -ne (Read-Secret $keyFile)) { Write-Host "Es gibt schon einen Signier-Schluessel ($keyFile) - nicht ersetzt"; exit 1 }
Write-Secret $keyFile $private
Write-Host "Signier-Schluessel uebernommen: $keyFile"
exit 0
}
# ---------------------------------------------------------------- Hochladen
if (-not $Product -or -not $File) { Write-Host "Aufruf: release.ps1 -Product mod|launcher -File <datei> | -SaveToken | -ExportSigningKey <datei> | -ImportSigningKey <datei>"; exit 1 }
if ($env:BSO_NO_RELEASE -eq "1") { Write-Host "BSO release: BSO_NO_RELEASE=1 - nicht hochgeladen."; exit 0 }
$token = $env:BSO_RELEASE_TOKEN
if (-not $token) { $token = [Environment]::GetEnvironmentVariable("BSO_RELEASE_TOKEN", "User") } # z.B. Visual Studio vor dem Setzen gestartet
if (-not $token) {
try { $stored = Read-Secret $tokenFile; if ($null -ne $stored) { $token = [Text.Encoding]::UTF8.GetString($stored) } }
catch { Write-Host "BSO release: gespeicherter Token nicht lesbar (anderer Benutzer/PC?) - nicht hochgeladen."; exit 0 }
}
if (-not $token) { Write-Host "BSO release: kein Release-Token ($tokenFile oder BSO_RELEASE_TOKEN) - einrichten mit 'release.ps1 -SaveToken'. Nicht hochgeladen."; exit 0 }
$master = $env:BSO_MASTER_URL
if (-not $master) { $master = [Environment]::GetEnvironmentVariable("BSO_MASTER_URL", "User") }
if (-not $master) { $master = "https://bso.mbm.wtf" }
$master = $master.TrimEnd('/')
if (-not (Test-Path $File)) { Write-Host "BSO release: $File nicht gefunden"; exit 1 }
if (-not $Version) { $Version = ((Get-Item $File).VersionInfo.ProductVersion -split '\+')[0] }
$Version = $Version.Split('+')[0]
if (-not $Version) { Write-Host "BSO release: Version unbekannt - -Version angeben"; exit 1 }
# Changelog: Abschnitt "## <Version>" aus CHANGELOG.md neben diesem Skript
if (-not $Changelog) { if (-not $Changelog) {
$md = Join-Path $PSScriptRoot "CHANGELOG.md" $md = Join-Path $here "CHANGELOG.md"
if (Test-Path $md) { if (Test-Path $md) {
$lines = [IO.File]::ReadAllLines($md) $lines = [IO.File]::ReadAllLines($md)
$start = [Array]::FindIndex($lines, [Predicate[string]] { param($l) $l -match "^##\s+v?$([regex]::Escape($Version))(\s|$)" }) $start = [Array]::FindIndex($lines, [Predicate[string]] { param($l) $l -match "^##\s+v?$([regex]::Escape($Version))(\s|$)" })
@@ -45,41 +147,37 @@ if (-not $Changelog) {
$Changelog = ($section -join "`n").Trim() $Changelog = ($section -join "`n").Trim()
} }
} }
if ($Changelog) { Write-Host "Changelog aus CHANGELOG.md ($Version)" } else { Write-Host "Kein Changelog fuer $Version in CHANGELOG.md" }
} }
$path = (Resolve-Path $File).Path # Signieren: passt nur zu genau dieser Datei, diesem Produkt und dieser Version
$fileName = if ($Product -eq "launcher") { "BSO-Launcher-Setup.exe" } else { Split-Path $path -Leaf } $data = [IO.File]::ReadAllBytes((Resolve-Path $File))
$data = [IO.File]::ReadAllBytes($path) $sha = ([Security.Cryptography.SHA256]::Create().ComputeHash($data) | ForEach-Object { $_.ToString("x2") }) -join ""
$sha = ([System.Security.Cryptography.SHA256]::Create().ComputeHash($data) | ForEach-Object { $_.ToString("x2") }) -join "" try { $private = Read-SigningKey }
$private = [System.Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($keyFile), $null, catch { Write-Host "BSO release: Signier-Schluessel nicht lesbar (anderer Benutzer/PC?) - nicht hochgeladen"; exit 0 }
[System.Security.Cryptography.DataProtectionScope]::CurrentUser) if ($null -eq $private) { Write-Host "BSO release: kein Signier-Schluessel ($keyFile) - mit 'release.ps1 -ImportSigningKey <sicherung>' uebernehmen. Nicht hochgeladen."; exit 0 }
$rsa = [System.Security.Cryptography.RSA]::Create() try {
$rsa.FromXmlString([Text.Encoding]::UTF8.GetString($private)) $rsa = [Security.Cryptography.RSA]::Create()
$message = [Text.Encoding]::UTF8.GetBytes("BSO-RELEASE|$Product|$Version|$sha") $rsa.FromXmlString([Text.Encoding]::UTF8.GetString($private))
$signature = [Convert]::ToBase64String($rsa.SignData($message, [System.Security.Cryptography.HashAlgorithmName]::SHA256, $message = [Text.Encoding]::UTF8.GetBytes("BSO-RELEASE|$Product|$Version|$sha")
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)) $signature = [Convert]::ToBase64String($rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1))
$manifest = [ordered]@{ product = $Product; version = $Version; sha256 = $sha; size = $data.Length; file = $fileName; signature = $signature } | $private = $null
ConvertTo-Json -Compress } catch { Write-Host "BSO release: Signieren fehlgeschlagen ($($_.Exception.Message)) - nicht hochgeladen"; exit 0 }
$http = New-Object System.Net.Http.HttpClient $http = New-Object System.Net.Http.HttpClient
$http.Timeout = [TimeSpan]::FromMinutes(10) $http.Timeout = [TimeSpan]::FromMinutes(10)
$http.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("token", $token) $http.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", $token)
$form = New-Object System.Net.Http.MultipartFormDataContent
# Release anlegen (Tag v<Version>) foreach ($pair in @(@("product", $Product), @("version", $Version), @("changelog", $Changelog), @("signature", $signature))) {
$json = @{ tag_name = "v$Version"; name = "$Product $Version"; body = $Changelog; draft = $false; prerelease = $false } | ConvertTo-Json -Compress $form.Add((New-Object System.Net.Http.StringContent($pair[1], [Text.Encoding]::UTF8)), "`"$($pair[0])`"")
$res = $http.PostAsync("$api/releases", (New-Object System.Net.Http.StringContent($json, [Text.Encoding]::UTF8, "application/json"))).Result
$body = $res.Content.ReadAsStringAsync().Result
if ($res.StatusCode -eq [Net.HttpStatusCode]::Conflict) { Write-Host "$repo v$Version gibt es schon - Version erhoehen"; return }
if (-not $res.IsSuccessStatusCode) { throw "Release anlegen fehlgeschlagen ($([int]$res.StatusCode)): $body" }
$id = ($body | ConvertFrom-Json).id
function Upload([string]$name, [byte[]]$bytes) {
$form = New-Object System.Net.Http.MultipartFormDataContent
$form.Add((New-Object System.Net.Http.ByteArrayContent(, $bytes)), "`"attachment`"", "`"$name`"")
$r = $http.PostAsync("$api/releases/$id/assets?name=$([Uri]::EscapeDataString($name))", $form).Result
if (-not $r.IsSuccessStatusCode) { throw "Hochladen von $name fehlgeschlagen ($([int]$r.StatusCode)): $($r.Content.ReadAsStringAsync().Result)" }
} }
Upload $fileName $data $form.Add((New-Object System.Net.Http.ByteArrayContent(, $data)), "`"file`"", "`"$(Split-Path $File -Leaf)`"")
Upload "bso-release.json" ([Text.Encoding]::UTF8.GetBytes($manifest)) try {
Write-Host "$Product $Version veroeffentlicht: $gitea/MrMaho/$repo/releases/tag/v$Version (sha256 $sha)" $res = $http.PostAsync("$master/api/releases/upload", $form).GetAwaiter().GetResult()
$body = $res.Content.ReadAsStringAsync().GetAwaiter().GetResult()
} catch { Write-Host "BSO release: $master nicht erreichbar ($($_.Exception.Message)) - nicht hochgeladen"; exit 0 }
switch ([int]$res.StatusCode) {
200 { Write-Host "BSO release: $Product $Version hochgeladen nach $master" }
409 { Write-Host "BSO release: $Product $Version gibt es auf $master schon - zum Veroeffentlichen die Version erhoehen" }
default { Write-Host "BSO release: Hochladen fehlgeschlagen (HTTP $([int]$res.StatusCode)): $body" }
}
exit 0
+4 -3
View File
@@ -178,14 +178,15 @@ nav button.on { background: var(--panel-2); color: var(--text); }
background: var(--panel-2); color: var(--accent-2); border: 1px solid var(--border); } background: var(--panel-2); color: var(--accent-2); border: 1px solid var(--border); }
.release pre { margin: 0; white-space: pre-wrap; font: inherit; color: #c4ccd8; } .release pre { margin: 0; white-space: pre-wrap; font: inherit; color: #c4ccd8; }
/* Update-Abfrage beim Start */ /* Launcher-Update beim Start (wie beim MSCO Launcher) */
.modal-back { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center; padding: 24px; background: rgba(5, 7, 10, .72); } .modal-back { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center; padding: 24px; background: rgba(5, 7, 10, .72); }
.modal { width: min(520px, 100%); max-height: calc(100vh - 48px); overflow: auto; display: grid; gap: 14px; background: var(--panel); .modal { width: min(520px, 100%); max-height: calc(100vh - 48px); overflow: auto; display: grid; gap: 14px; background: var(--panel);
border: 1px solid var(--border); border-radius: 18px; padding: 24px; box-shadow: 0 24px 64px rgba(0, 0, 0, .5); } border: 1px solid var(--border); border-radius: 18px; padding: 24px; box-shadow: 0 24px 64px rgba(0, 0, 0, .5); }
.modal h2 { margin: 0; } .modal h2 { margin: 0; }
.modal p { margin: 0; } .modal p { margin: 0; }
.update-item { display: grid; gap: 6px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 12px; padding: 12px 14px; } .update-box { text-align: center; justify-items: center; width: min(420px, 100%); }
.update-item pre { margin: 0; max-height: 160px; overflow: auto; white-space: pre-wrap; font: inherit; font-size: 13px; color: #c4ccd8; } .update-box img { width: 72px; height: 72px; }
.update-box .progress { width: 100%; text-align: left; }
/* ---------------------------------------------------------------- Einstellungen */ /* ---------------------------------------------------------------- Einstellungen */
.settings { display: grid; gap: 14px; } .settings { display: grid; gap: 14px; }
+63 -37
View File
@@ -141,6 +141,7 @@ function showApp(username) {
$("meName").textContent = username; $("meName").textContent = username;
$("meAvatar").textContent = (username || "?").slice(0, 1).toUpperCase(); $("meAvatar").textContent = (username || "?").slice(0, 1).toUpperCase();
showPage("home"); showPage("home");
autoUpdateMod();
} }
// ---------------------------------------------------------------- Seiten // ---------------------------------------------------------------- Seiten
@@ -419,9 +420,28 @@ async function renderSettings(root) {
uninstall.addEventListener("click", () => busy(uninstall, () => call("uninstall"))); uninstall.addEventListener("click", () => busy(uninstall, () => call("uninstall")));
const langBox = h("div", { class: "lang-switch", "data-lang-switch": true }); const langBox = h("div", { class: "lang-switch", "data-lang-switch": true });
// Launcher-Update von Hand (wie beim MSCO Launcher: "Nach Updates suchen" -> "Installieren und neu starten")
const launcherBox = h("div", {});
const check = h("button", { class: "btn small", type: "button" }, t("Nach Updates suchen"));
const install = h("button", { class: "btn small primary", type: "button" }, t("Installieren und neu starten"));
install.addEventListener("click", () => busy(install, async () => {
progressBox = h("div", { class: "progress" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div")));
launcherBox.append(progressBox);
try { await call("installLauncherUpdate"); install.textContent = t("Neustart …"); install.disabled = true; }
catch (e) { progressBox.remove(); throw e; }
}));
check.addEventListener("click", () => busy(check, async () => {
const u = await call("checkLauncherUpdate");
launcherBox.replaceChildren(u.available
? h("div", { class: "row" }, h("b", {}, t("Launcher {0} ist da", u.latest)), install)
: h("span", { class: "muted small" }, u.latest ? t("Der Launcher ist aktuell") : t("Keine Launcher-Version auf dem Server")));
}));
root.replaceChildren(h("h1", {}, t("Einstellungen")), h("div", { class: "settings" }, root.replaceChildren(h("h1", {}, t("Einstellungen")), h("div", { class: "settings" },
h("div", { class: "card" }, h("div", { class: "card" },
toggle(t("BSO automatisch aktualisieren"), t("Vor jedem Start die neueste Mod-Version installieren"), "autoUpdateMod", s?.settings.autoUpdateMod ?? true), toggle(t("Launcher automatisch aktualisieren"), t("Beim Start wird eine neue Launcher-Version gleich installiert (er startet von selbst neu)"),
"autoUpdateLauncher", init.autoUpdateLauncher ?? true),
toggle(t("BSO automatisch aktualisieren"), t("Neue BSO-Versionen beim Start des Launchers und vor jedem Spielstart installieren"), "autoUpdateMod", s?.settings.autoUpdateMod ?? true),
toggle(t("Launcher offen lassen"), t("Sonst schliesst er sich nach dem Spielstart"), "keepOpen", s?.settings.keepOpen ?? false), toggle(t("Launcher offen lassen"), t("Sonst schliesst er sich nach dem Spielstart"), "keepOpen", s?.settings.keepOpen ?? false),
h("div", { class: "switch" }, h("span", {}, t("Sprache")), langBox)), h("div", { class: "switch" }, h("span", {}, t("Sprache")), langBox)),
h("div", { class: "card" }, h("div", { class: "card" },
@@ -431,50 +451,56 @@ async function renderSettings(root) {
h("div", { class: "card" }, h("div", { class: "card" },
h("h3", {}, "Launcher"), h("h3", {}, "Launcher"),
h("span", { class: "muted small" }, t("Version {0} · Server {1}", init.version, init.master)), h("span", { class: "muted small" }, t("Version {0} · Server {1}", init.version, init.master)),
h("div", { class: "row" }, log, uninstall)))); launcherBox,
h("div", { class: "row" }, check, log, uninstall))));
renderLangSwitch(setLang); renderLangSwitch(setLang);
} }
// ---------------------------------------------------------------- Update-Abfrage // ---------------------------------------------------------------- Updates (wie beim MSCO Launcher)
/** Bei jedem Start: gibt es einen neuen Launcher (oder eine neue Mod)? Dann fragen - "Später" fragt beim naechsten Start wieder. */ /**
async function askForUpdates() { * Beim Start: gibt es einen neueren Launcher, gleich installieren und neu starten (Einstellung "Launcher automatisch
let s; * aktualisieren"). Hoechstens 8 s auf die Pruefung warten - ohne Netz startet der Launcher normal.
try { s = await call("status"); } catch { return; } */
const mod = s.mod.installed && s.mod.updateAvailable && !s.game.running ? s.mod : null; async function startupUpdate(init) {
if (!s.launcherUpdate && !mod) return; let u;
try {
u = await Promise.race([call("checkLauncherUpdate"), new Promise((_, no) => setTimeout(() => no(new Error("timeout")), 8000))]);
} catch { return false; }
if (!u || !u.available) return false;
if (init.autoUpdateLauncher === false) { toast(t("Launcher {0} ist da – unter Einstellungen installieren", u.latest)); return false; }
const item = (name, from, to, changelog) => h("div", { class: "update-item" }, const progress = h("div", { class: "progress" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div")));
h("div", {}, h("b", {}, name + " " + to), h("span", { class: "muted small" }, " " + t("installiert: {0}", from))), const text = h("p", { class: "muted" }, t("Launcher wird auf {0} aktualisiert …", u.latest));
h("pre", {}, changelog || t("Keine Beschreibung"))); const overlay = h("div", { class: "modal-back" }, h("div", { class: "modal update-box" },
const progress = h("div", { class: "progress hidden" }, h("span", { class: "small muted" }), h("div", { class: "bar" }, h("div"))); h("img", { src: "logo.svg", alt: "" }),
const later = h("button", { class: "btn", type: "button" }, t("Später")); h("h2", {}, t("Launcher {0} ist da", u.latest)),
const now = h("button", { class: "btn primary", type: "button" }, t("Jetzt aktualisieren")); text, progress,
const box = h("div", { class: "modal-back" }, h("div", { class: "modal", role: "dialog" }, h("small", { class: "muted" }, t("Der Launcher startet danach von selbst neu."))));
h("h2", {}, t("Update verfügbar")), document.body.append(overlay);
h("p", { class: "muted" }, t("Beim Start gefunden – jetzt aktualisieren?")),
s.launcherUpdate ? item("Launcher", s.launcherVersion, s.launcherUpdate, s.launcherChangelog) : null,
mod ? item(t("BSO-Mod"), mod.installed, mod.latest, mod.changelog) : null,
progress,
h("div", { class: "row", style: "justify-content:flex-end" }, later, now)));
document.body.append(box);
now.focus();
later.addEventListener("click", () => box.remove());
now.addEventListener("click", async () => {
now.disabled = later.disabled = true;
progressBox = progress; progressBox = progress;
try { try {
if (mod) await call("installMod"); await call("installLauncherUpdate");
if (s.launcherUpdate) { await call("installLauncherUpdate"); toast(t("Launcher startet neu …")); return; } // beendet sich gleich text.textContent = t("Neustart …");
box.remove(); return true;
toast(t("BSO {0} installiert", mod.latest));
if (!$("app").classList.contains("hidden")) showPage(page);
} catch (e) { } catch (e) {
toast(e.message, true); overlay.remove();
now.disabled = later.disabled = false; progressBox = null;
toast(t("Launcher-Update fehlgeschlagen: {0}", e.message), true);
return false;
} }
}); }
/** Nach dem Anmelden: eine neue BSO-Version gleich installieren (Einstellung "BSO automatisch aktualisieren"). */
async function autoUpdateMod() {
let s;
try { s = await call("status"); } catch { return; }
if (!s.settings.autoUpdateMod || !s.game.found || s.game.running || !s.mod.updateAvailable || !s.mod.latest) return;
try {
const r = await call("installMod");
toast(t("BSO {0} installiert", r.version));
if (page === "home") showPage("home");
} catch (e) { toast(e.message, true); }
} }
// ---------------------------------------------------------------- Start // ---------------------------------------------------------------- Start
@@ -494,7 +520,7 @@ async function askForUpdates() {
$("meVersion").textContent += " · DEV"; $("meVersion").textContent += " · DEV";
} }
$("authServer").textContent = t("Server: {0}", init.master.replace(/^https?:\/\//, "")); $("authServer").textContent = t("Server: {0}", init.master.replace(/^https?:\/\//, ""));
askForUpdates(); // auch vor dem Anmelden if (await startupUpdate(init)) return; // neue Version startet gleich
if (!init.loggedIn) { showAuth(); return; } if (!init.loggedIn) { showAuth(); return; }
const me = await call("me"); const me = await call("me");
if (me.loggedIn) showApp(me.username); if (me.loggedIn) showApp(me.username);
+11 -2
View File
@@ -45,8 +45,17 @@ const EN = {
"Freund entfernen": "Remove friend", "{0} als Freund entfernen?": "Remove {0} as a friend?", "eingeladen": "invited", "Einladen": "Invite", "Freund entfernen": "Remove friend", "{0} als Freund entfernen?": "Remove {0} as a friend?", "eingeladen": "invited", "Einladen": "Invite",
"in der Lobby": "in the lobby", "Gesendet": "Sent", "wartet …": "pending …", "Zurückziehen": "Withdraw", "in der Lobby": "in the lobby", "Gesendet": "Sent", "wartet …": "pending …", "Zurückziehen": "Withdraw",
// Neuigkeiten // Neuigkeiten
"Update verfügbar": "Update available", "Beim Start gefunden – jetzt aktualisieren?": "Found at startup – update now?", "Launcher {0} ist da – unter Einstellungen installieren": "Launcher {0} is available – install it in the settings",
"installiert: {0}": "installed: {0}", "Später": "Later", "BSO-Mod": "BSO mod", "Launcher wird auf {0} aktualisiert …": "Updating the launcher to {0} …", "Launcher {0} ist da": "Launcher {0} is available",
"Der Launcher startet danach von selbst neu.": "The launcher restarts by itself.", "Neustart …": "Restarting …",
"Launcher-Update fehlgeschlagen: {0}": "Launcher update failed: {0}", "Nach Updates suchen": "Check for updates",
"Installieren und neu starten": "Install and restart", "Der Launcher ist aktuell": "The launcher is up to date",
"Keine Launcher-Version auf dem Server": "No launcher version on the server",
"Launcher automatisch aktualisieren": "Update the launcher automatically",
"Beim Start wird eine neue Launcher-Version gleich installiert (er startet von selbst neu)":
"When the launcher starts, a new launcher version is installed right away (it restarts by itself)",
"Neue BSO-Versionen beim Start des Launchers und vor jedem Spielstart installieren":
"Installs new BSO versions when the launcher starts and before every game start",
"Keine Beschreibung": "No description", "Noch keine Versionen veroeffentlicht.": "No versions published yet.", "Keine Beschreibung": "No description", "Noch keine Versionen veroeffentlicht.": "No versions published yet.",
// Einstellungen // Einstellungen
"Spielordner waehlen": "Choose game folder", "Spielordner gespeichert": "Game folder saved", "Protokoll-Ordner oeffnen": "Open log folder", "Spielordner waehlen": "Choose game folder", "Spielordner gespeichert": "Game folder saved", "Protokoll-Ordner oeffnen": "Open log folder",