Мод-менеджер с галочками и коллекции мастерской
Вместо двух отдельных списков (Workshop ID и Mod ID) — один список модов: галочка включает мод, перетаскивание задаёт порядок загрузки, кнопка убирает пакет вместе со всеми его модами. Чтобы галочкам было за что зацепиться, панель читает mod.info уже скачанных модов: серверу в Mods нужен именно Mod ID, а он лежит внутри пакета мастерской и вручную его приходилось искать в описании мода. Пока мод не скачан, строка помечена и галочка недоступна — Mod ID неизвестен. Один пакет мастерской может содержать несколько модов; теперь они видны отдельными строками и включаются по отдельности. Коллекции: кнопка разворачивает коллекцию Steam в список входящих в неё модов, сохраняя порядок со страницы коллекции. И мод, и коллекция принимаются как ссылкой, так и голым идентификатором. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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 != ""
|
||||
}
|
||||
Reference in New Issue
Block a user