- 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>
109 lines
4.6 KiB
C#
109 lines
4.6 KiB
C#
using System;
|
|
using System.Reflection;
|
|
using Harmony;
|
|
using HutongGames.PlayMaker;
|
|
using UnityEngine;
|
|
|
|
namespace MSCO
|
|
{
|
|
/// <summary>
|
|
/// Schutz vor Abstürzen durch nachgespielte Aktionen: Wird beim Beitreten eine Montage-Aktion in einer
|
|
/// etwas anderen Welt nachgespielt, kann die "SetParent"-Aktion eines Teils ein Objekt unter sein eigenes
|
|
/// Kind hängen. Das ergibt einen Kreis in der Hierarchie und Unity stürzt nativ ab (Stack Overflow in
|
|
/// Transform::GetClassIDStatic). Solche Aufrufe werden hier abgefangen und übersprungen.
|
|
/// </summary>
|
|
public static class SafetyPatches
|
|
{
|
|
static bool applied;
|
|
static FieldInfo gameObjectField, parentField;
|
|
static int blocked;
|
|
|
|
public static void Apply()
|
|
{
|
|
if (applied) return;
|
|
applied = true;
|
|
try
|
|
{
|
|
Type action = null;
|
|
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
action = asm.GetType("HutongGames.PlayMaker.Actions.SetParent");
|
|
if (action != null) break;
|
|
}
|
|
MethodInfo onEnter = action != null ? action.GetMethod("OnEnter", BindingFlags.Instance | BindingFlags.Public) : null;
|
|
if (onEnter == null) { MscoLog.Warning("[MSCO] SetParent action not found - hierarchy guard disabled"); return; }
|
|
|
|
gameObjectField = action.GetField("gameObject");
|
|
parentField = action.GetField("parent");
|
|
HarmonyInstance.Create("mbm.msco.safety").Patch(onEnter,
|
|
new HarmonyMethod(typeof(SafetyPatches).GetMethod("SetParentPrefix", BindingFlags.Static | BindingFlags.NonPublic)), null);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
MscoLog.Warning("[MSCO] Could not install hierarchy guard: " + e.Message);
|
|
}
|
|
PatchMenuQuit();
|
|
}
|
|
|
|
/// <summary>
|
|
/// ESC im Hauptmenü beendet beim Spiel sofort das Programm (PlayMaker-Aktion "ApplicationQuit" im Menü). Im
|
|
/// MSCO-Hauptmenü gibt es dafür den QUIT-Knopf - dort also nicht über die Spiel-Logik beenden. Im Spiel selbst
|
|
/// (Pausemenü) bleibt alles wie gehabt.
|
|
/// </summary>
|
|
static void PatchMenuQuit()
|
|
{
|
|
try
|
|
{
|
|
Type action = null;
|
|
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
action = asm.GetType("HutongGames.PlayMaker.Actions.ApplicationQuit");
|
|
if (action != null) break;
|
|
}
|
|
MethodInfo onEnter = action != null ? action.GetMethod("OnEnter", BindingFlags.Instance | BindingFlags.Public) : null;
|
|
if (onEnter == null) return;
|
|
HarmonyInstance.Create("mbm.msco.menuquit").Patch(onEnter,
|
|
new HarmonyMethod(typeof(SafetyPatches).GetMethod("QuitPrefix", BindingFlags.Static | BindingFlags.NonPublic)), null);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
MscoLog.Warning("[MSCO] Could not block ESC-quit in the main menu: " + e.Message);
|
|
}
|
|
}
|
|
|
|
static bool QuitPrefix(FsmStateAction __instance)
|
|
{
|
|
if (!MenuHelper.InMenu) return true;
|
|
MscoLog.Info("[MSCO] The game tried to quit from the main menu (ESC) - use QUIT in the MSCO menu to exit");
|
|
try { __instance.Finish(); } catch { }
|
|
return false;
|
|
}
|
|
|
|
static bool SetParentPrefix(FsmStateAction __instance)
|
|
{
|
|
try
|
|
{
|
|
var owner = gameObjectField != null ? gameObjectField.GetValue(__instance) as FsmOwnerDefault : null;
|
|
var parent = parentField != null ? parentField.GetValue(__instance) as FsmGameObject : null;
|
|
if (owner == null || parent == null || parent.Value == null) return true;
|
|
GameObject go = __instance.Fsm.GetOwnerDefaultTarget(owner);
|
|
if (go == null) return true;
|
|
|
|
Transform child = go.transform, newParent = parent.Value.transform;
|
|
if (newParent != child && !newParent.IsChildOf(child)) return true;
|
|
|
|
blocked++;
|
|
if (blocked <= 20)
|
|
MscoLog.Warning("[MSCO] Blocked SetParent that would crash the game: " + go.name + " -> " + parent.Value.name +
|
|
" (" + __instance.Fsm.Name + ")");
|
|
__instance.Finish();
|
|
return false;
|
|
}
|
|
catch
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|