280 lines
12 KiB
C#
280 lines
12 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: <Spiel>\MSCO\MSCO_Loader.txt.
|
|
/// </summary>
|
|
public static class Bootstrap
|
|
{
|
|
static string folder;
|
|
/// <summary>Ordner von MSCO.dll (<Spiel>\MSCO).</summary>
|
|
public static string Folder { get { return folder; } }
|
|
static bool patchedGame, patchedPlayMaker, started;
|
|
|
|
/// <summary>
|
|
/// Singleplayer mit Mods (Launcher: SINGLEPLAYER, "-msco-singleplayer"): nur die eigenen Mods laden - kein
|
|
/// MSCO-Menü, kein Multiplayer, das Spiel bleibt sonst unverändert.
|
|
/// </summary>
|
|
public static readonly bool Singleplayer = Array.IndexOf(Environment.GetCommandLineArgs(), "-msco-singleplayer") >= 0;
|
|
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()));
|
|
if (Singleplayer) Log("Singleplayer mode: only mods, no multiplayer");
|
|
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()
|
|
{
|
|
host = this;
|
|
MscoLog.Info("Welcome to MSCO by mbm.wtf");
|
|
if (!Bootstrap.Singleplayer) 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;
|
|
}
|
|
if (!Bootstrap.Singleplayer) 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();
|
|
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); }
|
|
}
|
|
|
|
readonly System.Diagnostics.Stopwatch modWatch = new System.Diagnostics.Stopwatch();
|
|
|
|
/// <summary>Zeit der Mods messen (PERF-Bericht, Konsole "perf").</summary>
|
|
void MeasureMods(string part, Action call)
|
|
{
|
|
modWatch.Reset();
|
|
modWatch.Start();
|
|
try { call(); }
|
|
finally
|
|
{
|
|
modWatch.Stop();
|
|
PerfStats.Add(part, modWatch.Elapsed.TotalMilliseconds);
|
|
}
|
|
}
|
|
|
|
static readonly Action modFixedUpdate = ModLoader.FixedUpdate, modUpdate = ModLoader.Update, modOnGui = ModLoader.OnGUI;
|
|
|
|
void FixedUpdate()
|
|
{
|
|
MeasureMods("mods (physics)", modFixedUpdate);
|
|
}
|
|
|
|
bool skipping, loadingScreen;
|
|
float previousVolume = -1f;
|
|
int skipFrame;
|
|
Texture2D black;
|
|
|
|
void Update()
|
|
{
|
|
MeasureMods("mods", modUpdate);
|
|
if (!skipping || Time.frameCount < skipFrame + 2) return;
|
|
skipping = false;
|
|
loadingScreen = true;
|
|
Application.LoadLevel("MainMenu");
|
|
}
|
|
|
|
void OnGUI()
|
|
{
|
|
if (!skipping && !loadingScreen)
|
|
{
|
|
// Mods zeichnen mit der dynamischen Schrift: Schriftgröße/Fett gehen sonst nicht, und Unity schreibt bei jedem
|
|
// Zeichnen eine Warnung samt Stacktrace ins Log (z.B. Show Bolt Size: hunderte pro Minute, kostet FPS)
|
|
GUISkin skin = GUI.skin;
|
|
Font previous = skin.font, font = UiTheme.DynamicFont;
|
|
if (font != null) skin.font = font;
|
|
try { MeasureMods("mods (interface)", modOnGui); }
|
|
finally { skin.font = previous; }
|
|
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);
|
|
if (!Bootstrap.Singleplayer)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|