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:
+98
@@ -0,0 +1,98 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MSCO.Server;
|
||||
|
||||
/// <summary>Eine Mod, die der Server an seine Spieler verteilt (data/mods/<id>.dll).</summary>
|
||||
public sealed record ServerMod(string Id, string Name, string Version, string Sha256, byte[] Data)
|
||||
{
|
||||
public int Size => Data.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-Mods: MSCO-Mods (DLLs) in data/mods. Beim Beitreten bekommt jeder Spieler die Liste; fehlende Mods lädt die
|
||||
/// MSCO-Mod nach Zustimmung des Spielers vom Server herunter (MsgType.ModFileRequest). Verwalten: Dateien in den Ordner
|
||||
/// legen bzw. im Web-Panel unter "Mods" hochladen, danach "mods reload" (Panel: automatisch).
|
||||
/// </summary>
|
||||
public static class ModStore
|
||||
{
|
||||
public const int MaxModBytes = 20 * 1024 * 1024;
|
||||
public const int MaxMods = 30;
|
||||
static readonly Regex IdPattern = new("^[A-Za-z0-9_.-]{1,64}$");
|
||||
|
||||
public static string Folder(string dataDir) => Path.Combine(dataDir, "mods");
|
||||
|
||||
/// <summary>Alle Mods aus data/mods lesen (ungültige werden mit Meldung übersprungen).</summary>
|
||||
public static List<ServerMod> Load(string dataDir, Action<string> log)
|
||||
{
|
||||
var list = new List<ServerMod>();
|
||||
string dir = Folder(dataDir);
|
||||
if (!Directory.Exists(dir)) return list;
|
||||
foreach (string file in Directory.GetFiles(dir, "*.dll").OrderBy(f => f, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (list.Count >= MaxMods) { log?.Invoke($"More than {MaxMods} mods in data/mods - the rest is ignored"); break; }
|
||||
try
|
||||
{
|
||||
byte[] data = File.ReadAllBytes(file);
|
||||
string id = Path.GetFileNameWithoutExtension(file);
|
||||
string error = Check(id, data);
|
||||
if (error != null) { log?.Invoke($"Mod {Path.GetFileName(file)} skipped: {error}"); continue; }
|
||||
list.Add(Describe(file, id, data));
|
||||
}
|
||||
catch (Exception e) { log?.Invoke($"Mod {Path.GetFileName(file)} could not be read: {e.Message}"); }
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
static ServerMod Describe(string file, string id, byte[] data)
|
||||
{
|
||||
string name = id, version = "";
|
||||
try
|
||||
{
|
||||
var info = FileVersionInfo.GetVersionInfo(file);
|
||||
if (!string.IsNullOrWhiteSpace(info.ProductName)) name = info.ProductName.Trim();
|
||||
version = (info.ProductVersion ?? info.FileVersion ?? "").Split('+')[0].Trim();
|
||||
}
|
||||
catch { }
|
||||
string sha = Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant();
|
||||
return new ServerMod(id, Clip(name, 64), Clip(version, 32), sha, data);
|
||||
}
|
||||
|
||||
static string Clip(string s, int max) => s.Length > max ? s[..max] : s;
|
||||
|
||||
static string Check(string id, byte[] data)
|
||||
{
|
||||
if (!IdPattern.IsMatch(id)) return "the file name may only contain letters, digits, '.', '-' and '_'";
|
||||
if (data.Length < 64 || data.Length > MaxModBytes) return $"size must be up to {MaxModBytes / 1024 / 1024} MB";
|
||||
if (data[0] != 0x4d || data[1] != 0x5a) return "not a .NET DLL";
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Mod hochladen (Panel). Gibt einen Fehler oder null zurück.</summary>
|
||||
public static string Add(string dataDir, string fileName, byte[] data)
|
||||
{
|
||||
fileName = Path.GetFileName(fileName ?? "");
|
||||
if (!fileName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) return "Choose a .dll file";
|
||||
string id = Path.GetFileNameWithoutExtension(fileName);
|
||||
string error = Check(id, data);
|
||||
if (error != null) return "This mod cannot be used: " + error;
|
||||
string dir = Folder(dataDir);
|
||||
Directory.CreateDirectory(dir);
|
||||
if (!File.Exists(Path.Combine(dir, id + ".dll")) && Directory.GetFiles(dir, "*.dll").Length >= MaxMods)
|
||||
return $"At most {MaxMods} mods";
|
||||
string target = Path.Combine(dir, id + ".dll"), tmp = target + ".upload";
|
||||
File.WriteAllBytes(tmp, data);
|
||||
File.Move(tmp, target, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool Remove(string dataDir, string id)
|
||||
{
|
||||
if (!IdPattern.IsMatch(id ?? "")) return false;
|
||||
string file = Path.Combine(Folder(dataDir), id + ".dll");
|
||||
if (!File.Exists(file)) return false;
|
||||
File.Delete(file);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user