using System; using System.IO; using System.Reflection; using Harmony; using UnityEngine; namespace Doorstop { /// /// 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). /// public static class Entrypoint { public static void Start() { MSCO.Bootstrap.Start(); } } } namespace MSCO { /// /// 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: <Spiel>\MSCO\MSCO_Loader.txt. /// public static class Bootstrap { static string folder; /// Ordner von MSCO.dll (<Spiel>\MSCO). 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 { } } /// Unsere Abhängigkeiten liegen neben MSCO.dll (das Spiel sucht nur in Managed). 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); } } /// /// 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. /// 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(); Log("MSCO started"); } catch (Exception e) { Log("Start failed: " + e); } } } /// Lebt über alle Szenen: legt im Hauptmenü den MultiplayerManager an (früher MSCLoaders OnMenuLoad). public class MscoHost : MonoBehaviour { void Start() { host = this; 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); } /// Mods: beim ersten Hauptmenü laden, dann Menü/Spiel melden. static void SceneChanged(string scene) { if (scene == "MainMenu") { ModLoader.LoadLocal(); ModLoader.MenuLoad(); } else if (scene == "GAME") { ModLoader.GameLoad(); string[] cmd = Environment.GetCommandLineArgs(); int i = Array.IndexOf(cmd, "-msco-dump"); if (i >= 0 && i + 1 < cmd.Length && host != null) host.StartCoroutine(DumpLater(cmd[i + 1])); } } static MscoHost host; static System.Collections.IEnumerator DumpLater(string filter) { yield return new WaitForSeconds(25f); try { Bootstrap.Log("FSM dump: " + FsmDump.Write(filter)); } catch (Exception e) { Bootstrap.Log("FSM dump failed: " + e); } } 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) { MenuHelper.SceneChanged(); 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); } /// Logo im Hauptmenü ersetzen - ein paar Mal, falls das Menü es erst nach dem Laden zeigt. 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); } } }