Настройки сервера и песочницы разложены по разделам
Полторы сотни параметров сплошным списком читать невозможно. Теперь они разбиты на разделы — «Основное», «PVP и урон», «Убежища», «Античит» и так далее, — свёрнутые по умолчанию, кроме первого. Поиск раскрывает разделы с совпадениями и прячет остальные, а раскрытые разделы не схлопываются при возврате на вкладку. Категории составлены по настоящим конфигам Build 41 и 42: все 144 ключа .ini и 270 параметров песочницы раскладываются без остатка. Нумерованные и однотипные ключи (AntiCheatProtectionType1..24, *Loot, Animal*, Firearm*) подхватываются по префиксу, а незнакомые попадают в «Прочее» — в новых версиях игры ключи добавляют, и терять их со страницы нельзя. Описания параметров панель больше не выдумывает, а берёт из самого конфига: игра пишет их комментариями на языке сервера. Полный текст — во всплывающей подсказке, начало — под полем; поиск ищет и по описанию, так что параметр можно найти по-русски. Заодно исправлено сохранение файлов с виндовыми переводами строк: панель переписывала их в LF, и правка одного параметра меняла каждую строку файла. Проверено на реальном SandboxVars.lua: правка одного значения меняет ровно одну строку, размер файла не меняется. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,9 @@ func (s *Server) handleGetServerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
"path": path,
|
||||
"keys": ini.Keys(),
|
||||
"values": ini.Map(),
|
||||
// Игра сама пишет в конфиг описания параметров на языке сервера —
|
||||
// показываем их, вместо того чтобы придумывать свои.
|
||||
"descriptions": ini.Comments(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ type iniLine struct {
|
||||
Key string
|
||||
Value string
|
||||
Raw string
|
||||
// Comment — описание параметра: строки комментария прямо перед ним.
|
||||
// Project Zomboid пишет их на языке сервера, так что панели есть что
|
||||
// показать человеку, не выдумывая своих формулировок.
|
||||
Comment string
|
||||
}
|
||||
|
||||
// INI — конфиг PZ в виде плоских key=value. Разбор сохраняет исходный порядок
|
||||
@@ -23,22 +27,46 @@ type iniLine struct {
|
||||
type INI struct {
|
||||
lines []iniLine
|
||||
index map[string]int
|
||||
// crlf — файл был с виндовыми переводами строк. Сохраняем как было: иначе
|
||||
// правка одного параметра переписывает весь файл целиком.
|
||||
crlf bool
|
||||
}
|
||||
|
||||
// ParseINI разбирает содержимое конфига.
|
||||
func ParseINI(data []byte) *INI {
|
||||
ini := &INI{index: make(map[string]int)}
|
||||
text := strings.ReplaceAll(string(data), "\r\n", "\n")
|
||||
raw := string(data)
|
||||
ini.crlf = strings.Contains(raw, "\r\n")
|
||||
text := strings.ReplaceAll(raw, "\r\n", "\n")
|
||||
|
||||
// Комментарии копим до ближайшего параметра: в конфигах PZ описание стоит
|
||||
// строкой выше. Пустая строка обрывает связь — иначе описание прилипло бы
|
||||
// к следующему блоку.
|
||||
var comment []string
|
||||
|
||||
for _, raw := range strings.Split(text, "\n") {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
key, value, found := strings.Cut(raw, "=")
|
||||
if !found || trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
comment = append(comment, strings.TrimSpace(strings.TrimPrefix(trimmed, "#")))
|
||||
ini.lines = append(ini.lines, iniLine{Raw: raw})
|
||||
continue
|
||||
}
|
||||
if !found || trimmed == "" {
|
||||
comment = nil
|
||||
ini.lines = append(ini.lines, iniLine{Raw: raw})
|
||||
continue
|
||||
}
|
||||
|
||||
key = strings.TrimSpace(key)
|
||||
ini.index[key] = len(ini.lines)
|
||||
ini.lines = append(ini.lines, iniLine{Key: key, Value: strings.TrimSpace(value)})
|
||||
ini.lines = append(ini.lines, iniLine{
|
||||
Key: key,
|
||||
Value: strings.TrimSpace(value),
|
||||
Comment: strings.Join(comment, " "),
|
||||
})
|
||||
comment = nil
|
||||
}
|
||||
return ini
|
||||
}
|
||||
@@ -90,6 +118,17 @@ func (i *INI) Keys() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// Comments возвращает описания параметров: ключ -> текст комментария.
|
||||
func (i *INI) Comments() map[string]string {
|
||||
out := make(map[string]string, len(i.index))
|
||||
for _, l := range i.lines {
|
||||
if l.Key != "" && l.Comment != "" {
|
||||
out[l.Key] = l.Comment
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Map отдаёт все параметры в виде словаря.
|
||||
func (i *INI) Map() map[string]string {
|
||||
out := make(map[string]string, len(i.index))
|
||||
@@ -103,6 +142,11 @@ func (i *INI) Map() map[string]string {
|
||||
|
||||
// Render собирает файл обратно в текст.
|
||||
func (i *INI) Render() []byte {
|
||||
newline := "\n"
|
||||
if i.crlf {
|
||||
newline = "\r\n"
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for n, l := range i.lines {
|
||||
if l.Key == "" {
|
||||
@@ -113,7 +157,7 @@ func (i *INI) Render() []byte {
|
||||
buf.WriteString(l.Value)
|
||||
}
|
||||
if n < len(i.lines)-1 {
|
||||
buf.WriteByte('\n')
|
||||
buf.WriteString(newline)
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
|
||||
@@ -154,3 +154,34 @@ func TestSandboxEntriesFollowFileOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ type Sandbox struct {
|
||||
lines []string
|
||||
// index сопоставляет путь параметра с номером строки.
|
||||
index map[string]int
|
||||
// crlf — файл был с виндовыми переводами строк; сохраняем как было.
|
||||
crlf bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -34,7 +36,9 @@ var (
|
||||
// ParseSandbox разбирает содержимое SandboxVars.lua.
|
||||
func ParseSandbox(data []byte) *Sandbox {
|
||||
s := &Sandbox{index: make(map[string]int)}
|
||||
text := strings.ReplaceAll(string(data), "\r\n", "\n")
|
||||
raw := string(data)
|
||||
s.crlf = strings.Contains(raw, "\r\n")
|
||||
text := strings.ReplaceAll(raw, "\r\n", "\n")
|
||||
s.lines = strings.Split(text, "\n")
|
||||
|
||||
var stack []string
|
||||
@@ -139,11 +143,16 @@ func (s *Sandbox) Set(path, value string) error {
|
||||
|
||||
// Render собирает файл обратно.
|
||||
func (s *Sandbox) Render() []byte {
|
||||
newline := "\n"
|
||||
if s.crlf {
|
||||
newline = "\r\n"
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
for n, line := range s.lines {
|
||||
buf.WriteString(line)
|
||||
if n < len(s.lines)-1 {
|
||||
buf.WriteByte('\n')
|
||||
buf.WriteString(newline)
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
|
||||
Reference in New Issue
Block a user