Импорт пресета песочницы .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
+55 -21
View File
@@ -32,28 +32,35 @@ const (
// ErrNoServerFiles возвращается, когда в архиве нет ни одного серверного файла.
var ErrNoServerFiles = errors.New("в архиве нет серверных файлов Project Zomboid")
// ErrNoSandboxPreset — загруженный .cfg не похож на пресет песочницы.
var ErrNoSandboxPreset = errors.New("файл не похож на пресет песочницы")
// Files — серверная часть пакета: то, что PZ читает при запуске.
type Files struct {
// Name — имя, под которым файлы лежали в архиве (sophie-server-42-2).
Name string
INI []byte
Sandbox []byte
SpawnRegions []byte
Name string
INI []byte
Sandbox []byte
// SandboxPreset — пресет песочницы одиночной игры (.cfg). Готового файла
// сервера в пакете может не быть: сборки нередко раздают только его.
SandboxPreset []byte
SpawnRegions []byte
}
// Summary — то, что панель показывает до импорта, чтобы человек понимал, что
// именно он собирается записать поверх своих настроек.
type Summary struct {
Name string `json:"name"`
ServerName string `json:"server_name"`
Maps []string `json:"maps"`
Mods int `json:"mods"`
WorkshopItems int `json:"workshop_items"`
Settings int `json:"settings"`
SandboxSettings int `json:"sandbox_settings"`
HasINI bool `json:"has_ini"`
HasSandbox bool `json:"has_sandbox"`
HasSpawnRegions bool `json:"has_spawn_regions"`
Name string `json:"name"`
ServerName string `json:"server_name"`
Maps []string `json:"maps"`
Mods int `json:"mods"`
WorkshopItems int `json:"workshop_items"`
Settings int `json:"settings"`
SandboxSettings int `json:"sandbox_settings"`
HasINI bool `json:"has_ini"`
HasSandbox bool `json:"has_sandbox"`
HasSandboxPreset bool `json:"has_sandbox_preset"`
HasSpawnRegions bool `json:"has_spawn_regions"`
}
// markers — параметры, по которым .ini опознаётся как конфиг сервера. В
@@ -67,6 +74,19 @@ var markers = []string{
const markersRequired = 3
// FromUpload разбирает то, что загрузили в панель: ZIP-пакет сборки или
// одиночный пресет песочницы.
func FromUpload(filename string, data []byte) ([]Files, error) {
if strings.HasSuffix(strings.ToLower(filename), ".cfg") {
if !looksLikeSandboxPreset(data) {
return nil, ErrNoSandboxPreset
}
name := strings.TrimSuffix(path.Base(filename), path.Ext(filename))
return []Files{{Name: name, SandboxPreset: data}}, nil
}
return FromZip(data)
}
// FromZip достаёт из архива все наборы серверных файлов. Наборов может быть
// несколько: сборки иногда кладут рядом варианты конфигов.
func FromZip(data []byte) ([]Files, error) {
@@ -106,6 +126,11 @@ func FromZip(data []byte) ([]Files, error) {
if kind == kindINI && !looksLikeServerConfig(content) {
continue
}
// .cfg в архиве хватает и помимо песочницы: списки модов, настройки
// папок. Отличаем по знакомым параметрам.
if kind == kindSandboxPreset && !looksLikeSandboxPreset(content) {
continue
}
set := sets[base]
if set == nil {
@@ -117,6 +142,8 @@ func FromZip(data []byte) ([]Files, error) {
set.INI = content
case kindSandbox:
set.Sandbox = content
case kindSandboxPreset:
set.SandboxPreset = content
case kindSpawnRegions:
set.SpawnRegions = content
}
@@ -125,7 +152,7 @@ func FromZip(data []byte) ([]Files, error) {
out := make([]Files, 0, len(sets))
for _, set := range sets {
// Одни точки появления без конфига и песочницы импортировать некуда.
if set.INI == nil && set.Sandbox == nil {
if set.INI == nil && set.Sandbox == nil && set.SandboxPreset == nil {
continue
}
out = append(out, *set)
@@ -143,6 +170,7 @@ const (
kindOther kind = iota
kindINI
kindSandbox
kindSandboxPreset
kindSpawnRegions
)
@@ -156,6 +184,8 @@ func classify(name string) (string, kind) {
return name[:len(name)-len("_spawnregions.lua")], kindSpawnRegions
case strings.HasSuffix(lower, ".ini"):
return name[:len(name)-len(".ini")], kindINI
case strings.HasSuffix(lower, ".cfg"):
return name[:len(name)-len(".cfg")], kindSandboxPreset
}
return "", kindOther
}
@@ -193,11 +223,12 @@ func readEntry(entry *zip.File) ([]byte, error) {
// Describe собирает сводку по набору: сколько модов, карт и параметров.
func (f Files) Describe() Summary {
s := Summary{
Name: f.Name,
HasINI: f.INI != nil,
HasSandbox: f.Sandbox != nil,
HasSpawnRegions: f.SpawnRegions != nil,
Maps: []string{},
Name: f.Name,
HasINI: f.INI != nil,
HasSandbox: f.Sandbox != nil,
HasSandboxPreset: f.SandboxPreset != nil,
HasSpawnRegions: f.SpawnRegions != nil,
Maps: []string{},
}
if f.INI != nil {
ini := pzconfig.ParseINI(f.INI)
@@ -207,8 +238,11 @@ func (f Files) Describe() Summary {
s.Maps = splitList(ini.Get("Map"))
s.Settings = len(ini.Keys())
}
if f.Sandbox != nil {
switch {
case f.Sandbox != nil:
s.SandboxSettings = len(pzconfig.ParseSandbox(f.Sandbox).Entries())
case f.SandboxPreset != nil:
s.SandboxSettings = len(ParseSandboxPreset(f.SandboxPreset).Values)
}
return s
}