Files
ElHombreAlex--FTL-Faster-Th…/tools/setup_environment.py
T
ElHombreAlex 35cdf71054 V0.1
First publishable version of the mod
2026-10-03 12:59:33 +02:00

130 lines
5.9 KiB
Python

"""Create this source checkout's Python environment, or check launch readiness.
Setup installs Python packages and creates local configuration only. It never
copies, patches, launches or attaches to FTL. --check and --dry-run are read-only.
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
PROJECT = Path(__file__).resolve().parents[1]
VENV = PROJECT / ".venv"
VENV_PYTHON = VENV / "Scripts" / "python.exe"
LOCAL = PROJECT / "local_game_data"
CONFIG = LOCAL / "launcher.json"
def require_platform() -> None:
if os.name != "nt":
raise ValueError("The live game bridge currently supports Windows only.")
if sys.version_info < (3, 10):
raise ValueError("Python 3.10 or newer is required; Python 3.12 is recommended.")
def setup(dry_run: bool = False) -> None:
require_platform()
steps = [
f"Create or reuse {VENV}",
"Install the packages from requirements.txt inside .venv (internet access required)",
"Copy config/launcher.example.json to local_game_data/launcher.json if absent",
"Keep all game paths unconfigured until you edit launcher.json",
]
for step in steps:
print(step, flush=True)
if dry_run:
print("Dry run complete: no files or dependencies were changed.")
return
if VENV.exists() and not VENV_PYTHON.is_file():
raise ValueError(".venv exists but is incomplete. Rename that folder and run SETUP.cmd again.")
if not VENV_PYTHON.is_file():
subprocess.run([sys.executable, "-m", "venv", str(VENV)], check=True)
subprocess.run(
[str(VENV_PYTHON), "-m", "pip", "install", "-r", str(PROJECT / "requirements.txt")],
check=True,
)
LOCAL.mkdir(exist_ok=True)
(LOCAL / ".gdignore").touch()
if not CONFIG.exists():
shutil.copyfile(PROJECT / "config" / "launcher.example.json", CONFIG)
print("\nPython environment ready. Finish docs/INSTALLATION.md before launching.")
print(f"Edit your local paths in: {CONFIG}")
print("SETUP.cmd does not install Hyperspace or prepare a game copy.")
def check(desktop: bool = False) -> None:
require_platform()
missing = [name for name in ("frida", "PIL", "numpy", "capstone")
if importlib.util.find_spec(name) is None]
if missing:
raise ValueError("Missing Python packages: " + ", ".join(missing) + ". Run SETUP.cmd first.")
if not CONFIG.is_file():
raise ValueError("Missing local_game_data/launcher.json. Run SETUP.cmd and edit its paths.")
config = json.loads(CONFIG.read_text(encoding="utf-8-sig"))
if not isinstance(config, dict):
raise ValueError("launcher.json must contain a JSON object with lab, hooks and godot paths.")
for key in ("lab", "hooks", "godot"):
if not isinstance(config.get(key), str) or not config[key].strip():
raise ValueError(f"launcher.json needs a nonempty {key} path.")
if not Path(config[key]).is_absolute():
raise ValueError(f"Use an absolute path for {key} in launcher.json.")
lab = Path(config["lab"])
if not lab.is_dir():
raise ValueError("The configured lab directory does not exist. Complete isolated lab preparation.")
for key in ("hooks", "godot"):
if not Path(config[key]).is_file():
raise ValueError(f"The configured {key} file does not exist: {config[key]}")
if not (lab / "ftlvr-lab.json").is_file():
raise ValueError("The configured lab has no ftlvr-lab.json marker. Complete isolated lab preparation.")
marker = json.loads((lab / "ftlvr-lab.json").read_text(encoding="utf-8-sig"))
if not isinstance(marker, dict):
raise ValueError("The lab marker is malformed. Use a correctly prepared isolated lab.")
source = marker.get("source_game")
if not isinstance(source, str) or not (Path(source) / "ftl.dat").is_file():
raise ValueError("The owned original installation recorded by the lab is unavailable. Restore that path.")
if not (LOCAL / "manifest.json").is_file():
raise ValueError("Local game assets are missing. Run tools/extract_ftl.py on your owned ftl.dat.")
manifest = json.loads((LOCAL / "manifest.json").read_text(encoding="utf-8-sig"))
if not isinstance(manifest, dict):
raise ValueError("The local asset manifest is malformed. Run tools/extract_ftl.py again.")
if not manifest.get("ships") or not manifest.get("fonts") or not manifest.get("ui_assets"):
raise ValueError("Local extraction is incomplete. Run tools/extract_ftl.py again.")
# The production launcher's own checks include executable fingerprints,
# required hook families, dependencies, loader and the OpenXR runtime.
sys.dont_write_bytecode = True
from launch import preflight
result = preflight(config, desktop=desktop)
print(json.dumps(result, indent=2))
print("Setup checks passed. No game was launched or modified.")
print("This checks readiness; it does not measure headset performance.")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
modes = parser.add_mutually_exclusive_group()
modes.add_argument("--check", action="store_true", help="Read-only local launch preflight")
modes.add_argument("--dry-run", action="store_true", help="Describe setup without writing or installing")
parser.add_argument("--desktop", action="store_true", help="Skip the SteamVR runtime check with --check")
args = parser.parse_args()
if args.desktop and not args.check:
parser.error("--desktop is only valid with --check")
if args.check:
check(desktop=args.desktop)
else:
setup(dry_run=args.dry_run)
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
print(f"FTL Tabletop VR setup: {error}", file=sys.stderr)
sys.exit(1)