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:
@@ -171,6 +171,7 @@ const PAGES = [
|
||||
{ id: "admins", label: "Admins", icon: "★", role: "Moderator", group: "Moderation", render: renderAdmins },
|
||||
{ id: "bans", label: "Bans", icon: "⊘", role: "Moderator", group: "Moderation", render: renderBans },
|
||||
{ id: "world", label: "World & Backups", icon: "⟲", role: "Admin", group: "Management", render: renderWorld },
|
||||
{ id: "mods", label: "Mods", icon: "⧉", role: "Admin", group: "Management", render: renderMods },
|
||||
{ id: "settings", label: "Settings", icon: "⚙", role: "Admin", group: "Management", render: renderSettings },
|
||||
{ id: "update", label: "Update", icon: "⇪", role: "Admin", group: "Management", render: renderUpdate },
|
||||
{ id: "users", label: "Panel Users", icon: "⚿", role: "Owner", group: "Management", render: renderUsers },
|
||||
@@ -754,6 +755,63 @@ function renderWorld(page) {
|
||||
every(15000, loadBackups);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Mods
|
||||
|
||||
function renderMods(page) {
|
||||
const rows = h("tbody");
|
||||
const fileInput = h("input", { type: "file", accept: ".dll" });
|
||||
const kb = (n) => (n / 1024).toFixed(n < 102400 ? 1 : 0) + " KB";
|
||||
const upload = async () => {
|
||||
const file = fileInput.files && fileInput.files[0];
|
||||
if (!file) { toast("Choose a mod (.dll) first", true); return; }
|
||||
try {
|
||||
const res = await fetch("api/mods/upload?name=" + encodeURIComponent(file.name), { method: "POST", credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/octet-stream", "X-MSCO-Panel": "1" }, body: file });
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error((data && data.error) || "HTTP " + res.status);
|
||||
toast(data.message);
|
||||
fileInput.value = "";
|
||||
load();
|
||||
} catch (e) { toast(e.message, true); }
|
||||
};
|
||||
const remove = async (m) => {
|
||||
const ok = await modal({ title: "Remove " + m.name + "?", danger: true, confirm: "Remove",
|
||||
text: "New players no longer get this mod. Players who already have it keep it on their PC." });
|
||||
if (!ok) return;
|
||||
await act(api("POST", "mods/" + encodeURIComponent(m.id) + "/remove"));
|
||||
load();
|
||||
};
|
||||
|
||||
page.append(
|
||||
h("div", { class: "cards wide" },
|
||||
h("div", { class: "card" },
|
||||
h("div", { class: "stat-label" }, "Add a mod"),
|
||||
h("p", { class: "muted" }, "MSCO mods (.dll) from this list are offered to every player when they join. The player sees name, version, size and checksum and decides whether to install them. Only add mods you trust - they run on the players' PCs."),
|
||||
h("div", { class: "row" }, fileInput,
|
||||
h("button", { class: "btn primary", type: "button", onclick: upload }, "Upload"),
|
||||
h("button", { class: "btn ghost", type: "button", onclick: async () => { await act(api("POST", "mods/reload")); load(); } }, "Reload"))),
|
||||
h("div", { class: "card" },
|
||||
h("div", { class: "stat-label" }, "Folder"),
|
||||
h("p", { class: "muted" }, "The mods are stored in data/mods. Files copied there by hand are used after “Reload” (console: mods reload)."))),
|
||||
h("div", { class: "section" },
|
||||
h("div", { class: "section-head" }, h("h2", {}, "Server mods"), h("span", { class: "muted" }, "players get these when they join")),
|
||||
h("div", { class: "table-wrap" }, h("table", {},
|
||||
h("thead", {}, h("tr", {}, h("th", {}, "Mod"), h("th", {}, "Version"), h("th", {}, "Size"), h("th", {}, "SHA-256"), h("th", {}, ""))), rows))));
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const list = await api("GET", "mods");
|
||||
if (!list.length) { rows.replaceChildren(h("tr", {}, h("td", { colspan: 5, class: "empty" }, "No mods - players join without mods."))); return; }
|
||||
rows.replaceChildren(...list.map((m) => h("tr", {},
|
||||
h("td", {}, h("b", {}, m.name), h("div", { class: "muted mono" }, m.id + ".dll")),
|
||||
h("td", {}, m.version || "–"), h("td", {}, kb(m.size)),
|
||||
h("td", { class: "mono muted", title: m.sha256 }, m.sha256.slice(0, 16) + "…"),
|
||||
h("td", {}, h("div", { class: "actions" }, h("button", { class: "btn small danger", type: "button", onclick: () => remove(m) }, "Remove"))))));
|
||||
} catch (e) { toast(e.message, true); }
|
||||
}
|
||||
load();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Benutzer
|
||||
|
||||
function renderUsers(page) {
|
||||
|
||||
@@ -16,6 +16,23 @@
|
||||
"The web panel is turned off - turn it on again with 'panel on' in the server console or PanelEnabled in server.json":
|
||||
"Das Web-Panel ist ausgeschaltet - wieder an mit 'panel on' in der Server-Konsole oder PanelEnabled in server.json",
|
||||
|
||||
// Mods
|
||||
"Mods": "Mods",
|
||||
"Add a mod": "Mod hinzufügen",
|
||||
"MSCO mods (.dll) from this list are offered to every player when they join. The player sees name, version, size and checksum and decides whether to install them. Only add mods you trust - they run on the players' PCs.":
|
||||
"MSCO-Mods (.dll) aus dieser Liste werden jedem Spieler beim Beitreten angeboten. Er sieht Name, Version, Größe und Prüfsumme und entscheidet, ob er sie installiert. Nur Mods hinzufügen, denen du vertraust - sie laufen auf den PCs der Spieler.",
|
||||
"Reload": "Neu laden",
|
||||
"The mods are stored in data/mods. Files copied there by hand are used after “Reload” (console: mods reload).":
|
||||
"Die Mods liegen in data/mods. Von Hand dorthin kopierte Dateien gelten nach „Neu laden“ (Konsole: mods reload).",
|
||||
"Server mods": "Server-Mods",
|
||||
"players get these when they join": "bekommen Spieler beim Beitreten",
|
||||
"Mod": "Mod",
|
||||
"Size": "Größe",
|
||||
"No mods - players join without mods.": "Keine Mods - Spieler treten ohne Mods bei.",
|
||||
"Choose a mod (.dll) first": "Erst eine Mod (.dll) auswählen",
|
||||
"New players no longer get this mod. Players who already have it keep it on their PC.":
|
||||
"Neue Spieler bekommen diese Mod nicht mehr. Wer sie schon hat, behält sie auf seinem PC.",
|
||||
|
||||
// Neues Spiel
|
||||
"New game": "Neues Spiel",
|
||||
"Start a completely fresh My Summer Car game instead of an uploaded save. The current save game is backed up first. The first player to join creates the new world.":
|
||||
|
||||
Reference in New Issue
Block a user