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:
+25
-1
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 <Spiel>\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 -> 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
@@ -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: <Spiel>\MSCO\Mods\*.dll (immer aktiv). Mods eines Servers:
|
||||
/// <Spiel>\MSCO\Mods\Servers\<id>.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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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 "<Name> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user