package pzconfig import ( "bytes" "fmt" "os" "regexp" "strings" ) // SandboxEntry — один параметр песочницы. Path включает вложенные таблицы // (например ZombieLore.Speed), Value хранится как в файле. type SandboxEntry struct { Path string `json:"path"` Value string `json:"value"` } // Sandbox — разбор _SandboxVars.lua. Файл генерируется игрой и имеет // предсказуемый вид (одна пара ключ-значение на строку), поэтому вместо // полноценного интерпретатора Lua достаточно построчного разбора: он // гарантирует, что при сохранении файл останется байт-в-байт прежним, кроме // изменённых значений. type Sandbox struct { lines []string // index сопоставляет путь параметра с номером строки. index map[string]int } 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)} text := strings.ReplaceAll(string(data), "\r\n", "\n") s.lines = strings.Split(text, "\n") var stack []string for n, line := range s.lines { trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "--") { 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] } continue } path := name if len(stack) > 0 { path = strings.Join(stack, ".") + "." + name } s.index[path] = n } 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)}) } 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 { var buf bytes.Buffer for n, line := range s.lines { buf.WriteString(line) if n < len(s.lines)-1 { buf.WriteByte('\n') } } 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) }