- 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>
99 lines
4.3 KiB
C#
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/<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;
|
|
}
|
|
}
|