Files
pzmanager/internal/pzconfig/installed.go
T
mrleo1nidandClaude Opus 5 fe517cfb71 Мод-менеджер с галочками и коллекции мастерской
Вместо двух отдельных списков (Workshop ID и Mod ID) — один список модов:
галочка включает мод, перетаскивание задаёт порядок загрузки, кнопка
убирает пакет вместе со всеми его модами.

Чтобы галочкам было за что зацепиться, панель читает mod.info уже
скачанных модов: серверу в Mods нужен именно Mod ID, а он лежит внутри
пакета мастерской и вручную его приходилось искать в описании мода. Пока
мод не скачан, строка помечена и галочка недоступна — Mod ID неизвестен.

Один пакет мастерской может содержать несколько модов; теперь они видны
отдельными строками и включаются по отдельности.

Коллекции: кнопка разворачивает коллекцию Steam в список входящих в неё
модов, сохраняя порядок со страницы коллекции. И мод, и коллекция
принимаются как ссылкой, так и голым идентификатором.

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

144 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pzconfig
import (
"bufio"
"os"
"path/filepath"
"sort"
"strings"
)
// pzAppID — идентификатор Project Zomboid в Steam. По нему сервер
// раскладывает скачанные моды мастерской.
const pzAppID = "108600"
// InstalledMod — мод, найденный на диске. Именно ModID сервер ждёт в списке
// Mods, а Workshop ID — это пакет мастерской, внутри которого мод лежит.
type InstalledMod struct {
ModID string `json:"mod_id"`
WorkshopID string `json:"workshop_id"`
Name string `json:"name"`
Description string `json:"description"`
Dir string `json:"dir"`
}
// ScanInstalledMods обходит места, куда попадают моды, и читает их mod.info.
// Панель по этому каталогу показывает настоящие названия и, главное, знает
// Mod ID: без него мод нельзя включить, а вручную его приходится искать в
// описании на странице мастерской.
//
// roots — директории, внутри которых ищем steamapps/workshop (директория
// сервера и ~/Zomboid), localMods — папка с модами, положенными руками.
func ScanInstalledMods(roots []string, localMods string) []InstalledMod {
seen := make(map[string]bool)
var out []InstalledMod
add := func(mod InstalledMod) {
// Один и тот же мод может лежать и в мастерской, и в локальной папке.
if mod.ModID == "" || seen[mod.ModID] {
return
}
seen[mod.ModID] = true
out = append(out, mod)
}
for _, root := range roots {
if root == "" {
continue
}
content := filepath.Join(root, "steamapps", "workshop", "content", pzAppID)
entries, err := os.ReadDir(content)
if err != nil {
continue
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
workshopID := entry.Name()
// Внутри пакета моды лежат в mods/<имя>/mod.info, но встречается
// и раскладка без промежуточной папки mods.
for _, modsDir := range []string{
filepath.Join(content, workshopID, "mods"),
filepath.Join(content, workshopID),
} {
for _, mod := range readModsDir(modsDir, workshopID) {
add(mod)
}
}
}
}
for _, mod := range readModsDir(localMods, "") {
add(mod)
}
sort.Slice(out, func(i, j int) bool {
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
})
return out
}
// readModsDir читает mod.info во всех поддиректориях dir.
func readModsDir(dir, workshopID string) []InstalledMod {
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var out []InstalledMod
for _, entry := range entries {
if !entry.IsDir() {
continue
}
modDir := filepath.Join(dir, entry.Name())
mod, ok := readModInfo(filepath.Join(modDir, "mod.info"))
if !ok {
continue
}
mod.WorkshopID = workshopID
mod.Dir = modDir
if mod.Name == "" {
mod.Name = mod.ModID
}
out = append(out, mod)
}
return out
}
// readModInfo разбирает mod.info — файл вида "ключ=значение" с необязательными
// комментариями. Нас интересуют id, name и description.
func readModInfo(path string) (InstalledMod, bool) {
f, err := os.Open(path)
if err != nil {
return InstalledMod{}, false
}
defer f.Close()
var mod InstalledMod
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, 8*1024), 256*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, found := strings.Cut(line, "=")
if !found {
continue
}
value = strings.TrimSpace(value)
switch strings.ToLower(strings.TrimSpace(key)) {
case "id":
mod.ModID = value
case "name":
mod.Name = value
case "description":
if len(value) > 300 {
value = value[:300] + "…"
}
mod.Description = value
}
}
return mod, mod.ModID != ""
}