MSCO 2.10.0: mod support, ESC no longer quits from the main menu

- MscoMod API (ModApi.cs) and ModLoader: own mods from MSCO\Mods, hooks for menu/game/update/GUI,
  session and player events, Send/OnMessage between players (MsgType.ModMessage)
- Server mods: ModList before Welcome, consent prompt (INSTALL & JOIN), download with SHA-256 check
  into MSCO\Mods\Servers, active only on that server; game start waits for the download
- Mods page in the MSCO menu, console command "mods"
- Server browser: MODS pill, VANILLA/MODDED filters, mod list in the server profile
- PlayMaker ApplicationQuit is ignored in the main menu (ESC used to close the game)
- MODDING.md and examples/HelloMod

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 01:00:16 +02:00
co-authored by Claude Opus 5.5
parent 027296d27c
commit 4c1366b5d9
16 changed files with 815 additions and 15 deletions
+25 -1
View File
@@ -64,6 +64,7 @@ namespace MSCO
try
{
string name = new AssemblyName(args.Name).Name;
if (name == typeof(Bootstrap).Assembly.GetName().Name) return typeof(Bootstrap).Assembly; // Mods verweisen auf MSCO
string file = Path.Combine(folder, name + ".dll");
if (File.Exists(file)) return Assembly.LoadFrom(file);
}
@@ -141,6 +142,23 @@ namespace MSCO
return;
}
MultiplayerManager.Ensure();
SceneChanged(scene);
}
/// <summary>Mods: beim ersten Hauptmenü laden, dann Menü/Spiel melden.</summary>
static void SceneChanged(string scene)
{
if (scene == "MainMenu")
{
ModLoader.LoadLocal();
ModLoader.MenuLoad();
}
else if (scene == "GAME") ModLoader.GameLoad();
}
void FixedUpdate()
{
ModLoader.FixedUpdate();
}
bool skipping, loadingScreen;
@@ -150,6 +168,7 @@ namespace MSCO
void Update()
{
ModLoader.Update();
if (!skipping || Time.frameCount < skipFrame + 2) return;
skipping = false;
loadingScreen = true;
@@ -158,7 +177,11 @@ namespace MSCO
void OnGUI()
{
if (!skipping && !loadingScreen) return;
if (!skipping && !loadingScreen)
{
ModLoader.OnGUI();
return;
}
GUI.depth = -10000;
if (black == null)
{
@@ -183,6 +206,7 @@ namespace MSCO
MenuBranding.ApplyWindow();
if (Application.loadedLevelName == "MainMenu") StartCoroutine(BrandMenu());
MultiplayerManager.Ensure();
SceneChanged(Application.loadedLevelName);
}
/// <summary>Logo im Hauptmenü ersetzen - ein paar Mal, falls das Menü es erst nach dem Laden zeigt.</summary>
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## 2.10.0
**Mod-Support**
- Eigene MSCO-Mods: DLLs in `<Spielordner>\MSCO\Mods` mit einer Klasse, die von `MscoMod` erbt (OnLoad, OnMenuLoad,
OnGameLoad, Update, OnGUI, Spieler kommen/gehen, Sitzung, eigene Netzwerk-Nachrichten zwischen den Spielern).
Anleitung und Beispiel: `MODDING.md`, `examples/HelloMod`
- **Server-Mods**: Server mit Mods schicken beim Beitreten die Liste; fehlende Mods lädt das Spiel nach Zustimmung
(„INSTALL & JOIN“) direkt vom Server (SHA-256 geprüft). Sie sind nur auf diesem Server aktiv
- Neue Seite **Mods** im MSCO-Menü, Konsole `mods`
- Server-Browser: Pille **MODS n**, Filter **VANILLA** / **MODDED**, Mod-Liste im Server-Profil
- **ESC im Hauptmenü** beendet das Spiel nicht mehr (zum Beenden: QUIT)
- Netzwerk-Version bleibt 2.8 – Server und Mitspieler mit 2.8.x/2.9.x funktionieren weiter
## 2.9.1
**Fehler ohne MSCLoader behoben**
+11
View File
@@ -19,6 +19,7 @@ namespace MSCO
"host [port] / join <ip> [port] - direct LAN game (no Steam)\n" +
"save - save the game (host / singleplayer)\n" +
"bind [action key] - show / change keys (passenger, console, chat)\n" +
"mods - loaded MSCO mods (folder MSCO\\Mods)\n" +
"ping [on|off] - show your ping to the server (toggle)\n" +
"fps [on|off] - show the frames per second (toggle)\n" +
"/admin - admin menu (kick / ban / teleport / cheats)\n" +
@@ -117,6 +118,16 @@ namespace MSCO
else mp.ShowPing = !mp.ShowPing;
print("Ping: " + mp.PingText + " (display " + (mp.ShowPing ? "on" : "off") + ")");
break;
case "mods":
if (ModLoader.All.Count == 0) { print("No mods. Put MSCO mods (.dll) into " + ModLoader.Folder + " and restart the game."); break; }
foreach (ModLoader.LoadedMod m in ModLoader.All)
{
string name = "?", version = "";
try { name = m.Mod.Name; version = m.Mod.Version; } catch { }
print(" " + name + " " + version + (m.FromServer ? " [server]" : "") +
(m.Error != null ? " turned off: " + m.Error : m.Active ? " active" : " inactive"));
}
break;
case "bind":
{
// Tasten ändern: "bind" zeigt alle, "bind passenger O" setzt eine (früher MSCLoader-Einstellungen)
+94 -2
View File
@@ -10,7 +10,7 @@ namespace MSCO
/// </summary>
public class LobbyUI
{
enum Page { P2P, ServerBrowser, Favorites }
enum Page { P2P, ServerBrowser, Favorites, Mods }
// Vollbild, automatisch skaliert: gebaut für RefHeight x RefWidth, vergrößert/verkleinert passend zur Auflösung
const float RefHeight = 760f;
@@ -54,7 +54,7 @@ namespace MSCO
bool browserLoaded;
string selectedServer; // "adresse:port" des geöffneten Server-Profils
string searchInput = "";
bool showSteam = true, showNonSteam = true, hideFull;
bool showSteam = true, showNonSteam = true, hideFull, showVanilla = true, showModded = true;
readonly Dictionary<string, string> nicknameEdits = new Dictionary<string, string>(); // Favoriten-Tab: Adresse -> bearbeiteter Kurzname
string favoriteAddInput = "";
Vector2 favoritesScroll;
@@ -106,6 +106,7 @@ namespace MSCO
if (Minimized && !mp.IsInSession && (mp.DisconnectNotice != null || mp.PendingJoinHost != null || mp.PendingVersionWarning != null))
Minimized = false;
if (Minimized && mp.IsReconnecting) Minimized = false;
if (Minimized && mp.PendingMods != null) Minimized = false;
if (Minimized) { DrawMinimized(mp); return; }
ScreenRect = new Rect(0, 0, Screen.width, Screen.height);
DrawScaled(mp);
@@ -122,6 +123,7 @@ namespace MSCO
// Getrennt / Neu-Verbinden: eigenes Fenster in der Bildmitte, alles andere gesperrt
if (disconnected) { DrawPrompt(mp, false); return; }
if (mp.IsReconnecting) { DrawPrompt(mp, true); return; }
if (mp.PendingMods != null && mp.IsInSession) { DrawModsPrompt(mp); return; }
var window = new Rect(0, 0, vw, vh);
GUI.Box(window, GUIContent.none, windowBox);
float glowWidth = Mathf.Min(vw * 0.9f, S(1300));
@@ -143,6 +145,7 @@ namespace MSCO
else if (mp.IsInSession) DrawSession(mp);
else if (page == Page.P2P) DrawP2P(mp);
else if (page == Page.Favorites) DrawFavorites(mp);
else if (page == Page.Mods) DrawMods();
else DrawServerBrowser(mp);
GUILayout.EndArea();
GUI.skin.verticalScrollbar = oldBar;
@@ -200,6 +203,9 @@ namespace MSCO
y += S(44);
int favCount = Favorites.All.Count;
if (NavButton(new Rect(r.x, y, r.width, S(42)), "Favorites" + (favCount > 0 ? " (" + favCount + ")" : ""), page == Page.Favorites)) page = Page.Favorites;
y += S(44);
int modCount = ModLoader.All.Count;
if (NavButton(new Rect(r.x, y, r.width, S(42)), "Mods" + (modCount > 0 ? " (" + modCount + ")" : ""), page == Page.Mods)) page = Page.Mods;
// Eigenes Hauptmenü: Spiel beenden (die Knöpfe des Spiel-Menüs sind ausgeblendet)
if (quitConfirm)
@@ -287,6 +293,10 @@ namespace MSCO
showNonSteam = Chip(showNonSteam, "NON-STEAM");
GUILayout.Space(S(6));
hideFull = Chip(hideFull, "HIDE FULL");
GUILayout.Space(S(12));
showVanilla = Chip(showVanilla, "VANILLA");
GUILayout.Space(S(6));
showModded = Chip(showModded, "MODDED");
GUILayout.EndHorizontal();
GUILayout.Space(S(6));
@@ -295,6 +305,7 @@ namespace MSCO
shownServers = all.FindAll(s =>
(s.RequireSteam ? showSteam : showNonSteam) &&
(!hideFull || s.Players < s.MaxPlayers) &&
(s.Modded ? showModded : showVanilla) &&
(search.Length == 0 || (s.Name ?? "").ToLower().Contains(search) || (s.Owner ?? "").ToLower().Contains(search) ||
(s.Tags != null && s.Tags.Exists(t => (t ?? "").Contains(search)))));
List<ServerListEntry> servers = shownServers;
@@ -334,6 +345,7 @@ namespace MSCO
Favorites.Favorite fav = Favorites.FindByAddress(s.Address, s.Port);
if (s.Featured) nameX += Pill(nameX, row, "FEATURED", pillFeaturedStyle);
if (fav != null) nameX += Pill(nameX, row, fav.Nickname, pillMutedStyle);
if (s.Modded) nameX += Pill(nameX, row, s.Mods.Count > 0 ? "MODS " + s.Mods.Count : "MODDED", pillStyle);
if (s.IsLocal)
{
// Server auf diesem PC (localhost)
@@ -649,6 +661,7 @@ namespace MSCO
InfoLine("ONLINE FOR", s.UptimeSeconds >= 0 ? FormatUptime(s.UptimeSeconds) : "-", rowNameStyle);
InfoLine("PING", s.Ping < 0 ? "-" : s.Ping + " ms", PingStyle(s.Ping));
InfoLine("ADDRESS", s.Address + ":" + s.Port, rowNameStyle);
if (s.Mods.Count > 0) InfoLine("MODS", string.Join(", ", s.Mods.ToArray()), textStyle);
GUILayout.EndVertical();
GUILayout.Space(S(16));
@@ -819,6 +832,85 @@ namespace MSCO
if (GUI.Button(new Rect(r.xMax - S(32) - S(170), r.yMax - S(28) - S(42), S(170), S(42)), "OK", primaryStyle)) mp.ClearDisconnectNotice();
}
// ---------------------------------------------------------------- Mods
Vector2 modsScroll;
/// <summary>Server verlangt Mods, die hier fehlen: zeigen, was geladen würde, und fragen.</summary>
void DrawModsPrompt(MultiplayerManager mp)
{
if (texDim == null) texDim = Solid(new Color(0f, 0f, 0f, 0.6f));
GUI.DrawTexture(new Rect(0, 0, vw, vh), texDim);
List<MultiplayerManager.ServerModInfo> list = mp.PendingMods;
float height = S(250) + S(46) * Mathf.Min(list.Count, 6);
var r = new Rect(Mathf.Round((vw - S(620)) / 2f), Mathf.Round((vh - height) / 2f), S(620), height);
UiTheme.DrawShadow(r);
GUI.Box(r, GUIContent.none, windowBox);
GUI.DrawTexture(new Rect(r.x, r.y, r.width, S(4)), texAccent);
float x = r.x + S(32), w = r.width - S(64), y = r.y + S(26);
GUI.Label(new Rect(x, y, w, S(34)), "THIS SERVER USES MODS", titleStyle);
y += S(44);
GUI.Label(new Rect(x, y, w, S(44)), "To play here you need these mods. Mods are programs - they run on your PC. " +
"Only install them from servers you trust.", textStyle);
y += S(52);
for (int i = 0; i < list.Count && i < 6; i++)
{
MultiplayerManager.ServerModInfo m = list[i];
var row = new Rect(x, y, w, S(40));
GUI.Box(row, GUIContent.none, rowBox);
GUI.Label(new Rect(row.x + S(14), row.y, w * 0.55f, row.height), m.Name + (string.IsNullOrEmpty(m.Version) ? "" : " " + m.Version), rowNameStyle);
GUI.Label(new Rect(row.x + w * 0.55f, row.y, w * 0.45f - S(14), row.height),
(m.Size / 1024) + " KB · SHA-256 " + (m.Sha256.Length > 12 ? m.Sha256.Substring(0, 12) + "…" : m.Sha256), headerCellStyle);
y += S(46);
}
if (list.Count > 6) GUI.Label(new Rect(x, y - S(6), w, S(20)), "+ " + (list.Count - 6) + " more", textStyle);
float by = r.yMax - S(28) - S(42);
if (GUI.Button(new Rect(r.xMax - S(32) - S(220), by, S(220), S(42)), "INSTALL & JOIN", primaryStyle)) mp.AcceptServerMods();
if (GUI.Button(new Rect(r.xMax - S(32) - S(220) - S(12) - S(140), by, S(140), S(42)), "CANCEL", secondaryStyle)) mp.DeclineServerMods();
}
/// <summary>Seite "Mods": geladene MSCO-Mods, Ordner öffnen.</summary>
void DrawMods()
{
GUILayout.BeginHorizontal();
GUILayout.Label("Mods", titleStyle, GUILayout.ExpandWidth(true));
if (GUILayout.Button("OPEN FOLDER", tabStyle, GUILayout.Height(S(28)), GUILayout.Width(S(130))))
{
try
{
System.IO.Directory.CreateDirectory(ModLoader.Folder);
System.Diagnostics.Process.Start("explorer.exe", "\"" + ModLoader.Folder + "\"");
}
catch { }
}
GUILayout.EndHorizontal();
GUILayout.Label("Put MSCO mods (.dll) into MSCO\\Mods in the game folder and restart the game - or manage them in the MSCO Launcher. " +
"Mods from a server are downloaded when you join it (you are asked first) and only run on that server.", textStyle);
GUILayout.Space(S(12));
modsScroll = GUILayout.BeginScrollView(modsScroll, GUILayout.Height(ListHeight(S(150f), S(140f))));
if (ModLoader.All.Count == 0) GUILayout.Label("No mods installed.", textStyle);
foreach (ModLoader.LoadedMod m in ModLoader.All)
{
Rect row = GUILayoutUtility.GetRect(S(10), S(52), GUILayout.ExpandWidth(true));
var box = new Rect(row.x, row.y + S(3), row.width, row.height - S(6));
GUI.Box(box, GUIContent.none, rowBox);
GUI.DrawTexture(new Rect(box.x + S(14), box.y + (box.height - S(8)) / 2f, S(8), S(8)),
m.Error != null ? texPending : m.Active ? texOnline : texMuted);
string name = "?", version = "", author = "";
try { name = m.Mod.Name; version = m.Mod.Version; author = m.Mod.Author; } catch { }
float nx = box.x + S(32);
if (m.FromServer) nx += Pill(nx, new Rect(box.x, box.y, box.width, box.height), "SERVER", pillMutedStyle);
GUI.Label(new Rect(nx, box.y, box.width * 0.5f, box.height), name + " " + version, rowNameStyle);
string state = m.Error != null ? "turned off: " + m.Error : m.Active ? "active" : m.FromServer ? "only on its server" : "off";
GUI.Label(new Rect(box.x + box.width * 0.55f, box.y, box.width * 0.45f - S(14), box.height),
(string.IsNullOrEmpty(author) ? "" : "by " + author + " · ") + state, headerCellStyle);
}
GUILayout.EndScrollView();
}
bool newGameConfirm;
/// <summary>Host: eigenen Spielstand weiterspielen oder ein neues Spiel anlegen (mit Sicherheitsabfrage).</summary>
+85
View File
@@ -0,0 +1,85 @@
# MSCO-Mods schreiben
MSCO läuft ohne MSCLoader und hat eine eigene, kleine Mod-Schnittstelle. Eine MSCO-Mod ist eine .NET-3.5-DLL mit einer
Klasse, die von `MSCO.MscoMod` erbt. MSCLoader-Mods laufen damit **nicht** (sie brauchen MSCLoader).
## Wo liegen Mods?
| Ordner | |
|---|---|
| `<Spielordner>\MSCO\Mods\*.dll` | eigene Mods – immer aktiv, werden beim Spielstart geladen |
| `<Spielordner>\MSCO\Mods\*.dll.off` | ausgeschaltet (Launcher → *Mods*) |
| `<Spielordner>\MSCO\Mods\Servers\*.dll` | Mods von Servern – nur aktiv, solange man auf einem Server ist, der sie verlangt |
| Server: `data/mods/*.dll` | Mods, die der dedizierte Server an seine Spieler verteilt (Web-Panel → *Mods*) |
Im Spiel: Seite **Mods** im MSCO-Menü, Konsole (F7) `mods`. Im Launcher: Seite **Mods** (hinzufügen, aus-/einschalten,
entfernen).
## Server-Mods
Liegen Mods in `data/mods` des Servers, bekommt jeder Spieler beim Beitreten die Liste (Name, Version, Größe, SHA-256).
Fehlt ihm eine Mod, fragt das Spiel: **INSTALL & JOIN** lädt sie direkt vom Server (Prüfsumme wird kontrolliert),
**CANCEL** verlässt den Server. Danach geht es ins Spiel. Hat der Spieler dieselbe Datei schon (gleiche Prüfsumme), wird
nichts geladen.
Server mit Mods bekommen in der Serverliste automatisch den Tag **MODDED**; im Server-Browser (Spiel, Launcher, Website)
steht, welche Mods sie nutzen, und es gibt Filter *VANILLA* / *MODDED*.
> Mods sind Programme und laufen mit vollen Rechten auf dem PC der Spieler. Serverbetreiber: nur Mods anbieten, denen ihr
> vertraut. Spieler: nur auf Servern zustimmen, denen ihr vertraut.
## Eine Mod bauen
Vorlage: [`examples/HelloMod`](examples/HelloMod) – Projekt (.NET Framework 3.5) mit Verweisen auf `UnityEngine.dll`
(aus `mysummercar_Data\Managed`) und `MSCO.dll` (aus `<Spielordner>\MSCO`, *Copy Local* aus).
```
msbuild examples/HelloMod/HelloMod.csproj -p:Configuration=Release -p:GamePath="C:\...\My Summer Car"
```
```csharp
using MSCO;
using UnityEngine;
public class HelloMod : MscoMod
{
public override string ID { get { return "hellomod"; } } // eindeutig: Buchstaben, Ziffern, . - _
public override string Name { get { return "Hello Mod"; } }
public override string Version { get { return "1.0.0"; } }
public override string Author { get { return "Ich"; } }
public override void OnGameLoad() { Log("Im Spiel!"); }
public override void Update()
{
if (Input.GetKeyDown(KeyCode.H)) Send(System.Text.Encoding.UTF8.GetBytes("wave"));
}
public override void OnMessage(int fromPlayerId, byte[] data)
{
string name;
Log((Players.TryGetValue(fromPlayerId, out name) ? name : "?") + " winkt");
}
}
```
Name und Version, die Launcher und Server-Panel anzeigen, kommen aus den Assembly-Attributen
(`AssemblyProduct`, `AssemblyInformationalVersion`).
## Schnittstelle
| Mitglied | |
|---|---|
| `ID`, `Name`, `Version`, `Author` | Angaben zur Mod (`ID` und `Name` müssen überschrieben werden) |
| `OnLoad()` | einmal nach dem Laden |
| `OnMenuLoad()`, `OnGameLoad()` | Hauptmenü bzw. Spiel (Szene `GAME`) geladen |
| `Update()`, `FixedUpdate()`, `OnGUI()` | jeden Frame (wie bei Unity) |
| `OnSessionStart()`, `OnSessionEnd()` | mit Server/Host verbunden bzw. getrennt |
| `OnPlayerJoined(id, name)`, `OnPlayerLeft(id, name)` | Mitspieler kommt/geht |
| `OnMessage(fromPlayerId, data)` | Daten, die dieselbe Mod (gleiche `ID`) bei einem anderen Spieler mit `Send` geschickt hat |
| `Send(byte[] data)` | an dieselbe Mod bei allen anderen Spielern (bis 60 KB; P2P und dedizierter Server) |
| `IsInSession`, `IsHost`, `LocalPlayerId`, `Players` | Sitzung, eigene ID, Spieler (ID → Name) |
| `Log(text)`, `LogWarning(text)` | in die MSCO-Konsole (F7) und `output_log.txt` |
Fehler in einer Mod fängt MSCO ab und schreibt sie ins Log. Wirft eine Mod ständig Fehler (50×), wird sie abgeschaltet.
Harmony 1.2 (`0Harmony.dll`) liegt neben `MSCO.dll` und kann von Mods mitbenutzt werden.
+1 -1
View File
@@ -11,7 +11,7 @@ namespace MSCO
/// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
/// </summary>
public const string ModVersion = "2.9.1";
public const string ModVersion = "2.10.0";
/// <summary>
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server.
+2
View File
@@ -94,6 +94,8 @@
<Compile Include="MenuHelper.cs" />
<Compile Include="MenuInputBlocker.cs" />
<Compile Include="MiniJson.cs" />
<Compile Include="ModApi.cs" />
<Compile Include="ModLoader.cs" />
<Compile Include="MenuBranding.cs" />
<Compile Include="MSCO.cs" />
<Compile Include="MultiplayerManager.cs" />
+69
View File
@@ -0,0 +1,69 @@
using System.Collections.Generic;
namespace MSCO
{
/// <summary>
/// Basis für MSCO-Mods. Eine Mod ist eine .NET-3.5-DLL in &lt;Spiel&gt;\MSCO\Mods mit (mindestens) einer Klasse, die
/// von MscoMod erbt. MSCO lädt sie beim Spielstart; Mods eines Servers bekommen Spieler beim Beitreten (nach
/// Zustimmung) und sie sind nur auf diesem Server aktiv. Anleitung: MODDING.md im MSCO-Repository.
/// </summary>
public abstract class MscoMod
{
/// <summary>Eindeutige ID (Buchstaben, Ziffern, . - _). Mods mit derselben ID reden über Send/OnMessage miteinander.</summary>
public abstract string ID { get; }
public abstract string Name { get; }
public virtual string Version { get { return "1.0.0"; } }
public virtual string Author { get { return ""; } }
// ---------------------------------------------------------------- Lebenszyklus (überschreiben nach Bedarf)
/// <summary>Einmal nach dem Laden der Mod (Hauptmenü oder - bei Server-Mods - beim Beitreten).</summary>
public virtual void OnLoad() { }
/// <summary>Hauptmenü wurde geladen.</summary>
public virtual void OnMenuLoad() { }
/// <summary>Das Spiel (Szene GAME) wurde geladen.</summary>
public virtual void OnGameLoad() { }
public virtual void Update() { }
public virtual void FixedUpdate() { }
public virtual void OnGUI() { }
/// <summary>Mit einem Server/Host verbunden (bzw. selbst P2P-Host geworden).</summary>
public virtual void OnSessionStart() { }
/// <summary>Sitzung beendet (getrennt, gekickt, Host weg ...).</summary>
public virtual void OnSessionEnd() { }
public virtual void OnPlayerJoined(int playerId, string name) { }
public virtual void OnPlayerLeft(int playerId, string name) { }
/// <summary>Daten, die dieselbe Mod bei einem anderen Spieler mit Send() geschickt hat.</summary>
public virtual void OnMessage(int fromPlayerId, byte[] data) { }
// ---------------------------------------------------------------- Helfer
/// <summary>Daten an dieselbe Mod bei allen anderen Spielern schicken (max. 60 KB, nur in einer Sitzung).</summary>
protected bool Send(byte[] data)
{
MultiplayerManager mp = MultiplayerManager.Instance;
return mp != null && mp.SendModMessage(ID, data);
}
protected void Log(string text) { MscoLog.Info("[" + Name + "] " + text); }
protected void LogWarning(string text) { MscoLog.Warning("[" + Name + "] " + text); }
/// <summary>In einer Sitzung (Server oder P2P)?</summary>
protected bool IsInSession { get { return MultiplayerManager.Instance != null && MultiplayerManager.Instance.IsInSession; } }
/// <summary>Selbst P2P-Host?</summary>
protected bool IsHost { get { return MultiplayerManager.Instance != null && MultiplayerManager.Instance.IsHost; } }
/// <summary>Eigene Spieler-ID in der Sitzung.</summary>
protected int LocalPlayerId { get { return MultiplayerManager.Instance != null ? MultiplayerManager.Instance.LocalId : -1; } }
/// <summary>Spieler der Sitzung: ID -&gt; Name.</summary>
protected Dictionary<int, string> Players
{
get
{
var result = new Dictionary<int, string>();
MultiplayerManager mp = MultiplayerManager.Instance;
if (mp != null) foreach (PlayerInfo p in mp.Players) result[p.Id] = p.Name;
return result;
}
}
}
}
+167
View File
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
namespace MSCO
{
/// <summary>
/// Lädt MSCO-Mods und ruft sie auf. Eigene Mods: &lt;Spiel&gt;\MSCO\Mods\*.dll (immer aktiv). Mods eines Servers:
/// &lt;Spiel&gt;\MSCO\Mods\Servers\&lt;id&gt;.dll - nur aktiv, solange man auf einem Server ist, der sie verlangt.
/// Fehler einer Mod werden abgefangen und protokolliert; wirft sie ständig Fehler, wird sie abgeschaltet.
/// </summary>
public static class ModLoader
{
public sealed class LoadedMod
{
public MscoMod Mod;
public string File, Sha256;
public bool FromServer, Active;
public int Errors;
public string Error;
}
const int MaxErrors = 50;
static readonly List<LoadedMod> mods = new List<LoadedMod>();
static bool localLoaded;
public static IList<LoadedMod> All { get { return mods.AsReadOnly(); } }
public static string Folder { get { return Path.Combine(Bootstrap.Folder ?? ".", "Mods"); } }
public static string ServerFolder { get { return Path.Combine(Folder, "Servers"); } }
public static string ServerFile(string id) { return Path.Combine(ServerFolder, id + ".dll"); }
/// <summary>Eigene Mods einmal beim ersten Hauptmenü laden.</summary>
public static void LoadLocal()
{
if (localLoaded) return;
localLoaded = true;
try
{
Directory.CreateDirectory(Folder);
foreach (string file in Directory.GetFiles(Folder, "*.dll"))
Load(file, false);
if (mods.Count > 0) MscoLog.Info("[MSCO] " + mods.Count + " mod(s) loaded from " + Folder);
}
catch (Exception e) { MscoLog.Warning("[MSCO] Could not load mods: " + e.Message); }
}
public static string Sha256Of(byte[] data)
{
using (var sha = SHA256.Create())
return BitConverter.ToString(sha.ComputeHash(data)).Replace("-", "").ToLowerInvariant();
}
public static LoadedMod FindBySha(string sha)
{
foreach (LoadedMod m in mods) if (string.Equals(m.Sha256, sha, StringComparison.OrdinalIgnoreCase)) return m;
return null;
}
/// <summary>
/// DLL laden (aus den Bytes - die Datei bleibt frei, und eine neue Version einer Server-Mod kann neben der alten
/// geladen werden) und alle MscoMod-Klassen darin anlegen. Gibt die erste Mod zurück (oder null).
/// </summary>
public static LoadedMod Load(string file, bool fromServer)
{
LoadedMod first = null;
try
{
byte[] data = System.IO.File.ReadAllBytes(file);
string sha = Sha256Of(data);
LoadedMod existing = FindBySha(sha);
if (existing != null) return existing;
Assembly asm = Assembly.Load(data);
Type[] types;
try { types = asm.GetTypes(); }
catch (ReflectionTypeLoadException e) { types = Array.FindAll(e.Types, t => t != null); }
foreach (Type t in types)
{
if (t.IsAbstract || !typeof(MscoMod).IsAssignableFrom(t)) continue;
MscoMod mod;
try { mod = (MscoMod)Activator.CreateInstance(t); }
catch (Exception e)
{
MscoLog.Warning("[MSCO] Mod " + t.FullName + " could not be created: " + (e.InnerException ?? e).Message);
continue;
}
var loaded = new LoadedMod { Mod = mod, File = file, Sha256 = sha, FromServer = fromServer, Active = !fromServer };
mods.Add(loaded);
if (first == null) first = loaded;
MscoLog.Info("[MSCO] Mod loaded: " + Safe(() => mod.Name, t.Name) + " " + Safe(() => mod.Version, "") +
(fromServer ? " (from the server)" : ""));
Call(loaded, m => m.OnLoad());
}
if (first == null) MscoLog.Warning("[MSCO] " + Path.GetFileName(file) + " contains no MSCO mod (class deriving from MscoMod)");
}
catch (Exception e)
{
MscoLog.Warning("[MSCO] Could not load mod " + Path.GetFileName(file) + ": " + e.Message);
}
return first;
}
static string Safe(Func<string> get, string fallback)
{
try { return get() ?? fallback; } catch { return fallback; }
}
/// <summary>Server-Mods für die aktuelle Sitzung einschalten (alle anderen Server-Mods aus).</summary>
public static void ActivateServerMods(ICollection<string> shas)
{
foreach (LoadedMod m in mods)
if (m.FromServer && m.Error == null) m.Active = shas != null && shas.Contains(m.Sha256);
}
// ---------------------------------------------------------------- Aufrufe
static void Call(LoadedMod m, Action<MscoMod> action)
{
try { action(m.Mod); }
catch (Exception e)
{
m.Errors++;
if (m.Errors <= 3) MscoLog.Warning("[MSCO] Mod " + Safe(() => m.Mod.Name, "?") + ": " + e);
if (m.Errors >= MaxErrors)
{
m.Active = false;
m.Error = e.Message;
MscoLog.Warning("[MSCO] Mod " + Safe(() => m.Mod.Name, "?") + " was turned off (too many errors)");
}
}
}
static void Each(Action<MscoMod> action)
{
for (int i = 0; i < mods.Count; i++)
if (mods[i].Active) Call(mods[i], action);
}
public static void MenuLoad() { Each(m => m.OnMenuLoad()); }
public static void GameLoad() { Each(m => m.OnGameLoad()); }
public static void Update() { if (mods.Count > 0) Each(m => m.Update()); }
public static void FixedUpdate() { if (mods.Count > 0) Each(m => m.FixedUpdate()); }
public static void OnGUI() { if (mods.Count > 0) Each(m => m.OnGUI()); }
public static void SessionStart() { Each(m => m.OnSessionStart()); }
public static void PlayerJoined(int id, string name) { Each(m => m.OnPlayerJoined(id, name)); }
public static void PlayerLeft(int id, string name) { Each(m => m.OnPlayerLeft(id, name)); }
/// <summary>Sitzung vorbei: Mods informieren, Server-Mods ausschalten.</summary>
public static void SessionEnd()
{
Each(m => m.OnSessionEnd());
ActivateServerMods(null);
}
public static void Message(string modId, int from, byte[] data)
{
for (int i = 0; i < mods.Count; i++)
{
LoadedMod m = mods[i];
if (!m.Active || Safe(() => m.Mod.ID, null) != modId) continue;
Call(m, mod => mod.OnMessage(from, data));
}
}
}
}
+181 -9
View File
@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using HutongGames.PlayMaker;
using UnityEngine;
@@ -192,6 +192,7 @@ namespace MSCO
objects.LocalId = localId;
AddPlayer(localId, playerName, true);
SetStatus("Hosting on port " + port + " - waiting for players...");
ModLoader.SessionStart();
}
catch (System.Exception e)
{
@@ -883,6 +884,7 @@ namespace MSCO
objects.LocalId = localId;
AddPlayer(localId, OwnName(), true);
SetStatus("Steam lobby ready - invite your friends!");
ModLoader.SessionStart();
}
void OnSteamLobbyEntered(Steamworks.CSteamID lobby, Steamworks.CSteamID owner)
@@ -1339,6 +1341,10 @@ namespace MSCO
connectingToServer = false;
readySent = false;
pendingSnapshot = null;
PendingMods = null;
downloadingMods.Clear();
deferredStart = null;
ModLoader.SessionEnd();
SteamLobby.CancelAuthTicket();
passenger.Exit(localPlayer, playerCamera); // Beifahrer: neben das Auto stellen
foreach (PlayerInfo p in players.Values) DestroyAvatar(p);
@@ -1379,7 +1385,7 @@ namespace MSCO
|| t == MsgType.Chat || t == MsgType.VarSync || t == MsgType.SpawnItem
|| t == MsgType.ItemAppeared || t == MsgType.ObjDestroy || t == MsgType.VehicleDamage
|| t == MsgType.TrafficStates || t == MsgType.WorldVars || t == MsgType.HingeStates
|| t == MsgType.VehicleControls;
|| t == MsgType.VehicleControls || t == MsgType.ModMessage;
}
void OnMessage(int peerId, Message m)
@@ -1499,9 +1505,39 @@ namespace MSCO
reconnectMessage = null; // (wieder) verbunden
reconnectAttempts = 0;
SetStatus(dedicated ? "Connected to server - loading..." : "Connected - waiting for host to start...");
if (PendingMods != null) SetStatus("This server uses mods - confirm to download them");
ModLoader.SessionStart();
break;
}
case MsgType.ModList:
{
var list = new List<ServerModInfo>();
int n = r.ReadInt32();
for (int i = 0; i < n && i < 64; i++)
list.Add(new ServerModInfo { Id = r.ReadString(), Name = r.ReadString(), Version = r.ReadString(), Sha256 = r.ReadString(), Size = r.ReadInt32() });
ResolveServerMods(list);
break;
}
case MsgType.ModFilePart:
{
string id = r.ReadString();
int total = r.ReadInt32(), offset = r.ReadInt32(), len = r.ReadInt32();
byte[] part = r.ReadBytes(len);
ServerModInfo info = downloadingMods.Find(x => x.Id == id);
if (info == null || total != info.Size || total > MaxModBytes || offset < 0 || offset + part.Length > total) break;
if (info.Buffer == null) info.Buffer = new byte[total];
System.Buffer.BlockCopy(part, 0, info.Buffer, offset, part.Length);
info.Received += part.Length;
SetStatus("Downloading mods... " + ModDownloadPercent() + "%");
break;
}
case MsgType.ModFileDone:
FinishModDownload(r.ReadString());
break;
case MsgType.Reject:
{
string why = r.ReadString();
@@ -1630,28 +1666,50 @@ namespace MSCO
}
case MsgType.StartGame:
{
if (!receivingSave) return;
receivingSave = false;
// Später beigetreten: nach dem Laden neben den Host setzen
teleportToHost = r.BaseStream.Length > 0 && r.ReadBoolean();
// Server mit "neues Spiel" (Panel): kein Spielstand - hier ein neues Spiel starten (Server ab 2.9.3)
bool serverNewGame = r.BaseStream.Length - r.BaseStream.Position >= 1 && r.ReadBoolean();
if (serverNewGame)
// Fehlen noch Mods des Servers: erst nach Zustimmung + Download ins Spiel
if (PendingMods != null || ModsDownloading)
{
SetStatus("The server starts a new game - creating a fresh world...");
MscoLog.Info("[MSCO] Server has a new game - starting a fresh world");
MenuHelper.StartNewGame();
deferredStart = serverNewGame;
break;
}
SetStatus(dedicated ? "Loading server world..." : "Loading host's game...");
MenuHelper.StartGame();
BeginServerGame(serverNewGame);
break;
}
}
}
/// <summary>Spielstand ist da: ins Spiel (bzw. bei einem neuen Server-Spiel ein neues Spiel anlegen).</summary>
void BeginServerGame(bool serverNewGame)
{
if (serverNewGame)
{
SetStatus("The server starts a new game - creating a fresh world...");
MscoLog.Info("[MSCO] Server has a new game - starting a fresh world");
MenuHelper.StartNewGame();
return;
}
SetStatus(dedicated ? "Loading server world..." : "Loading host's game...");
MenuHelper.StartGame();
}
void HandleGameMessage(int from, MsgType type, BinaryReader r)
{
if (from == localId) return;
if (type == MsgType.ModMessage)
{
string modId = r.ReadString();
int len = r.ReadInt32();
if (len < 0 || len > MaxModMessageBytes) return;
ModLoader.Message(modId, from, r.ReadBytes(len));
return;
}
if (type == MsgType.Chat)
{
string text = r.ReadString();
@@ -1724,6 +1782,117 @@ namespace MSCO
}
/// <summary>Spiel-Nachricht an alle anderen (Client: über den Host).</summary>
// ---------------------------------------------------------------- MSCO-Mods
public sealed class ServerModInfo
{
public string Id, Name, Version, Sha256;
public int Size, Received;
public byte[] Buffer;
}
const int MaxModBytes = 20 * 1024 * 1024, MaxModMessageBytes = 60000;
/// <summary>Mods, die der Server verlangt und die hier fehlen - warten auf Zustimmung des Spielers (null = keine).</summary>
public List<ServerModInfo> PendingMods { get; private set; }
readonly List<ServerModInfo> downloadingMods = new List<ServerModInfo>();
readonly List<string> serverModShas = new List<string>();
bool? deferredStart;
public bool ModsDownloading { get { return downloadingMods.Count > 0; } }
int ModDownloadPercent()
{
long total = 0, done = 0;
foreach (ServerModInfo m in downloadingMods) { total += m.Size; done += m.Received; }
return total > 0 ? (int)(done * 100 / total) : 100;
}
/// <summary>Liste vom Server: vorhandene Mods (gleiche Prüfsumme) einschalten, fehlende zum Bestätigen merken.</summary>
void ResolveServerMods(List<ServerModInfo> list)
{
serverModShas.Clear();
var missing = new List<ServerModInfo>();
foreach (ServerModInfo m in list)
{
if (string.IsNullOrEmpty(m.Id) || m.Id.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || m.Id.Contains("..")) continue;
serverModShas.Add(m.Sha256);
if (ModLoader.FindBySha(m.Sha256) != null) continue;
// Schon einmal geladen (gleiche Datei auf der Platte)?
string file = ModLoader.ServerFile(m.Id);
if (File.Exists(file) && ModLoader.Sha256Of(File.ReadAllBytes(file)) == m.Sha256 && ModLoader.Load(file, true) != null) continue;
missing.Add(m);
}
ModLoader.ActivateServerMods(serverModShas);
PendingMods = missing.Count > 0 ? missing : null;
if (PendingMods != null) MscoLog.Info("[MSCO] The server uses " + list.Count + " mod(s), " + missing.Count + " missing");
}
/// <summary>Spieler stimmt zu: fehlende Mods vom Server laden.</summary>
public void AcceptServerMods()
{
if (PendingMods == null || session == null) return;
downloadingMods.Clear();
downloadingMods.AddRange(PendingMods);
PendingMods = null;
foreach (ServerModInfo m in downloadingMods)
{
string id = m.Id;
session.SendTo(NetSession.HostId, MsgType.ModFileRequest, NetSession.Build(w => w.Write(id)));
}
SetStatus("Downloading mods... 0%");
}
/// <summary>Spieler lehnt ab: ohne die Mods geht es auf diesem Server nicht.</summary>
public void DeclineServerMods()
{
PendingMods = null;
serverReason = "This server needs its mods - you declined to install them.";
reconnectMessage = null;
if (session != null) session.Close("Declined the server's mods");
OnSessionEnded("Declined the server's mods");
}
void FinishModDownload(string id)
{
ServerModInfo m = downloadingMods.Find(x => x.Id == id);
if (m == null) return;
downloadingMods.Remove(m);
if (m.Buffer == null || m.Received < m.Size || ModLoader.Sha256Of(m.Buffer) != m.Sha256)
{
MscoLog.Warning("[MSCO] Mod " + m.Name + " arrived damaged (checksum mismatch) - not loaded");
serverReason = "The mod " + m.Name + " could not be downloaded correctly - try again.";
if (session != null) session.Close("Mod download failed");
OnSessionEnded("Mod download failed");
return;
}
try
{
Directory.CreateDirectory(ModLoader.ServerFolder);
File.WriteAllBytes(ModLoader.ServerFile(m.Id), m.Buffer);
}
catch (System.Exception e) { MscoLog.Warning("[MSCO] Could not save mod " + m.Name + ": " + e.Message); }
if (ModLoader.Load(ModLoader.ServerFile(m.Id), true) == null)
MscoLog.Warning("[MSCO] Mod " + m.Name + " could not be started - see the log");
ModLoader.ActivateServerMods(serverModShas);
if (MenuHelper.InMenu) ModLoader.MenuLoad();
if (downloadingMods.Count > 0) return;
SetStatus("Mods installed - loading the world...");
if (deferredStart != null)
{
bool newGame = deferredStart.Value;
deferredStart = null;
BeginServerGame(newGame);
}
}
/// <summary>Für MscoMod.Send: Daten einer Mod an dieselbe Mod bei allen anderen Spielern.</summary>
public bool SendModMessage(string modId, byte[] data)
{
if (session == null || !handshakeDone || string.IsNullOrEmpty(modId) || data == null || data.Length > MaxModMessageBytes) return false;
SendGame(MsgType.ModMessage, NetSession.Build(w => { w.Write(modId); w.Write(data.Length); w.Write(data); }));
return true;
}
void SendGame(MsgType type, byte[] payload)
{
if (session == null) return;
@@ -1736,9 +1905,11 @@ namespace MSCO
PlayerInfo AddPlayer(int id, string name, bool isLocal)
{
PlayerInfo p;
if (!players.TryGetValue(id, out p)) players[id] = p = new PlayerInfo { Id = id };
bool isNew = !players.TryGetValue(id, out p);
if (isNew) players[id] = p = new PlayerInfo { Id = id };
p.Name = name;
p.IsLocal = isLocal;
if (isNew && !isLocal) ModLoader.PlayerJoined(id, name);
return p;
}
@@ -1746,6 +1917,7 @@ namespace MSCO
{
PlayerInfo p;
if (!players.TryGetValue(id, out p)) return;
if (!p.IsLocal) ModLoader.PlayerLeft(id, p.Name);
DestroyAvatar(p);
objects.ReleaseAllOf(id);
players.Remove(id);
+6 -1
View File
@@ -52,7 +52,12 @@ namespace MSCO
WorldVars = 41, // Jobs/Laden: int n, je [int Schlüssel, byte Typ (2 int, 3 bool), Wert]
HingeStates = 42, // Autotüren/Haube/Kofferraum: int n, je [int Schlüssel, float Scharnier-Winkel]
VehicleControls = 43, // Zündung/Motor/Licht ...: int n, je [int FSM-Schlüssel, string Zustand]
PlayerNeeds = 44 // Client -> Server: eigene Bedürfnisse (Hunger, Durst ...): int n, je [string Name, float Wert]
PlayerNeeds = 44, // Client -> Server: eigene Bedürfnisse (Hunger, Durst ...): int n, je [string Name, float Wert]
ModMessage = 45, // Nachricht einer MSCO-Mod an die anderen Spieler: string Mod-ID, int Länge, bytes
ModList = 46, // Server -> Client (vor Welcome): int n, je [string ID, string Name, string Version, string SHA-256, int Größe]
ModFileRequest = 47, // Client -> Server: string Mod-ID
ModFilePart = 48, // Server -> Client: string Mod-ID, int Gesamtgröße, int Offset, int Länge, bytes
ModFileDone = 49 // Server -> Client: string Mod-ID
}
public struct Message
+6 -1
View File
@@ -52,7 +52,10 @@ einem dedizierten Server aus dem Server-Browser. MSCO läuft **eigenständig, oh
- **P** / **Leertaste** – aussteigen; **P** neben einem Auto – auf den Beifahrersitz
- Tasten ändern mit `bind` in der F7-Konsole (z.B. `bind passenger O`)
- Rich Presence: zeigt Freunden deinen Status („In the Menus“, Server, Spielerzahl)
- MSCO-Look: Fenstertitel, Symbol und Logo im Hauptmenü; der „Press any key“-Startbildschirm wird übersprungen
- MSCO-Look: Fenstertitel, Symbol und Logo im Hauptmenü; der „Press any key“-Startbildschirm wird übersprungen;
ESC im Hauptmenü beendet das Spiel nicht
- **Mods**: eigene MSCO-Mods in `<Spielordner>\MSCO\Mods`, Server können eigene Mods mitbringen (Download nach
Zustimmung) – siehe [MODDING.md](MODDING.md)
## Installation
@@ -111,6 +114,7 @@ In der F7-Konsole:
| `switch <kurzname>`, `fav …` | Zu einem Favoriten wechseln, Favoriten verwalten |
| `model [game\|classic]` | Mitspieler als MSC-Menschen oder als einfache Figur |
| `vars <filter>` | Diagnose: synchronisierte Fahrzeugwerte |
| `mods` | geladene MSCO-Mods |
| `admin` bzw. `/admin` im Chat | Admin-Menü |
| `vehicles`, `rig` | Diagnose: Fahrzeuge / Spielermodelle |
| `debug` | Sync-Debugausgaben an/aus |
@@ -150,6 +154,7 @@ msbuild MSCO.csproj -p:Configuration=Release
|---|---|
| `Bootstrap.cs`, `MSCO.cs` | Start ohne MSCLoader (Doorstop), Ladebildschirm, Versionen, Tastenbelegung |
| `MenuBranding.cs`, `MiniJson.cs` | Fenstertitel/-symbol und Menü-Logo, eigener JSON-Leser |
| `ModApi.cs`, `ModLoader.cs` | Mod-Schnittstelle `MscoMod`, Laden und Aufrufen der Mods (siehe MODDING.md) |
| `MultiplayerManager.cs` | Sitzung, Nachrichten, Spielerliste, Spielstart, HUD |
| `NetSession.cs` | Netzwerk (TCP und Steam P2P), Protokoll |
| `LobbyUI.cs`, `Overlays.cs`, `UiTheme.cs` | Launcher, Konsole/Chat/Admin-Menü, gemeinsamer Stil |
+35
View File
@@ -42,6 +42,41 @@ namespace MSCO
{
MscoLog.Warning("[MSCO] Could not install hierarchy guard: " + e.Message);
}
PatchMenuQuit();
}
/// <summary>
/// ESC im Hauptmenü beendet beim Spiel sofort das Programm (PlayMaker-Aktion "ApplicationQuit" im Menü). Im
/// MSCO-Hauptmenü gibt es dafür den QUIT-Knopf - dort also nicht über die Spiel-Logik beenden. Im Spiel selbst
/// (Pausemenü) bleibt alles wie gehabt.
/// </summary>
static void PatchMenuQuit()
{
try
{
Type action = null;
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
{
action = asm.GetType("HutongGames.PlayMaker.Actions.ApplicationQuit");
if (action != null) break;
}
MethodInfo onEnter = action != null ? action.GetMethod("OnEnter", BindingFlags.Instance | BindingFlags.Public) : null;
if (onEnter == null) return;
HarmonyInstance.Create("mbm.msco.menuquit").Patch(onEnter,
new HarmonyMethod(typeof(SafetyPatches).GetMethod("QuitPrefix", BindingFlags.Static | BindingFlags.NonPublic)), null);
}
catch (Exception e)
{
MscoLog.Warning("[MSCO] Could not block ESC-quit in the main menu: " + e.Message);
}
}
static bool QuitPrefix(FsmStateAction __instance)
{
if (!MenuHelper.InMenu) return true;
MscoLog.Info("[MSCO] The game tried to quit from the main menu (ESC) - use QUIT in the MSCO menu to exit");
try { __instance.Finish(); } catch { }
return false;
}
static bool SetParentPrefix(FsmStateAction __instance)
+19
View File
@@ -24,6 +24,8 @@ namespace MSCO
public string Owner; // Besitzer des Lizenzschlüssels
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> Mods = new List<string>(); // Mods des Servers ("Name Version") - bekommt man beim Beitreten
public bool Modded { get { return Mods.Count > 0 || Tags.Contains("modded"); } }
public int UptimeSeconds = -1;
public List<string> PlayerNames = new List<string>();
@@ -137,11 +139,28 @@ namespace MSCO
Owner = MiniJson.Str(o, "owner"),
Featured = MiniJson.Bool(o, "featured"),
Tags = MiniJson.Strings(o, "tags"),
Mods = ModNames(o),
});
}
return list;
}
static List<string> ModNames(Dictionary<string, object> o)
{
var names = new List<string>();
object v;
var list = o.TryGetValue("mods", out v) ? v as List<object> : null;
if (list == null) return names;
foreach (object item in list)
{
var m = item as Dictionary<string, object>;
if (m == null) continue;
string name = MiniJson.Str(m, "name", MiniJson.Str(m, "id", "?")), version = MiniJson.Str(m, "version", "");
names.Add(string.IsNullOrEmpty(version) ? name : name + " " + version);
}
return names;
}
/// <summary>Server auf diesem PC suchen (127.0.0.1, übliche Ports); nur antwortende werden gezeigt.</summary>
void ProbeLocalServers()
{
+62
View File
@@ -0,0 +1,62 @@
using System.Reflection;
using System.Text;
using MSCO;
using UnityEngine;
[assembly: AssemblyTitle("Hello Mod")]
[assembly: AssemblyProduct("Hello Mod")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
namespace HelloMod
{
/// <summary>
/// Beispiel-Mod: zeigt im Spiel oben links einen Hinweis. Mit H winkst du - alle anderen Spieler, die die Mod auch
/// haben, sehen "&lt;Name&gt; waves!".
/// </summary>
public class HelloMod : MscoMod
{
public override string ID { get { return "hellomod"; } }
public override string Name { get { return "Hello Mod"; } }
public override string Version { get { return "1.0.0"; } }
public override string Author { get { return "MBM"; } }
bool inGame;
string lastWave = "";
float waveUntil;
public override void OnLoad() { Log("loaded"); }
public override void OnMenuLoad() { inGame = false; }
public override void OnGameLoad() { inGame = true; }
public override void Update()
{
if (!inGame || !IsInSession || !Input.GetKeyDown(KeyCode.H)) return;
Send(Encoding.UTF8.GetBytes("wave"));
Show("You wave!");
}
public override void OnMessage(int fromPlayerId, byte[] data)
{
if (Encoding.UTF8.GetString(data) != "wave") return;
string name;
Show((Players.TryGetValue(fromPlayerId, out name) ? name : "Someone") + " waves!");
}
public override void OnPlayerJoined(int playerId, string name) { Show(name + " joined - press H to wave"); }
void Show(string text)
{
lastWave = text;
waveUntil = Time.realtimeSinceStartup + 4f;
}
public override void OnGUI()
{
if (!inGame) return;
GUI.Label(new Rect(20, 60, 400, 24), "Hello Mod: press H to wave");
if (Time.realtimeSinceStartup < waveUntil) GUI.Label(new Rect(20, 84, 400, 24), lastWave);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Beispiel-Mod für MSCO. Bauen: msbuild HelloMod.csproj -p:Configuration=Release
Ergebnis bin\Release\HelloMod.dll nach <Spielordner>\MSCO\Mods kopieren (oder auf dem Server nach data/mods).
GamePath anpassen, wenn My Summer Car woanders liegt.
-->
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputType>Library</OutputType>
<RootNamespace>HelloMod</RootNamespace>
<AssemblyName>HelloMod</AssemblyName>
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
<TargetFrameworkProfile>Unity Full v3.5</TargetFrameworkProfile>
<LangVersion>13</LangVersion>
<OutputPath>bin\$(Configuration)\</OutputPath>
<DebugType>none</DebugType>
<Optimize>true</Optimize>
<GamePath Condition=" '$(GamePath)' == '' ">F:\SteamLibrary\steamapps\common\My Summer Car</GamePath>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="UnityEngine">
<HintPath>$(GamePath)\mysummercar_Data\Managed\UnityEngine.dll</HintPath>
<Private>False</Private>
</Reference>
<!-- MSCO selbst (die Mod-Schnittstelle MscoMod) -->
<Reference Include="MSCO">
<HintPath>$(GamePath)\MSCO\MSCO.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="HelloMod.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>