fix: path discovery of cosmoteer for manjaro linux

This commit is contained in:
Manuel Ganter
2026-08-29 17:06:21 +02:00
parent 68af02fb3f
commit 7c1ee563e7
6 changed files with 435 additions and 57 deletions
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# ABOUT ME
# Locates Cosmoteer's installed Data folder by asking Steam where its libraries
# are.
#
# Why this exists: the mod tests validate against vanilla game data, and the
# game can be installed to any Steam library on any disk. A fixed list of
# guessed paths misses real installs, so this reads the library roots out of
# Steam's own libraryfolders.vdf and looks for the game under each one.
#
# install.sh carries the same logic in bash for finding the Mods folder.
import os
import re
COSMOTEER_APPID = 799600
# One "path" entry per library root; the app data sits in <path>/steamapps.
_LIBRARY_PATH_RE = re.compile(r'^\s*"path"\s*"(.*)"\s*$')
def steam_roots(home):
"""The Steam client directories, one per install flavour."""
return [
os.path.join(home, ".local/share/Steam"),
os.path.join(home, ".steam/steam"),
os.path.join(home, ".steam/root"),
os.path.join(home, ".var/app/com.valvesoftware.Steam/data/Steam"),
]
def steam_libraries(home):
"""Every Steam library root, in search order and without duplicates.
Steam's own roots come first so the default library stays searchable even
when libraryfolders.vdf is missing or unreadable.
"""
libraries = list(steam_roots(home))
for root in steam_roots(home):
vdf = os.path.join(root, "steamapps", "libraryfolders.vdf")
try:
with open(vdf, encoding="utf-8", errors="replace") as handle:
lines = handle.readlines()
except OSError:
continue
for line in lines:
match = _LIBRARY_PATH_RE.match(line)
if match:
libraries.append(match.group(1).replace("\\\\", "\\"))
seen = set()
unique = []
for library in libraries:
if library not in seen:
seen.add(library)
unique.append(library)
return unique
def find_data_dir(home=None, environ=None):
"""The Cosmoteer Data folder, or None when the game is not installed.
COSMOTEER_DATA overrides discovery so an unusual install can be pointed at
by hand.
"""
environ = os.environ if environ is None else environ
override = environ.get("COSMOTEER_DATA")
if override:
return override
home = os.path.expanduser("~") if home is None else home
for library in steam_libraries(home):
data = os.path.join(library, "steamapps", "common", "Cosmoteer", "Data")
if os.path.isdir(data):
return data
return None
def find_bin_dir(home=None, environ=None):
"""The Cosmoteer Bin folder, which sits beside Data."""
data = find_data_dir(home=home, environ=environ)
if not data:
return None
return os.path.join(os.path.dirname(data), "Bin")
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# ABOUT ME
# Tests install.sh's discovery of the Cosmoteer Mods folder.
#
# Why it exists: the Mods folder sits inside a Proton prefix under whichever
# Steam library the game was installed to, and that library can live anywhere.
# Guessing paths misses real installs, so discovery reads Steam's own
# libraryfolders.vdf. These tests build fake Steam trees and check that the
# discovery finds the Mods folder in each layout.
#
# Usage: ./tests/test_install.sh
set -uo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APPID=799600
PROFILE=76561198000495479
# Sourcing install.sh must define the functions without installing anything.
source "$REPO_DIR/install.sh"
failures=0
tmp_root="$(mktemp -d)"
trap 'rm -rf "$tmp_root"' EXIT
check() {
# check <name> <expected> <actual>
if [ "$2" = "$3" ]; then
echo "PASS: $1"
else
echo "FAIL: $1"
echo " expected: $2"
echo " actual: $3"
failures=$((failures + 1))
fi
}
# Builds a Steam client dir at <root>/.local/share/Steam whose
# libraryfolders.vdf lists every library path passed after the root.
make_steam_home() {
local home="$1"
shift
local vdf="$home/.local/share/Steam/steamapps/libraryfolders.vdf"
mkdir -p "$(dirname "$vdf")"
{
echo '"libraryfolders"'
echo '{'
local i=0
for lib in "$@"; do
printf '\t"%s"\n\t{\n\t\t"path"\t\t"%s"\n\t}\n' "$i" "$lib"
i=$((i + 1))
done
echo '}'
} >"$vdf"
}
# Creates the Proton-prefix Mods folder for Cosmoteer inside a library root.
make_proton_mods() {
local lib="$1"
local mods="$lib/steamapps/compatdata/$APPID/pfx/drive_c/users/steamuser/Saved Games/Cosmoteer/$PROFILE/Mods"
mkdir -p "$mods"
printf '%s\n' "$mods"
}
# 1. The game lives in a secondary library on another disk, which is the case
# whenever Steam is told to install to a second drive.
case1="$tmp_root/case1"
mkdir -p "$case1/home"
lib="$case1/disk2/steamapps"
make_steam_home "$case1/home" "$case1/home/.local/share/Steam" "$lib"
expected="$(make_proton_mods "$lib")"
actual="$(HOME="$case1/home" find_mods_dir)"
check "finds Mods in a secondary library from libraryfolders.vdf" "$expected" "$actual"
# 2. The game lives in the default library inside the Steam client dir.
case2="$tmp_root/case2"
mkdir -p "$case2/home"
make_steam_home "$case2/home" "$case2/home/.local/share/Steam"
expected="$(make_proton_mods "$case2/home/.local/share/Steam")"
actual="$(HOME="$case2/home" find_mods_dir)"
check "finds Mods in the default Steam library" "$expected" "$actual"
# 3. Library paths may contain spaces, so discovery must not word-split them.
case3="$tmp_root/case3"
mkdir -p "$case3/home"
lib="$case3/my games/steamapps"
make_steam_home "$case3/home" "$case3/home/.local/share/Steam" "$lib"
expected="$(make_proton_mods "$lib")"
actual="$(HOME="$case3/home" find_mods_dir)"
check "finds Mods under a library path containing spaces" "$expected" "$actual"
# 4. A Windows-style profile in the user's home, for a non-Proton install.
case4="$tmp_root/case4"
mkdir -p "$case4/home/Saved Games/Cosmoteer/$PROFILE/Mods"
expected="$case4/home/Saved Games/Cosmoteer/$PROFILE/Mods"
actual="$(HOME="$case4/home" find_mods_dir)"
check "finds Mods in the home Saved Games folder" "$expected" "$actual"
# 5. Nothing installed at all: discovery reports failure instead of guessing.
case5="$tmp_root/case5"
mkdir -p "$case5/home"
actual="$(HOME="$case5/home" find_mods_dir)"
status=$?
check "reports no Mods folder when there is no install" "" "$actual"
check "exits non-zero when there is no install" "1" "$status"
# 6. COSMOTEER_MODS_DIR overrides discovery, so an unusual install can be
# pointed at by hand.
case6="$tmp_root/case6"
mkdir -p "$case6/elsewhere/Mods"
make_steam_home "$case6/home" "$case6/home/.local/share/Steam"
make_proton_mods "$case6/home/.local/share/Steam" >/dev/null
actual="$(HOME="$case6/home" COSMOTEER_MODS_DIR="$case6/elsewhere/Mods" resolve_mods_dir)"
check "COSMOTEER_MODS_DIR overrides discovery" "$case6/elsewhere/Mods" "$actual"
echo
if [ "$failures" -eq 0 ]; then
echo "All install.sh discovery tests passed."
else
echo "$failures test(s) failed."
exit 1
fi
+2 -17
View File
@@ -17,15 +17,10 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import rules_parser
import steam_paths
MOD_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CANDIDATE_DATA_DIRS = [
"/mnt/SSD4TB/SteamLibrary/steamapps/common/Cosmoteer/Data",
os.path.expanduser("~/.local/share/Steam/steamapps/common/Cosmoteer/Data"),
os.path.expanduser("~/.steam/steam/steamapps/common/Cosmoteer/Data"),
]
# Component fields whose value names another component in the same part.
COMPONENT_REF_FIELDS = {
"OperationalToggle",
@@ -55,17 +50,7 @@ def check(condition, message):
_failures.append(message)
def find_data_dir():
override = os.environ.get("COSMOTEER_DATA")
if override:
return override
for path in CANDIDATE_DATA_DIRS:
if os.path.isdir(path):
return path
return None
DATA = find_data_dir()
DATA = steam_paths.find_data_dir()
_vanilla_cache = {}
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# ABOUT ME
# Tests steam_paths.find_data_dir, which locates Cosmoteer's Data folder.
#
# Why this exists: the game can be installed to any Steam library on any disk,
# so the Data folder cannot be guessed from a fixed list of paths. Discovery
# reads Steam's own libraryfolders.vdf instead. These tests build fake Steam
# trees and check that each realistic layout is found.
#
# Usage: python3 tests/test_steam_paths.py
import os
import shutil
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import steam_paths
_failures = []
def check(name, expected, actual):
if expected == actual:
print(f"PASS: {name}")
else:
print(f"FAIL: {name}")
print(f" expected: {expected}")
print(f" actual: {actual}")
_failures.append(name)
def make_steam_home(home, libraries):
"""Write a libraryfolders.vdf under `home` listing the given library roots."""
vdf = os.path.join(home, ".local/share/Steam/steamapps/libraryfolders.vdf")
os.makedirs(os.path.dirname(vdf), exist_ok=True)
with open(vdf, "w", encoding="utf-8") as handle:
handle.write('"libraryfolders"\n{\n')
for index, library in enumerate(libraries):
handle.write(f'\t"{index}"\n\t{{\n\t\t"path"\t\t"{library}"\n\t}}\n')
handle.write("}\n")
def make_data_dir(library):
"""Create the Cosmoteer Data folder inside a library root."""
data = os.path.join(library, "steamapps/common/Cosmoteer/Data")
os.makedirs(data, exist_ok=True)
return data
def main():
tmp_root = tempfile.mkdtemp()
try:
# 1. The game lives in a secondary library on another disk.
case = os.path.join(tmp_root, "case1")
home = os.path.join(case, "home")
library = os.path.join(case, "disk2/steamapps")
make_steam_home(home, [os.path.join(home, ".local/share/Steam"), library])
expected = make_data_dir(library)
check(
"finds Data in a secondary library from libraryfolders.vdf",
expected,
steam_paths.find_data_dir(home=home, environ={}),
)
# 2. The game lives in the default library inside the Steam client dir.
case = os.path.join(tmp_root, "case2")
home = os.path.join(case, "home")
steam = os.path.join(home, ".local/share/Steam")
make_steam_home(home, [steam])
expected = make_data_dir(steam)
check(
"finds Data in the default Steam library",
expected,
steam_paths.find_data_dir(home=home, environ={}),
)
# 3. Library paths may contain spaces.
case = os.path.join(tmp_root, "case3")
home = os.path.join(case, "home")
library = os.path.join(case, "my games/steamapps")
make_steam_home(home, [os.path.join(home, ".local/share/Steam"), library])
expected = make_data_dir(library)
check(
"finds Data under a library path containing spaces",
expected,
steam_paths.find_data_dir(home=home, environ={}),
)
# 4. Nothing installed: discovery reports None instead of guessing.
case = os.path.join(tmp_root, "case4")
home = os.path.join(case, "home")
os.makedirs(home, exist_ok=True)
check(
"returns None when there is no install",
None,
steam_paths.find_data_dir(home=home, environ={}),
)
# 5. COSMOTEER_DATA overrides discovery.
case = os.path.join(tmp_root, "case5")
home = os.path.join(case, "home")
steam = os.path.join(home, ".local/share/Steam")
make_steam_home(home, [steam])
make_data_dir(steam)
override = os.path.join(case, "elsewhere/Data")
os.makedirs(override, exist_ok=True)
check(
"COSMOTEER_DATA overrides discovery",
override,
steam_paths.find_data_dir(home=home, environ={"COSMOTEER_DATA": override}),
)
finally:
shutil.rmtree(tmp_root, ignore_errors=True)
print()
if _failures:
print(f"{len(_failures)} test(s) failed.")
return 1
print("All Steam path discovery tests passed.")
return 0
if __name__ == "__main__":
sys.exit(main())