Server 2.10.0: distribute MSCO mods from data/mods

- ModStore: mods in data/mods (id, name, version, SHA-256), console "mods [reload]"
- ModList is sent before Welcome, files on request in 60 KB parts (once per connection)
- ModMessage relayed between players (max 64 KB)
- Heartbeat reports the mods to the keymaster (automatic "modded" tag)
- Web panel page "Mods": upload, remove, reload (DE/EN)
- Tests for list, download, checksum, relay and limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 01:00:23 +02:00
co-authored by Claude Opus 5.5
parent 5e07fe1001
commit 3a79c928d1
10 changed files with 355 additions and 8 deletions
+42
View File
@@ -497,6 +497,48 @@ public sealed class AdminPanel
return await RunText(s => s.RunCommandCaptured(command, by));
});
// --- Mods (data/mods): bekommen die Spieler beim Beitreten vom Server
app.MapGet("/api/mods", async (HttpContext ctx) =>
{
var user = Require(ctx, PanelRole.Admin, out var denied); if (user == null) return denied;
return await Run(s => s.PanelMods(), () => ModStore.Load(dataDir, null)
.Select(m => new { id = m.Id, name = m.Name, version = m.Version, sha256 = m.Sha256, size = m.Size }).ToList());
});
app.MapPost("/api/mods/upload", async (HttpContext ctx) =>
{
var user = Require(ctx, PanelRole.Admin, out var denied); if (user == null) return denied;
var sizeFeature = ctx.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature>();
if (sizeFeature != null && !sizeFeature.IsReadOnly) sizeFeature.MaxRequestBodySize = ModStore.MaxModBytes + 1024;
byte[] data;
try
{
using var ms = new MemoryStream();
await ctx.Request.Body.CopyToAsync(ms);
data = ms.ToArray();
}
catch (Exception e) { return Error(400, "Upload failed: " + e.Message); }
string name = ctx.Request.Query["name"].ToString();
string error = ModStore.Add(dataDir, name, data);
if (error != null) return Error(400, error);
GameServer.Log($"[panel] {By(user)} uploaded the mod {Path.GetFileName(name)}");
return await RunText(s => s.ReloadMods(), () => "Mod uploaded - players get it when the server runs");
});
app.MapPost("/api/mods/{id}/remove", async (HttpContext ctx, string id) =>
{
var user = Require(ctx, PanelRole.Admin, out var denied); if (user == null) return denied;
if (!ModStore.Remove(dataDir, id)) return Error(404, "Mod not found");
GameServer.Log($"[panel] {By(user)} removed the mod {id}");
return await RunText(s => s.ReloadMods(), () => "Mod removed");
});
app.MapPost("/api/mods/reload", async (HttpContext ctx) =>
{
var user = Require(ctx, PanelRole.Admin, out var denied); if (user == null) return denied;
return await RunText(s => s.ReloadMods(), () => "The server is stopped - mods are loaded when it starts");
});
// --- Welt / Backups
app.MapGet("/api/backups", async (HttpContext ctx) =>
{