From bba20cf9914098d8ea8829382fa9cac0001829fd Mon Sep 17 00:00:00 2001 From: MrMaho Date: Sun, 27 Sep 2026 10:41:54 +0200 Subject: [PATCH] Own main menu (game's Continue/New Game/Credits/Changelog/Quit hidden, QUIT in the launcher); new server profile layout; admin items: ratchet set, battery, shop items; session shows server player limit Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++ ItemSpawner.cs | 50 +++++++++++++++--- LobbyUI.cs | 116 ++++++++++++++++++++++++++++-------------- MenuHelper.cs | 21 ++++++++ MultiplayerManager.cs | 19 ++++++- 5 files changed, 166 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b89a37a..c693ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ Server und Mod müssen beide 2.7 sein (Server: `git pull && docker compose up -d - Letzte Position wird zuverlässig gespeichert (nicht mehr immer an der Toilette spawnen) - Server-Browser: Klick auf den Server öffnet das Profil (JOIN-Knopf in der Liste entfernt) - Alle Mod-Meldungen nur noch in der MSCO-Konsole (F7) +- Eigenes Hauptmenü: Continue, New Game, Credits, Changelog und Quit des Spiels sind ausgeblendet, der MSCO-Launcher + ist das Hauptmenü (mit eigenem QUIT) +- Server-Profil neu: JOIN oben, Spielerliste rechts, Namensfeld ohne Steam in eigenem Kasten +- Admin-ITEMS: Werkzeuge und Laden-Artikel (Ratschenkasten, Batterie, Motoröl, Kühlmittel, Bremsflüssigkeit ...) **Behoben** - Auto fiel beim Fahren eines anderen Spielers auseinander (angebaute Teile rissen an ihren Gelenken ab) diff --git a/ItemSpawner.cs b/ItemSpawner.cs index 6ee291c..dc1b8a3 100644 --- a/ItemSpawner.cs +++ b/ItemSpawner.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using HutongGames.PlayMaker; using UnityEngine; namespace MSCO @@ -7,31 +8,59 @@ namespace MSCO /// Admin: Items spawnen. Kopiert ein vorhandenes Item des Spiels (z.B. "beer case(itemx)" aus dem Laden) und /// setzt die Kopie an die gewünschte Stelle. Die Kopie behält den Namen des Originals, damit sich die Spiel-Logik /// (Trinken, Essen, Tanken ...) wie beim echten Item verhält und der Objekt-Sync sie bei allen gleich benennt. + /// Werkzeuge wie der Ratschenkasten sind im Spiel keine Gegenstände, sondern ein Schalter ("hat der Spieler") - + /// die werden dem Admin direkt gegeben. /// public static class ItemSpawner { - /// Auswahl im Admin-Menü (Name ohne "(itemx)"). + /// Werkzeug statt Gegenstand: globale Spielvariable, die "gekauft" bedeutet. + public const string RatchetSet = "ratchet set"; + + /// Auswahl im Admin-Menü (Anzeigename). public static readonly string[] Items = { - "beer case", "sausages", "pizza", "macaron box", "milk", "juice", "ground coffee", "sugar", "yeast", "booze", - "gasoline", "diesel", "spray can", "fire extinguisher", "flashlight", "lantern", "car jack", "floor jack", - "spanner set", "sledgehammer", "radio", "camera", "grill charcoal", "fireworks bag", "water bucket" + // Werkzeug / Auto + RatchetSet, "spanner set", "battery", "car jack", "floor jack", "motor hoist", "flashlight", "lantern", + "fire extinguisher", "spray can", "sledgehammer", "ax", "digging bar", + // Flüssigkeiten / Kraftstoff + "motor oil", "coolant", "brake fluid", "two stroke fuel", "gasoline", "diesel", "n2o bottle", + // Essen / Trinken / Laden + "beer case", "beer", "booze", "vodka shot", "milk", "juice", "coffee", "ground coffee", "sugar", "yeast", + "sausages", "pizza", "macaron box", "potato chips", "cigarettes", "mosquito spray", "grill charcoal", + // Sonstiges + "grill", "radio", "camera", "helmet", "bucket", "water bucket", "fireworks bag", "parts magazine" + }; + + // Items, die im Spiel nicht "(itemx)" heißen + static readonly Dictionary objectNames = new Dictionary + { + { "battery", "battery(Clone)" } }; static readonly Dictionary templates = new Dictionary(); + public static bool IsKnown(string item) { return System.Array.IndexOf(Items, item) >= 0; } + + static string ObjectName(string item) + { + string name; + return objectNames.TryGetValue(item, out name) ? name : item + "(itemx)"; + } + static GameObject FindTemplate(string item) { GameObject cached; if (templates.TryGetValue(item, out cached) && cached != null) return cached; - string name = item + "(itemx)"; + string name = ObjectName(item); GameObject best = null; foreach (Object o in Resources.FindObjectsOfTypeAll(typeof(GameObject))) { var go = (GameObject)o; if (go.name != name || go.GetComponent() == null) continue; - if (best == null || (go.activeInHierarchy && !best.activeInHierarchy)) best = go; // am liebsten ein echtes in der Welt + // Keine eingebauten Teile kopieren (z.B. die Batterie im Auto): nicht unter einem anderen Rigidbody + if (go.transform.parent != null && go.transform.parent.GetComponentInParent() != null) continue; + if (best == null || (go.activeInHierarchy && !best.activeInHierarchy)) best = go; // am liebsten eins aus der Welt } templates[item] = best; return best; @@ -60,6 +89,15 @@ namespace MSCO return true; } + /// Ratschenkasten "kaufen": der Spieler hat ihn dann wie nach dem Kauf im Laden. + public static bool GiveRatchetSet() + { + FsmBool has = FsmVariables.GlobalVariables.FindFsmBool("PlayerHasRatchet"); + if (has == null) return false; + has.Value = true; + return true; + } + public static void ClearCache() { templates.Clear(); } } } diff --git a/LobbyUI.cs b/LobbyUI.cs index 7ddee05..d484a02 100644 --- a/LobbyUI.cs +++ b/LobbyUI.cs @@ -31,6 +31,8 @@ namespace MSCO static readonly Color Pending = Hex(0xf2, 0xc2, 0x1d); Page page = Page.P2P; + Vector2 profileScroll; + bool quitConfirm; bool minimized; Vector2 playerScroll; Vector2 serverScroll; @@ -46,6 +48,7 @@ namespace MSCO Texture2D texBackground, texSidebar, texAccent, texOnline, texPending, texMuted, texRow; List shownAll, shownServers; // Stand des letzten Layout-Durchgangs string layoutError; + GUIStyle dangerStyle; GUIStyle windowBox, sidebarBox, rowBox, accentBox, pillStyle, pillMutedStyle, chipOnStyle, chipOffStyle; GUIStyle logoStyle, taglineStyle, navStyle, navActiveStyle, titleStyle, textStyle, captionStyle, fieldStyle, primaryStyle, secondaryStyle, statusStyle, tabStyle, headerCellStyle, @@ -112,6 +115,14 @@ namespace MSCO y += 44; if (NavButton(new Rect(r.x, y, r.width, 42), "Server Browser", page == Page.ServerBrowser)) page = Page.ServerBrowser; + // Eigenes Hauptmenü: Spiel beenden (die Knöpfe des Spiel-Menüs sind ausgeblendet) + if (quitConfirm) + { + if (GUI.Button(new Rect(r.x + 16, r.yMax - 118, r.width - 32, 36), "QUIT GAME?", dangerStyle)) Application.Quit(); + if (GUI.Button(new Rect(r.x + 16, r.yMax - 76, r.width - 32, 30), "CANCEL", tabStyle)) quitConfirm = false; + } + else if (GUI.Button(new Rect(r.x + 16, r.yMax - 76, r.width - 32, 36), "QUIT", tabStyle)) quitConfirm = true; + GUI.Label(new Rect(r.x + 24, r.yMax - 30, r.width - 48, 20), "v" + MSCO.ModVersion, taglineStyle); } @@ -295,7 +306,7 @@ namespace MSCO selectedServer = null; return; } - GUILayout.Space(6); + GUILayout.Space(8); if (s == null) { @@ -304,64 +315,94 @@ namespace MSCO return; } - // Kopf: Name + Abzeichen links, JOIN (und ohne Steam das Namensfeld) rechts - immer sichtbar, - // egal wie lang Beschreibung und Spielerliste darunter sind + // Kopf: Name + Abzeichen links, JOIN rechts - immer sichtbar GUILayout.BeginHorizontal(); GUILayout.BeginVertical(); GUILayout.Label(s.Name, titleStyle); GUILayout.BeginHorizontal(); - GUILayout.Label(s.RequireSteam ? "STEAM" : "NON-STEAM", badgeStyle, GUILayout.ExpandWidth(false)); - if (s.IsLocal) { GUILayout.Space(10); GUILayout.Label("LOCAL", badgeStyle, GUILayout.ExpandWidth(false)); } - GUILayout.FlexibleSpace(); + Rect badgeRow = GUILayoutUtility.GetRect(10, 20, GUILayout.ExpandWidth(true)); + float bx = badgeRow.x; + bx += Pill(bx, badgeRow, s.RequireSteam ? "STEAM" : "NON-STEAM", s.RequireSteam ? pillStyle : pillMutedStyle); + if (s.IsLocal) bx += Pill(bx, badgeRow, "LOCAL", pillStyle); + if (s.Version != MSCO.ModVersion) Pill(bx, badgeRow, "v" + s.Version, pillStyle); GUILayout.EndHorizontal(); GUILayout.EndVertical(); - GUILayout.FlexibleSpace(); - GUILayout.BeginVertical(GUILayout.Width(190)); + GUILayout.Space(12); GUI.enabled = CanJoin(mp, s); - if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(42), GUILayout.Width(190))) + if (GUILayout.Button(s.Players >= s.MaxPlayers ? "SERVER FULL" : "JOIN SERVER", primaryStyle, GUILayout.Height(44), GUILayout.Width(180))) mp.JoinServer(s.Address, s.Port, s.Name, s.MaxPlayers); GUI.enabled = true; - if (s.RequireSteam && !mp.SteamAvailable) - GUILayout.Label("This server requires Steam.", textStyle); - else if (!mp.SteamAvailable) - { - GUILayout.Space(4); - GUILayout.BeginHorizontal(); - GUILayout.Label("NAME", captionStyle, GUILayout.Width(42)); - DrawNameField(mp, 140); - GUILayout.EndHorizontal(); - } - GUILayout.EndVertical(); GUILayout.EndHorizontal(); if (!string.IsNullOrEmpty(s.Description)) GUILayout.Label(s.Description, textStyle); GUILayout.Space(10); + // Ohne Steam: Name wählen (sauber in einem eigenen Kasten) + if (s.RequireSteam && !mp.SteamAvailable) + { + GUILayout.BeginHorizontal(rowBox, GUILayout.Height(40)); + GUILayout.Label("This server requires Steam - start the game through Steam to join.", textStyle); + GUILayout.EndHorizontal(); + GUILayout.Space(10); + } + else if (!mp.SteamAvailable) + { + GUILayout.BeginHorizontal(rowBox, GUILayout.Height(44)); + GUILayout.Space(6); + GUILayout.BeginVertical(GUILayout.Width(110)); + GUILayout.Space(4); + GUILayout.Label("PLAYING AS", captionStyle); + GUILayout.Label("no Steam", textStyle); + GUILayout.EndVertical(); + GUILayout.BeginVertical(); + GUILayout.Space(5); + DrawNameField(mp, 220); + GUILayout.EndVertical(); + GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); + GUILayout.Space(10); + } + + // Infos links, Spielerliste rechts bool versionOk = s.Version == MSCO.ModVersion; GUILayout.BeginHorizontal(); GUILayout.BeginVertical(GUILayout.Width(250)); - InfoRow("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle); - InfoRow("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle); - InfoRow("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle); - InfoRow("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle); + InfoLine("HOSTED BY", string.IsNullOrEmpty(s.Owner) ? "unknown" : s.Owner, rowNameStyle); + InfoLine("VERSION", "v" + s.Version + (versionOk ? "" : " (you: v" + MSCO.ModVersion + ")"), versionOk ? rowNameStyle : badgeStyle); + InfoLine("STEAM", s.RequireSteam ? "Required" : "Not required", rowNameStyle); + InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle); + InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", rowNameStyle); + InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle); GUILayout.EndVertical(); - GUILayout.BeginVertical(); - InfoRow("PLAYERS", s.Players + " / " + s.MaxPlayers, rowNameStyle); - InfoRow("PING", s.Ping < 0 ? "-" : s.Ping + " ms", rowNameStyle); - InfoRow("ADDRESS", s.Address + ":" + s.Port, rowNameStyle); + GUILayout.Space(16); + + GUILayout.BeginVertical(rowBox, GUILayout.ExpandWidth(true), GUILayout.MinHeight(150)); + GUILayout.Space(6); + GUILayout.Label("PLAYERS " + s.Players + " / " + s.MaxPlayers, captionStyle); + profileScroll = GUILayout.BeginScrollView(profileScroll, GUILayout.Height(130)); + if (s.PlayerNames == null || s.PlayerNames.Count == 0) + GUILayout.Label("Nobody online right now.", textStyle); + else + foreach (string name in s.PlayerNames) + { + Rect line = GUILayoutUtility.GetRect(10, 24, GUILayout.ExpandWidth(true)); + GUI.DrawTexture(new Rect(line.x + 2, line.y + 8, 8, 8), texOnline); + GUI.Label(new Rect(line.x + 18, line.y, line.width - 18, line.height), name, rowNameStyle); + } + GUILayout.EndScrollView(); GUILayout.EndVertical(); GUILayout.EndHorizontal(); - - // Spielerliste - if (s.PlayerNames != null && s.PlayerNames.Count > 0) - { - GUILayout.Space(4); - GUILayout.Label("ONLINE", captionStyle); - GUILayout.Label(string.Join(", ", s.PlayerNames.ToArray()), textStyle); - } - GUILayout.FlexibleSpace(); } + /// Eine Info-Zeile: Bezeichnung links (grau), Wert rechts. + void InfoLine(string caption, string value, GUIStyle valueStyle) + { + GUILayout.BeginHorizontal(GUILayout.Height(26)); + GUILayout.Label(caption, captionStyle, GUILayout.Width(100)); + GUILayout.Label(value, valueStyle); + GUILayout.EndHorizontal(); + } + /// Kleines abgerundetes Abzeichen, so breit wie sein Text. Gibt die belegte Breite zurück. static float Pill(float x, Rect row, string text, GUIStyle style) { @@ -576,6 +617,7 @@ namespace MSCO navActiveStyle.padding = new RectOffset(26, 10, 0, 0); primaryStyle = Button(Accent, AccentHover, Color.white, 14, FontStyle.Bold); + dangerStyle = Button(UiTheme.Danger, UiTheme.DangerHover, Color.white, 13, FontStyle.Bold); secondaryStyle = Button(Secondary, SecondaryHover, Color.white, 14, FontStyle.Bold); tabStyle = Button(Sidebar, Secondary, Muted, 11, FontStyle.Bold); diff --git a/MenuHelper.cs b/MenuHelper.cs index c57bf1e..7e7fe86 100644 --- a/MenuHelper.cs +++ b/MenuHelper.cs @@ -51,6 +51,27 @@ namespace MSCO } /// Startet das Spiel wie ein Klick auf CONTINUE. + // Knöpfe des Spiel-Hauptmenüs, die MSCO ersetzt (der MSCO-Launcher ist das Hauptmenü) + static readonly string[] HiddenMenuButtons = { "ButtonContinue", "ButtonNewgame", "ButtonCredits", "ButtonQuit", "ButtonChangelog" }; + + /// + /// Continue, New Game, Credits, Changelog und Quit des Spiels unsichtbar und unklickbar machen. Nicht abschalten: + /// die Logik hinter CONTINUE lädt beim Beitreten den übertragenen Spielstand (StartGame). Gibt die Anzahl zurück. + /// + public static int HideGameMenuButtons() + { + int hidden = 0; + foreach (string name in HiddenMenuButtons) + { + GameObject button = GameObject.Find(name); + if (button == null) continue; + foreach (Renderer r in button.GetComponentsInChildren(true)) r.enabled = false; + foreach (Collider c in button.GetComponentsInChildren(true)) c.enabled = false; + hidden++; + } + return hidden; + } + public static void StartGame() { PlayMakerFSM fsm; diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index 6c521f9..9f659dc 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -344,6 +344,13 @@ namespace MSCO public void SpawnItem(string item) { if (!CanCheat || localPlayer == null) return; + if (item == ItemSpawner.RatchetSet) + { + // Werkzeug, kein Gegenstand: nur für einen selbst freischalten + overlays.Log(ItemSpawner.GiveRatchetSet() ? "You now have the ratchet set" : "Ratchet set not found (only works in the game)"); + AnnounceCheat("gave themselves the ratchet set"); + return; + } Transform head = playerCamera != null ? playerCamera.transform : localPlayer; Vector3 forward = new Vector3(head.forward.x, 0f, head.forward.z).normalized; if (forward.sqrMagnitude < 0.01f) forward = localPlayer.forward; @@ -366,7 +373,7 @@ namespace MSCO string item = r.ReadString(); var pos = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); var rot = new Quaternion(r.ReadSingle(), r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); - if (System.Array.IndexOf(ItemSpawner.Items, item) < 0) return false; // nur bekannte Items + if (!ItemSpawner.IsKnown(item) || item == ItemSpawner.RatchetSet) return false; // nur bekannte Gegenstände return ItemSpawner.Spawn(item, pos, rot); } @@ -740,6 +747,12 @@ namespace MSCO // Solange das Multiplayer-Fenster offen ist, reagiert das Spielmenü (CONTINUE, QUIT ...) gar nicht; // erst nach MINIMIZE wieder. Über der eingeklappten Leiste ebenfalls nicht (sonst öffnet sich z.B. der Changelog). MenuInputBlocker.SetBlocked(MenuHelper.InMenu && (lobby.IsOpen || lobby.ContainsMouse() || overlays.AnyOpen)); + // Eigenes Hauptmenü: Singleplayer-Knöpfe des Spiels ausblenden (einmal pro Menü-Besuch, sobald sie da sind) + if (MenuHelper.InMenu && !menuButtonsHidden && Time.realtimeSinceStartup >= nextMenuButtonCheck) + { + nextMenuButtonCheck = Time.realtimeSinceStartup + 0.5f; + menuButtonsHidden = MenuHelper.HideGameMenuButtons() > 0; + } UpdateOverlayKeys(); MscoLog.Drain(overlays.Log); // alle Mod-Meldungen in die MSCO-Konsole (F7) UpdatePresence(); @@ -1232,8 +1245,12 @@ namespace MSCO MscoLog.Info("[MSCO] Sent save (" + files.Count + " files) to " + targets.Count + " player(s)"); } + bool menuButtonsHidden; + float nextMenuButtonCheck; + void OnLevelWasLoaded(int level) { + menuButtonsHidden = false; // neues Hauptmenü: Knöpfe wieder ausblenden nextPresenceUpdate = 0f; // Discord gleich nach dem Szenenwechsel aktualisieren if (MenuHelper.InGame) {