using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Security.Cryptography; namespace MSCO { /// /// Lädt MSCO-Mods und ruft sie auf. Eigene Mods: <Spiel>\MSCO\Mods\*.dll (immer aktiv). Mods eines Servers: /// <Spiel>\MSCO\Mods\Servers\<id>.dll - nur aktiv, solange man auf einem Server ist, der sie verlangt. /// Fehler einer Mod werden abgefangen und protokolliert; wirft sie ständig Fehler, wird sie abgeschaltet. /// public static class ModLoader { public sealed class LoadedMod { public MscoMod Mod; public string File, Sha256; public bool FromServer, Active; public int Errors; public string Error; } const int MaxErrors = 50; static readonly List mods = new List(); static bool localLoaded; public static IList All { get { return mods.AsReadOnly(); } } public static string Folder { get { return Path.Combine(Bootstrap.Folder ?? ".", "Mods"); } } public static string ServerFolder { get { return Path.Combine(Folder, "Servers"); } } public static string ServerFile(string id) { return Path.Combine(ServerFolder, id + ".dll"); } /// Eigene Mods einmal beim ersten Hauptmenü laden. public static void LoadLocal() { if (localLoaded) return; localLoaded = true; try { Directory.CreateDirectory(Folder); foreach (string file in Directory.GetFiles(Folder, "*.dll")) Load(file, false); if (mods.Count > 0) MscoLog.Info("[MSCO] " + mods.Count + " mod(s) loaded from " + Folder); } catch (Exception e) { MscoLog.Warning("[MSCO] Could not load mods: " + e.Message); } } public static string Sha256Of(byte[] data) { using (var sha = SHA256.Create()) return BitConverter.ToString(sha.ComputeHash(data)).Replace("-", "").ToLowerInvariant(); } public static LoadedMod FindBySha(string sha) { foreach (LoadedMod m in mods) if (string.Equals(m.Sha256, sha, StringComparison.OrdinalIgnoreCase)) return m; return null; } /// /// DLL laden (aus den Bytes - die Datei bleibt frei, und eine neue Version einer Server-Mod kann neben der alten /// geladen werden) und alle MscoMod-Klassen darin anlegen. Gibt die erste Mod zurück (oder null). /// public static LoadedMod Load(string file, bool fromServer) { LoadedMod first = null; try { byte[] data = System.IO.File.ReadAllBytes(file); string sha = Sha256Of(data); LoadedMod existing = FindBySha(sha); if (existing != null) return existing; Assembly asm = Assembly.Load(data); Type[] types; try { types = asm.GetTypes(); } catch (ReflectionTypeLoadException e) { types = Array.FindAll(e.Types, t => t != null); } foreach (Type t in types) { if (t.IsAbstract || !typeof(MscoMod).IsAssignableFrom(t)) continue; MscoMod mod; try { mod = (MscoMod)Activator.CreateInstance(t); } catch (Exception e) { MscoLog.Warning("[MSCO] Mod " + t.FullName + " could not be created: " + (e.InnerException ?? e).Message); continue; } var loaded = new LoadedMod { Mod = mod, File = file, Sha256 = sha, FromServer = fromServer, Active = !fromServer }; mods.Add(loaded); if (first == null) first = loaded; MscoLog.Info("[MSCO] Mod loaded: " + Safe(() => mod.Name, t.Name) + " " + Safe(() => mod.Version, "") + (fromServer ? " (from the server)" : "")); Call(loaded, m => m.OnLoad()); } if (first == null) MscoLog.Warning("[MSCO] " + Path.GetFileName(file) + " contains no MSCO mod (class deriving from MscoMod)"); } catch (Exception e) { MscoLog.Warning("[MSCO] Could not load mod " + Path.GetFileName(file) + ": " + e.Message); } return first; } static string Safe(Func get, string fallback) { try { return get() ?? fallback; } catch { return fallback; } } /// Server-Mods für die aktuelle Sitzung einschalten (alle anderen Server-Mods aus). public static void ActivateServerMods(ICollection shas) { foreach (LoadedMod m in mods) if (m.FromServer && m.Error == null) m.Active = shas != null && shas.Contains(m.Sha256); } // ---------------------------------------------------------------- Aufrufe static void Call(LoadedMod m, Action action) { try { action(m.Mod); } catch (Exception e) { m.Errors++; if (m.Errors <= 3) MscoLog.Warning("[MSCO] Mod " + Safe(() => m.Mod.Name, "?") + ": " + e); if (m.Errors >= MaxErrors) { m.Active = false; m.Error = e.Message; MscoLog.Warning("[MSCO] Mod " + Safe(() => m.Mod.Name, "?") + " was turned off (too many errors)"); } } } static void Each(Action action) { for (int i = 0; i < mods.Count; i++) if (mods[i].Active) Call(mods[i], action); } public static void MenuLoad() { Each(m => m.OnMenuLoad()); } public static void GameLoad() { Each(m => m.OnGameLoad()); } public static void Update() { if (mods.Count > 0) Each(m => m.Update()); } public static void FixedUpdate() { if (mods.Count > 0) Each(m => m.FixedUpdate()); } public static void OnGUI() { if (mods.Count > 0) Each(m => m.OnGUI()); } public static void SessionStart() { Each(m => m.OnSessionStart()); } public static void PlayerJoined(int id, string name) { Each(m => m.OnPlayerJoined(id, name)); } public static void PlayerLeft(int id, string name) { Each(m => m.OnPlayerLeft(id, name)); } /// Sitzung vorbei: Mods informieren, Server-Mods ausschalten. public static void SessionEnd() { Each(m => m.OnSessionEnd()); ActivateServerMods(null); } public static void Message(string modId, int from, byte[] data) { for (int i = 0; i < mods.Count; i++) { LoadedMod m = mods[i]; if (!m.Active || Safe(() => m.Mod.ID, null) != modId) continue; Call(m, mod => mod.OnMessage(from, data)); } } } }