Импорт пресета песочницы .cfg и списка модов
CI / test (push) Successful in 24s
Release / release (push) Successful in 32s

Сборки нередко раздают только пресет одиночной игры. Внутри это плоский
список «Параметр=значение» с точками вместо вложенности, поэтому значения
переносятся в SandboxVars.lua профиля по одному: комментарии игры и порядок
остаются на месте, а параметры модов, которых в файле ещё нет, пропускаются
со счётом. Если файла нет вовсе, он собирается из пресета целиком.

Заодно панель принимает строку из игровой кнопки «Поделиться»
(«Название:ModA;ModB;…»): она заменяет список включённых модов, сохраняя
порядок загрузки. Идентификаторов мастерской в такой строке нет, поэтому
пакеты добавляются только для модов, чьи файлы уже на диске, а про остальные
панель предупреждает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 17:32:28 +03:00
co-authored by Claude Opus 5
parent aec932f68f
commit bfb9271635
9 changed files with 532 additions and 43 deletions
+61 -3
View File
@@ -1327,6 +1327,51 @@ $("#mod-list").addEventListener("drop", (event) => {
renderMods();
});
// Игра умеет делиться набором модов строкой вида «Название:ModA;ModB;…».
// Workshop ID в ней нет, поэтому список задаёт только порядок загрузки: файлы
// сервер возьмёт из пакетов мастерской, которые уже перечислены в конфиге.
function parseModList(text) {
// Перед двоеточием стоит имя набора; у самих модов двоеточий не бывает.
const colon = text.indexOf(":");
const body = colon >= 0 ? text.slice(colon + 1) : text;
const seen = new Set();
const out = [];
body.split(/[;\n\r]+/).forEach((raw) => {
const id = raw.trim();
if (!id || seen.has(id)) return;
seen.add(id);
out.push(id);
});
return out;
}
$("#mod-list-form").addEventListener("submit", (event) => {
event.preventDefault();
const ids = parseModList($("#mod-list-input").value);
if (!ids.length) {
toast(t("mods.listEmpty"), "err");
return;
}
if (!confirm(t("mods.listConfirm", { count: ids.length }))) return;
mods.enabled = ids;
// Пакет мастерской известен для тех модов, чьи файлы уже лежат на диске:
// добавляем их, чтобы сервер не потерял мод при следующем запуске.
mods.installed.forEach((mod) => {
if (!ids.includes(mod.mod_id) || !mod.workshop_id) return;
if (!mods.workshop.includes(mod.workshop_id)) mods.workshop.push(mod.workshop_id);
});
const installed = new Set(mods.installed.map((mod) => mod.mod_id));
const missing = ids.filter((id) => !installed.has(id)).length;
$("#mod-list-input").value = "";
renderMods();
ok(t("mods.listDone", { count: ids.length }));
if (missing) toast(t("mods.listNoFiles", { count: missing }));
});
$("#map-list").addEventListener("input", refreshModsSaveState);
$("#mod-filter").addEventListener("input", renderMods);
$("#mod-only-enabled").addEventListener("change", renderMods);
@@ -1642,11 +1687,21 @@ function renderPresetSummary() {
if (set.has_ini) {
lines.push(t("import.mods", { mods: set.mods, workshop: set.workshop_items }));
if ((set.maps || []).length) lines.push(t("import.maps", { maps: set.maps.join(", ") }));
} else {
} else if (!set.has_sandbox_preset) {
// У пресета песочницы конфига сервера и не должно быть — про него сказано
// отдельной строкой ниже.
lines.push(t("import.noINI"));
}
lines.push(t("import.counts", { settings: set.settings, sandbox: set.sandbox_settings }));
lines.push(t(set.has_spawn_regions ? "import.spawnYes" : "import.spawnNo"));
if (set.has_sandbox_preset) {
// Пресет одиночной игры — не файл сервера: он переносится по значениям.
lines.push(t("import.sandboxPreset", { count: set.sandbox_settings }));
lines.push(t("import.sandboxPresetNote"));
} else {
lines.push(t("import.counts", { settings: set.settings, sandbox: set.sandbox_settings }));
}
if (set.has_ini || set.has_spawn_regions) {
lines.push(t(set.has_spawn_regions ? "import.spawnYes" : "import.spawnNo"));
}
$("#preset-summary").replaceChildren(...lines.map((line) => {
const el = document.createElement("div");
@@ -1702,6 +1757,9 @@ $("#btn-preset-import").addEventListener("click", async () => {
if ((result.kept || []).length) {
toast(t("import.kept", { profile: result.kept_from, keys: result.kept.join(", ") }));
}
if (result.applied !== undefined) {
toast(t("import.applied", { applied: result.applied, skipped: result.skipped }));
}
$("#preset-file").value = "";
$("#preset-details").hidden = true;
await loadProfiles();