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; + 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 ReadObject(string s, ref int i) + { + var o = new Dictionary(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 ReadArray(string s, ref int i) + { + var list = new List(); + 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++; + } + } +} diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 080f17a..babdc3a 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -455,6 +455,20 @@ namespace MSCO } /// Admin: Satsuma neben sich holen (z.B. wenn er verschwunden oder im Boden ist). + /// Admin: sich selbst zu einem Ort teleportieren (Haus, Laden ... siehe Cheats.Places). + 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() { if (!CanCheat || localPlayer == null) return; @@ -611,6 +625,18 @@ namespace MSCO if (host != null) JoinServer(host, port); } + /// JOIN-Link aus dem Browser, während das Spiel schon läuft (über den Launcher, siehe JoinLinks). + 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() { PendingJoinHost = null; @@ -974,6 +1000,7 @@ namespace MSCO void Update() { CheckHitch(); + CheckJoinRequest(); MeasureFps(); // 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; diff --git a/Overlays.cs b/Overlays.cs index 077c1c3..b087193 100644 --- a/Overlays.cs +++ b/Overlays.cs @@ -293,22 +293,56 @@ namespace MSCO // Reiter 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++) { - 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.EndHorizontal(); GUILayout.Space(10); - if (adminTab == 1) DrawAdminItems(mp); - else if (adminTab == 2) DrawAdminBans(mp); - else if (adminTab == 3) DrawAdminWorld(mp); + if (adminTab == 1) DrawAdminTeleport(mp); + else if (adminTab == 2) DrawAdminItems(mp); + else if (adminTab == 3) DrawAdminBans(mp); + else if (adminTab == 4) DrawAdminWorld(mp); else DrawAdminPlayers(mp); 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) { // Cheats für den eigenen Spieler (nur im Spiel) diff --git a/README.md b/README.md index 54959fb..e3d96f0 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,12 @@ einem dedizierten Server aus dem Server-Browser. MSCO läuft **eigenständig, oh - **T** – Chat - **F7** – MSCO-Konsole mit allen Befehlen - **/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 - **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz - Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`) - 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 @@ -62,7 +63,7 @@ Was der Launcher im Spielordner einrichtet: | 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) | | `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. **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 `\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 @@ -121,7 +122,8 @@ In der F7-Konsole: - 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\` (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 `\MSCO` kopiert (Ziel `CopyToGame` in der csproj). - Einstieg: `Bootstrap.cs` (`Doorstop.Entrypoint.Start` → Harmony-Haken in `PlayMakerArrayListProxy.Awake` → `MSCO_Host` im Hauptmenü, überspringt den Nachrichten-Bildschirm). @@ -146,7 +148,8 @@ msbuild MSCO.csproj -p:Configuration=Release | 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 | | `NetSession.cs` | Netzwerk (TCP und Steam P2P), Protokoll | | `LobbyUI.cs`, `Overlays.cs`, `UiTheme.cs` | Launcher, Konsole/Chat/Admin-Menü, gemeinsamer Stil | diff --git a/ServerBrowser.cs b/ServerBrowser.cs index 537179b..d6fbccd 100644 --- a/ServerBrowser.cs +++ b/ServerBrowser.cs @@ -6,7 +6,6 @@ using System.IO; using System.Net.Sockets; using System.Threading; // (Unitys WWW wird nicht mehr benutzt - siehe WinHttp) -using Newtonsoft.Json; using UnityEngine; namespace MSCO @@ -26,11 +25,11 @@ namespace MSCO public bool Featured; // vom Keymaster-Admin hervorgehoben (steht in der Liste oben) public List Tags = new List(); // Admin-Tags (z.B. "official") und eigene Tags des Servers (Web-Panel) - [JsonIgnore] public int UptimeSeconds = -1; - [JsonIgnore] public List PlayerNames = new List(); - [JsonIgnore] public int Ping = -1; // -1 = noch nicht abgefragt - [JsonIgnore] public bool Offline; - [JsonIgnore] public bool IsLocal; // Server auf diesem PC (localhost) + public int UptimeSeconds = -1; + public List PlayerNames = new List(); + public int Ping = -1; // -1 = noch nicht abgefragt + public bool Offline; + public bool IsLocal; // Server auf diesem PC (localhost) } /// @@ -104,7 +103,7 @@ namespace MSCO } List list; - try { list = JsonConvert.DeserializeObject>(text) ?? new List(); } + try { list = ParseList(text); } catch (Exception e) { Error = "Invalid server list: " + e.Message; @@ -115,6 +114,34 @@ namespace MSCO foreach (ServerListEntry entry in list) QueryAsync(entry); } + /// Antwort von /api/servers lesen (eigener JSON-Leser, siehe MiniJson). + static List ParseList(string text) + { + var list = new List(); + var items = MiniJson.Parse(text) as List; + if (items == null) throw new FormatException("Expected a list"); + foreach (object item in items) + { + var o = item as Dictionary; + 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; + } + /// Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt. void ProbeLocalServers() { diff --git a/lib/Newtonsoft.Json.dll b/lib/Newtonsoft.Json.dll deleted file mode 100644 index 3bc80ac..0000000 Binary files a/lib/Newtonsoft.Json.dll and /dev/null differ diff --git a/lib/THIRD-PARTY.txt b/lib/THIRD-PARTY.txt index 1d97e21..080c66f 100644 --- a/lib/THIRD-PARTY.txt +++ b/lib/THIRD-PARTY.txt @@ -5,10 +5,6 @@ Third-party libraries shipped with MSCO https://github.com/pardeike/Harmony - MIT License 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 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, diff --git a/menu-logo.png b/menu-logo.png new file mode 100644 index 0000000..d7d8c95 Binary files /dev/null and b/menu-logo.png differ