Files
mrleo1nidandClaude Opus 5 c706665196
CI / test (push) Failing after 7s
Release / release (push) Successful in 29s
Описания параметров на русском независимо от локали сервера
Панель показывала комментарии из самого конфига, а игра пишет их на языке
сервера: на Windows-клиенте они русские, на Linux-сервере почти всегда
английские — и вся вкладка оказывалась на английском.

Теперь у панели свой словарь: подпись и описание для всех 153 параметров
servertest.ini, встречающихся в Build 41 и 42, плюс подписи для 182
параметров песочницы. Комментарий из файла остаётся запасным вариантом для
ключей, которых словарь не знает, — в новых версиях игры их добавляют.

Песочница заодно научилась отдавать свои комментарии: игра расписывает там
смысл каждого значения, и даже английский текст лучше пустоты.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 16:03:09 +03:00

190 lines
5.8 KiB
Go

package pzconfig
import (
"bytes"
"fmt"
"os"
"regexp"
"strings"
)
// SandboxEntry — один параметр песочницы. Path включает вложенные таблицы
// (например ZombieLore.Speed), Value хранится как в файле.
type SandboxEntry struct {
Path string `json:"path"`
Value string `json:"value"`
// Comment — строки "--" перед параметром: игра расписывает там смысл
// значения и расшифровку вариантов.
Comment string `json:"comment,omitempty"`
}
// Sandbox — разбор <server>_SandboxVars.lua. Файл генерируется игрой и имеет
// предсказуемый вид (одна пара ключ-значение на строку), поэтому вместо
// полноценного интерпретатора Lua достаточно построчного разбора: он
// гарантирует, что при сохранении файл останется байт-в-байт прежним, кроме
// изменённых значений.
type Sandbox struct {
lines []string
// index сопоставляет путь параметра с номером строки.
index map[string]int
// comments хранит описание параметра по его пути.
comments map[string]string
// crlf — файл был с виндовыми переводами строк; сохраняем как было.
crlf bool
}
var (
sandboxAssignRe = regexp.MustCompile(`^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+?)(,?)\s*$`)
sandboxCloseRe = regexp.MustCompile(`^\s*\}`)
)
// ParseSandbox разбирает содержимое SandboxVars.lua.
func ParseSandbox(data []byte) *Sandbox {
s := &Sandbox{index: make(map[string]int), comments: make(map[string]string)}
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
// Комментарии копим до ближайшего параметра, как и в .ini.
var comment []string
for n, line := range s.lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "--") {
comment = append(comment, strings.TrimSpace(strings.TrimPrefix(trimmed, "--")))
continue
}
if trimmed == "" {
comment = nil
continue
}
if sandboxCloseRe.MatchString(line) {
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
continue
}
m := sandboxAssignRe.FindStringSubmatch(line)
if m == nil {
continue
}
name, value := m[2], strings.TrimSpace(m[3])
if strings.HasPrefix(value, "{") {
// Начало вложенной таблицы: сам SandboxVars = { тоже сюда попадёт,
// но его имя в путях не нужно.
if name != "SandboxVars" {
stack = append(stack, name)
} else {
stack = stack[:0]
}
comment = nil
continue
}
path := name
if len(stack) > 0 {
path = strings.Join(stack, ".") + "." + name
}
s.index[path] = n
if len(comment) > 0 {
s.comments[path] = strings.Join(comment, " ")
}
comment = nil
}
return s
}
// LoadSandbox читает файл песочницы с диска.
func LoadSandbox(path string) (*Sandbox, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseSandbox(data), nil
}
// Entries возвращает все параметры в порядке следования в файле.
func (s *Sandbox) Entries() []SandboxEntry {
out := make([]SandboxEntry, 0, len(s.index))
// Обходим строки, а не карту: порядок в файле осмысленный.
byLine := make(map[int]string, len(s.index))
for path, line := range s.index {
byLine[line] = path
}
for n := range s.lines {
path, ok := byLine[n]
if !ok {
continue
}
out = append(out, SandboxEntry{
Path: path,
Value: s.valueAt(n),
Comment: s.comments[path],
})
}
return out
}
func (s *Sandbox) valueAt(line int) string {
m := sandboxAssignRe.FindStringSubmatch(s.lines[line])
if m == nil {
return ""
}
return strings.TrimSpace(m[3])
}
// Get возвращает значение параметра по пути.
func (s *Sandbox) Get(path string) (string, bool) {
n, ok := s.index[path]
if !ok {
return "", false
}
return s.valueAt(n), true
}
// Set меняет значение существующего параметра. Новые параметры не добавляются:
// игра всё равно игнорирует незнакомые ключи.
func (s *Sandbox) Set(path, value string) error {
n, ok := s.index[path]
if !ok {
return fmt.Errorf("параметр %q не найден в SandboxVars", path)
}
value = strings.TrimSpace(value)
if strings.ContainsAny(value, "\n\r{}") {
return fmt.Errorf("недопустимое значение для %q", path)
}
m := sandboxAssignRe.FindStringSubmatch(s.lines[n])
if m == nil {
return fmt.Errorf("не удалось разобрать строку параметра %q", path)
}
indent, name, comma := m[1], m[2], m[4]
s.lines[n] = fmt.Sprintf("%s%s = %s%s", indent, name, value, comma)
return nil
}
// 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.WriteString(newline)
}
}
return buf.Bytes()
}
// SaveSandbox пишет файл песочницы, сохранив предыдущую версию в .bak.
func SaveSandbox(path string, s *Sandbox) error {
if err := backupFile(path); err != nil {
return err
}
return writeAtomic(path, s.Render(), 0o644)
}