Импорт пресета песочницы .cfg и списка модов
Сборки нередко раздают только пресет одиночной игры. Внутри это плоский список «Параметр=значение» с точками вместо вложенности, поэтому значения переносятся в SandboxVars.lua профиля по одному: комментарии игры и порядок остаются на месте, а параметры модов, которых в файле ещё нет, пропускаются со счётом. Если файла нет вовсе, он собирается из пресета целиком. Заодно панель принимает строку из игровой кнопки «Поделиться» («Название:ModA;ModB;…»): она заменяет список включённых модов, сохраняя порядок загрузки. Идентификаторов мастерской в такой строке нет, поэтому пакеты добавляются только для модов, чьи файлы уже на диске, а про остальные панель предупреждает. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+61
-3
@@ -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();
|
||||
|
||||
+24
-2
@@ -300,7 +300,7 @@ const STRINGS = {
|
||||
"profiles.create": "Создать профиль",
|
||||
|
||||
"import.title": "Импорт пресета сборки",
|
||||
"import.fileHint": "ZIP-пакет сборки: панель возьмёт из него серверный .ini, настройки песочницы и точки появления, остальное пропустит.",
|
||||
"import.fileHint": "ZIP-пакет сборки или пресет песочницы .cfg. Из архива панель возьмёт серверный .ini, настройки песочницы и точки появления, остальное пропустит.",
|
||||
"import.reading": "Читаю архив…",
|
||||
"import.set": "Набор из архива",
|
||||
"import.server": "Сервер: {name}",
|
||||
@@ -325,6 +325,17 @@ const STRINGS = {
|
||||
"import.confirm": "Импортировать набор «{name}» в профиль {profile}?\n\nКонфиги профиля будут заменены, прежние останутся рядом с расширением .bak.",
|
||||
"import.done": "Пресет импортирован в профиль {profile}. Записано файлов: {files}.",
|
||||
"import.kept": "Свои значения взяты из профиля {profile}: {keys}",
|
||||
"import.sandboxPreset": "Пресет песочницы одиночной игры: {count} параметров",
|
||||
"import.sandboxPresetNote": "Значения перенесутся в песочницу профиля. Параметры модов, которых в ней ещё нет, пропускаются — такой пресет лучше импортировать после первого запуска с модами.",
|
||||
"import.applied": "Перенесено параметров: {applied}, пропущено: {skipped}",
|
||||
"mods.listTitle": "Импорт списка модов",
|
||||
"mods.listHint": "Строка из кнопки «Поделиться» в игре: «Название:ModA;ModB;…». Заменит список включённых модов — он сохранится по кнопке «Сохранить».",
|
||||
"mods.listPlaceholder": "Mymods:ModA;ModB;ModC",
|
||||
"mods.listApply": "Применить список",
|
||||
"mods.listEmpty": "В строке нет идентификаторов модов",
|
||||
"mods.listConfirm": "Заменить список включённых модов на {count} из вставленной строки?",
|
||||
"mods.listDone": "Список принят: модов {count}. Нажмите «Сохранить», чтобы записать его в конфиг.",
|
||||
"mods.listNoFiles": "Файлов не найдено у {count} модов. Чтобы сервер их скачал, добавьте коллекцию мастерской.",
|
||||
|
||||
"settings.versionTitle": "Версия панели",
|
||||
"settings.title": "Настройки менеджера",
|
||||
@@ -624,7 +635,7 @@ const STRINGS = {
|
||||
"profiles.create": "Create profile",
|
||||
|
||||
"import.title": "Import a collection preset",
|
||||
"import.fileHint": "A collection ZIP package: the panel takes the server .ini, the sandbox settings and the spawn regions out of it and skips the rest.",
|
||||
"import.fileHint": "A collection ZIP package or a .cfg sandbox preset. From the archive the panel takes the server .ini, the sandbox settings and the spawn regions, and skips the rest.",
|
||||
"import.reading": "Reading the archive…",
|
||||
"import.set": "Preset from the archive",
|
||||
"import.server": "Server: {name}",
|
||||
@@ -649,6 +660,17 @@ const STRINGS = {
|
||||
"import.confirm": "Import the preset \"{name}\" into the profile {profile}?\n\nThe profile configs will be replaced, the previous ones stay next to them with a .bak extension.",
|
||||
"import.done": "Preset imported into the profile {profile}. Files written: {files}.",
|
||||
"import.kept": "Your own values were taken from the profile {profile}: {keys}",
|
||||
"import.sandboxPreset": "Single-player sandbox preset: {count} settings",
|
||||
"import.sandboxPresetNote": "The values are transferred into the profile's sandbox. Mod settings that are not there yet are skipped — such a preset is best imported after the first start with the mods.",
|
||||
"import.applied": "Settings transferred: {applied}, skipped: {skipped}",
|
||||
"mods.listTitle": "Import a mod list",
|
||||
"mods.listHint": "The line from the game's Share button: \"Name:ModA;ModB;…\". It replaces the list of enabled mods — press Save to write it into the config.",
|
||||
"mods.listPlaceholder": "Mymods:ModA;ModB;ModC",
|
||||
"mods.listApply": "Apply the list",
|
||||
"mods.listEmpty": "The line has no mod identifiers",
|
||||
"mods.listConfirm": "Replace the list of enabled mods with the {count} from the pasted line?",
|
||||
"mods.listDone": "List accepted: {count} mods. Press Save to write it into the config.",
|
||||
"mods.listNoFiles": "No files found for {count} mods. Add the Workshop collection so the server downloads them.",
|
||||
|
||||
"settings.versionTitle": "Panel version",
|
||||
"settings.title": "Manager settings",
|
||||
|
||||
@@ -213,6 +213,14 @@
|
||||
|
||||
<ul class="mod-list" id="mod-list"></ul>
|
||||
|
||||
<h3 data-i18n="mods.listTitle"></h3>
|
||||
<p class="muted small" data-i18n="mods.listHint"></p>
|
||||
<form class="mod-add" id="mod-list-form">
|
||||
<textarea id="mod-list-input" rows="3" spellcheck="false"
|
||||
data-i18n-placeholder="mods.listPlaceholder"></textarea>
|
||||
<button class="btn" type="submit" data-i18n="mods.listApply"></button>
|
||||
</form>
|
||||
|
||||
<h3 data-i18n="mods.mapsTitle"></h3>
|
||||
<textarea id="map-list" rows="3" spellcheck="false" data-i18n-placeholder="mods.mapsPlaceholder"></textarea>
|
||||
<p class="muted small" data-i18n="mods.mapsHint"></p>
|
||||
@@ -258,7 +266,7 @@
|
||||
<h3 data-i18n="import.title"></h3>
|
||||
<div class="card import-card">
|
||||
<p class="muted small" data-i18n="import.fileHint"></p>
|
||||
<input type="file" id="preset-file" accept=".zip">
|
||||
<input type="file" id="preset-file" accept=".zip,.cfg">
|
||||
|
||||
<div id="preset-details" hidden>
|
||||
<div class="cfg-field" id="preset-pick" hidden>
|
||||
|
||||
@@ -330,6 +330,10 @@ input.input-sm, .input-sm { width: auto; padding: 6px 10px; font-size: 13px; }
|
||||
|
||||
.mod-add { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
|
||||
.mod-add input { flex: 1; min-width: 180px; }
|
||||
/* Списком модов делятся одной длинной строкой: поле должно быть шире поля ввода. */
|
||||
.mod-add textarea { flex: 1; min-width: 260px; resize: vertical; }
|
||||
/* Кнопка не должна растягиваться на всю высоту поля со списком. */
|
||||
#mod-list-form { align-items: flex-start; }
|
||||
|
||||
.mod-list { list-style: none; padding: 0; margin: 0 0 18px; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user