Полторы сотни параметров сплошным списком читать невозможно. Теперь они разбиты на разделы — «Основное», «PVP и урон», «Убежища», «Античит» и так далее, — свёрнутые по умолчанию, кроме первого. Поиск раскрывает разделы с совпадениями и прячет остальные, а раскрытые разделы не схлопываются при возврате на вкладку. Категории составлены по настоящим конфигам Build 41 и 42: все 144 ключа .ini и 270 параметров песочницы раскладываются без остатка. Нумерованные и однотипные ключи (AntiCheatProtectionType1..24, *Loot, Animal*, Firearm*) подхватываются по префиксу, а незнакомые попадают в «Прочее» — в новых версиях игры ключи добавляют, и терять их со страницы нельзя. Описания параметров панель больше не выдумывает, а берёт из самого конфига: игра пишет их комментариями на языке сервера. Полный текст — во всплывающей подсказке, начало — под полем; поиск ищет и по описанию, так что параметр можно найти по-русски. Заодно исправлено сохранение файлов с виндовыми переводами строк: панель переписывала их в LF, и правка одного параметра меняла каждую строку файла. Проверено на реальном SandboxVars.lua: правка одного значения меняет ровно одну строку, размер файла не меняется. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
6.1 KiB
Go
188 lines
6.1 KiB
Go
package pzconfig
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const sampleINI = `# Comment line
|
|
PublicName=My Server
|
|
Public=true
|
|
|
|
MaxPlayers=32
|
|
Mods=modA;modB
|
|
WorkshopItems=111;222
|
|
`
|
|
|
|
func TestINIPreservesLayout(t *testing.T) {
|
|
ini := ParseINI([]byte(sampleINI))
|
|
|
|
if got := ini.Get("PublicName"); got != "My Server" {
|
|
t.Fatalf("PublicName = %q, ожидалось %q", got, "My Server")
|
|
}
|
|
// Правка одного ключа не должна трогать комментарии и пустые строки.
|
|
ini.Set("MaxPlayers", "64")
|
|
|
|
got := string(ini.Render())
|
|
if !strings.HasPrefix(got, "# Comment line\n") {
|
|
t.Errorf("комментарий потерян:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "\n\nMaxPlayers=64\n") {
|
|
t.Errorf("пустая строка или новое значение потеряны:\n%s", got)
|
|
}
|
|
if strings.Contains(got, "MaxPlayers=32") {
|
|
t.Errorf("старое значение осталось:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestINIAddsMissingKey(t *testing.T) {
|
|
ini := ParseINI([]byte("A=1"))
|
|
ini.Set("B", "2")
|
|
if got := ini.Get("B"); got != "2" {
|
|
t.Fatalf("B = %q", got)
|
|
}
|
|
if got := string(ini.Render()); got != "A=1\nB=2" {
|
|
t.Fatalf("render = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestINIRejectsNewlineInValue(t *testing.T) {
|
|
ini := ParseINI([]byte("A=1"))
|
|
ini.Set("A", "нет\nпереносам")
|
|
if strings.Contains(string(ini.Render()), "\nп") {
|
|
t.Fatalf("перенос строки попал в значение: %q", ini.Get("A"))
|
|
}
|
|
}
|
|
|
|
func TestModsRoundTrip(t *testing.T) {
|
|
ini := ParseINI([]byte(sampleINI))
|
|
mods, workshop := ini.Mods()
|
|
if len(mods) != 2 || mods[0] != "modA" {
|
|
t.Fatalf("mods = %v", mods)
|
|
}
|
|
if len(workshop) != 2 || workshop[1] != "222" {
|
|
t.Fatalf("workshop = %v", workshop)
|
|
}
|
|
|
|
// Дубликаты отбрасываются, порядок сохраняется.
|
|
ini.SetMods([]string{"modB", "modA", "modB"}, []string{"222"})
|
|
if got := ini.Get(KeyMods); got != "modB;modA" {
|
|
t.Fatalf("Mods = %q", got)
|
|
}
|
|
if got := ini.Get(KeyWorkshopItems); got != "222" {
|
|
t.Fatalf("WorkshopItems = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestSplitListIgnoresBlanks(t *testing.T) {
|
|
if got := SplitList(" a ; ; b;"); len(got) != 2 || got[0] != "a" || got[1] != "b" {
|
|
t.Fatalf("SplitList = %v", got)
|
|
}
|
|
if got := SplitList(" "); len(got) != 0 {
|
|
t.Fatalf("пустая строка дала %v", got)
|
|
}
|
|
}
|
|
|
|
const sampleSandbox = `SandboxVars = {
|
|
VERSION = 5,
|
|
Zombies = 3,
|
|
Speed = 2,
|
|
ZombieLore = {
|
|
Speed = 2,
|
|
Strength = 2,
|
|
},
|
|
WaterShutModifier = 14,
|
|
}
|
|
`
|
|
|
|
func TestSandboxParsesNestedPaths(t *testing.T) {
|
|
sb := ParseSandbox([]byte(sampleSandbox))
|
|
|
|
if v, ok := sb.Get("Zombies"); !ok || v != "3" {
|
|
t.Fatalf("Zombies = %q (найдено: %v)", v, ok)
|
|
}
|
|
// Одноимённые ключи на разных уровнях не должны перетирать друг друга.
|
|
if v, ok := sb.Get("ZombieLore.Speed"); !ok || v != "2" {
|
|
t.Fatalf("ZombieLore.Speed = %q (найдено: %v)", v, ok)
|
|
}
|
|
if v, ok := sb.Get("Speed"); !ok || v != "2" {
|
|
t.Fatalf("Speed верхнего уровня = %q (найдено: %v)", v, ok)
|
|
}
|
|
if v, ok := sb.Get("WaterShutModifier"); !ok || v != "14" {
|
|
t.Fatalf("WaterShutModifier после закрытия таблицы = %q (найдено: %v)", v, ok)
|
|
}
|
|
}
|
|
|
|
func TestSandboxSetKeepsFormatting(t *testing.T) {
|
|
sb := ParseSandbox([]byte(sampleSandbox))
|
|
if err := sb.Set("ZombieLore.Speed", "1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(sb.Render())
|
|
|
|
if !strings.Contains(got, " Speed = 1,") {
|
|
t.Errorf("отступ или запятая потеряны:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, " Speed = 2,") {
|
|
t.Errorf("параметр верхнего уровня изменён по ошибке:\n%s", got)
|
|
}
|
|
if !strings.HasSuffix(got, "}\n") {
|
|
t.Errorf("хвост файла изменён:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestSandboxRejectsUnknownAndBrokenValues(t *testing.T) {
|
|
sb := ParseSandbox([]byte(sampleSandbox))
|
|
if err := sb.Set("НетТакого", "1"); err == nil {
|
|
t.Error("ожидалась ошибка для неизвестного параметра")
|
|
}
|
|
if err := sb.Set("Zombies", "{ 1, 2 }"); err == nil {
|
|
t.Error("ожидалась ошибка для значения с фигурными скобками")
|
|
}
|
|
}
|
|
|
|
func TestSandboxEntriesFollowFileOrder(t *testing.T) {
|
|
entries := ParseSandbox([]byte(sampleSandbox)).Entries()
|
|
want := []string{"VERSION", "Zombies", "Speed", "ZombieLore.Speed", "ZombieLore.Strength", "WaterShutModifier"}
|
|
|
|
if len(entries) != len(want) {
|
|
t.Fatalf("получено %d параметров, ожидалось %d: %v", len(entries), len(want), entries)
|
|
}
|
|
for i, path := range want {
|
|
if entries[i].Path != path {
|
|
t.Errorf("entries[%d] = %q, ожидалось %q", i, entries[i].Path, path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestKeepsWindowsLineEndings: конфиги, созданные игрой на Windows, приходят с
|
|
// CRLF. Если при сохранении переписать их в LF, изменится каждая строка файла —
|
|
// правка одного параметра превратится в полную перезапись.
|
|
func TestKeepsWindowsLineEndings(t *testing.T) {
|
|
ini := ParseINI([]byte("# Комментарий\r\nMaxPlayers=32\r\nPVP=true\r\n"))
|
|
ini.Set("MaxPlayers", "64")
|
|
|
|
got := string(ini.Render())
|
|
if strings.Count(got, "\r\n") != 3 {
|
|
t.Errorf("переводы строк не сохранены:\n%q", got)
|
|
}
|
|
if strings.Contains(strings.ReplaceAll(got, "\r\n", ""), "\n") {
|
|
t.Errorf("в файле появились одиночные LF:\n%q", got)
|
|
}
|
|
|
|
// Файл с LF таким и остаётся.
|
|
unix := ParseINI([]byte("MaxPlayers=32\nPVP=true"))
|
|
unix.Set("PVP", "false")
|
|
if strings.Contains(string(unix.Render()), "\r") {
|
|
t.Error("в файл с LF добавились CR")
|
|
}
|
|
|
|
sandbox := ParseSandbox([]byte("SandboxVars = {\r\n Zombies = 3,\r\n}\r\n"))
|
|
if err := sandbox.Set("Zombies", "1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Count(string(sandbox.Render()), "\r\n") != 3 {
|
|
t.Errorf("песочница потеряла CRLF:\n%q", string(sandbox.Render()))
|
|
}
|
|
}
|