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)
{