using System.Diagnostics; using System.Security.Cryptography; using System.Text.RegularExpressions; namespace MSCO.Server; /// Eine Mod, die der Server an seine Spieler verteilt (data/mods/<id>.dll). public sealed record ServerMod(string Id, string Name, string Version, string Sha256, byte[] Data) { public int Size => Data.Length; } /// /// 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). /// 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"); /// Alle Mods aus data/mods lesen (ungültige werden mit Meldung übersprungen). public static List Load(string dataDir, Action log) { var list = new List(); 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; } /// Mod hochladen (Panel). Gibt einen Fehler oder null zurück. 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; } }