diff --git a/Bootstrap.cs b/Bootstrap.cs
index a95a22c..b7d77ca 100644
--- a/Bootstrap.cs
+++ b/Bootstrap.cs
@@ -23,14 +23,16 @@ namespace Doorstop
namespace MSCO
{
///
- /// 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: <Spiel>\MSCO\MSCO_Loader.txt.
///
public static class Bootstrap
{
static string folder;
- static bool patched, started;
+ /// 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()
@@ -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); }
}
- /// Haken in PlayMakerArrayListProxy.Awake (läuft im Hauptmenü früh auf dem Hauptthread).
- 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.
+ ///
+ 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();
}
+ /// 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)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e4608ef..bb257ba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# 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
**Eigenständig – ohne MSCLoader**
diff --git a/Cheats.cs b/Cheats.cs
index c9633bf..33b330d 100644
--- a/Cheats.cs
+++ b/Cheats.cs
@@ -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)";
}
+ /// Teleport-Ziele im Admin-Menü: Anzeigename und Objekt im Spiel.
+ 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" },
+ };
+
+ ///
+ /// 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.
+ ///
+ 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(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())
+ if (candidate.name == name && candidate.transform.parent == null && candidate.hideFlags == HideFlags.None) return candidate;
+ return null;
+ }
+
+ /// Höchster fester Boden unter einem Punkt (ohne Trigger).
+ 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;
+ }
+
/// Eigenen Spieler an eine Position setzen (etwas darüber, damit er nicht im Boden steckt).
public static bool Teleport(Transform player, Vector3 position)
{
diff --git a/InputLock.cs b/InputLock.cs
index aeed2f6..5e7383c 100644
--- a/InputLock.cs
+++ b/InputLock.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using Harmony;
+using HutongGames.PlayMaker;
using UnityEngine;
namespace MSCO
@@ -20,7 +21,7 @@ namespace MSCO
static readonly List disabled = new List();
static bool locked;
- static bool previousVisible;
+ static bool previousVisible, previousInMenu;
static CursorLockMode previousLock;
static bool patched;
@@ -37,6 +38,8 @@ namespace MSCO
PatchCursorActions();
previousVisible = Cursor.visible;
previousLock = Cursor.lockState;
+ FsmBool inMenu = PlayerInMenu();
+ previousInMenu = inMenu != null && inMenu.Value;
GameObject player = GameObject.Find("PLAYER");
if (player != null)
{
@@ -57,6 +60,8 @@ namespace MSCO
PlayMakerGUI.HideCursor = !previousVisible;
Cursor.visible = previousVisible;
Cursor.lockState = previousLock;
+ FsmBool inMenu = PlayerInMenu();
+ if (inMenu != null) inMenu.Value = previousInMenu;
}
}
@@ -71,6 +76,16 @@ namespace MSCO
PlayMakerGUI.HideCursor = false;
Cursor.visible = true;
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;
+ }
+
+ /// Globale Spielvariable "PlayerInMenu": true = Zeiger frei, Mausblick aus.
+ static FsmBool PlayerInMenu()
+ {
+ try { return FsmVariables.GlobalVariables.FindFsmBool("PlayerInMenu"); }
+ catch { return null; }
}
///
diff --git a/JoinLinks.cs b/JoinLinks.cs
index 613e58a..2144b5e 100644
--- a/JoinLinks.cs
+++ b/JoinLinks.cs
@@ -60,6 +60,29 @@ namespace MSCO
}
}
+ static float nextPoll;
+
+ ///
+ /// JOIN-Link, während das Spiel schon läuft: der MSCO Launcher legt ihn als <Spiel>\MSCO\join.txt ab (Steam
+ /// gibt Links an ein laufendes Spiel nicht weiter). Einmal pro Sekunde nachsehen.
+ ///
+ 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; }
+ }
+
/// Wurde das Spiel über einen JOIN-Link gestartet?
public static bool TryGetFromCommandLine(out string host, out int port)
{
diff --git a/LobbyUI.cs b/LobbyUI.cs
index 478ccf3..a8fd997 100644
--- a/LobbyUI.cs
+++ b/LobbyUI.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using UnityEngine;
namespace MSCO
@@ -33,10 +33,10 @@ namespace MSCO
float ListHeight(float reserved, float min) { return Mathf.Max(min, contentHeight - reserved); }
- static readonly Color Background = Hex(0x17, 0x17, 0x1b);
- static readonly Color Sidebar = Hex(0x0f, 0x0f, 0x12);
- static readonly Color Field = Hex(0x24, 0x24, 0x2a);
- static readonly Color FieldFocus = Hex(0x2e, 0x2e, 0x36);
+ static readonly Color Background = Hex(0x11, 0x11, 0x14);
+ static readonly Color Sidebar = Hex(0x0c, 0x0c, 0x0f);
+ static readonly Color Field = Hex(0x1b, 0x1b, 0x20);
+ static readonly Color FieldFocus = Hex(0x24, 0x24, 0x2a);
static readonly Color Accent = Hex(0xf2, 0x6b, 0x1d);
static readonly Color AccentHover = Hex(0xff, 0x82, 0x3a);
static readonly Color Secondary = Hex(0x2c, 0x2c, 0x33);
@@ -65,7 +65,8 @@ namespace MSCO
bool stylesReady;
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;
List shownAll, shownServers; // Stand des letzten Layout-Durchgangs
string layoutError;
@@ -123,6 +124,8 @@ namespace MSCO
if (mp.IsReconnecting) { DrawPrompt(mp, true); return; }
var window = new Rect(0, 0, vw, vh);
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)));
@@ -131,6 +134,9 @@ namespace MSCO
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));
contentHeight = content.height;
+ GUIStyle oldBar = GUI.skin.verticalScrollbar, oldThumb = GUI.skin.verticalScrollbarThumb;
+ GUI.skin.verticalScrollbar = scrollbarStyle;
+ GUI.skin.verticalScrollbarThumb = scrollThumbStyle;
GUILayout.BeginArea(content);
if (versionWarning) DrawVersionWarning(mp);
else if (pendingJoin) DrawJoinConfirm(mp);
@@ -139,6 +145,8 @@ namespace MSCO
else if (page == Page.Favorites) DrawFavorites(mp);
else DrawServerBrowser(mp);
GUILayout.EndArea();
+ GUI.skin.verticalScrollbar = oldBar;
+ GUI.skin.verticalScrollbarThumb = oldThumb;
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)
{
- 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
- return GUI.Button(r, text, active ? navActiveStyle : navStyle);
+ var item = new Rect(r.x + S(12), r.y, r.width - S(24), r.height);
+ 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)
@@ -291,8 +301,9 @@ namespace MSCO
string shownError = Event.current.type == EventType.Layout ? (layoutError = browser.Error) : layoutError;
// Tabellenkopf
+ GUILayout.Space(S(6));
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 + 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);
@@ -933,8 +944,16 @@ namespace MSCO
// Fenster und Seitenleiste mit feinem Rand (Seitenleiste: ihr Rand trennt sie vom Inhalt)
windowBox = UiTheme.Card(Background, P(10));
sidebarBox = UiTheme.Card(Sidebar, P(10));
- rowBox = UiTheme.Panel9(Field, P(UiTheme.Radius));
- rowHoverBox = UiTheme.Card(FieldFocus, P(UiTheme.Radius));
+ rowBox = UiTheme.Card(Field, P(10));
+ 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));
texGold = Solid(Gold);
texBarBack = Solid(new Color(1f, 1f, 1f, 0.1f));
@@ -955,10 +974,11 @@ namespace MSCO
pillFeaturedStyle = new GUIStyle(pillStyle);
pillFeaturedStyle.normal.background = UiTheme.Rounded(Gold, P(4));
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);
- chipOffStyle = Button(Field, FieldFocus, Muted, 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, P(11), UiTheme.Border);
+ chipOffStyle.hover.textColor = Color.white;
chipOnStyle.padding = chipOffStyle.padding = new RectOffset(P(12), P(12), P(0), P(0));
logoStyle = Label(P(28), Color.white, FontStyle.Bold);
@@ -992,22 +1012,28 @@ namespace MSCO
toggleStyle.fontSize = P(11);
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.alignment = TextAnchor.MiddleLeft;
- navStyle.padding = new RectOffset(P(26), P(10), P(0), P(0));
- navActiveStyle = Button(Secondary, Secondary, Color.white, P(14), FontStyle.Bold);
+ navStyle.padding = new RectOffset(P(22), P(10), P(0), P(0));
+ 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.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);
- UiTheme.ApplyPrimaryGradient(primaryStyle);
- dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, P(13), FontStyle.Bold);
- secondaryStyle = Button(Secondary, SecondaryHover, Color.white, P(14), FontStyle.Bold);
- tabStyle = Button(Sidebar, Secondary, Muted, P(11), FontStyle.Bold);
+ primaryStyle = Button(Accent, AccentHover, Color.white, P(14), FontStyle.Bold, P(10));
+ primaryStyle.normal.background = primaryStyle.focused.background = UiTheme.RoundedEx(AccentHover, Accent, Color.clear, false, P(10));
+ primaryStyle.hover.background = primaryStyle.active.background = primaryStyle.onNormal.background =
+ UiTheme.RoundedEx(Color.Lerp(AccentHover, Color.white, 0.12f), AccentHover, Color.clear, false, P(10));
+ 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;
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;
}
@@ -1021,21 +1047,46 @@ namespace MSCO
return s;
}
- static GUIStyle Button(Color normal, Color hover, Color text, int size, FontStyle font)
+ /// Knopf mit runden Ecken (9-Slice, siehe UiTheme), optional mit feinem Rand wie .btn auf der Website.
+ 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);
- // abgerundete Ecken (9-Slice), siehe UiTheme
- s.normal.background = UiTheme.Rounded(normal);
- s.hover.background = UiTheme.Rounded(hover);
- s.active.background = UiTheme.Rounded(hover);
- s.focused.background = UiTheme.Rounded(normal);
- s.normal.textColor = s.hover.textColor = s.active.textColor = s.focused.textColor = text;
- s.border = new RectOffset(UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2, UiTheme.Radius + 2);
+ bool withBorder = border.a > 0f;
+ int h = radius * 2 + 4;
+ Texture2D n = UiTheme.RoundedEx(normal, normal, border, withBorder, radius, h);
+ 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.normal.background = n;
+ s.focused.background = n;
+ 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.fontStyle = font;
return s;
}
+ /// Weicher runder Schein (Mitte deckend, nach außen durchsichtig) - für den orangen Schimmer oben.
+ 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)
{
var tex = new Texture2D(1, 1);
diff --git a/MSCO.cs b/MSCO.cs
index b7af382..0a853d9 100644
--- a/MSCO.cs
+++ b/MSCO.cs
@@ -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
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
///
- public const string ModVersion = "2.9.0";
+ public const string ModVersion = "2.9.1";
///
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server.
diff --git a/MSCO.csproj b/MSCO.csproj
index 70d701a..a517911 100644
--- a/MSCO.csproj
+++ b/MSCO.csproj
@@ -42,10 +42,6 @@
F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\UnityEngine.dll
False
-
- lib\Newtonsoft.Json.dll
- False
-
F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Assembly-CSharp-firstpass.dll
False
@@ -97,6 +93,8 @@
+
+
@@ -109,6 +107,9 @@
+
+ MSCO.menu-logo.png
+
MSCO.logo.png
diff --git a/MenuBranding.cs b/MenuBranding.cs
new file mode 100644
index 0000000..29abbb2
--- /dev/null
+++ b/MenuBranding.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ 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;
+
+ /// Titel und Symbol des Spielfensters auf MSCO setzen (beim Start und nach jedem Szenenwechsel).
+ 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 fitted = new Dictionary();
+ 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;
+ }
+ }
+
+ /// Ist das eine Textur des Spiel-Logos? (Name enthält "logo", aber nicht vom Auto/Laden ...)
+ 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");
+ }
+
+ /// Im Hauptmenü das Spiel-Logo ersetzen. Gibt die Anzahl ersetzter Texturen zurück.
+ public static int ReplaceMenuLogo()
+ {
+ if (Source == null) return 0;
+ int replaced = 0;
+ var seen = new List();
+ // Auch ausgeblendete Objekte (das Menü wechselt zwischen mehreren Logo-Objekten)
+ foreach (Renderer r in Resources.FindObjectsOfTypeAll())
+ {
+ 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())
+ {
+ 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())
+ {
+ 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;
+ }
+
+ /// MSCO-Logo mittig in eine Textur der Größe des Originals (gleiches Seitenverhältnis wie dessen Fläche).
+ 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;
+ }
+ }
+}
diff --git a/MiniJson.cs b/MiniJson.cs
new file mode 100644
index 0000000..41ad5bb
--- /dev/null
+++ b/MiniJson.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace MSCO
+{
+ ///
+ /// Kleiner eigener JSON-Leser (ohne Newtonsoft.Json - das braucht System.Runtime.Serialization, das ohne MSCLoader im
+ /// Spiel fehlt). Ergebnis: Dictionary<string, object> (Objekt, Schlüssel ohne Groß-/Kleinschreibung),
+ /// List<object>, string, double, bool oder null.
+ ///
+ 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 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 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 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 Strings(Dictionary o, string key)
+ {
+ var result = new List();
+ object v;
+ if (o == null || !o.TryGetValue(key, out v)) return result;
+ var list = v as List