Launcher 0.3.2: Updates aus Gitea-Releases, Server-Karte entfernt

Neueste Mod/Launcher-Version und Neuigkeiten kommen von git.mbm.wtf (bso-release.json mit Signatur),
die Website ist nur noch Reserve (mit -dev zuerst). SPIELEN auf der Startseite startet ohne Server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 21:01:20 +01:00
co-authored by Claude Opus 5.5
parent 5fd162fecb
commit 9cd5f2b153
9 changed files with 155 additions and 56 deletions
+85
View File
@@ -0,0 +1,85 @@
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();
}
}