Панель показывала комментарии из самого конфига, а игра пишет их на языке сервера: на Windows-клиенте они русские, на Linux-сервере почти всегда английские — и вся вкладка оказывалась на английском. Теперь у панели свой словарь: подпись и описание для всех 153 параметров servertest.ini, встречающихся в Build 41 и 42, плюс подписи для 182 параметров песочницы. Комментарий из файла остаётся запасным вариантом для ключей, которых словарь не знает, — в новых версиях игры их добавляют. Песочница заодно научилась отдавать свои комментарии: игра расписывает там смысл каждого значения, и даже английский текст лучше пустоты. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
190 lines
5.8 KiB
Go
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)
|
|
}
|