Files
MSCO/Bootstrap.cs
T
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

230 lines
9.4 KiB
C#

using System;
using System.IO;
using System.Reflection;
using Harmony;
using UnityEngine;
namespace Doorstop
{
/// <summary>
/// Einstieg ohne MSCLoader: Unity Doorstop (winhttp.dll im Spielordner) ruft beim Spielstart diese Methode auf - der
/// MSCO Launcher startet das Spiel mit "--doorstop-target-assembly ...\MSCO\MSCO.dll", MSCLoader wird dann gar nicht
/// geladen. Hier läuft noch nichts von Unity: nur vorbereiten, gestartet wird im Hauptmenü (siehe MSCO.Bootstrap).
/// </summary>
public static class Entrypoint
{
public static void Start()
{
MSCO.Bootstrap.Start();
}
}
}
namespace MSCO
{
/// <summary>
/// Startet MSCO eigenständig: Abhängigkeiten (Harmony) aus dem MSCO-Ordner laden, warten bis das
/// Spiel seinen Code geladen hat, dann beim ersten PlayMakerArrayListProxy.Awake (Hauptmenü, Hauptthread) den
/// MultiplayerManager anlegen. Protokoll: &lt;Spiel&gt;\MSCO\MSCO_Loader.txt.
/// </summary>
public static class Bootstrap
{
static string folder;
/// <summary>Ordner von MSCO.dll (&lt;Spiel&gt;\MSCO).</summary>
public static string Folder { get { return folder; } }
static bool patchedGame, patchedPlayMaker, started;
static StreamWriter log;
public static void Start()
{
try
{
folder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
log = new StreamWriter(Path.Combine(folder, "MSCO_Loader.txt"), false) { AutoFlush = true };
Log("MSCO " + MSCO.ModVersion + " loader started (standalone, without MSCLoader)");
Log("Command line: " + string.Join(" ", Environment.GetCommandLineArgs()));
AppDomain.CurrentDomain.AssemblyResolve += Resolve;
AppDomain.CurrentDomain.AssemblyLoad += (_, e) => OnAssemblyLoaded(e.LoadedAssembly);
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies()) OnAssemblyLoaded(a);
}
catch (Exception e)
{
Log("Loader failed: " + e);
}
}
public static void Log(string line)
{
try { if (log != null) log.WriteLine(DateTime.Now.ToString("HH:mm:ss") + " " + line); } catch { }
}
/// <summary>Unsere Abhängigkeiten liegen neben MSCO.dll (das Spiel sucht nur in Managed).</summary>
static Assembly Resolve(object sender, ResolveEventArgs args)
{
try
{
string name = new AssemblyName(args.Name).Name;
if (name == typeof(Bootstrap).Assembly.GetName().Name) return typeof(Bootstrap).Assembly; // Mods verweisen auf MSCO
string file = Path.Combine(folder, name + ".dll");
if (File.Exists(file)) return Assembly.LoadFrom(file);
}
catch (Exception e) { Log("Resolve " + args.Name + " failed: " + e.Message); }
return null;
}
static void OnAssemblyLoaded(Assembly assembly)
{
if (patchedGame && patchedPlayMaker) return;
string name;
try { name = assembly.GetName().Name; } catch { return; }
try
{
if (name == "Assembly-CSharp" && !patchedGame) { patchedGame = true; Hook(assembly, "PlayMakerArrayListProxy", "Awake"); }
// PlayMaker läuft schon im Startbildschirm (Nachrichten, "Press any key") - so startet MSCO sofort
if (name == "PlayMaker" && !patchedPlayMaker) { patchedPlayMaker = true; Hook(assembly, "PlayMakerFSM", "Awake"); }
}
catch (Exception e) { Log("Could not hook into " + name + ": " + e); }
}
/// <summary>
/// Haken in eine Awake-Methode, die früh auf dem Hauptthread läuft (PlayMakerFSM schon im Startbildschirm,
/// PlayMakerArrayListProxy im Hauptmenü als Rückfallebene). Der erste, der läuft, startet MSCO.
/// </summary>
static void Hook(Assembly assembly, string typeName, string methodName)
{
Type type = assembly.GetType(typeName);
MethodInfo method = type != null ? type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : null;
if (method == null)
{
Log(typeName + "." + methodName + " not found");
return;
}
HarmonyInstance.Create("mbm.msco.bootstrap." + typeName).Patch(method,
new HarmonyMethod(typeof(Bootstrap).GetMethod("BeforeAwake", BindingFlags.Static | BindingFlags.NonPublic)), null);
Log("Hooked into " + typeName + "." + methodName);
}
static void BeforeAwake()
{
if (started) return;
started = true;
try
{
var go = new GameObject("MSCO_Host");
UnityEngine.Object.DontDestroyOnLoad(go);
go.AddComponent<MscoHost>();
Log("MSCO started");
}
catch (Exception e) { Log("Start failed: " + e); }
}
}
/// <summary>Lebt über alle Szenen: legt im Hauptmenü den MultiplayerManager an (früher MSCLoaders OnMenuLoad).</summary>
public class MscoHost : MonoBehaviour
{
void Start()
{
MscoLog.Info("Welcome to MSCO by mbm.wtf");
MenuBranding.ApplyWindow();
// Den Nachrichten-Bildschirm des Spiels ("Latest news ... press any key") überspringen - direkt ins Hauptmenü
string scene = Application.loadedLevelName;
Bootstrap.Log("First scene: " + scene);
if (scene != "MainMenu" && scene != "GAME")
{
// Erst einen Frame lang den MSCO-Ladebildschirm zeigen (Kameras und Ton des Startbildschirms aus), dann
// laden - sonst steht während des Ladens (ein paar Sekunden) "Press any key" auf dem Bildschirm
Bootstrap.Log("Skipping the intro/news screen");
skipping = true;
skipFrame = Time.frameCount;
foreach (Camera c in Camera.allCameras) c.enabled = false;
previousVolume = AudioListener.volume;
AudioListener.volume = 0f;
return;
}
MultiplayerManager.Ensure();
SceneChanged(scene);
}
/// <summary>Mods: beim ersten Hauptmenü laden, dann Menü/Spiel melden.</summary>
static void SceneChanged(string scene)
{
if (scene == "MainMenu")
{
ModLoader.LoadLocal();
ModLoader.MenuLoad();
}
else if (scene == "GAME") ModLoader.GameLoad();
}
void FixedUpdate()
{
ModLoader.FixedUpdate();
}
bool skipping, loadingScreen;
float previousVolume = -1f;
int skipFrame;
Texture2D black;
void Update()
{
ModLoader.Update();
if (!skipping || Time.frameCount < skipFrame + 2) return;
skipping = false;
loadingScreen = true;
Application.LoadLevel("MainMenu");
}
void OnGUI()
{
if (!skipping && !loadingScreen)
{
ModLoader.OnGUI();
return;
}
GUI.depth = -10000;
if (black == null)
{
black = new Texture2D(1, 1);
black.SetPixel(0, 0, new Color(0.05f, 0.05f, 0.063f));
black.Apply();
}
GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), black);
Texture2D logo = Branding.LogoTexture;
float size = Mathf.Round(Screen.height * 0.14f);
if (logo != null) GUI.DrawTexture(new Rect((Screen.width - size) / 2f, Screen.height * 0.40f - size / 2f, size, size), logo);
var style = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = Mathf.RoundToInt(Screen.height * 0.022f) };
style.normal.textColor = new Color(0.56f, 0.56f, 0.6f);
GUI.Label(new Rect(0, Screen.height * 0.40f + size / 2f + 16f, Screen.width, 40f), "Loading MSCO …", style);
}
void OnLevelWasLoaded(int level)
{
loadingScreen = false;
if (previousVolume >= 0f) { AudioListener.volume = previousVolume; previousVolume = -1f; }
Bootstrap.Log("Level loaded: " + Application.loadedLevelName);
MenuBranding.ApplyWindow();
if (Application.loadedLevelName == "MainMenu") StartCoroutine(BrandMenu());
MultiplayerManager.Ensure();
SceneChanged(Application.loadedLevelName);
}
/// <summary>Logo im Hauptmenü ersetzen - ein paar Mal, falls das Menü es erst nach dem Laden zeigt.</summary>
System.Collections.IEnumerator BrandMenu()
{
for (int i = 0; i < 4 && Application.loadedLevelName == "MainMenu"; i++)
{
yield return new WaitForSeconds(i == 0 ? 0.1f : 1f);
try { MenuBranding.ReplaceMenuLogo(); }
catch (System.Exception e) { Bootstrap.Log("Menu logo: " + e.Message); yield break; }
}
}
void OnApplicationQuit()
{
// Wer beendet das Spiel? (Diagnose)
Bootstrap.Log("Game is quitting (scene " + Application.loadedLevelName + "): " + System.Environment.StackTrace);
}
}
}