Строчные ссылки на файлы модов и своё ожидание готовности
Игра приводит путь к вложенным файлам анимаций в нижний регистр целиком, вместе с именем папки мода. На Linux такие файлы не находятся, и сервер сыплет в лог сотнями FileNotFoundException. Кнопка на вкладке «Моды» раскладывает рядом строчные симлинки: файлы модов не меняются, повторный запуск после докачки безопасен, а на файловых системах без различия регистра кнопка честно говорит, что делать нечего. Ожидание строки о готовности стало настройкой профиля со значением по умолчанию 30 минут вместо общих 15. Сборке на три сотни модов пятнадцати не хватает, и панель объявляла бы работающим сервер, который ещё никого не пустит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package casefix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// content собирает дерево мастерской с файлами в разном регистре.
|
||||
func content(t *testing.T) (root, dir string) {
|
||||
t.Helper()
|
||||
root = t.TempDir()
|
||||
dir = filepath.Join(root, "steamapps", "workshop", "content", pzAppID,
|
||||
"3722134990", "mods", "GunsOfMarz", "42.16", "media", "AnimSets")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, name := range []string{"LoadShotgun_HB.xml", "aim_down.xml"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte("<anim/>"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return root, dir
|
||||
}
|
||||
|
||||
// Проверять создание ссылок есть смысл только там, где регистр вообще
|
||||
// различается: на Windows и macOS чинить нечего.
|
||||
func skipCaseInsensitive(t *testing.T, result Result) {
|
||||
t.Helper()
|
||||
if result.CaseInsensitive {
|
||||
t.Skip("файловая система не различает регистр")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyLinksUppercaseNames(t *testing.T) {
|
||||
root, dir := content(t)
|
||||
|
||||
result, err := Apply(context.Background(), []string{root}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
skipCaseInsensitive(t, result)
|
||||
if result.Created == 0 {
|
||||
t.Fatalf("не создано ни одной ссылки: %+v", result)
|
||||
}
|
||||
|
||||
// Ссылка нужна и на файл, и на каждую папку пути: игра опускает в нижний
|
||||
// регистр путь целиком, включая имя мода.
|
||||
link := filepath.Join(dir, "loadshotgun_hb.xml")
|
||||
if data, err := os.ReadFile(link); err != nil || string(data) != "<anim/>" {
|
||||
t.Fatalf("ссылка на файл не читается: %v", err)
|
||||
}
|
||||
modDir := filepath.Join(root, "steamapps", "workshop", "content", pzAppID,
|
||||
"3722134990", "mods", "gunsofmarz")
|
||||
if _, err := os.Stat(modDir); err != nil {
|
||||
t.Fatalf("нет ссылки на папку мода: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(modDir, "42.16", "media", "animsets", "loadshotgun_hb.xml")); err != nil {
|
||||
t.Fatalf("путь целиком строчными не собрался: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Повторный запуск после докачки модов не должен ничего ломать.
|
||||
func TestApplyIsIdempotent(t *testing.T) {
|
||||
root, _ := content(t)
|
||||
|
||||
first, err := Apply(context.Background(), []string{root}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
skipCaseInsensitive(t, first)
|
||||
second, err := Apply(context.Background(), []string{root}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second.Created != 0 {
|
||||
t.Errorf("второй проход создал ссылки заново: %d", second.Created)
|
||||
}
|
||||
if second.Existing != first.Created {
|
||||
t.Errorf("готовых ссылок %d, а создано было %d", second.Existing, first.Created)
|
||||
}
|
||||
}
|
||||
|
||||
// Имя, которое уже занято настоящим файлом, не трогаем.
|
||||
func TestApplyKeepsExistingLowercaseFile(t *testing.T) {
|
||||
root, dir := content(t)
|
||||
path := filepath.Join(dir, "loadshotgun_hb.xml")
|
||||
if err := os.WriteFile(path, []byte("свой файл"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := Apply(context.Background(), []string{root}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil || string(data) != "свой файл" {
|
||||
t.Fatalf("существующий файл перезаписан: %q, %v", data, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyWithoutWorkshopDir(t *testing.T) {
|
||||
if _, err := Apply(context.Background(), []string{t.TempDir()}, nil); err == nil {
|
||||
t.Fatal("ожидалась ошибка про ненайденную папку модов")
|
||||
}
|
||||
}
|
||||
|
||||
// На Windows и macOS кнопка должна честно сказать, что чинить нечего, а не
|
||||
// пересчитывать все имена как «уже готовые».
|
||||
func TestApplyReportsCaseInsensitiveFilesystem(t *testing.T) {
|
||||
root, dir := content(t)
|
||||
if !caseInsensitive(dir) {
|
||||
t.Skip("файловая система различает регистр")
|
||||
}
|
||||
|
||||
result, err := Apply(context.Background(), []string{root}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.CaseInsensitive {
|
||||
t.Error("нечувствительность к регистру не распознана")
|
||||
}
|
||||
if result.Created != 0 || result.Existing != 0 || result.Scanned != 0 {
|
||||
t.Errorf("дерево обошли впустую: %+v", result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user