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 namespace MSCO
{ {
/// <summary> /// <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 /// Spiel seinen Code geladen hat, dann beim ersten PlayMakerArrayListProxy.Awake (Hauptmenü, Hauptthread) den
/// MultiplayerManager anlegen. Protokoll: &lt;Spiel&gt;\MSCO\MSCO_Loader.txt. /// MultiplayerManager anlegen. Protokoll: &lt;Spiel&gt;\MSCO\MSCO_Loader.txt.
/// </summary> /// </summary>
public static class Bootstrap public static class Bootstrap
{ {
static string folder; 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; static StreamWriter log;
public static void Start() public static void Start()
@@ -71,28 +73,34 @@ namespace MSCO
static void OnAssemblyLoaded(Assembly assembly) static void OnAssemblyLoaded(Assembly assembly)
{ {
if (patched) return; if (patchedGame && patchedPlayMaker) return;
string name; string name;
try { name = assembly.GetName().Name; } catch { return; } try { name = assembly.GetName().Name; } catch { return; }
if (name != "Assembly-CSharp") return; try
patched = true; {
try { Patch(assembly); } if (name == "Assembly-CSharp" && !patchedGame) { patchedGame = true; Hook(assembly, "PlayMakerArrayListProxy", "Awake"); }
catch (Exception e) { Log("Could not hook into the game: " + e); } // 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> /// <summary>
static void Patch(Assembly game) /// 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"); Type type = assembly.GetType(typeName);
MethodInfo awake = proxy != null ? proxy.GetMethod("Awake", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : null; MethodInfo method = type != null ? type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) : null;
if (awake == null) if (method == null)
{ {
Log("PlayMakerArrayListProxy.Awake not found - MSCO cannot start"); Log(typeName + "." + methodName + " not found");
return; 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); 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() static void BeforeAwake()
@@ -116,24 +124,78 @@ namespace MSCO
void Start() void Start()
{ {
MscoLog.Info("Welcome to MSCO by mbm.wtf"); MscoLog.Info("Welcome to MSCO by mbm.wtf");
MenuBranding.ApplyWindow();
// Den Nachrichten-Bildschirm des Spiels ("Latest news ... press any key") überspringen - direkt ins Hauptmenü // Den Nachrichten-Bildschirm des Spiels ("Latest news ... press any key") überspringen - direkt ins Hauptmenü
string scene = Application.loadedLevelName; string scene = Application.loadedLevelName;
Bootstrap.Log("First scene: " + scene); Bootstrap.Log("First scene: " + scene);
if (scene != "MainMenu" && scene != "GAME") 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"); 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; return;
} }
MultiplayerManager.Ensure(); 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) void OnLevelWasLoaded(int level)
{ {
loadingScreen = false;
if (previousVolume >= 0f) { AudioListener.volume = previousVolume; previousVolume = -1f; }
Bootstrap.Log("Level loaded: " + Application.loadedLevelName); Bootstrap.Log("Level loaded: " + Application.loadedLevelName);
MenuBranding.ApplyWindow();
if (Application.loadedLevelName == "MainMenu") StartCoroutine(BrandMenu());
MultiplayerManager.Ensure(); 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() void OnApplicationQuit()
{ {
// Wer beendet das Spiel? (Diagnose) // Wer beendet das Spiel? (Diagnose)
+18
View File
@@ -1,5 +1,23 @@
# Changelog # Changelog
## 2.9.1
**Fehler ohne MSCLoader behoben**
- Server-Browser: „Could not load System.Runtime.Serialization“ – MSCO braucht Newtonsoft.Json nicht mehr (eigener
kleiner JSON-Leser); die Bibliothek kam vorher von MSCLoader
- Mauszeiger im Admin-Menü, Chat und in der Konsole wieder sichtbar (wie im Pausemenü des Spiels)
- Startbildschirm „Press any key“: statt ihm erscheint sofort ein MSCO-Ladebildschirm, dann das Hauptmenü
- JOIN im Browser, während das Spiel schon läuft: der Link kommt im Spiel an (Hinweis bzw. Beitreten im Hauptmenü)
**Admin-Menü: Teleport**
- Neuer Reiter **TELEPORT**: Haus, Laden (Teimo), Werkstatt, Prüfstelle, Peräjärvi, Müllkippe, Tanzhalle, Ferienhaus,
Hütte, Rallye, Dragrace, Fußballplatz, Kilju-Käufer, Theater, Satsuma – man landet neben dem Ort auf dem Boden
**MSCO-Look**
- Fenstertitel „MSCO - My Summer Car Online“ und MSCO-Symbol; im Hauptmenü ist das MSCO-Logo statt des Spiel-Logos
- Oberfläche im Stil der Website: rundere Karten mit feinem Rand, Filter als Pillen, oranger Schimmer, schlanke
Scrollleisten, Navigation mit abgerundeten Einträgen
## 2.9.0 ## 2.9.0
**Eigenständig – ohne MSCLoader** **Eigenständig – ohne MSCLoader**
+68
View File
@@ -42,6 +42,74 @@ namespace MSCO
return done > 0 ? "Hunger, thirst, stress, fatigue, bladder and dirt reset" : "Player needs not found (only works in the game)"; return done > 0 ? "Hunger, thirst, stress, fatigue, bladder and dirt reset" : "Player needs not found (only works in the game)";
} }
/// <summary>Teleport-Ziele im Admin-Menü: Anzeigename und Objekt im Spiel.</summary>
public static readonly string[][] Places =
{
new[] { "Home", "YARD" }, new[] { "Shop (Teimo)", "STORE" }, new[] { "Repair shop", "REPAIRSHOP" },
new[] { "Inspection", "INSPECTION" }, new[] { "Peräjärvi", "PERAJARVI" }, new[] { "Landfill", "LANDFILL" },
new[] { "Dance hall", "DANCEHALL" }, new[] { "Cottage", "COTTAGE" }, new[] { "Cabin", "CABIN" },
new[] { "Rally", "RALLY" }, new[] { "Drag race", "DRAGRACE" }, new[] { "Soccer field", "SOCCER" },
new[] { "Kilju buyer", "KILJUGUY" }, new[] { "Theatre", "THEATRE" }, new[] { "Satsuma", "SATSUMA" },
};
/// <summary>
/// Wohin bei einem Teleport-Ziel? Neben das Objekt (Mitte seiner sichtbaren Teile, ein Stück außerhalb, damit man
/// nicht im Dach oder in einer Wand landet) und dort auf den Boden.
/// </summary>
public static bool FindPlace(string objectName, out Vector3 position)
{
position = Vector3.zero;
GameObject root = FindRoot(objectName);
if (root == null) return false;
Bounds bounds = new Bounds(root.transform.position, Vector3.zero);
bool any = false;
foreach (Renderer r in root.GetComponentsInChildren<Renderer>(true))
{
if (r == null || r.bounds.size == Vector3.zero) continue;
if (!any) { bounds = r.bounds; any = true; }
else bounds.Encapsulate(r.bounds);
}
Vector3 center = bounds.center;
float ox = Mathf.Min(bounds.extents.x, 25f) + 3f, oz = Mathf.Min(bounds.extents.z, 25f) + 3f;
Vector3[] candidates =
{
center + new Vector3(ox, 0f, 0f), center + new Vector3(-ox, 0f, 0f),
center + new Vector3(0f, 0f, oz), center + new Vector3(0f, 0f, -oz), center,
};
foreach (Vector3 c in candidates)
{
Vector3 ground;
if (Ground(c, bounds.max.y + 30f, out ground)) { position = ground; return true; }
}
position = center;
return true;
}
static GameObject FindRoot(string name)
{
GameObject go = GameObject.Find(name);
if (go != null) return go;
// auch ausgeblendete Objekte (das Spiel schaltet entfernte Orte ab)
foreach (GameObject candidate in Resources.FindObjectsOfTypeAll<GameObject>())
if (candidate.name == name && candidate.transform.parent == null && candidate.hideFlags == HideFlags.None) return candidate;
return null;
}
/// <summary>Höchster fester Boden unter einem Punkt (ohne Trigger).</summary>
static bool Ground(Vector3 at, float top, out Vector3 ground)
{
ground = at;
bool found = false;
foreach (RaycastHit hit in Physics.RaycastAll(new Vector3(at.x, top, at.z), Vector3.down, 1000f))
{
if (hit.collider == null || hit.collider.isTrigger || hit.normal.y < 0.6f) continue;
if (!found || hit.point.y > ground.y) { ground = hit.point; found = true; }
}
return found;
}
/// <summary>Eigenen Spieler an eine Position setzen (etwas darüber, damit er nicht im Boden steckt).</summary> /// <summary>Eigenen Spieler an eine Position setzen (etwas darüber, damit er nicht im Boden steckt).</summary>
public static bool Teleport(Transform player, Vector3 position) public static bool Teleport(Transform player, Vector3 position)
{ {
+16 -1
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using Harmony; using Harmony;
using HutongGames.PlayMaker;
using UnityEngine; using UnityEngine;
namespace MSCO namespace MSCO
@@ -20,7 +21,7 @@ namespace MSCO
static readonly List<Behaviour> disabled = new List<Behaviour>(); static readonly List<Behaviour> disabled = new List<Behaviour>();
static bool locked; static bool locked;
static bool previousVisible; static bool previousVisible, previousInMenu;
static CursorLockMode previousLock; static CursorLockMode previousLock;
static bool patched; static bool patched;
@@ -37,6 +38,8 @@ namespace MSCO
PatchCursorActions(); PatchCursorActions();
previousVisible = Cursor.visible; previousVisible = Cursor.visible;
previousLock = Cursor.lockState; previousLock = Cursor.lockState;
FsmBool inMenu = PlayerInMenu();
previousInMenu = inMenu != null && inMenu.Value;
GameObject player = GameObject.Find("PLAYER"); GameObject player = GameObject.Find("PLAYER");
if (player != null) if (player != null)
{ {
@@ -57,6 +60,8 @@ namespace MSCO
PlayMakerGUI.HideCursor = !previousVisible; PlayMakerGUI.HideCursor = !previousVisible;
Cursor.visible = previousVisible; Cursor.visible = previousVisible;
Cursor.lockState = previousLock; Cursor.lockState = previousLock;
FsmBool inMenu = PlayerInMenu();
if (inMenu != null) inMenu.Value = previousInMenu;
} }
} }
@@ -71,6 +76,16 @@ namespace MSCO
PlayMakerGUI.HideCursor = false; PlayMakerGUI.HideCursor = false;
Cursor.visible = true; Cursor.visible = true;
Cursor.lockState = CursorLockMode.None; Cursor.lockState = CursorLockMode.None;
// So zeigt MSC selbst den Mauszeiger (Pausemenü, Kasse ...) - ohne MSCLoader brauchen wir das auch
FsmBool inMenu = PlayerInMenu();
if (inMenu != null) inMenu.Value = true;
}
/// <summary>Globale Spielvariable "PlayerInMenu": true = Zeiger frei, Mausblick aus.</summary>
static FsmBool PlayerInMenu()
{
try { return FsmVariables.GlobalVariables.FindFsmBool("PlayerInMenu"); }
catch { return null; }
} }
/// <summary> /// <summary>
+23
View File
@@ -60,6 +60,29 @@ namespace MSCO
} }
} }
static float nextPoll;
/// <summary>
/// JOIN-Link, während das Spiel schon läuft: der MSCO Launcher legt ihn als &lt;Spiel&gt;\MSCO\join.txt ab (Steam
/// gibt Links an ein laufendes Spiel nicht weiter). Einmal pro Sekunde nachsehen.
/// </summary>
public static bool TryTakeRequest(out string host, out int port)
{
host = null;
port = 0;
if (UnityEngine.Time.realtimeSinceStartup < nextPoll) return false;
nextPoll = UnityEngine.Time.realtimeSinceStartup + 1f;
try
{
string file = System.IO.Path.Combine(Bootstrap.Folder ?? "", "join.txt");
if (Bootstrap.Folder == null || !System.IO.File.Exists(file)) return false;
string text = System.IO.File.ReadAllText(file);
System.IO.File.Delete(file);
return TryParse(text, out host, out port);
}
catch { return false; }
}
/// <summary>Wurde das Spiel über einen JOIN-Link gestartet?</summary> /// <summary>Wurde das Spiel über einen JOIN-Link gestartet?</summary>
public static bool TryGetFromCommandLine(out string host, out int port) public static bool TryGetFromCommandLine(out string host, out int port)
{ {
+82 -31
View File
@@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace MSCO namespace MSCO
@@ -33,10 +33,10 @@ namespace MSCO
float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); } float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); }
static readonly Color Background = Hex(0x17, 0x17, 0x1b); static readonly Color Background = Hex(0x11, 0x11, 0x14);
static readonly Color Sidebar = Hex(0x0f, 0x0f, 0x12); static readonly Color Sidebar = Hex(0x0c, 0x0c, 0x0f);
static readonly Color Field = Hex(0x24, 0x24, 0x2a); static readonly Color Field = Hex(0x1b, 0x1b, 0x20);
static readonly Color FieldFocus = Hex(0x2e, 0x2e, 0x36); static readonly Color FieldFocus = Hex(0x24, 0x24, 0x2a);
static readonly Color Accent = Hex(0xf2, 0x6b, 0x1d); static readonly Color Accent = Hex(0xf2, 0x6b, 0x1d);
static readonly Color AccentHover = Hex(0xff, 0x82, 0x3a); static readonly Color AccentHover = Hex(0xff, 0x82, 0x3a);
static readonly Color Secondary = Hex(0x2c, 0x2c, 0x33); static readonly Color Secondary = Hex(0x2c, 0x2c, 0x33);
@@ -65,7 +65,8 @@ namespace MSCO
bool stylesReady; bool stylesReady;
Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow; Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow;
Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull; Texture2D texDivider, texGold, texBarBack, texBarFill, texBarFull, texGlow;
GUIStyle scrollbarStyle, scrollThumbStyle;
GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle; GUIStyle rowHoverBox, pingGoodStyle, pingMidStyle, pingBadStyle;
List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs List<ServerListEntry> shownAll, shownServers; // Stand des letzten Layout-Durchgangs
string layoutError; string layoutError;
@@ -123,6 +124,8 @@ namespace MSCO
if (mp.IsReconnecting) { DrawPrompt(mp, true); return; } if (mp.IsReconnecting) { DrawPrompt(mp, true); return; }
var window = new Rect(0, 0, vw, vh); var window = new Rect(0, 0, vw, vh);
GUI.Box(window, GUIContent.none, windowBox); GUI.Box(window, GUIContent.none, windowBox);
float glowWidth = Mathf.Min(vw * 0.9f, S(1300));
GUI.DrawTexture(new Rect(S(SidebarWidth) + (vw - S(SidebarWidth) - glowWidth) / 2f, -S(60), glowWidth, S(420)), texGlow);
DrawSidebar(new Rect(0, 0, S(SidebarWidth), vh - S(StatusHeight))); DrawSidebar(new Rect(0, 0, S(SidebarWidth), vh - S(StatusHeight)));
@@ -131,6 +134,9 @@ namespace MSCO
float width = Mathf.Min(S(MaxContentWidth), free - S(96f)); float width = Mathf.Min(S(MaxContentWidth), free - S(96f));
var content = new Rect(S(SidebarWidth) + Mathf.Round((free - width) / 2f), S(40f), width, vh - S(StatusHeight) - S(72f)); var content = new Rect(S(SidebarWidth) + Mathf.Round((free - width) / 2f), S(40f), width, vh - S(StatusHeight) - S(72f));
contentHeight = content.height; contentHeight = content.height;
GUIStyle oldBar = GUI.skin.verticalScrollbar, oldThumb = GUI.skin.verticalScrollbarThumb;
GUI.skin.verticalScrollbar = scrollbarStyle;
GUI.skin.verticalScrollbarThumb = scrollThumbStyle;
GUILayout.BeginArea(content); GUILayout.BeginArea(content);
if (versionWarning) DrawVersionWarning(mp); if (versionWarning) DrawVersionWarning(mp);
else if (pendingJoin) DrawJoinConfirm(mp); else if (pendingJoin) DrawJoinConfirm(mp);
@@ -139,6 +145,8 @@ namespace MSCO
else if (page == Page.Favorites) DrawFavorites(mp); else if (page == Page.Favorites) DrawFavorites(mp);
else DrawServerBrowser(mp); else DrawServerBrowser(mp);
GUILayout.EndArea(); GUILayout.EndArea();
GUI.skin.verticalScrollbar = oldBar;
GUI.skin.verticalScrollbarThumb = oldThumb;
DrawStatusBar(mp, new Rect(0, vh - S(StatusHeight), vw, S(StatusHeight))); DrawStatusBar(mp, new Rect(0, vh - S(StatusHeight), vw, S(StatusHeight)));
@@ -206,8 +214,10 @@ namespace MSCO
bool NavButton(Rect r, string text, bool active) bool NavButton(Rect r, string text, bool active)
{ {
if (active) GUI.Box(new Rect(r.x + S(4), r.y + S(8), S(4), r.height - S(16)), GUIContent.none, accentBox); // Akzentbalken links var item = new Rect(r.x + S(12), r.y, r.width - S(24), r.height);
return GUI.Button(r, text, active ? navActiveStyle : navStyle); bool clicked = GUI.Button(item, text, active ? navActiveStyle : navStyle);
if (active) GUI.Box(new Rect(item.x + S(8), item.y + S(11), S(3), item.height - S(22)), GUIContent.none, accentBox); // Akzentbalken
return clicked;
} }
void DrawP2P(MultiplayerManager mp) void DrawP2P(MultiplayerManager mp)
@@ -291,8 +301,9 @@ namespace MSCO
string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError; string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError;
// Tabellenkopf // Tabellenkopf
GUILayout.Space(S(6));
Rect header = GUILayoutUtility.GetRect(S(10), S(26), GUILayout.ExpandWidth(true)); Rect header = GUILayoutUtility.GetRect(S(10), S(26), GUILayout.ExpandWidth(true));
GUI.Box(header, GUIContent.none, rowBox); GUI.DrawTexture(new Rect(header.x, header.yMax - S(1), header.width, S(1)), texDivider);
GUI.Label(new Rect(header.x + S(12), header.y, header.width * 0.48f, header.height), "SERVER NAME", headerCellStyle); GUI.Label(new Rect(header.x + S(12), header.y, header.width * 0.48f, header.height), "SERVER NAME", headerCellStyle);
GUI.Label(new Rect(header.x + header.width * 0.5f, header.y, S(70), header.height), "PLAYERS", headerCellStyle); GUI.Label(new Rect(header.x + header.width * 0.5f, header.y, S(70), header.height), "PLAYERS", headerCellStyle);
GUI.Label(new Rect(header.x + header.width * 0.62f, header.y, S(70), header.height), "TYPE", headerCellStyle); GUI.Label(new Rect(header.x + header.width * 0.62f, header.y, S(70), header.height), "TYPE", headerCellStyle);
@@ -933,8 +944,16 @@ namespace MSCO
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt) // Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
windowBox = UiTheme.Card(Background, P(10)); windowBox = UiTheme.Card(Background, P(10));
sidebarBox = UiTheme.Card(Sidebar, P(10)); sidebarBox = UiTheme.Card(Sidebar, P(10));
rowBox = UiTheme.Panel9(Field, P(UiTheme.Radius)); rowBox = UiTheme.Card(Field, P(10));
rowHoverBox = UiTheme.Card(FieldFocus, P(UiTheme.Radius)); rowHoverBox = new GUIStyle(rowBox);
rowHoverBox.normal.background = UiTheme.RoundedEx(FieldFocus, FieldFocus, new Color(Accent.r, Accent.g, Accent.b, 0.45f), true, P(10), P(10) * 2 + 4);
texGlow = Glow(new Color(Accent.r, Accent.g, Accent.b, 0.16f));
scrollbarStyle = new GUIStyle();
scrollbarStyle.fixedWidth = S(8);
scrollbarStyle.margin = new RectOffset(P(6), 0, 0, 0);
scrollThumbStyle = UiTheme.Panel9(new Color(1f, 1f, 1f, 0.14f), P(3));
scrollThumbStyle.fixedWidth = S(6);
scrollThumbStyle.hover.background = UiTheme.Rounded(new Color(1f, 1f, 1f, 0.24f), P(3));
texDivider = Solid(new Color(1f, 1f, 1f, 0.07f)); texDivider = Solid(new Color(1f, 1f, 1f, 0.07f));
texGold = Solid(Gold); texGold = Solid(Gold);
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f)); texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
@@ -955,10 +974,11 @@ namespace MSCO
pillFeaturedStyle = new GUIStyle(pillStyle); pillFeaturedStyle = new GUIStyle(pillStyle);
pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4)); pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4));
pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06); pillFeaturedStyle.normal.textColor = Hex(0x1a, 0x12, 0x06);
featuredRowBox = UiTheme.Panel9(Color.Lerp(Field, Gold, 0.16f), P(UiTheme.Radius)); featuredRowBox = UiTheme.Card(Color.Lerp(Field, Gold, 0.14f), P(10));
chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.22f), new Color(Accent.r, Accent.g, Accent.b, 0.32f), AccentHover, P(11), FontStyle.Bold); chipOnStyle = Button(new Color(Accent.r, Accent.g, Accent.b, 0.2f), new Color(Accent.r, Accent.g, Accent.b, 0.3f), AccentHover, P(11), FontStyle.Bold, P(11), new Color(Accent.r, Accent.g, Accent.b, 0.55f));
chipOffStyle = Button(Field, FieldFocus, Muted, P(11), FontStyle.Bold); chipOffStyle = Button(Field, FieldFocus, Muted, P(11), FontStyle.Bold, P(11), UiTheme.Border);
chipOffStyle.hover.textColor = Color.white;
chipOnStyle.padding = chipOffStyle.padding = new RectOffset(P(12), P(12), P(0), P(0)); chipOnStyle.padding = chipOffStyle.padding = new RectOffset(P(12), P(12), P(0), P(0));
logoStyle = Label(P(28), Color.white, FontStyle.Bold); logoStyle = Label(P(28), Color.white, FontStyle.Bold);
@@ -992,22 +1012,28 @@ namespace MSCO
toggleStyle.fontSize = P(11); toggleStyle.fontSize = P(11);
toggleStyle.fontStyle = FontStyle.Bold; toggleStyle.fontStyle = FontStyle.Bold;
navStyle = Button(Sidebar, Secondary, Muted, P(14), FontStyle.Normal); navStyle = Button(Sidebar, new Color(1f, 1f, 1f, 0.05f), Muted, P(14), FontStyle.Normal, P(10));
navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau) navStyle.hover.textColor = Color.white; // Hover: Text hell (vorher blieb er grau)
navStyle.alignment = TextAnchor.MiddleLeft; navStyle.alignment = TextAnchor.MiddleLeft;
navStyle.padding = new RectOffset(P(26), P(10), P(0), P(0)); navStyle.padding = new RectOffset(P(22), P(10), P(0), P(0));
navActiveStyle = Button(Secondary, Secondary, Color.white, P(14), FontStyle.Bold); navActiveStyle = Button(Color.Lerp(Sidebar, Accent, 0.12f), Color.Lerp(Sidebar, Accent, 0.16f), Color.white, P(14), FontStyle.Bold, P(10), new Color(Accent.r, Accent.g, Accent.b, 0.25f));
navActiveStyle.alignment = TextAnchor.MiddleLeft; navActiveStyle.alignment = TextAnchor.MiddleLeft;
navActiveStyle.padding = new RectOffset(P(26), P(10), P(0), P(0)); navActiveStyle.padding = new RectOffset(P(22), P(10), P(0), P(0));
primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold); primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold, P(10));
UiTheme.ApplyPrimaryGradient(primaryStyle); primaryStyle.normal.background = primaryStyle.focused.background = UiTheme.RoundedEx(AccentHover, Accent, Color.clear, false, P(10));
dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold); primaryStyle.hover.background = primaryStyle.active.background = primaryStyle.onNormal.background =
secondaryStyle = Button(Secondary, SecondaryHover, Color.white, P(14), FontStyle.Bold); UiTheme.RoundedEx(Color.Lerp(AccentHover, Color.white, 0.12f), AccentHover, Color.clear, false, P(10));
tabStyle = Button(Sidebar, Secondary, Muted, P(11), FontStyle.Bold); dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold, P(10));
secondaryStyle = Button(Hex(0x22, 0x22, 0x28), Hex(0x2a, 0x2a, 0x31), Color.white, P(14), FontStyle.Bold, P(10), UiTheme.Border);
tabStyle = Button(Hex(0x1b, 0x1b, 0x20), Hex(0x24, 0x24, 0x2a), Muted, P(11), FontStyle.Bold, P(10), UiTheme.Border);
tabStyle.hover.textColor = Color.white; tabStyle.hover.textColor = Color.white;
fieldStyle = UiTheme.Field(P(14)); fieldStyle = UiTheme.Field(P(14));
fieldStyle.normal.background = UiTheme.RoundedEx(Hex(0x16, 0x16, 0x1a), Hex(0x16, 0x16, 0x1a), UiTheme.Border, true, P(10), P(10) * 2 + 4);
fieldStyle.hover.background = UiTheme.RoundedEx(Hex(0x19, 0x19, 0x1e), Hex(0x19, 0x19, 0x1e), new Color(1f, 1f, 1f, 0.14f), true, P(10), P(10) * 2 + 4);
fieldStyle.focused.background = UiTheme.RoundedEx(Hex(0x19, 0x19, 0x1e), Hex(0x19, 0x19, 0x1e), Accent, true, P(10), P(10) * 2 + 4);
fieldStyle.border = new RectOffset(P(10) + 2, P(10) + 2, P(10) + 2, P(10) + 2);
stylesReady = true; stylesReady = true;
} }
@@ -1021,21 +1047,46 @@ namespace MSCO
return s; return s;
} }
static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font) /// <summary>Knopf mit runden Ecken (9-Slice, siehe UiTheme), optional mit feinem Rand wie .btn auf der Website.</summary>
static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font, int radius = UiTheme.Radius, Color border = default(Color))
{ {
var s = new GUIStyle(GUI.skin.button); var s = new GUIStyle(GUI.skin.button);
// abgerundete Ecken (9-Slice), siehe UiTheme bool withBorder = border.a > 0f;
s.normal.background = UiTheme.Rounded(normal); int h = radius * 2 + 4;
s.hover.background = UiTheme.Rounded(hover); Texture2D n = UiTheme.RoundedEx(normal, normal, border, withBorder, radius, h);
s.active.background = UiTheme.Rounded(hover); Texture2D o = UiTheme.RoundedEx(hover, hover, withBorder ? new Color(border.r, border.g, border.b, Mathf.Min(1f, border.a * 1.8f)) : border, withBorder, radius, h);
s.focused.background = UiTheme.Rounded(normal); s.normal.background = n;
s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = text; s.focused.background = n;
s.border = new RectOffset(UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2); s.hover.background = o;
s.active.background = o;
s.onNormal.background = o;
s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = s.onNormal.textColor = text;
s.border = new RectOffset(radius + 2, radius + 2, radius + 2, radius + 2);
s.fontSize = size; s.fontSize = size;
s.fontStyle = font; s.fontStyle = font;
return s; return s;
} }
/// <summary>Weicher runder Schein (Mitte deckend, nach außen durchsichtig) - für den orangen Schimmer oben.</summary>
static Texture2D Glow(Color color)
{
const int w = 128, h = 64;
var tex = new Texture2D(w, h, TextureFormat.ARGB32, false);
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
{
float dx = (x + 0.5f) / w * 2f - 1f, dy = (y + 0.5f) / h * 2f - 1f;
float d = Mathf.Clamp01(Mathf.Sqrt(dx * dx + dy * dy));
float a = 1f - d;
tex.SetPixel(x, y, new Color(color.r, color.g, color.b, color.a * a * a));
}
tex.Apply();
tex.hideFlags = HideFlags.HideAndDontSave;
return tex;
}
static Texture2D Solid(Color color) static Texture2D Solid(Color color)
{ {
var tex = new Texture2D(1, 1); var tex = new Texture2D(1, 1);
+1 -1
View File
@@ -11,7 +11,7 @@ namespace MSCO
/// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit /// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel. /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
/// </summary> /// </summary>
public const string ModVersion = "2.9.0"; public const string ModVersion = "2.9.1";
/// <summary> /// <summary>
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server. /// auch GameServer.ModVersion im Server.
+5 -4
View File
@@ -42,10 +42,6 @@
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\UnityEngine.dll</HintPath> <HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\UnityEngine.dll</HintPath>
<Private>False</Private> <Private>False</Private>
</Reference> </Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>lib\Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Assembly-CSharp-firstpass"> <Reference Include="Assembly-CSharp-firstpass">
<HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Assembly-CSharp-firstpass.dll</HintPath> <HintPath>F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Assembly-CSharp-firstpass.dll</HintPath>
<Private>False</Private> <Private>False</Private>
@@ -97,6 +93,8 @@
<Compile Include="LobbyUI.cs" /> <Compile Include="LobbyUI.cs" />
<Compile Include="MenuHelper.cs" /> <Compile Include="MenuHelper.cs" />
<Compile Include="MenuInputBlocker.cs" /> <Compile Include="MenuInputBlocker.cs" />
<Compile Include="MiniJson.cs" />
<Compile Include="MenuBranding.cs" />
<Compile Include="MSCO.cs" /> <Compile Include="MSCO.cs" />
<Compile Include="MultiplayerManager.cs" /> <Compile Include="MultiplayerManager.cs" />
<Compile Include="NetSession.cs" /> <Compile Include="NetSession.cs" />
@@ -109,6 +107,9 @@
<Compile Include="VehicleFx.cs" /> <Compile Include="VehicleFx.cs" />
<Compile Include="WinHttp.cs" /> <Compile Include="WinHttp.cs" />
<Compile Include="WorldSync.cs" /> <Compile Include="WorldSync.cs" />
<EmbeddedResource Include="menu-logo.png">
<LogicalName>MSCO.menu-logo.png</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="logo.png"> <EmbeddedResource Include="logo.png">
<LogicalName>MSCO.logo.png</LogicalName> <LogicalName>MSCO.logo.png</LogicalName>
</EmbeddedResource> </EmbeddedResource>
+182
View File
@@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
namespace MSCO
{
/// <summary>
/// MSCO statt "My Summer Car": Fenstertitel und -symbol, und im Hauptmenü das große Spiel-Logo durch das MSCO-Logo
/// (branding/menu-logo.png, in die DLL eingebettet) ersetzen.
/// </summary>
public static class MenuBranding
{
const string WindowTitle = "MSCO - My Summer Car Online";
const string MenuLogoResource = "MSCO.menu-logo.png";
// ---------------------------------------------------------------- Fenster
delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassNameW(IntPtr hwnd, StringBuilder name, int max);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool SetWindowTextW(IntPtr hwnd, string text);
[DllImport("user32.dll")] static extern IntPtr SendMessageW(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")] static extern IntPtr CreateIconFromResourceEx(byte[] data, uint size, bool icon, uint version, int cx, int cy, uint flags);
static readonly EnumWindowsProc enumProc = OnWindow; // Delegate festhalten (sonst räumt der GC ihn weg)
static uint processId;
static IntPtr bigIcon, smallIcon;
/// <summary>Titel und Symbol des Spielfensters auf MSCO setzen (beim Start und nach jedem Szenenwechsel).</summary>
public static void ApplyWindow()
{
try
{
processId = (uint)Process.GetCurrentProcess().Id;
if (bigIcon == IntPtr.Zero && Branding.LogoPng.Length > 0)
{
// PNG-Daten direkt als Symbol (geht seit Windows Vista)
bigIcon = CreateIconFromResourceEx(Branding.LogoPng, (uint)Branding.LogoPng.Length, true, 0x00030000, 0, 0, 0);
smallIcon = CreateIconFromResourceEx(Branding.LogoPng, (uint)Branding.LogoPng.Length, true, 0x00030000, 16, 16, 0);
}
EnumWindows(enumProc, IntPtr.Zero);
}
catch (Exception e) { Bootstrap.Log("Window title/icon: " + e.Message); }
}
static bool OnWindow(IntPtr hwnd, IntPtr lParam)
{
uint pid;
GetWindowThreadProcessId(hwnd, out pid);
if (pid != processId) return true;
var name = new StringBuilder(64);
GetClassNameW(hwnd, name, name.Capacity);
if (name.ToString() != "UnityWndClass") return true;
SetWindowTextW(hwnd, WindowTitle);
const uint WM_SETICON = 0x0080;
if (bigIcon != IntPtr.Zero) SendMessageW(hwnd, WM_SETICON, (IntPtr)1, bigIcon);
if (smallIcon != IntPtr.Zero) SendMessageW(hwnd, WM_SETICON, IntPtr.Zero, smallIcon);
return true;
}
// ---------------------------------------------------------------- Logo im Hauptmenü
static Texture2D source;
static readonly Dictionary<string, Texture2D> fitted = new Dictionary<string, Texture2D>();
static bool dumped;
static Texture2D Source
{
get
{
if (source != null) return source;
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(MenuLogoResource))
{
if (s == null) return null;
var data = new byte[s.Length];
int read = 0;
while (read < data.Length) read += s.Read(data, read, data.Length - read);
source = new Texture2D(2, 2, TextureFormat.ARGB32, false);
source.LoadImage(data);
source.hideFlags = HideFlags.HideAndDontSave;
}
return source;
}
}
/// <summary>Ist das eine Textur des Spiel-Logos? (Name enthält "logo", aber nicht vom Auto/Laden ...)</summary>
static bool IsGameLogo(Texture t)
{
if (t == null || t == source) return false;
string n = t.name.ToLowerInvariant();
return n.Contains("logo") && !n.Contains("msco") && !n.Contains("satsuma") && !n.Contains("teimo") && !n.Contains("amistech");
}
/// <summary>Im Hauptmenü das Spiel-Logo ersetzen. Gibt die Anzahl ersetzter Texturen zurück.</summary>
public static int ReplaceMenuLogo()
{
if (Source == null) return 0;
int replaced = 0;
var seen = new List<string>();
// Auch ausgeblendete Objekte (das Menü wechselt zwischen mehreren Logo-Objekten)
foreach (Renderer r in Resources.FindObjectsOfTypeAll<Renderer>())
{
if (r == null || r.gameObject.hideFlags != HideFlags.None) continue;
Material[] materials = r.sharedMaterials;
for (int i = 0; i < materials.Length; i++)
{
Material m = materials[i];
if (m == null || m.mainTexture == null) continue;
if (!dumped && seen.Count < 200) seen.Add(Path(r.transform) + " : " + m.mainTexture.name);
if (!IsGameLogo(m.mainTexture)) continue;
Texture original = m.mainTexture;
m.mainTexture = Fit(original.width, original.height);
Bootstrap.Log("Menu logo replaced: " + Path(r.transform) + " (" + original.name + ", " + original.width + "x" + original.height + ")");
replaced++;
}
}
foreach (SpriteRenderer sr in UnityEngine.Object.FindObjectsOfType<SpriteRenderer>())
{
if (sr == null || sr.sprite == null || !IsGameLogo(sr.sprite.texture)) continue;
Texture2D tex = Fit(sr.sprite.texture.width, sr.sprite.texture.height);
sr.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), sr.sprite.pixelsPerUnit);
Bootstrap.Log("Menu logo (sprite) replaced: " + Path(sr.transform));
replaced++;
}
foreach (GUITexture g in UnityEngine.Object.FindObjectsOfType<GUITexture>())
{
if (g == null || !IsGameLogo(g.texture)) continue;
g.texture = Fit(g.texture.width, g.texture.height);
Bootstrap.Log("Menu logo (GUITexture) replaced: " + Path(g.transform));
replaced++;
}
if (!dumped && replaced == 0)
{
// Zur Diagnose: welche Texturen gibt es im Menü? (nur einmal)
Bootstrap.Log("Menu logo not found. Textures in the menu:\n " + string.Join("\n ", seen.ToArray()));
}
dumped = true;
return replaced;
}
/// <summary>MSCO-Logo mittig in eine Textur der Größe des Originals (gleiches Seitenverhältnis wie dessen Fläche).</summary>
static Texture2D Fit(int width, int height)
{
width = Mathf.Clamp(width, 64, 2048);
height = Mathf.Clamp(height, 64, 2048);
string key = width + "x" + height;
Texture2D tex;
if (fitted.TryGetValue(key, out tex) && tex != null) return tex;
tex = new Texture2D(width, height, TextureFormat.ARGB32, true);
tex.hideFlags = HideFlags.HideAndDontSave;
tex.wrapMode = TextureWrapMode.Clamp;
float scale = Mathf.Min((float)width / Source.width, (float)height / Source.height);
float w = Source.width * scale, h = Source.height * scale;
float x0 = (width - w) / 2f, y0 = (height - h) / 2f;
var pixels = new Color[width * height];
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
{
float u = (x - x0) / w, v = (y - y0) / h;
pixels[y * width + x] = u < 0f || u > 1f || v < 0f || v > 1f ? new Color(0, 0, 0, 0) : Source.GetPixelBilinear(u, v);
}
tex.SetPixels(pixels);
tex.Apply(true);
fitted[key] = tex;
return tex;
}
static string Path(Transform t)
{
string path = t.name;
for (Transform p = t.parent; p != null; p = p.parent) path = p.name + "/" + path;
return path;
}
}
}
+166
View File
@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace MSCO
{
/// <summary>
/// Kleiner eigener JSON-Leser (ohne Newtonsoft.Json - das braucht System.Runtime.Serialization, das ohne MSCLoader im
/// Spiel fehlt). Ergebnis: Dictionary&lt;string, object&gt; (Objekt, Schlüssel ohne Groß-/Kleinschreibung),
/// List&lt;object&gt;, string, double, bool oder null.
/// </summary>
public static class MiniJson
{
public static object Parse(string json)
{
if (json == null) throw new FormatException("No JSON");
int i = 0;
object value = ReadValue(json, ref i);
SkipWhitespace(json, ref i);
if (i < json.Length) throw new FormatException("Unexpected text at " + i);
return value;
}
// ---------------------------------------------------------------- Zugriff
public static string Str(Dictionary<string, object> o, string key, string fallback = null)
{
object v;
if (o == null || !o.TryGetValue(key, out v) || v == null) return fallback;
if (v is double) return ((double)v).ToString(CultureInfo.InvariantCulture);
return v.ToString();
}
public static int Int(Dictionary<string, object> o, string key, int fallback = 0)
{
object v;
if (o == null || !o.TryGetValue(key, out v)) return fallback;
if (v is double) return (int)(double)v;
int parsed;
return v is string && int.TryParse((string)v, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : fallback;
}
public static bool Bool(Dictionary<string, object> o, string key, bool fallback = false)
{
object v;
if (o == null || !o.TryGetValue(key, out v)) return fallback;
return v is bool ? (bool)v : fallback;
}
public static List<string> Strings(Dictionary<string, object> o, string key)
{
var result = new List<string>();
object v;
if (o == null || !o.TryGetValue(key, out v)) return result;
var list = v as List<object>;
if (list != null) foreach (object item in list) if (item != null) result.Add(item.ToString());
return result;
}
// ---------------------------------------------------------------- Lesen
static object ReadValue(string s, ref int i)
{
SkipWhitespace(s, ref i);
if (i >= s.Length) throw new FormatException("Unexpected end of JSON");
char c = s[i];
if (c == '{') return ReadObject(s, ref i);
if (c == '[') return ReadArray(s, ref i);
if (c == '"') return ReadString(s, ref i);
if (c == '-' || (c >= '0' && c <= '9')) return ReadNumber(s, ref i);
if (Word(s, ref i, "true")) return true;
if (Word(s, ref i, "false")) return false;
if (Word(s, ref i, "null")) return null;
throw new FormatException("Unexpected '" + c + "' at " + i);
}
static Dictionary<string, object> ReadObject(string s, ref int i)
{
var o = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
i++; // {
SkipWhitespace(s, ref i);
if (i < s.Length && s[i] == '}') { i++; return o; }
while (true)
{
SkipWhitespace(s, ref i);
if (i >= s.Length || s[i] != '"') throw new FormatException("Expected a name at " + i);
string key = ReadString(s, ref i);
SkipWhitespace(s, ref i);
if (i >= s.Length || s[i] != ':') throw new FormatException("Expected ':' at " + i);
i++;
o[key] = ReadValue(s, ref i);
SkipWhitespace(s, ref i);
if (i >= s.Length) throw new FormatException("Unexpected end of JSON");
if (s[i] == ',') { i++; continue; }
if (s[i] == '}') { i++; return o; }
throw new FormatException("Expected ',' or '}' at " + i);
}
}
static List<object> ReadArray(string s, ref int i)
{
var list = new List<object>();
i++; // [
SkipWhitespace(s, ref i);
if (i < s.Length && s[i] == ']') { i++; return list; }
while (true)
{
list.Add(ReadValue(s, ref i));
SkipWhitespace(s, ref i);
if (i >= s.Length) throw new FormatException("Unexpected end of JSON");
if (s[i] == ',') { i++; continue; }
if (s[i] == ']') { i++; return list; }
throw new FormatException("Expected ',' or ']' at " + i);
}
}
static string ReadString(string s, ref int i)
{
var sb = new StringBuilder();
i++; // "
while (i < s.Length)
{
char c = s[i++];
if (c == '"') return sb.ToString();
if (c != '\\') { sb.Append(c); continue; }
if (i >= s.Length) break;
char e = s[i++];
switch (e)
{
case 'b': sb.Append('\b'); break;
case 'f': sb.Append('\f'); break;
case 'n': sb.Append('\n'); break;
case 'r': sb.Append('\r'); break;
case 't': sb.Append('\t'); break;
case 'u':
if (i + 4 > s.Length) throw new FormatException("Bad \\u escape");
sb.Append((char)int.Parse(s.Substring(i, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture));
i += 4;
break;
default: sb.Append(e); break; // \" \\ \/
}
}
throw new FormatException("Unterminated string");
}
static double ReadNumber(string s, ref int i)
{
int start = i;
while (i < s.Length && "+-0123456789.eE".IndexOf(s[i]) >= 0) i++;
return double.Parse(s.Substring(start, i - start), NumberStyles.Float, CultureInfo.InvariantCulture);
}
static bool Word(string s, ref int i, string word)
{
if (string.CompareOrdinal(s, i, word, 0, word.Length) != 0) return false;
i += word.Length;
return true;
}
static void SkipWhitespace(string s, ref int i)
{
while (i < s.Length && char.IsWhiteSpace(s[i])) i++;
}
}
}
+27
View File
@@ -455,6 +455,20 @@ namespace MSCO
} }
/// <summary>Admin: Satsuma neben sich holen (z.B. wenn er verschwunden oder im Boden ist).</summary> /// <summary>Admin: Satsuma neben sich holen (z.B. wenn er verschwunden oder im Boden ist).</summary>
/// <summary>Admin: sich selbst zu einem Ort teleportieren (Haus, Laden ... siehe Cheats.Places).</summary>
public void TeleportToPlace(string label, string objectName)
{
if (!CanCheat || localPlayer == null) return;
Vector3 pos;
if (!Cheats.FindPlace(objectName, out pos)) { overlays.Log(label + " not found (only works in the game)"); return; }
passenger.Exit(localPlayer, playerCamera);
if (Cheats.Teleport(localPlayer, pos))
{
overlays.Log("Teleported to " + label);
AnnounceCheat("teleported to " + label);
}
}
public void BringCar() public void BringCar()
{ {
if (!CanCheat || localPlayer == null) return; if (!CanCheat || localPlayer == null) return;
@@ -611,6 +625,18 @@ namespace MSCO
if (host != null) JoinServer(host, port); if (host != null) JoinServer(host, port);
} }
/// <summary>JOIN-Link aus dem Browser, während das Spiel schon läuft (über den Launcher, siehe JoinLinks).</summary>
void CheckJoinRequest()
{
string host;
int port;
if (!JoinLinks.TryTakeRequest(out host, out port)) return;
PendingJoinHost = host;
PendingJoinPort = port;
if (IsInSession || !MenuHelper.InMenu)
toasts.Add("Join link: " + host + ":" + port, "leave this game - then confirm JOIN in the main menu", UiTheme.Accent);
}
public void CancelPendingJoin() public void CancelPendingJoin()
{ {
PendingJoinHost = null; PendingJoinHost = null;
@@ -974,6 +1000,7 @@ namespace MSCO
void Update() void Update()
{ {
CheckHitch(); CheckHitch();
CheckJoinRequest();
MeasureFps(); MeasureFps();
// Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog) // Menü-Buttons des Spiels nicht durch unser Fenster hindurch auslösen (z.B. Changelog)
// Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht; // Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht;
+39 -5
View File
@@ -293,22 +293,56 @@ namespace MSCO
// Reiter // Reiter
GUILayout.BeginHorizontal(); GUILayout.BeginHorizontal();
string[] tabs = { "PLAYERS", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" }; string[] tabs = { "PLAYERS", "TELEPORT", "ITEMS", "BANS" + (bannedIds != null && bannedIds.Count > 0 ? " (" + bannedIds.Count + ")" : ""), "WORLD" };
for (int i = 0; i < tabs.Length; i++) for (int i = 0; i < tabs.Length; i++)
{ {
if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(130))) adminTab = i; if (GUILayout.Button(tabs[i], adminTab == i ? primary : secondary, GUILayout.Height(30), GUILayout.Width(120))) adminTab = i;
GUILayout.Space(6); GUILayout.Space(6);
} }
GUILayout.EndHorizontal(); GUILayout.EndHorizontal();
GUILayout.Space(10); GUILayout.Space(10);
if (adminTab == 1) DrawAdminItems(mp); if (adminTab == 1) DrawAdminTeleport(mp);
else if (adminTab == 2) DrawAdminBans(mp); else if (adminTab == 2) DrawAdminItems(mp);
else if (adminTab == 3) DrawAdminWorld(mp); else if (adminTab == 3) DrawAdminBans(mp);
else if (adminTab == 4) DrawAdminWorld(mp);
else DrawAdminPlayers(mp); else DrawAdminPlayers(mp);
GUILayout.EndArea(); GUILayout.EndArea();
} }
Vector2 teleportScroll;
void DrawAdminTeleport(MultiplayerManager mp)
{
if (!mp.CanCheat)
{
GUILayout.Label("Teleporting only works in the game.", small);
return;
}
GUILayout.Label("Teleport yourself to a place. Players: use GOTO / BRING on the PLAYERS tab.", small);
GUILayout.Space(8);
teleportScroll = GUILayout.BeginScrollView(teleportScroll);
const int columns = 3;
for (int i = 0; i < Cheats.Places.Length; i += columns)
{
GUILayout.BeginHorizontal();
for (int c = 0; c < columns; c++)
{
if (i + c < Cheats.Places.Length)
{
string[] place = Cheats.Places[i + c];
if (GUILayout.Button(place[0].ToUpperInvariant(), secondary, GUILayout.Height(38)))
mp.TeleportToPlace(place[0], place[1]);
}
else GUILayout.Label("", GUILayout.Height(38));
if (c < columns - 1) GUILayout.Space(6);
}
GUILayout.EndHorizontal();
GUILayout.Space(6);
}
GUILayout.EndScrollView();
}
void DrawAdminPlayers(MultiplayerManager mp) void DrawAdminPlayers(MultiplayerManager mp)
{ {
// Cheats für den eigenen Spieler (nur im Spiel) // Cheats für den eigenen Spieler (nur im Spiel)
+8 -5
View File
@@ -47,11 +47,12 @@ einem dedizierten Server aus dem Server-Browser. MSCO läuft **eigenständig, oh
- **T** – Chat - **T** – Chat
- **F7** – MSCO-Konsole mit allen Befehlen - **F7** – MSCO-Konsole mit allen Befehlen
- **/admin** – Admin-Menü für Server-Admins bzw. den P2P-Host: Spieler kicken/bannen, **Admin geben/nehmen**, - **/admin** – Admin-Menü für Server-Admins bzw. den P2P-Host: Spieler kicken/bannen, **Admin geben/nehmen**,
GOTO/BRING (teleportieren), Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen), Items spawnen, GOTO/BRING (teleportieren), **TELEPORT** zu Orten (Haus, Laden, Werkstatt, Prüfstelle, Peräjärvi …), Cheats (+1.000 / +10.000 MK, Bedürfnisse auffüllen, Satsuma zu sich holen), Items spawnen,
Welt zurücksetzen/Backups – Cheats werden im Chat angezeigt Welt zurücksetzen/Backups – Cheats werden im Chat angezeigt
- **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz - **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz
- Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`) - Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`)
- Rich Presence: zeigt Freunden deinen Status („In the Menus“, Server, Spielerzahl) - Rich Presence: zeigt Freunden deinen Status („In the Menus“, Server, Spielerzahl)
- MSCO-Look: Fenstertitel, Symbol und Logo im Hauptmenü; der „Press any key“-Startbildschirm wird übersprungen
## Installation ## Installation
@@ -62,7 +63,7 @@ Was der Launcher im Spielordner einrichtet:
| Datei | | | Datei | |
|---|---| |---|---|
| `MSCO\MSCO.dll` (+ `0Harmony.dll`, `Newtonsoft.Json.dll`) | die Mod | | `MSCO\MSCO.dll` (+ `0Harmony.dll`) | die Mod |
| `winhttp.dll` | Unity Doorstop 4 – nur wenn noch keiner da ist (MSCLoader bringt denselben mit) | | `winhttp.dll` | Unity Doorstop 4 – nur wenn noch keiner da ist (MSCLoader bringt denselben mit) |
| `doorstop_config.ini` | zeigt auf `MSCO\MSCO.dll` – eine MSCLoader-Konfiguration wird vorher als `doorstop_config.mscloader.ini` gesichert | | `doorstop_config.ini` | zeigt auf `MSCO\MSCO.dll` – eine MSCLoader-Konfiguration wird vorher als `doorstop_config.mscloader.ini` gesichert |
@@ -71,7 +72,7 @@ Steam mit MSCO und ohne MSCLoader. **MSCLoader wieder benutzen:** `doorstop_conf
`doorstop_config.ini` umbenennen. `doorstop_config.ini` umbenennen.
**Von Hand:** Doorstop 4 (`winhttp.dll`, x64) in den Spielordner, `MSCO.dll` von der **Von Hand:** Doorstop 4 (`winhttp.dll`, x64) in den Spielordner, `MSCO.dll` von der
[Download-Seite](https://msco.mbm.wtf/download) mit `0Harmony.dll` und `Newtonsoft.Json.dll` nach [Download-Seite](https://msco.mbm.wtf/download) mit `0Harmony.dll` (Harmony 1.2) nach
`<Spielordner>\MSCO\` und in `doorstop_config.ini` `target_assembly = MSCO\MSCO.dll` eintragen. `<Spielordner>\MSCO\` und in `doorstop_config.ini` `target_assembly = MSCO\MSCO.dll` eintragen.
Spiel **mit Steam** starten – ohne Steam gehen nur Non-Steam-Server. Das Lade-Protokoll steht in Spiel **mit Steam** starten – ohne Steam gehen nur Non-Steam-Server. Das Lade-Protokoll steht in
@@ -121,7 +122,8 @@ In der F7-Konsole:
- Eine C#-Entwicklungsumgebung, .NET Framework 3.5 (Unity 5.0 / Mono) - Eine C#-Entwicklungsumgebung, .NET Framework 3.5 (Unity 5.0 / Mono)
- Die Verweise in `MSCO.csproj` zeigen auf `F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\` - Die Verweise in `MSCO.csproj` zeigen auf `F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\`
(UnityEngine, PlayMaker, Assembly-CSharp, Assembly-CSharp-firstpass, cInput) – bei anderem Installationsort anpassen. (UnityEngine, PlayMaker, Assembly-CSharp, Assembly-CSharp-firstpass, cInput) – bei anderem Installationsort anpassen.
Harmony 1.2 und Newtonsoft.Json liegen in `lib/` (MIT, siehe `lib/THIRD-PARTY.txt`). Harmony 1.2 liegt in `lib/` (MIT, siehe `lib/THIRD-PARTY.txt`). JSON liest MSCO selbst (`MiniJson.cs`) – das Spiel
hat ohne MSCLoader kein `System.Runtime.Serialization`.
- Nach dem Build wird die DLL automatisch nach `<Spielordner>\MSCO` kopiert (Ziel `CopyToGame` in der csproj). - Nach dem Build wird die DLL automatisch nach `<Spielordner>\MSCO` kopiert (Ziel `CopyToGame` in der csproj).
- Einstieg: `Bootstrap.cs` (`Doorstop.Entrypoint.Start` → Harmony-Haken in `PlayMakerArrayListProxy.Awake` → `MSCO_Host` - Einstieg: `Bootstrap.cs` (`Doorstop.Entrypoint.Start` → Harmony-Haken in `PlayMakerArrayListProxy.Awake` → `MSCO_Host`
im Hauptmenü, überspringt den Nachrichten-Bildschirm). im Hauptmenü, überspringt den Nachrichten-Bildschirm).
@@ -146,7 +148,8 @@ msbuild MSCO.csproj -p:Configuration=Release
| Datei | Aufgabe | | Datei | Aufgabe |
|---|---| |---|---|
| `Bootstrap.cs`, `MSCO.cs` | Start ohne MSCLoader (Doorstop), Versionen, Tastenbelegung | | `Bootstrap.cs`, `MSCO.cs` | Start ohne MSCLoader (Doorstop), Ladebildschirm, Versionen, Tastenbelegung |
| `MenuBranding.cs`, `MiniJson.cs` | Fenstertitel/-symbol und Menü-Logo, eigener JSON-Leser |
| `MultiplayerManager.cs` | Sitzung, Nachrichten, Spielerliste, Spielstart, HUD | | `MultiplayerManager.cs` | Sitzung, Nachrichten, Spielerliste, Spielstart, HUD |
| `NetSession.cs` | Netzwerk (TCP und Steam P2P), Protokoll | | `NetSession.cs` | Netzwerk (TCP und Steam P2P), Protokoll |
| `LobbyUI.cs`, `Overlays.cs`, `UiTheme.cs` | Launcher, Konsole/Chat/Admin-Menü, gemeinsamer Stil | | `LobbyUI.cs`, `Overlays.cs`, `UiTheme.cs` | Launcher, Konsole/Chat/Admin-Menü, gemeinsamer Stil |
+34 -7
View File
@@ -6,7 +6,6 @@ using System.IO;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
// (Unitys WWW wird nicht mehr benutzt - siehe WinHttp) // (Unitys WWW wird nicht mehr benutzt - siehe WinHttp)
using Newtonsoft.Json;
using UnityEngine; using UnityEngine;
namespace MSCO namespace MSCO
@@ -26,11 +25,11 @@ namespace MSCO
public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben) public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben)
public List<string> Tags = new List<string>(); // Admin-Tags (z.B. "official") und eigene Tags des Servers (Web-Panel) public List<string> Tags = new List<string>(); // Admin-Tags (z.B. "official") und eigene Tags des Servers (Web-Panel)
[JsonIgnore] public int UptimeSeconds = -1; public int UptimeSeconds = -1;
[JsonIgnore] public List<string> PlayerNames = new List<string>(); public List<string> PlayerNames = new List<string>();
[JsonIgnore] public int Ping = -1; // -1 = noch nicht abgefragt public int Ping = -1; // -1 = noch nicht abgefragt
[JsonIgnore] public bool Offline; public bool Offline;
[JsonIgnore] public bool IsLocal; // Server auf diesem PC (localhost) public bool IsLocal; // Server auf diesem PC (localhost)
} }
/// <summary> /// <summary>
@@ -104,7 +103,7 @@ namespace MSCO
} }
List<ServerListEntry> list; List<ServerListEntry> list;
try { list = JsonConvert.DeserializeObject<List<ServerListEntry>>(text) ?? new List<ServerListEntry>(); } try { list = ParseList(text); }
catch (Exception e) catch (Exception e)
{ {
Error = "Invalid server list: " + e.Message; Error = "Invalid server list: " + e.Message;
@@ -115,6 +114,34 @@ namespace MSCO
foreach (ServerListEntry entry in list) QueryAsync(entry); foreach (ServerListEntry entry in list) QueryAsync(entry);
} }
/// <summary>Antwort von /api/servers lesen (eigener JSON-Leser, siehe MiniJson).</summary>
static List<ServerListEntry> ParseList(string text)
{
var list = new List<ServerListEntry>();
var items = MiniJson.Parse(text) as List<object>;
if (items == null) throw new FormatException("Expected a list");
foreach (object item in items)
{
var o = item as Dictionary<string, object>;
if (o == null) continue;
list.Add(new ServerListEntry
{
Name = MiniJson.Str(o, "name", ""),
Description = MiniJson.Str(o, "description", ""),
Address = MiniJson.Str(o, "address", ""),
Port = MiniJson.Int(o, "port", 7777),
Players = MiniJson.Int(o, "players"),
MaxPlayers = MiniJson.Int(o, "maxPlayers"),
Version = MiniJson.Str(o, "version", ""),
RequireSteam = MiniJson.Bool(o, "requireSteam", true),
Owner = MiniJson.Str(o, "owner"),
Featured = MiniJson.Bool(o, "featured"),
Tags = MiniJson.Strings(o, "tags"),
});
}
return list;
}
/// <summary>Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt.</summary> /// <summary>Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt.</summary>
void ProbeLocalServers() void ProbeLocalServers()
{ {
Binary file not shown.
-4
View File
@@ -5,10 +5,6 @@ Third-party libraries shipped with MSCO
https://github.com/pardeike/Harmony - MIT License https://github.com/pardeike/Harmony - MIT License
Copyright (c) 2017 Andreas Pardeike Copyright (c) 2017 Andreas Pardeike
Newtonsoft.Json.dll - Json.NET 13.0.4 by James Newton-King
https://www.newtonsoft.com/json - MIT License
Copyright (c) 2007 James Newton-King
MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and MIT License: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including without associated documentation files (the "Software"), to deal in the Software without restriction, including without
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB