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>
This commit is contained in:
+167
@@ -0,0 +1,167 @@
|
||||
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: <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.
|
||||
/// </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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user