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:
+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);
|
||||
|
||||
Reference in New Issue
Block a user