Files
MrMahoandClaude Opus 5.5 3a79c928d1 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>
2026-10-03 01:00:23 +02:00

99 lines
4.3 KiB
C#

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/&lt;id&gt;.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;
}
}