Профили серверов: несколько миров с раздельными настройками
Профиль — это отдельный сервер: свой мир, .ini, песочница, набор модов и лимит памяти JVM. Project Zomboid различает их по -servername, поэтому файлы профилей не пересекаются. Работает всегда один профиль: серверные файлы, игровые порты и файл запуска JVM общие, так что переключение разрешено только на остановленном сервере. - config: список профилей вместо одиночных server_name/admin_password/ java_memory; старые поля переносятся в первый профиль при загрузке; - supervisor: запускает активный профиль и выставляет его -Xmx на каждом старте, потому что файл запуска общий; - бэкапы: архив снимается с одного профиля (мир + его конфиги), имя профиля попадает в имя файла, ротация считается по профилям; - API профилей: список, создание с копированием настроек из другого профиля, правка, удаление и переключение активного; - панель: селектор профиля в шапке и вкладка со списком профилей. Удаление профиля убирает его только из панели: мир и конфиги остаются на диске, чтобы случайное нажатие не стоило сохранений. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+238
-27
@@ -5,12 +5,56 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config — всё, что менеджеру нужно знать о хосте и о сервере PZ.
|
||||
// profileIDRe ограничивает идентификатор профиля: он становится именем файлов
|
||||
// PZ (servertest.ini, servertest_SandboxVars.lua) и частью пути к миру.
|
||||
var profileIDRe = regexp.MustCompile(`^[A-Za-z0-9_-]{1,32}$`)
|
||||
|
||||
// Profile — один игровой сервер: собственный мир, конфиг, набор модов и
|
||||
// параметры запуска. ID профиля — это -servername для Project Zomboid.
|
||||
type Profile struct {
|
||||
// ID — имя набора конфигов PZ: servertest -> servertest.ini, мир лежит в
|
||||
// Saves/Multiplayer/servertest.
|
||||
ID string `yaml:"id"`
|
||||
|
||||
// Title — понятное человеку название для веб-панели.
|
||||
Title string `yaml:"title"`
|
||||
|
||||
// AdminPassword — пароль админа игрового сервера, нужен при первом
|
||||
// запуске PZ. Хранится здесь, чтобы не вводить его в консоли.
|
||||
AdminPassword string `yaml:"admin_password"`
|
||||
|
||||
// JavaMemory — -Xmx для JVM, например "8g". Применяется при каждом
|
||||
// запуске: файл запуска сервера общий для всех профилей.
|
||||
JavaMemory string `yaml:"java_memory"`
|
||||
|
||||
// ExtraArgs — дополнительные аргументы start-server.sh.
|
||||
ExtraArgs []string `yaml:"extra_args"`
|
||||
}
|
||||
|
||||
// Name возвращает название для интерфейса: заголовок, а если его нет — ID.
|
||||
func (p Profile) Name() string {
|
||||
if p.Title != "" {
|
||||
return p.Title
|
||||
}
|
||||
return p.ID
|
||||
}
|
||||
|
||||
// ValidateProfileID проверяет идентификатор, приходящий из веб-панели.
|
||||
func ValidateProfileID(id string) error {
|
||||
if !profileIDRe.MatchString(id) {
|
||||
return fmt.Errorf("идентификатор %q недопустим: разрешены латинские буквы, цифры, дефис и подчёркивание (до 32 символов)", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config — всё, что менеджеру нужно знать о хосте и о серверах PZ.
|
||||
type Config struct {
|
||||
// Listen — адрес веб-панели. Для доступа только через SSH-туннель
|
||||
// используйте 127.0.0.1:8080.
|
||||
@@ -19,7 +63,8 @@ type Config struct {
|
||||
// DataDir — рабочая директория менеджера (пользователи, сессии, бэкапы).
|
||||
DataDir string `yaml:"data_dir"`
|
||||
|
||||
// ServerDir — куда SteamCMD ставит серверные файлы PZ.
|
||||
// ServerDir — куда SteamCMD ставит серверные файлы PZ. Общий для всех
|
||||
// профилей: сама игра одна, различаются только конфиги и миры.
|
||||
ServerDir string `yaml:"server_dir"`
|
||||
|
||||
// ZomboidDir — ~/Zomboid: сохранения, конфиги, логи.
|
||||
@@ -28,18 +73,12 @@ type Config struct {
|
||||
// SteamCMDPath — путь к бинарю steamcmd.sh (пустой = ищем в PATH).
|
||||
SteamCMDPath string `yaml:"steamcmd_path"`
|
||||
|
||||
// ServerName — имя набора конфигов PZ (servertest -> servertest.ini).
|
||||
ServerName string `yaml:"server_name"`
|
||||
// Profiles — сконфигурированные серверы. Одновременно работает только
|
||||
// один: они делят серверные файлы и игровые порты.
|
||||
Profiles []Profile `yaml:"profiles"`
|
||||
|
||||
// AdminPassword — пароль админа игрового сервера, нужен при первом
|
||||
// запуске PZ. Хранится здесь, чтобы не вводить его в консоли.
|
||||
AdminPassword string `yaml:"admin_password"`
|
||||
|
||||
// JavaMemory — -Xmx для JVM сервера, например "8g".
|
||||
JavaMemory string `yaml:"java_memory"`
|
||||
|
||||
// ExtraArgs — дополнительные аргументы запуска сервера.
|
||||
ExtraArgs []string `yaml:"extra_args"`
|
||||
// Active — ID профиля, который запускается и настраивается сейчас.
|
||||
Active string `yaml:"active_profile"`
|
||||
|
||||
// AutoStart — поднимать игровой сервер сразу при старте менеджера.
|
||||
AutoStart bool `yaml:"autostart"`
|
||||
@@ -56,6 +95,13 @@ type Config struct {
|
||||
// Backup — настройки резервного копирования.
|
||||
Backup BackupConfig `yaml:"backup"`
|
||||
|
||||
// Поля ниже остались от версии с единственным сервером. При загрузке они
|
||||
// превращаются в первый профиль и больше не записываются в файл.
|
||||
LegacyServerName string `yaml:"server_name,omitempty"`
|
||||
LegacyAdminPassword string `yaml:"admin_password,omitempty"`
|
||||
LegacyJavaMemory string `yaml:"java_memory,omitempty"`
|
||||
LegacyExtraArgs []string `yaml:"extra_args,omitempty"`
|
||||
|
||||
path string
|
||||
}
|
||||
|
||||
@@ -65,13 +111,23 @@ type BackupConfig struct {
|
||||
Dir string `yaml:"dir"`
|
||||
// Schedule — интервал автобэкапа; 0 отключает автоматику.
|
||||
Schedule Duration `yaml:"schedule"`
|
||||
// Keep — сколько последних архивов хранить (0 = не удалять).
|
||||
// Keep — сколько последних архивов хранить для каждого профиля
|
||||
// (0 = не удалять).
|
||||
Keep int `yaml:"keep"`
|
||||
// StopServer — останавливать сервер на время бэкапа. Безопаснее, но
|
||||
// игроков выкидывает; по умолчанию выключено.
|
||||
StopServer bool `yaml:"stop_server"`
|
||||
}
|
||||
|
||||
// DefaultProfile — профиль, который создаётся на чистой установке.
|
||||
func DefaultProfile() Profile {
|
||||
return Profile{
|
||||
ID: "servertest",
|
||||
Title: "Основной сервер",
|
||||
JavaMemory: "4g",
|
||||
}
|
||||
}
|
||||
|
||||
// Default возвращает конфиг с разумными значениями для Ubuntu.
|
||||
func Default() *Config {
|
||||
home, _ := os.UserHomeDir()
|
||||
@@ -80,8 +136,8 @@ func Default() *Config {
|
||||
DataDir: filepath.Join(home, ".local", "share", "pzmanager"),
|
||||
ServerDir: filepath.Join(home, "pzserver"),
|
||||
ZomboidDir: filepath.Join(home, "Zomboid"),
|
||||
ServerName: "servertest",
|
||||
JavaMemory: "4g",
|
||||
Profiles: []Profile{DefaultProfile()},
|
||||
Active: DefaultProfile().ID,
|
||||
AutoRestart: true,
|
||||
StopTimeout: Duration(90 * time.Second),
|
||||
LogBufferLines: 5000,
|
||||
@@ -109,13 +165,58 @@ func Load(path string) (*Config, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("прочитать конфиг %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Профили из файла должны заменить умолчания, а не дополнить их.
|
||||
cfg.Profiles = nil
|
||||
if err := yaml.Unmarshal(raw, cfg); err != nil {
|
||||
return nil, fmt.Errorf("разобрать конфиг %s: %w", path, err)
|
||||
}
|
||||
|
||||
migrated := cfg.migrateLegacy()
|
||||
cfg.applyFallbacks()
|
||||
if migrated {
|
||||
// Сразу переписываем файл, чтобы старые поля не остались висеть
|
||||
// рядом с новым списком профилей и не путали при правке руками.
|
||||
if err := cfg.Save(); err != nil {
|
||||
return nil, fmt.Errorf("обновить конфиг %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// migrateLegacy превращает настройки единственного сервера в первый профиль.
|
||||
// Возвращает true, если конфиг пришлось поменять.
|
||||
func (c *Config) migrateLegacy() bool {
|
||||
hasLegacy := c.LegacyServerName != "" || c.LegacyAdminPassword != "" ||
|
||||
c.LegacyJavaMemory != "" || len(c.LegacyExtraArgs) > 0
|
||||
if !hasLegacy {
|
||||
return false
|
||||
}
|
||||
|
||||
profile := Profile{
|
||||
ID: c.LegacyServerName,
|
||||
AdminPassword: c.LegacyAdminPassword,
|
||||
JavaMemory: c.LegacyJavaMemory,
|
||||
ExtraArgs: c.LegacyExtraArgs,
|
||||
}
|
||||
if profile.ID == "" {
|
||||
profile.ID = DefaultProfile().ID
|
||||
}
|
||||
profile.Title = "Основной сервер"
|
||||
|
||||
// Если профили уже описаны, старые поля просто отбрасываем: список
|
||||
// профилей — источник истины.
|
||||
if len(c.Profiles) == 0 {
|
||||
c.Profiles = []Profile{profile}
|
||||
c.Active = profile.ID
|
||||
}
|
||||
c.LegacyServerName = ""
|
||||
c.LegacyAdminPassword = ""
|
||||
c.LegacyJavaMemory = ""
|
||||
c.LegacyExtraArgs = nil
|
||||
return true
|
||||
}
|
||||
|
||||
// applyFallbacks подставляет умолчания вместо пустых значений: конфиг,
|
||||
// написанный руками, редко заполнен целиком.
|
||||
func (c *Config) applyFallbacks() {
|
||||
@@ -132,12 +233,6 @@ func (c *Config) applyFallbacks() {
|
||||
if c.ZomboidDir == "" {
|
||||
c.ZomboidDir = d.ZomboidDir
|
||||
}
|
||||
if c.ServerName == "" {
|
||||
c.ServerName = d.ServerName
|
||||
}
|
||||
if c.JavaMemory == "" {
|
||||
c.JavaMemory = d.JavaMemory
|
||||
}
|
||||
if c.StopTimeout <= 0 {
|
||||
c.StopTimeout = d.StopTimeout
|
||||
}
|
||||
@@ -147,6 +242,102 @@ func (c *Config) applyFallbacks() {
|
||||
if c.Backup.Dir == "" {
|
||||
c.Backup.Dir = d.Backup.Dir
|
||||
}
|
||||
if len(c.Profiles) == 0 {
|
||||
c.Profiles = []Profile{DefaultProfile()}
|
||||
}
|
||||
for i := range c.Profiles {
|
||||
if c.Profiles[i].JavaMemory == "" {
|
||||
c.Profiles[i].JavaMemory = DefaultProfile().JavaMemory
|
||||
}
|
||||
}
|
||||
// Активный профиль мог быть удалён из файла руками.
|
||||
if _, ok := c.Profile(c.Active); !ok {
|
||||
c.Active = c.Profiles[0].ID
|
||||
}
|
||||
}
|
||||
|
||||
// Profile возвращает профиль по идентификатору.
|
||||
func (c *Config) Profile(id string) (Profile, bool) {
|
||||
for _, p := range c.Profiles {
|
||||
if p.ID == id {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return Profile{}, false
|
||||
}
|
||||
|
||||
// ActiveProfile возвращает профиль, который сейчас запускается и правится.
|
||||
func (c *Config) ActiveProfile() Profile {
|
||||
if p, ok := c.Profile(c.Active); ok {
|
||||
return p
|
||||
}
|
||||
// До сюда доходить не должно: applyFallbacks чинит ссылку на активный
|
||||
// профиль, а список никогда не бывает пустым.
|
||||
if len(c.Profiles) > 0 {
|
||||
return c.Profiles[0]
|
||||
}
|
||||
return DefaultProfile()
|
||||
}
|
||||
|
||||
// AddProfile добавляет новый профиль.
|
||||
func (c *Config) AddProfile(p Profile) error {
|
||||
if err := ValidateProfileID(p.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, exists := c.Profile(p.ID); exists {
|
||||
return fmt.Errorf("профиль %q уже существует", p.ID)
|
||||
}
|
||||
if p.JavaMemory == "" {
|
||||
p.JavaMemory = DefaultProfile().JavaMemory
|
||||
}
|
||||
p.Title = strings.TrimSpace(p.Title)
|
||||
c.Profiles = append(c.Profiles, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProfile заменяет настройки существующего профиля. Идентификатор
|
||||
// профиля неизменен: от него зависят имена файлов конфигов и путь к миру.
|
||||
func (c *Config) UpdateProfile(id string, p Profile) error {
|
||||
for i := range c.Profiles {
|
||||
if c.Profiles[i].ID != id {
|
||||
continue
|
||||
}
|
||||
p.ID = id
|
||||
if p.JavaMemory == "" {
|
||||
p.JavaMemory = c.Profiles[i].JavaMemory
|
||||
}
|
||||
p.Title = strings.TrimSpace(p.Title)
|
||||
c.Profiles[i] = p
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("профиль %q не найден", id)
|
||||
}
|
||||
|
||||
// DeleteProfile убирает профиль из конфига. Файлы мира при этом остаются на
|
||||
// диске: удалять чужие сохранения по нажатию кнопки в панели слишком опасно.
|
||||
func (c *Config) DeleteProfile(id string) error {
|
||||
if len(c.Profiles) == 1 {
|
||||
return fmt.Errorf("нельзя удалить единственный профиль")
|
||||
}
|
||||
if id == c.Active {
|
||||
return fmt.Errorf("нельзя удалить активный профиль — сначала переключитесь на другой")
|
||||
}
|
||||
for i, p := range c.Profiles {
|
||||
if p.ID == id {
|
||||
c.Profiles = append(c.Profiles[:i], c.Profiles[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("профиль %q не найден", id)
|
||||
}
|
||||
|
||||
// SetActive переключает активный профиль.
|
||||
func (c *Config) SetActive(id string) error {
|
||||
if _, ok := c.Profile(id); !ok {
|
||||
return fmt.Errorf("профиль %q не найден", id)
|
||||
}
|
||||
c.Active = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save записывает конфиг атомарно: сначала во временный файл рядом, затем
|
||||
@@ -185,17 +376,37 @@ func (c *Config) EnsureDirs() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerConfigPath — путь к servertest.ini (или другому имени сервера).
|
||||
// ServerConfigPathFor — путь к <profile>.ini.
|
||||
func (c *Config) ServerConfigPathFor(profileID string) string {
|
||||
return filepath.Join(c.ZomboidDir, "Server", profileID+".ini")
|
||||
}
|
||||
|
||||
// SandboxConfigPathFor — путь к <profile>_SandboxVars.lua.
|
||||
func (c *Config) SandboxConfigPathFor(profileID string) string {
|
||||
return filepath.Join(c.ZomboidDir, "Server", profileID+"_SandboxVars.lua")
|
||||
}
|
||||
|
||||
// WorldDirFor — директория мира профиля.
|
||||
func (c *Config) WorldDirFor(profileID string) string {
|
||||
return filepath.Join(c.ZomboidDir, "Saves", "Multiplayer", profileID)
|
||||
}
|
||||
|
||||
// ServerConfigPath — конфиг активного профиля.
|
||||
func (c *Config) ServerConfigPath() string {
|
||||
return filepath.Join(c.ZomboidDir, "Server", c.ServerName+".ini")
|
||||
return c.ServerConfigPathFor(c.ActiveProfile().ID)
|
||||
}
|
||||
|
||||
// SandboxConfigPath — путь к <name>_SandboxVars.lua.
|
||||
// SandboxConfigPath — настройки песочницы активного профиля.
|
||||
func (c *Config) SandboxConfigPath() string {
|
||||
return filepath.Join(c.ZomboidDir, "Server", c.ServerName+"_SandboxVars.lua")
|
||||
return c.SandboxConfigPathFor(c.ActiveProfile().ID)
|
||||
}
|
||||
|
||||
// SavesDir — директория с мирами (то, что бэкапим).
|
||||
// ServerDirPath — директория с серверными конфигами всех профилей.
|
||||
func (c *Config) ServerDirPath() string {
|
||||
return filepath.Join(c.ZomboidDir, "Server")
|
||||
}
|
||||
|
||||
// SavesDir — директория со всеми мирами.
|
||||
func (c *Config) SavesDir() string {
|
||||
return filepath.Join(c.ZomboidDir, "Saves")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLoadMigratesSingleServerConfig проверяет обновление с версии, где сервер
|
||||
// был один: его настройки должны стать первым профилем, а старые поля —
|
||||
// исчезнуть из файла.
|
||||
func TestLoadMigratesSingleServerConfig(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
legacy := strings.Join([]string{
|
||||
"listen: 127.0.0.1:8080",
|
||||
"server_name: myworld",
|
||||
"admin_password: hunter2",
|
||||
"java_memory: 8g",
|
||||
"extra_args:",
|
||||
" - -debug",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(path, []byte(legacy), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(cfg.Profiles) != 1 {
|
||||
t.Fatalf("получено %d профилей, ожидался 1: %+v", len(cfg.Profiles), cfg.Profiles)
|
||||
}
|
||||
p := cfg.Profiles[0]
|
||||
if p.ID != "myworld" || p.AdminPassword != "hunter2" || p.JavaMemory != "8g" {
|
||||
t.Errorf("настройки не перенесены в профиль: %+v", p)
|
||||
}
|
||||
if len(p.ExtraArgs) != 1 || p.ExtraArgs[0] != "-debug" {
|
||||
t.Errorf("extra_args не перенесены: %v", p.ExtraArgs)
|
||||
}
|
||||
if cfg.Active != "myworld" {
|
||||
t.Errorf("активный профиль = %q", cfg.Active)
|
||||
}
|
||||
|
||||
// Файл должен быть переписан без устаревших полей.
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
saved := string(raw)
|
||||
for _, key := range []string{"server_name:", "admin_password:", "java_memory:", "extra_args:"} {
|
||||
if strings.Contains(saved, "\n"+key) {
|
||||
t.Errorf("в файле осталось устаревшее поле %s:\n%s", key, saved)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(saved, "active_profile: myworld") {
|
||||
t.Errorf("активный профиль не записан:\n%s", saved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCreatesDefaultProfile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cfg.Profiles) != 1 || cfg.Profiles[0].ID != "servertest" {
|
||||
t.Fatalf("профиль по умолчанию не создан: %+v", cfg.Profiles)
|
||||
}
|
||||
if cfg.ActiveProfile().ID != "servertest" {
|
||||
t.Errorf("активный профиль = %q", cfg.ActiveProfile().ID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadRepairsDanglingActive: активный профиль могли удалить руками из
|
||||
// файла, панель не должна из-за этого падать.
|
||||
func TestLoadRepairsDanglingActive(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
raw := strings.Join([]string{
|
||||
"active_profile: удалённый",
|
||||
"profiles:",
|
||||
" - id: vanilla",
|
||||
" title: Ванилла",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Active != "vanilla" {
|
||||
t.Errorf("активный профиль = %q, ожидался vanilla", cfg.Active)
|
||||
}
|
||||
if cfg.Profiles[0].JavaMemory == "" {
|
||||
t.Error("профилю без java_memory не подставлено умолчание")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileCRUD(t *testing.T) {
|
||||
cfg := Default()
|
||||
|
||||
if err := cfg.AddProfile(Profile{ID: "modded", Title: "С модами"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.AddProfile(Profile{ID: "modded"}); err == nil {
|
||||
t.Error("дубликат профиля принят")
|
||||
}
|
||||
if err := cfg.AddProfile(Profile{ID: "плохой id"}); err == nil {
|
||||
t.Error("недопустимый идентификатор принят")
|
||||
}
|
||||
|
||||
if err := cfg.DeleteProfile("servertest"); err == nil {
|
||||
t.Error("активный профиль удалён без переключения")
|
||||
}
|
||||
if err := cfg.SetActive("modded"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.DeleteProfile("servertest"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.DeleteProfile("modded"); err == nil {
|
||||
t.Error("удалён единственный оставшийся профиль")
|
||||
}
|
||||
if err := cfg.SetActive("несуществующий"); err == nil {
|
||||
t.Error("активирован несуществующий профиль")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateProfileKeepsID(t *testing.T) {
|
||||
cfg := Default()
|
||||
err := cfg.UpdateProfile("servertest", Profile{ID: "другой", Title: "Переименован", JavaMemory: "16g"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := cfg.ActiveProfile()
|
||||
if p.ID != "servertest" {
|
||||
t.Errorf("идентификатор профиля изменился: %q", p.ID)
|
||||
}
|
||||
if p.Title != "Переименован" || p.JavaMemory != "16g" {
|
||||
t.Errorf("настройки не сохранены: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilePaths(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.ZomboidDir = filepath.Join("/home", "pz", "Zomboid")
|
||||
|
||||
if got := cfg.ServerConfigPathFor("modded"); filepath.Base(got) != "modded.ini" {
|
||||
t.Errorf("конфиг профиля: %s", got)
|
||||
}
|
||||
if got := cfg.SandboxConfigPathFor("modded"); filepath.Base(got) != "modded_SandboxVars.lua" {
|
||||
t.Errorf("песочница профиля: %s", got)
|
||||
}
|
||||
if got := cfg.WorldDirFor("modded"); filepath.Base(got) != "modded" ||
|
||||
!strings.Contains(filepath.ToSlash(got), "Saves/Multiplayer") {
|
||||
t.Errorf("мир профиля: %s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user