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