From ae8c511a9aecf33299d8b1cfb6b5b6d104f0efc6 Mon Sep 17 00:00:00 2001 From: mrleo1nid Date: Sun, 6 Sep 2026 20:21:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D0=BC=D0=BF=D0=BE=D1=80=D1=82=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=B5=D1=82=D0=B0=20=D0=BD=D0=B5=20=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=8F=D0=BB=20=D0=BC=D0=BE=D0=B4=D1=8B:=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9=20=D0=BD=D0=B0=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=20=D0=B8=20=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В пакете сборки рядом с комплектом сервера лежит пресет песочницы одиночной игры. Наборы сортировались по имени, и пресет песочницы часто оказывался первым — панель предлагала его по умолчанию, а модов и карт в нём нет. Теперь первым идёт набор с конфигом сервера, а в списке выбора видно, что внутри каждого. Вкладки показывают активный профиль, поэтому импорт в новый выглядел так, будто ничего не подтянулось. Появилась галочка «Сделать этот профиль активным»; если её снять, панель говорит, где искать импортированное. Co-Authored-By: Claude Opus 5 --- README.md | 13 +++++++++++-- internal/api/handlers_presets.go | 22 +++++++++++++++++++--- internal/preset/preset.go | 27 ++++++++++++++++++++++++++- internal/preset/preset_test.go | 22 ++++++++++++++++++++++ web/static/app.js | 25 ++++++++++++++++++++++--- web/static/i18n.js | 14 ++++++++++++++ web/static/index.html | 4 ++++ 7 files changed, 118 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d833ec1..7fc509c 100644 --- a/README.md +++ b/README.md @@ -193,8 +193,12 @@ curl -fsSL https://gitea.hsrv.site/mrleo1nid/pzmanager/raw/branch/main/deploy/ge На вкладке «Профили» есть «Импорт пресета сборки»: выберите ZIP, и панель покажет, что в нём нашла — название сервера, сколько модов и пакетов мастерской, какие карты, сколько параметров конфига и песочницы. Ничего не -записывается, пока вы не нажмёте «Импортировать». Если наборов в архиве -несколько, появится список выбора. +записывается, пока вы не нажмёте «Импортировать». + +Если наборов в архиве несколько, появится список выбора. Первым идёт самый +полный набор — тот, где есть конфиг сервера: только в нём лежат моды и карты. +Рядом с именем написано, что внутри, чтобы случайно не импортировать пресет +песочницы вместо комплекта сервера. Импортировать можно в новый профиль (идентификатор предлагается по имени файлов из архива) или поверх существующего. Прежние конфиги остаются рядом с @@ -202,6 +206,11 @@ curl -fsSL https://gitea.hsrv.site/mrleo1nid/pzmanager/raw/branch/main/deploy/ge импорт не принимает — сначала остановите сервер, иначе PZ перезапишет файлы своими при выходе. +Галочка «Сделать этот профиль активным» включена по умолчанию: вкладки «Моды» +и «Конфиг сервера» показывают активный профиль, и без переключения кажется, +что импорт ничего не сделал. Если галочку снять, панель напомнит, под каким +профилем искать импортированное. + Галочки «Оставить свои значения» переносят в импортированный конфиг то, что относится к вашей машине, а не к сборке: diff --git a/internal/api/handlers_presets.go b/internal/api/handlers_presets.go index 3c822c4..e000817 100644 --- a/internal/api/handlers_presets.go +++ b/internal/api/handlers_presets.go @@ -52,6 +52,7 @@ func (s *Server) handleImportPreset(w http.ResponseWriter, r *http.Request) { } createNew := r.FormValue("target") != "existing" + activate := r.FormValue("activate") == "1" id := strings.TrimSpace(r.FormValue("profile")) if id == "" && createNew { id = config.SanitizeProfileID(files.Name) @@ -68,8 +69,9 @@ func (s *Server) handleImportPreset(w http.ResponseWriter, r *http.Request) { } // Живой сервер держит мир и конфиг в памяти и переписывает файлы при - // остановке — импорт под ним просто пропал бы. - if id == s.cfg.Active && s.sup.State() != pzserver.StateStopped { + // остановке — импорт под ним просто пропал бы. Переключать профиль на + // живом сервере нельзя по той же причине. + if s.sup.State() != pzserver.StateStopped && (id == s.cfg.Active || activate) { writeError(w, r, http.StatusConflict, "сначала остановите игровой сервер") return } @@ -94,7 +96,17 @@ func (s *Server) handleImportPreset(w http.ResponseWriter, r *http.Request) { writeError(w, r, http.StatusInternalServerError, err.Error()) return } - if createNew { + // Вкладки панели показывают активный профиль: без переключения импорт + // выглядит так, будто моды и настройки не подтянулись. + switched := false + if activate && s.cfg.Active != id { + if err := s.cfg.SetActive(id); err != nil { + writeError(w, r, http.StatusInternalServerError, err.Error()) + return + } + switched = true + } + if createNew || switched { if err := s.cfg.Save(); err != nil { writeError(w, r, http.StatusInternalServerError, err.Error()) return @@ -104,9 +116,13 @@ func (s *Server) handleImportPreset(w http.ResponseWriter, r *http.Request) { summary := files.Describe() s.sup.AppendLog("manager", fmt.Sprintf("Импортирован пресет %q в профиль %q (модов: %d)", files.Name, id, summary.Mods)) + if switched { + s.sup.AppendLog("manager", fmt.Sprintf("Активный профиль: %s", s.cfg.ActiveProfile().Name())) + } result["profile"] = id result["created"] = createNew + result["switched"] = switched result["summary"] = summary writeJSON(w, http.StatusOK, result) } diff --git a/internal/preset/preset.go b/internal/preset/preset.go index 9443295..0425160 100644 --- a/internal/preset/preset.go +++ b/internal/preset/preset.go @@ -160,10 +160,35 @@ func FromZip(data []byte) ([]Files, error) { if len(out) == 0 { return nil, ErrNoServerFiles } - sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + // Первым идёт самый полный набор: панель предлагает его по умолчанию, а + // одинокий пресет песочницы переносит только значения песочницы — ни модов, + // ни карт в нём нет, и импорт выглядит как «ничего не подтянулось». + sort.Slice(out, func(i, j int) bool { + wi, wj := weight(out[i]), weight(out[j]) + if wi != wj { + return wi > wj + } + return out[i].Name < out[j].Name + }) return out, nil } +// weight — насколько набор полон. Конфиг сервера перевешивает всё остальное: +// только в нём есть список модов, карты и настройки самого сервера. +func weight(f Files) int { + total := 0 + if f.INI != nil { + total += 4 + } + if f.Sandbox != nil { + total += 2 + } + if f.SpawnRegions != nil { + total++ + } + return total +} + type kind int const ( diff --git a/internal/preset/preset_test.go b/internal/preset/preset_test.go index 5304e82..0d222b7 100644 --- a/internal/preset/preset_test.go +++ b/internal/preset/preset_test.go @@ -192,3 +192,25 @@ func TestFromZipRejectsGarbage(t *testing.T) { t.Fatal("мусор принят за архив") } } + +// В пакете сборки рядом с конфигом сервера лежит и пресет песочницы для +// одиночной игры. Первым должен идти набор с конфигом: панель предлагает его по +// умолчанию, а из пресета песочницы моды и карты не возьмутся. +func TestFromZipServerSetGoesFirst(t *testing.T) { + data := buildZip(t, map[string]string{ + // Имя пресета нарочно сортируется раньше по алфавиту. + "Sandbox Presets/Alpha-Sandbox.cfg": sandboxCFG, + "Server/zulu-server.ini": serverINI, + "Server/zulu-server_SandboxVars.lua": sandboxLua, + }) + sets, err := FromZip(data) + if err != nil { + t.Fatal(err) + } + if len(sets) != 2 { + t.Fatalf("наборов: %d", len(sets)) + } + if sets[0].INI == nil { + t.Fatalf("первым идёт набор без конфига сервера: %q", sets[0].Name) + } +} diff --git a/web/static/app.js b/web/static/app.js index ad198e5..420938b 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1664,7 +1664,14 @@ function renderPresetSets() { const picker = $("#preset-set"); picker.innerHTML = presetSets - .map((set) => ``) + .map((set) => { + // Подпись сразу говорит, что в наборе: пресет песочницы без конфига + // сервера не принесёт ни модов, ни карт. + const note = set.has_ini + ? t("import.setServer", { mods: set.mods }) + : t("import.setSandbox"); + return ``; + }) .join(""); // Выбор нужен, только когда в архиве несколько наборов. $("#preset-pick").hidden = presetSets.length < 2; @@ -1736,7 +1743,11 @@ $("#btn-preset-import").addEventListener("click", async () => { const existing = $("#preset-target").value === "existing"; const profile = existing ? $("#preset-profile").value : $("#preset-id").value.trim(); - if (!confirm(t("import.confirm", { name: set.name, profile }))) return; + const activate = $("#preset-activate").checked; + // Смена активного профиля — заметное действие: о нём предупреждаем заранее. + const question = t("import.confirm", { name: set.name, profile }) + + (activate && profile !== profiles.active ? "\n\n" + t("import.confirmActivate", { profile }) : ""); + if (!confirm(question)) return; const form = new FormData(); form.append("file", file); @@ -1748,6 +1759,7 @@ $("#btn-preset-import").addEventListener("click", async () => { .filter((box) => box.checked) .map((box) => box.dataset.keep) .join(",")); + form.append("activate", activate ? "1" : ""); const button = $("#btn-preset-import"); button.disabled = true; @@ -1760,10 +1772,17 @@ $("#btn-preset-import").addEventListener("click", async () => { if (result.applied !== undefined) { toast(t("import.applied", { applied: result.applied, skipped: result.skipped })); } + if (result.switched) { + toast(t("import.switched", { profile: result.profile })); + } else if (result.profile !== profiles.active) { + // Вкладки показывают активный профиль: без этого импорт выглядит так, + // будто моды и настройки никуда не попали. + toast(t("import.notActive", { profile: result.profile })); + } $("#preset-file").value = ""; $("#preset-details").hidden = true; await loadProfiles(); - // Импортированный профиль мог оказаться активным — вкладки показывают + // Импортированный профиль мог стать активным — вкладки показывают // именно его конфиг. loadTab(currentTab); } catch (error) { diff --git a/web/static/i18n.js b/web/static/i18n.js index 75d538d..710a7df 100644 --- a/web/static/i18n.js +++ b/web/static/i18n.js @@ -303,6 +303,8 @@ const STRINGS = { "import.fileHint": "ZIP-пакет сборки или пресет песочницы .cfg. Из архива панель возьмёт серверный .ini, настройки песочницы и точки появления, остальное пропустит.", "import.reading": "Читаю архив…", "import.set": "Набор из архива", + "import.setServer": "конфиг сервера, модов: {mods}", + "import.setSandbox": "только пресет песочницы, без модов", "import.server": "Сервер: {name}", "import.mods": "Моды: {mods} · пакеты мастерской: {workshop}", "import.maps": "Карты: {maps}", @@ -320,6 +322,11 @@ const STRINGS = { "import.keepPorts": "Порты и RCON", "import.keepAccess": "Пароль сервера и объявляемый IP", "import.keepIdentity": "Имя и описание сервера", + "import.activate": "Сделать этот профиль активным", + "import.confirmActivate": "Профиль {profile} станет активным — панель и запуск переключатся на него.", + "import.activateHint": "Вкладки «Моды» и «Настройки» показывают активный профиль. Без переключения импортированные моды там не появятся.", + "import.switched": "Активный профиль: {profile}", + "import.notActive": "Профиль {profile} не активен — импортированные моды и настройки видны только под ним. Переключиться можно на вкладке «Профили».", "import.apply": "Импортировать", "import.noFile": "Выберите ZIP-пакет сборки", "import.confirm": "Импортировать набор «{name}» в профиль {profile}?\n\nКонфиги профиля будут заменены, прежние останутся рядом с расширением .bak.", @@ -638,6 +645,8 @@ const STRINGS = { "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.setServer": "server config, mods: {mods}", + "import.setSandbox": "sandbox preset only, no mods", "import.server": "Server: {name}", "import.mods": "Mods: {mods} · Workshop items: {workshop}", "import.maps": "Maps: {maps}", @@ -655,6 +664,11 @@ const STRINGS = { "import.keepPorts": "Ports and RCON", "import.keepAccess": "Server password and announced IP", "import.keepIdentity": "Server name and description", + "import.activate": "Make this profile active", + "import.confirmActivate": "The profile {profile} will become active — the panel and the server start will switch to it.", + "import.activateHint": "The Mods and Settings tabs show the active profile. Without switching, the imported mods will not appear there.", + "import.switched": "Active profile: {profile}", + "import.notActive": "The profile {profile} is not active — the imported mods and settings are only visible under it. You can switch on the Profiles tab.", "import.apply": "Import", "import.noFile": "Choose a collection ZIP package", "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.", diff --git a/web/static/index.html b/web/static/index.html index f9fbf75..93d1fbe 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -306,6 +306,10 @@ + +
+