Files
MSCO/ModLoader.cs
MrMahoandClaude Opus 5.5 4c1366b5d9 MSCO 2.10.0: mod support, ESC no longer quits from the main menu
- MscoMod API (ModApi.cs) and ModLoader: own mods from MSCO\Mods, hooks for menu/game/update/GUI,
  session and player events, Send/OnMessage between players (MsgType.ModMessage)
- Server mods: ModList before Welcome, consent prompt (INSTALL & JOIN), download with SHA-256 check
  into MSCO\Mods\Servers, active only on that server; game start waits for the download
- Mods page in the MSCO menu, console command "mods"
- Server browser: MODS pill, VANILLA/MODDED filters, mod list in the server profile
- PlayMaker ApplicationQuit is ignored in the main menu (ESC used to close the game)
- MODDING.md and examples/HelloMod

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 01:00:16 +02:00

168 lines
7.1 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
namespace MSCO
{
/// <summary>
/// Lädt MSCO-Mods und ruft sie auf. Eigene Mods: &lt;Spiel&gt;\MSCO\Mods\*.dll (immer aktiv). Mods eines Servers:
/// &lt;Spiel&gt;\MSCO\Mods\Servers\&lt;id&gt;.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.
/// </summary>
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<LoadedMod> mods = new List<LoadedMod>();
static bool localLoaded;
public static IList<LoadedMod> 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"); }
/// <summary>Eigene Mods einmal beim ersten Hauptmenü laden.</summary>
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;
}
/// <summary>
/// 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).
/// </summary>
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<string> get, string fallback)
{
try { return get() ?? fallback; } catch { return fallback; }
}
/// <summary>Server-Mods für die aktuelle Sitzung einschalten (alle anderen Server-Mods aus).</summary>
public static void ActivateServerMods(ICollection<string> 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<MscoMod> 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<MscoMod> 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)); }
/// <summary>Sitzung vorbei: Mods informieren, Server-Mods ausschalten.</summary>
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));
}
}
}
}