package preset import ( "bytes" "fmt" "regexp" "strconv" "strings" ) // Пресет песочницы одиночной игры — это плоский key=value с точками вместо // вложенных таблиц: // // Version=6 // # Комментарий игры // -- 1 = Insane // Zombies=4 // ZombieLore.Speed=2 // KDOT.SpecialVehicleChance=4.0 // // Серверу нужен тот же набор значений, но в виде Lua-таблицы SandboxVars. var ( // sandboxKeyRe — путь параметра: имя или несколько имён через точку. sandboxKeyRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$`) // numberRe — то, что в Lua записывается без кавычек. numberRe = regexp.MustCompile(`^-?\d+(?:\.\d+)?$`) ) // sandboxMarkers — параметры, которые есть в любом пресете песочницы. По ним // он отличается от прочих .cfg в архиве (списки модов, настройки папок). var sandboxMarkers = []string{ "Zombies", "Distribution", "DayLength", "StartYear", "StartMonth", "WaterShut", "ElecShut", "XpMultiplier", "StatsDecrease", "Temperature", "Rain", "ZombieLore.Speed", "ZombieLore.Strength", "LootRespawn", "FoodRotSpeed", "Nutrition", } const sandboxMarkersRequired = 3 // SandboxPreset — разобранный пресет песочницы. type SandboxPreset struct { // Version — версия формата из первой строки пресета. Version string // Values — путь параметра → значение, уже записанное на языке Lua. Values map[string]string // order хранит порядок появления: файл песочницы читают глазами, и // перемешивать параметры незачем. order []string } // ParseSandboxPreset разбирает пресет. Строки, не похожие на параметр // (комментарии игры, расшифровки значений), пропускаются. func ParseSandboxPreset(data []byte) *SandboxPreset { p := &SandboxPreset{Values: map[string]string{}} text := strings.ReplaceAll(string(data), "\r\n", "\n") for _, line := range strings.Split(text, "\n") { trimmed := strings.TrimSpace(line) // Расшифровки значений игра пишет как "-- 1 = Insane", и знак равенства // в них есть — без этой проверки они стали бы параметрами. if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "--") { continue } key, value, found := strings.Cut(trimmed, "=") key = strings.TrimSpace(key) if !found || !sandboxKeyRe.MatchString(key) { continue } if strings.EqualFold(key, "Version") { p.Version = strings.TrimSpace(value) continue } if _, seen := p.Values[key]; !seen { p.order = append(p.order, key) } p.Values[key] = luaValue(value) } return p } // Order возвращает пути параметров в порядке их появления в пресете. func (p *SandboxPreset) Order() []string { return p.order } // looksLikeSandboxPreset отличает пресет песочницы от прочих .cfg. func looksLikeSandboxPreset(data []byte) bool { p := ParseSandboxPreset(data) found := 0 for _, key := range sandboxMarkers { if _, ok := p.Values[key]; ok { found++ } } return found >= sandboxMarkersRequired } // luaValue переводит значение пресета в запись Lua: числа и логические // значения идут как есть, всё остальное — строка в кавычках. func luaValue(raw string) string { value := strings.TrimSpace(raw) switch { case value == "true" || value == "false": return value case numberRe.MatchString(value): return value default: return strconv.Quote(value) } } // node — узел дерева параметров: либо значение, либо вложенная таблица. // Дерево нужно потому, что в Lua «ZombieLore.Speed = 2» внутри таблицы // написать нельзя — только настоящей вложенностью. type node struct { value string order []string children map[string]*node } func (n *node) child(name string) *node { if n.children == nil { n.children = map[string]*node{} } if _, ok := n.children[name]; !ok { n.children[name] = &node{} n.order = append(n.order, name) } return n.children[name] } // Lua собирает из пресета целый SandboxVars.lua. Нужен, когда переносить // значения некуда: профиль ещё ни разу не запускался, и своего файла // песочницы у него нет. func (p *SandboxPreset) Lua() []byte { root := &node{} version := p.Version if version == "" { version = "1" } root.child("VERSION").value = version for _, key := range p.order { current := root for _, name := range strings.Split(key, ".") { current = current.child(name) } current.value = p.Values[key] } var buf bytes.Buffer buf.WriteString("SandboxVars = {\n") writeNode(&buf, root, " ") buf.WriteString("}\n") return buf.Bytes() } func writeNode(buf *bytes.Buffer, n *node, indent string) { for _, name := range n.order { child := n.children[name] // Значение и вложенные параметры одновременно означали бы, что в // пресете есть и «A», и «A.B»: доверяем вложенности, она полнее. if len(child.order) == 0 { fmt.Fprintf(buf, "%s%s = %s,\n", indent, name, child.value) continue } fmt.Fprintf(buf, "%s%s = {\n", indent, name) writeNode(buf, child, indent+" ") fmt.Fprintf(buf, "%s},\n", indent) } }