feat: v4.1.0 -- workshop-ready (logo, path discovery, docs)
Players install from Steam Workshop instead of install.sh, which failed on Windows (bash) and a Manjaro box (hard-coded Mods globs). Adds Logo + logo.png the uploader ships, tools/steam_paths.py reading Steam's libraryfolders.vdf (Flatpak/Snap/second disk/Windows), and README install tiers plus in-game publishing steps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014TkQMhvEyoFqKzeCC69N6o
This commit is contained in:
@@ -96,26 +96,70 @@ No stat or number is changed. Only *when* radiators run.
|
|||||||
|
|
||||||
```
|
```
|
||||||
mod.rules manifest + patch actions
|
mod.rules manifest + patch actions
|
||||||
|
logo.png Workshop preview / in-game mod logo
|
||||||
components/radiator_auto.rules measures network headroom, gates the radiator
|
components/radiator_auto.rules measures network headroom, gates the radiator
|
||||||
tests/ offline validation suite
|
tests/ offline validation suite
|
||||||
|
tools/steam_paths.py finds the game and the Mods folder via Steam's library list
|
||||||
tools/dotnet_enum.py reads component schemas out of Cosmoteer.dll
|
tools/dotnet_enum.py reads component schemas out of Cosmoteer.dll
|
||||||
tools/ildump.py disassembles component methods to readable CIL
|
tools/ildump.py disassembles component methods to readable CIL
|
||||||
install.sh validate + copy into the Mods folder
|
install.sh author's dev loop: validate + copy into the Mods folder
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
### Steam Workshop (any OS)
|
||||||
|
|
||||||
|
Subscribe to **Auto Radiator** on the Steam Workshop and enable it in the
|
||||||
|
game's Mods menu. Steam places it under
|
||||||
|
`steamapps/workshop/content/799600/<item id>/` and the game loads it from
|
||||||
|
there; no files to copy.
|
||||||
|
|
||||||
|
*Workshop link: pending first upload.*
|
||||||
|
|
||||||
|
### Manual (any OS)
|
||||||
|
|
||||||
|
Copy `mod.rules`, `logo.png` and the `components/` folder into a folder named
|
||||||
|
`auto_radiator` inside your user Mods folder, then enable it in the Mods menu:
|
||||||
|
|
||||||
|
| | Mods folder |
|
||||||
|
|---|---|
|
||||||
|
| Windows | `%USERPROFILE%\Saved Games\Cosmoteer\<SteamID>\Mods\` |
|
||||||
|
| Linux (Proton) | `<Steam library>/steamapps/compatdata/799600/pfx/drive_c/users/steamuser/Saved Games/Cosmoteer/<SteamID>/Mods/` |
|
||||||
|
|
||||||
|
### From this repo (Linux, for development)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./install.sh
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Then enable **Auto Radiator** in the game's Mods menu. `./install.sh --uninstall`
|
Validates against the installed game first, then copies the same file set the
|
||||||
removes it. If the script cannot find your Mods folder, set `COSMOTEER_MODS_DIR`.
|
Workshop uploader ships. `./install.sh --uninstall` removes it. Discovery reads
|
||||||
|
Steam's `libraryfolders.vdf`, so a second disk, Flatpak, Snap or the Debian
|
||||||
|
package all work; if it still cannot find your Mods folder, set
|
||||||
|
`COSMOTEER_MODS_DIR` (and `COSMOTEER_DATA` for the game data).
|
||||||
|
|
||||||
|
## Publishing to the Workshop
|
||||||
|
|
||||||
|
The uploader is built into the game; no `steamcmd` needed.
|
||||||
|
|
||||||
|
1. `./install.sh` so the current files sit in the user Mods folder.
|
||||||
|
2. In game: **Mods** → find *Auto Radiator* under *User Mods* → right-click →
|
||||||
|
**Upload to Steam Workshop**.
|
||||||
|
3. Fill name, description, preview image (defaults to `logo.png`), visibility
|
||||||
|
and game version range → **Start Upload**.
|
||||||
|
4. Updates use the same menu item; it asks for change notes. Bump `Version`
|
||||||
|
in `mod.rules` first — multiplayer and save syncing match on it.
|
||||||
|
|
||||||
|
The **entire** mod folder is uploaded, which is why `install.sh` copies only
|
||||||
|
`mod.rules`, `logo.png` and `components/`. The Workshop item is bound to the
|
||||||
|
mod `ID`; keep `ganter.auto_radiator` stable. Steam does not refresh the
|
||||||
|
preview image on later uploads, and rejects previews over 500 KB.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 tests/test_mod.py
|
python3 tests/test_mod.py # mod against installed game data
|
||||||
|
python3 tests/test_steam_paths.py # Steam library discovery, on fake layouts
|
||||||
```
|
```
|
||||||
|
|
||||||
Cosmoteer applies mod actions to vanilla data at load time. Some mistakes crash
|
Cosmoteer applies mod actions to vanilla data at load time. Some mistakes crash
|
||||||
@@ -127,7 +171,8 @@ silently do nothing; see **Status** below for the four this project hit and the
|
|||||||
guard for each. The suite also self-checks its own parser against known vanilla
|
guard for each. The suite also self-checks its own parser against known vanilla
|
||||||
facts first, so a broken parser fails loudly instead of passing everything.
|
facts first, so a broken parser fails loudly instead of passing everything.
|
||||||
|
|
||||||
Set `COSMOTEER_DATA` to point at a different install, and `COSMOTEER_ASSEMBLY`
|
The game is found through Steam's library list (`tools/steam_paths.py`). Set
|
||||||
|
`COSMOTEER_DATA` to point at a different install, and `COSMOTEER_ASSEMBLY`
|
||||||
for the schema checks.
|
for the schema checks.
|
||||||
|
|
||||||
## How to tell it is working
|
## How to tell it is working
|
||||||
|
|||||||
+11
-24
@@ -2,10 +2,12 @@
|
|||||||
# ABOUT ME
|
# ABOUT ME
|
||||||
# Installs the Auto Radiator mod into Cosmoteer's user Mods folder.
|
# Installs the Auto Radiator mod into Cosmoteer's user Mods folder.
|
||||||
#
|
#
|
||||||
# Why it exists: Cosmoteer loads mods from the user profile directory, which on
|
# Why it exists: this is the author's dev loop. Cosmoteer loads user mods from
|
||||||
# a Proton install lives deep inside the Steam compatdata prefix and is awkward
|
# the profile directory, which on a Proton install lives deep inside the Steam
|
||||||
# to find by hand. This script locates it, validates the mod against the
|
# compatdata prefix. This script locates it, validates the mod against the
|
||||||
# installed game data first, and copies only the files the game needs.
|
# installed game data first, and copies only the files the game needs -- the
|
||||||
|
# exact set that "Upload to Steam Workshop" later ships. Players install from
|
||||||
|
# the Workshop instead; see README.md.
|
||||||
#
|
#
|
||||||
# Usage: ./install.sh install (validates first)
|
# Usage: ./install.sh install (validates first)
|
||||||
# ./install.sh --uninstall remove the installed copy
|
# ./install.sh --uninstall remove the installed copy
|
||||||
@@ -15,25 +17,10 @@ set -euo pipefail
|
|||||||
MOD_ID="auto_radiator"
|
MOD_ID="auto_radiator"
|
||||||
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
find_mods_dir() {
|
# Discovery reads Steam's own library list, so a second disk, Flatpak, Snap
|
||||||
# A Cosmoteer profile folder is named after the Steam ID, so glob for it.
|
# or the Debian package all work. See tools/steam_paths.py.
|
||||||
local candidates=(
|
if ! MODS_DIR="$(python3 "$SOURCE_DIR/tools/steam_paths.py" --mods)"; then
|
||||||
"$HOME"/.local/share/Steam/steamapps/compatdata/799600/pfx/drive_c/users/steamuser/"Saved Games"/Cosmoteer/*/Mods
|
echo "Set COSMOTEER_MODS_DIR to the Mods folder and run again." >&2
|
||||||
/mnt/*/SteamLibrary/steamapps/compatdata/799600/pfx/drive_c/users/steamuser/"Saved Games"/Cosmoteer/*/Mods
|
|
||||||
"$HOME"/"Saved Games"/Cosmoteer/*/Mods
|
|
||||||
"$HOME"/Documents/"My Games"/Cosmoteer/*/Mods
|
|
||||||
)
|
|
||||||
for path in "${candidates[@]}"; do
|
|
||||||
[ -d "$path" ] && printf '%s\n' "$path" && return 0
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
MODS_DIR="${COSMOTEER_MODS_DIR:-$(find_mods_dir || true)}"
|
|
||||||
|
|
||||||
if [ -z "$MODS_DIR" ]; then
|
|
||||||
echo "Could not find the Cosmoteer Mods folder." >&2
|
|
||||||
echo "Set COSMOTEER_MODS_DIR to it and run again." >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -52,7 +39,7 @@ echo
|
|||||||
echo "Installing to $TARGET"
|
echo "Installing to $TARGET"
|
||||||
rm -rf "$TARGET"
|
rm -rf "$TARGET"
|
||||||
mkdir -p "$TARGET"
|
mkdir -p "$TARGET"
|
||||||
cp "$SOURCE_DIR/mod.rules" "$TARGET/"
|
cp "$SOURCE_DIR/mod.rules" "$SOURCE_DIR/logo.png" "$TARGET/"
|
||||||
cp -r "$SOURCE_DIR/components" "$TARGET/"
|
cp -r "$SOURCE_DIR/components" "$TARGET/"
|
||||||
|
|
||||||
echo "Done. Enable \"Auto Radiator\" in the game's Mods menu."
|
echo "Done. Enable \"Auto Radiator\" in the game's Mods menu."
|
||||||
|
|||||||
@@ -22,10 +22,11 @@
|
|||||||
|
|
||||||
ID = ganter.auto_radiator
|
ID = ganter.auto_radiator
|
||||||
Name = "Auto Radiator"
|
Name = "Auto Radiator"
|
||||||
Version = 4.0.0
|
Version = 4.1.0
|
||||||
CompatibleGameVersions = ["0.30.4c", "0.30.4"]
|
CompatibleGameVersions = ["0.30.4c", "0.30.4"]
|
||||||
ModifiesGameplay = true
|
ModifiesGameplay = true
|
||||||
Author = "Tobias Ganter"
|
Author = "Tobias Ganter"
|
||||||
|
Logo = "logo.png"
|
||||||
|
|
||||||
Description = "Radiators switched OFF bank heat and vent only when the ship's heat storage is nearly full.\n\n"\
|
Description = "Radiators switched OFF bank heat and vent only when the ship's heat storage is nearly full.\n\n"\
|
||||||
"Thermal Canister Missiles are built from your ship's own heat, and in vanilla "\
|
"Thermal Canister Missiles are built from your ship's own heat, and in vanilla "\
|
||||||
|
|||||||
+25
-15
@@ -8,23 +8,20 @@
|
|||||||
# against the player's actual installed game data, before the game is launched.
|
# against the player's actual installed game data, before the game is launched.
|
||||||
#
|
#
|
||||||
# Run: python3 tests/test_mod.py
|
# Run: python3 tests/test_mod.py
|
||||||
# The game install is found automatically, or set COSMOTEER_DATA to override.
|
# The game install is found through Steam's own library list (tools/steam_paths.py),
|
||||||
|
# or set COSMOTEER_DATA to override.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
MOD_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, os.path.join(MOD_ROOT, "tools"))
|
||||||
|
|
||||||
import rules_parser
|
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 fields whose value names another component in the same part.
|
||||||
COMPONENT_REF_FIELDS = {
|
COMPONENT_REF_FIELDS = {
|
||||||
@@ -56,13 +53,8 @@ def check(condition, message):
|
|||||||
|
|
||||||
|
|
||||||
def find_data_dir():
|
def find_data_dir():
|
||||||
override = os.environ.get("COSMOTEER_DATA")
|
"""Locate the installed game's Data folder via tools/steam_paths.py."""
|
||||||
if override:
|
return steam_paths.find_data_dir()
|
||||||
return override
|
|
||||||
for path in CANDIDATE_DATA_DIRS:
|
|
||||||
if os.path.isdir(path):
|
|
||||||
return path
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
DATA = find_data_dir()
|
DATA = find_data_dir()
|
||||||
@@ -745,6 +737,24 @@ def test_manifest_is_well_formed():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_manifest_declares_existing_logo():
|
||||||
|
"""Steam Workshop shows the mod's Logo; the file must ship inside the mod
|
||||||
|
folder, be a real PNG, and stay under Steam's 500 KB preview limit."""
|
||||||
|
mod = rules_parser.parse_file(os.path.join(MOD_ROOT, "mod.rules"))
|
||||||
|
logo = mod.get("Logo")
|
||||||
|
check(logo is not None, "mod.rules is missing the 'Logo' field")
|
||||||
|
if logo is None:
|
||||||
|
return
|
||||||
|
path = os.path.join(MOD_ROOT, logo.value.strip('"'))
|
||||||
|
check(os.path.isfile(path), f"Logo file does not exist: {path}")
|
||||||
|
if not os.path.isfile(path):
|
||||||
|
return
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
magic = f.read(8)
|
||||||
|
check(magic == b"\x89PNG\r\n\x1a\n", f"Logo is not a PNG: {path}")
|
||||||
|
check(os.path.getsize(path) < 500_000, "Logo must be under 500 KB for Workshop upload")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if DATA is None or not os.path.isdir(DATA):
|
if DATA is None or not os.path.isdir(DATA):
|
||||||
print("SKIP: Cosmoteer Data folder not found. Set COSMOTEER_DATA to run these tests.")
|
print("SKIP: Cosmoteer Data folder not found. Set COSMOTEER_DATA to run these tests.")
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# ABOUT ME
|
||||||
|
# Tests for tools/steam_paths.py, the Steam library discovery shared by
|
||||||
|
# install.sh and tests/test_mod.py.
|
||||||
|
#
|
||||||
|
# Why this exists: the discovery must work on machines the author does not
|
||||||
|
# own -- Flatpak Steam, a second library on another disk, Windows profiles.
|
||||||
|
# Each test builds a fake filesystem under a temporary HOME, so no real Steam
|
||||||
|
# install is needed and every layout is exercised on every run.
|
||||||
|
#
|
||||||
|
# Run: python3 tests/test_steam_paths.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tools"))
|
||||||
|
|
||||||
|
import steam_paths
|
||||||
|
|
||||||
|
_failures = []
|
||||||
|
_checks = 0
|
||||||
|
|
||||||
|
|
||||||
|
def check(condition, message):
|
||||||
|
global _checks
|
||||||
|
_checks += 1
|
||||||
|
if not condition:
|
||||||
|
_failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
PROTON_MODS = "steamapps/compatdata/799600/pfx/drive_c/users/steamuser/Saved Games/Cosmoteer/76561190000000000/Mods"
|
||||||
|
|
||||||
|
|
||||||
|
def make_library(root, with_game=True, with_mods=True):
|
||||||
|
os.makedirs(os.path.join(root, "steamapps"), exist_ok=True)
|
||||||
|
if with_game:
|
||||||
|
os.makedirs(os.path.join(root, "steamapps/common/Cosmoteer/Data"))
|
||||||
|
if with_mods:
|
||||||
|
os.makedirs(os.path.join(root, PROTON_MODS))
|
||||||
|
|
||||||
|
|
||||||
|
def write_vdf(steam_root, library_paths):
|
||||||
|
entries = "".join(
|
||||||
|
f'\t"{i}"\n\t{{\n\t\t"path"\t\t"{p}"\n\t\t"label"\t\t""\n\t}}\n'
|
||||||
|
for i, p in enumerate(library_paths)
|
||||||
|
)
|
||||||
|
os.makedirs(os.path.join(steam_root, "steamapps"), exist_ok=True)
|
||||||
|
with open(os.path.join(steam_root, "steamapps/libraryfolders.vdf"), "w") as f:
|
||||||
|
f.write('"libraryfolders"\n{\n' + entries + "}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def test_second_library_from_vdf():
|
||||||
|
with tempfile.TemporaryDirectory() as home:
|
||||||
|
steam = os.path.join(home, ".local/share/Steam")
|
||||||
|
second = os.path.join(home, "disk2/SteamLibrary")
|
||||||
|
make_library(steam, with_game=False, with_mods=False)
|
||||||
|
make_library(second)
|
||||||
|
write_vdf(steam, [steam, second])
|
||||||
|
env = {"HOME": home}
|
||||||
|
check(
|
||||||
|
steam_paths.find_data_dir(env) == os.path.join(second, "steamapps/common/Cosmoteer/Data"),
|
||||||
|
"Data dir in a second library listed in libraryfolders.vdf was not found",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
steam_paths.find_mods_dir(env) == os.path.join(second, PROTON_MODS),
|
||||||
|
"Proton Mods dir in a second library was not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flatpak_steam_root():
|
||||||
|
with tempfile.TemporaryDirectory() as home:
|
||||||
|
steam = os.path.join(home, ".var/app/com.valvesoftware.Steam/.local/share/Steam")
|
||||||
|
make_library(steam)
|
||||||
|
env = {"HOME": home}
|
||||||
|
check(
|
||||||
|
steam_paths.find_data_dir(env) == os.path.join(steam, "steamapps/common/Cosmoteer/Data"),
|
||||||
|
"Flatpak Steam root was not searched for the Data dir",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
steam_paths.find_mods_dir(env) == os.path.join(steam, PROTON_MODS),
|
||||||
|
"Flatpak Steam root was not searched for the Mods dir",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_steam_root_without_vdf_still_works():
|
||||||
|
with tempfile.TemporaryDirectory() as home:
|
||||||
|
steam = os.path.join(home, ".steam/debian-installation")
|
||||||
|
make_library(steam)
|
||||||
|
env = {"HOME": home}
|
||||||
|
check(
|
||||||
|
steam_paths.find_data_dir(env) == os.path.join(steam, "steamapps/common/Cosmoteer/Data"),
|
||||||
|
"A Steam root with no libraryfolders.vdf should still be searched itself",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_windows_profile_mods_dir():
|
||||||
|
with tempfile.TemporaryDirectory() as profile:
|
||||||
|
mods = os.path.join(profile, "Saved Games/Cosmoteer/76561190000000000/Mods")
|
||||||
|
os.makedirs(mods)
|
||||||
|
env = {"USERPROFILE": profile}
|
||||||
|
check(steam_paths.find_mods_dir(env) == mods, "Windows Saved Games Mods dir was not found")
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_overrides_win():
|
||||||
|
with tempfile.TemporaryDirectory() as home:
|
||||||
|
env = {"HOME": home, "COSMOTEER_DATA": "/override/Data", "COSMOTEER_MODS_DIR": "/override/Mods"}
|
||||||
|
check(steam_paths.find_data_dir(env) == "/override/Data", "COSMOTEER_DATA override ignored")
|
||||||
|
check(steam_paths.find_mods_dir(env) == "/override/Mods", "COSMOTEER_MODS_DIR override ignored")
|
||||||
|
|
||||||
|
|
||||||
|
def test_nothing_found_returns_none_and_lists_roots():
|
||||||
|
with tempfile.TemporaryDirectory() as home:
|
||||||
|
env = {"HOME": home}
|
||||||
|
check(steam_paths.find_data_dir(env) is None, "Empty HOME must yield None for Data")
|
||||||
|
check(steam_paths.find_mods_dir(env) is None, "Empty HOME must yield None for Mods")
|
||||||
|
roots = steam_paths.steam_roots(env)
|
||||||
|
check(any(r.startswith(home) for r in roots), "steam_roots must report the candidates it would try")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for name, test in sorted(globals().items()):
|
||||||
|
if name.startswith("test_") and callable(test):
|
||||||
|
before = len(_failures)
|
||||||
|
test()
|
||||||
|
print(f" [{'PASS' if len(_failures) == before else 'FAIL'}] {name}")
|
||||||
|
print(f"\n{_checks} checks run.")
|
||||||
|
if _failures:
|
||||||
|
print(f"{len(_failures)} FAILED:\n")
|
||||||
|
for failure in _failures:
|
||||||
|
print(f" - {failure}")
|
||||||
|
return 1
|
||||||
|
print("All checks passed.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Executable
+119
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# ABOUT ME
|
||||||
|
# Finds the installed Cosmoteer game data and the user Mods folder, wherever
|
||||||
|
# Steam put them. Shared by install.sh and tests/test_mod.py.
|
||||||
|
#
|
||||||
|
# Why it exists: Steam lives in different places (native, Flatpak, Snap,
|
||||||
|
# Debian package, Windows) and a game can sit in any library listed in
|
||||||
|
# steamapps/libraryfolders.vdf, often on another disk. Hard-coded globs miss
|
||||||
|
# those; this reads Steam's own library list instead.
|
||||||
|
#
|
||||||
|
# Environment overrides always win: COSMOTEER_DATA for the Data folder,
|
||||||
|
# COSMOTEER_MODS_DIR for the Mods folder.
|
||||||
|
#
|
||||||
|
# Usage: python3 tools/steam_paths.py --data print the Data folder
|
||||||
|
# python3 tools/steam_paths.py --mods print the Mods folder
|
||||||
|
# Exit 1 with the roots tried on stderr when nothing is found.
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
APP_ID = "799600"
|
||||||
|
PROTON_PROFILE = "compatdata/" + APP_ID + "/pfx/drive_c/users/steamuser/Saved Games/Cosmoteer"
|
||||||
|
|
||||||
|
# Steam roots relative to HOME, in the order they are tried.
|
||||||
|
STEAM_ROOTS_UNDER_HOME = [
|
||||||
|
".local/share/Steam",
|
||||||
|
".steam/steam",
|
||||||
|
".steam/debian-installation",
|
||||||
|
".var/app/com.valvesoftware.Steam/.local/share/Steam",
|
||||||
|
".snap/steam/common/.local/share/Steam",
|
||||||
|
"snap/steam/common/.local/share/Steam",
|
||||||
|
]
|
||||||
|
|
||||||
|
_VDF_PATH = re.compile(r'"path"\s+"([^"]+)"')
|
||||||
|
|
||||||
|
|
||||||
|
def steam_roots(env=None):
|
||||||
|
"""Candidate Steam install roots for this environment (existing or not)."""
|
||||||
|
env = os.environ if env is None else env
|
||||||
|
home = env.get("HOME")
|
||||||
|
roots = [os.path.join(home, rel) for rel in STEAM_ROOTS_UNDER_HOME] if home else []
|
||||||
|
program_files = env.get("PROGRAMFILES(X86)") or env.get("ProgramFiles(x86)")
|
||||||
|
if program_files:
|
||||||
|
roots.append(os.path.join(program_files, "Steam"))
|
||||||
|
return roots
|
||||||
|
|
||||||
|
|
||||||
|
def libraries(env=None):
|
||||||
|
"""Every Steam library folder: each root plus what its libraryfolders.vdf lists."""
|
||||||
|
seen = []
|
||||||
|
for root in steam_roots(env):
|
||||||
|
for candidate in [root] + _vdf_libraries(root):
|
||||||
|
candidate = os.path.realpath(candidate)
|
||||||
|
if candidate not in seen and os.path.isdir(os.path.join(candidate, "steamapps")):
|
||||||
|
seen.append(candidate)
|
||||||
|
return seen
|
||||||
|
|
||||||
|
|
||||||
|
def _vdf_libraries(root):
|
||||||
|
vdf = os.path.join(root, "steamapps", "libraryfolders.vdf")
|
||||||
|
if not os.path.isfile(vdf):
|
||||||
|
return []
|
||||||
|
with open(vdf, encoding="utf-8", errors="replace") as f:
|
||||||
|
return [p.replace("\\\\", "\\") for p in _VDF_PATH.findall(f.read())]
|
||||||
|
|
||||||
|
|
||||||
|
def find_data_dir(env=None):
|
||||||
|
env = os.environ if env is None else env
|
||||||
|
override = env.get("COSMOTEER_DATA")
|
||||||
|
if override:
|
||||||
|
return override
|
||||||
|
for library in libraries(env):
|
||||||
|
data = os.path.join(library, "steamapps", "common", "Cosmoteer", "Data")
|
||||||
|
if os.path.isdir(data):
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_mods_dir(env=None):
|
||||||
|
env = os.environ if env is None else env
|
||||||
|
override = env.get("COSMOTEER_MODS_DIR")
|
||||||
|
if override:
|
||||||
|
return override
|
||||||
|
patterns = [
|
||||||
|
os.path.join(library, "steamapps", PROTON_PROFILE, "*", "Mods") for library in libraries(env)
|
||||||
|
]
|
||||||
|
profile = env.get("USERPROFILE")
|
||||||
|
if profile:
|
||||||
|
patterns.append(os.path.join(profile, "Saved Games", "Cosmoteer", "*", "Mods"))
|
||||||
|
for pattern in patterns:
|
||||||
|
for match in sorted(glob.glob(pattern)):
|
||||||
|
if os.path.isdir(match):
|
||||||
|
return match
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
if argv[1:] == ["--data"]:
|
||||||
|
found = find_data_dir()
|
||||||
|
what = "Cosmoteer Data folder"
|
||||||
|
elif argv[1:] == ["--mods"]:
|
||||||
|
found = find_mods_dir()
|
||||||
|
what = "Cosmoteer Mods folder"
|
||||||
|
else:
|
||||||
|
print(__doc__ or "usage: steam_paths.py --data | --mods", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
if found:
|
||||||
|
print(found)
|
||||||
|
return 0
|
||||||
|
print(f"Could not find the {what}. Steam roots tried:", file=sys.stderr)
|
||||||
|
for root in steam_roots():
|
||||||
|
print(f" {root}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv))
|
||||||
Reference in New Issue
Block a user