Server 2.10.0: distribute MSCO mods from data/mods
- ModStore: mods in data/mods (id, name, version, SHA-256), console "mods [reload]" - ModList is sent before Welcome, files on request in 60 KB parts (once per connection) - ModMessage relayed between players (max 64 KB) - Heartbeat reports the mods to the keymaster (automatic "modded" tag) - Web panel page "Mods": upload, remove, reload (DE/EN) - Tests for list, download, checksum, relay and limits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,61 @@ try
|
||||
spammer.Close();
|
||||
await Task.Delay(300);
|
||||
|
||||
Console.WriteLine("== Mods (data/mods)");
|
||||
var noModsClient = await TestClient.Join("NoMods", 0, null);
|
||||
Check(await noModsClient.WaitFor(MsgType.ModList, 300) == null, "Without mods the server sends no mod list");
|
||||
noModsClient.Close();
|
||||
Directory.CreateDirectory(Path.Combine(dir, "mods"));
|
||||
var modBytes = new byte[150_000];
|
||||
new Random(7).NextBytes(modBytes);
|
||||
modBytes[0] = 0x4d; modBytes[1] = 0x5a; // "MZ"
|
||||
File.WriteAllBytes(Path.Combine(dir, "mods", "testmod.dll"), modBytes);
|
||||
File.WriteAllText(Path.Combine(dir, "mods", "bad name!.dll"), "MZ nope");
|
||||
Srv().EnqueueCommand("mods reload");
|
||||
await Task.Delay(500);
|
||||
var modder = await TestClient.Join("Modder", 0, null);
|
||||
var modList = await modder.WaitFor(MsgType.ModList, 1000);
|
||||
Check(modList != null, "Players get the server's mod list when they join");
|
||||
if (modList != null)
|
||||
{
|
||||
var mr = new BinaryReader(new MemoryStream(modList));
|
||||
int modCount = mr.ReadInt32();
|
||||
string modId = mr.ReadString(), modName = mr.ReadString(), modVersion = mr.ReadString(), modSha = mr.ReadString();
|
||||
int modSize = mr.ReadInt32();
|
||||
Check(modCount == 1 && modId == "testmod" && modSize == modBytes.Length, $"Mod list has the valid mod only ({modCount}: {modId}, {modSize} bytes)");
|
||||
modder.Send(MsgType.ModFileRequest, Wire.Build(w => w.Write("testmod")));
|
||||
var modData = new byte[modSize];
|
||||
int received = 0;
|
||||
byte[] part;
|
||||
while ((part = await modder.WaitFor(MsgType.ModFilePart, 1500)) != null)
|
||||
{
|
||||
var pr = new BinaryReader(new MemoryStream(part));
|
||||
pr.ReadString(); int total = pr.ReadInt32(), off = pr.ReadInt32(), len = pr.ReadInt32();
|
||||
pr.ReadBytes(len).CopyTo(modData, off);
|
||||
received += len;
|
||||
if (received >= total) break;
|
||||
}
|
||||
Check(await modder.WaitFor(MsgType.ModFileDone, 1500) != null, "Mod download ends with ModFileDone");
|
||||
string gotSha = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(modData)).ToLowerInvariant();
|
||||
Check(received == modSize && gotSha == modSha, "Downloaded mod matches the checksum from the list");
|
||||
modder.Send(MsgType.ModFileRequest, Wire.Build(w => w.Write("testmod")));
|
||||
Check(await modder.WaitFor(MsgType.ModFilePart, 600) == null, "Each mod is sent only once per connection");
|
||||
modder.Send(MsgType.ModFileRequest, Wire.Build(w => w.Write("../server")));
|
||||
Check(await modder.WaitFor(MsgType.ModFilePart, 400) == null, "Unknown mod ids are ignored");
|
||||
}
|
||||
var modPeer = await TestClient.Join("ModPeer", 0, null);
|
||||
modder.Send(MsgType.ModMessage, Wire.Build(w => { w.Write("hellomod"); w.Write(4); w.Write(new byte[] { 1, 2, 3, 4 }); }));
|
||||
var modMsg = await modPeer.WaitFor(MsgType.ModMessage);
|
||||
Check(modMsg != null && BitConverter.ToInt32(modMsg, 0) == modder.Id, "Mod messages are relayed to the other players with the sender");
|
||||
modder.Send(MsgType.ModMessage, new byte[70_000]);
|
||||
Check(await modPeer.WaitFor(MsgType.ModMessage, 600) == null, "Oversized mod messages are dropped");
|
||||
modder.Close();
|
||||
modPeer.Close();
|
||||
File.Delete(Path.Combine(dir, "mods", "testmod.dll"));
|
||||
File.Delete(Path.Combine(dir, "mods", "bad name!.dll"));
|
||||
Srv().EnqueueCommand("mods reload");
|
||||
await Task.Delay(300);
|
||||
|
||||
Console.WriteLine("== reset-world makes everyone reconnect");
|
||||
var r1 = await TestClient.Join("Resetter", 0, null);
|
||||
r1.Send(MsgType.Ready, Array.Empty<byte>());
|
||||
|
||||
Reference in New Issue
Block a user