MSCO 2.9.1: fixes without MSCLoader, admin teleport, MSCO branding

- Own JSON reader (MiniJson) instead of Newtonsoft.Json - the server browser failed with
  "Could not load System.Runtime.Serialization" once MSCLoader was gone
- Mouse cursor visible in admin menu/chat/console (PlayerInMenu)
- Intro "Press any key" screen replaced by an MSCO loading screen; hook PlayMakerFSM.Awake too
- Join links while the game runs: picked up from MSCO\join.txt (written by the launcher)
- Admin menu: TELEPORT tab (home, shop, repair shop, inspection, villages, events, Satsuma)
- Window title/icon "MSCO - My Summer Car Online", MSCO logo in the main menu
- In-game UI restyled like the keymaster website (rounded cards, pills, glow, slim scrollbars)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 00:34:03 +02:00
co-authored by Claude Opus 5.5
parent 166f873669
commit 027296d27c
17 changed files with 747 additions and 74 deletions
+78 -16
View File
@@ -23,14 +23,16 @@ namespace Doorstop
namespace MSCO
{
/// <summary>
/// Startet MSCO eigenständig: Abhängigkeiten (Harmony, Newtonsoft.Json) aus dem MSCO-Ordner laden, warten bis das
/// 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;
static bool patched, started;
/// <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()
@@ -71,28 +73,34 @@ namespace MSCO
static void OnAssemblyLoaded(Assembly assembly)
{
if (patched) return;
if (patchedGame && patchedPlayMaker) return;
string name;
try { name = assembly.GetName().Name; } catch { return; }
if (name != "Assembly-CSharp") return;
patched = true;
try { Patch(assembly); }
catch (Exception e) { Log("Could not hook into the game: " + e); }
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 PlayMakerArrayListProxy.Awake (läuft im Hauptmenü früh auf dem Hauptthread).</summary>
static void Patch(Assembly game)
/// <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 proxy = game.GetType("PlayMakerArrayListProxy");
MethodInfo awake = proxy != null ? proxy.GetMethod("Awake", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : null;
if (awake == null)
Type type = assembly.GetType(typeName);
MethodInfo method = type != null ? type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : null;
if (method == null)
{
Log("PlayMakerArrayListProxy.Awake not found - MSCO cannot start");
Log(typeName + "." + methodName + " not found");
return;
}
HarmonyInstance.Create("mbm.msco.bootstrap").Patch(awake,
HarmonyInstance.Create("mbm.msco.bootstrap." + typeName).Patch(method,
new HarmonyMethod(typeof(Bootstrap).GetMethod("BeforeAwake", BindingFlags.Static | BindingFlags.NonPublic)), null);
Log("Hooked into the game, waiting for the main menu");
Log("Hooked into " + typeName + "." + methodName);
}
static void BeforeAwake()
@@ -116,24 +124,78 @@ namespace MSCO
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");
Application.LoadLevel("MainMenu");
skipping = true;
skipFrame = Time.frameCount;
foreach (Camera c in Camera.allCameras) c.enabled = false;
previousVolume = AudioListener.volume;
AudioListener.volume = 0f;
return;
}
MultiplayerManager.Ensure();
}
bool skipping, loadingScreen;
float previousVolume = -1f;
int skipFrame;
Texture2D black;
void Update()
{
if (!skipping || Time.frameCount < skipFrame + 2) return;
skipping = false;
loadingScreen = true;
Application.LoadLevel("MainMenu");
}
void OnGUI()
{
if (!skipping && !loadingScreen) 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();
}
/// <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)