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