#!/usr/bin/env python3 """ setup-quake: Install ioquake3 and download Quake 3 data files to the real user's home directory for use at a retro gaming fair. Usage: sudo ./setup-quake """ import argparse import configparser import os import pwd import random import subprocess import sys import urllib.request import zipfile PLAYER_NAMES = [ "Ranger", "Keel", "Doom", "Slash", "Orbb", "Bones", "Hunter", "Major", "Mynx", "Sorlag", "Xaero", "Anarki", "Bitterman", "Grunt", "Hossman", "Klesk", "Lucy", "Patriot", "Phobos", "Razor", "Sarge", "Stripe", "Tank Jr", "Uriel", "Visor", "Wrack", "Cadavre", "Daemia", "Gorre", "Krusade", "Lynx", "Mouser", "Smear", "Tokay", "Twilight", "Warhero", "Erebus", "Galena", "Gargoyle", "Kiltron", "Merman", "Moloch", "Nailgun", "Peeker", "Rayne", "Skelebot", "Skrotch", "Swelt", "Thorn", "Whiskey", ] def get_script_dir(): """Return the directory containing the script or binary.""" if getattr(sys, "frozen", False): return os.path.dirname(sys.executable) return os.path.dirname(os.path.abspath(__file__)) def parse_args(): parser = argparse.ArgumentParser( description="Install ioquake3 and download Quake 3 data files." ) parser.add_argument( "--name", default=None, help="Player name to set in autoexec.cfg (default: random from built-in pool)", ) return parser.parse_args() def check_root(): if os.geteuid() != 0: print("Error: this script must be run as root (use sudo).", file=sys.stderr) sys.exit(1) def load_config(): config_path = os.path.join(get_script_dir(), "config.ini") cfg = configparser.ConfigParser() cfg.read(config_path) files_url = cfg.get("quake", "files_url", fallback="https://php.sustancia.synology.me/files/ioquake3-files.zip") install_dir = cfg.get("quake", "install_dir", fallback=".q3a") return files_url, install_dir def get_real_home(): """Resolve the actual user's home when running under sudo.""" sudo_user = os.environ.get("SUDO_USER") if sudo_user: home_dir = pwd.getpwnam(sudo_user).pw_dir return home_dir, sudo_user home_dir = os.path.expanduser("~") return home_dir, "root" def install_ioquake3(): print(" Running: apt-get install -y ioquake3") subprocess.run(["apt-get", "install", "-y", "ioquake3"], check=True) def download_files(url): """Download the Quake 3 files zip to /tmp. Returns destination path.""" dest = "/tmp/ioquake3-files.zip" print(f" Downloading: {url}") print(f" Destination: {dest}") with urllib.request.urlopen(url) as response: total = response.headers.get("Content-Length") total_mb = f"{int(total) / 1024 / 1024:.1f} MB" if total else "unknown size" print(f" File size: {total_mb}") downloaded = 0 chunk_size = 1024 * 1024 # 1 MB with open(dest, "wb") as f: while True: chunk = response.read(chunk_size) if not chunk: break f.write(chunk) downloaded += len(chunk) print(f" Downloaded: {downloaded / 1024 / 1024:.1f} MB", end="\r") print() # newline after progress return dest def extract_files(zip_path, home_dir, install_dir): print(f" Extracting to: {home_dir}") with zipfile.ZipFile(zip_path) as zf: zf.extractall(home_dir) os.remove(zip_path) print(f" Cleaned up: {zip_path}") def chown_extracted(target_dir, username): pw = pwd.getpwnam(username) for dirpath, dirnames, filenames in os.walk(target_dir): os.chown(dirpath, pw.pw_uid, pw.pw_gid) for name in filenames: os.chown(os.path.join(dirpath, name), pw.pw_uid, pw.pw_gid) def chmod_scripts(home_dir, install_dir): scripts_dir = os.path.join(home_dir, install_dir, "scripts") if not os.path.isdir(scripts_dir): print(f" Scripts dir not found: {scripts_dir}") return for name in os.listdir(scripts_dir): if name.endswith(".sh"): path = os.path.join(scripts_dir, name) os.chmod(path, os.stat(path).st_mode | 0o111) print(f" chmod +x: {path}") def set_player_name(home_dir, install_dir, name): autoexec = os.path.join(home_dir, install_dir, "baseq3", "autoexec.cfg") if not os.path.exists(autoexec): print(f" autoexec.cfg not found: {autoexec}") return # File may be read-only (set_client_mode runs after), ensure writable first os.chmod(autoexec, 0o644) with open(autoexec, "r") as f: lines = f.readlines() with open(autoexec, "w") as f: for line in lines: if line.startswith("set name "): f.write(f'set name "{name}"\n') else: f.write(line) print(f" Player name set to: {name}") def set_client_mode(home_dir, install_dir): autoexec = os.path.join(home_dir, install_dir, "baseq3", "autoexec.cfg") if os.path.exists(autoexec): os.chmod(autoexec, 0o444) print(f" Locked (read-only): {autoexec}") def main(): args = parse_args() check_root() player_name = args.name if args.name else random.choice(PLAYER_NAMES) files_url, install_dir = load_config() home_dir, username = get_real_home() print("=== setup-quake ===") print(f" User : {username}") print(f" Home directory : {home_dir}") print(f" Install dir : {os.path.join(home_dir, install_dir)}") print(f" Player name : {player_name}") print() print("[1/7] Installing ioquake3...") install_ioquake3() print() print("[2/7] Downloading Quake 3 data files...") zip_path = download_files(files_url) print() print("[3/7] Extracting data files...") extract_files(zip_path, home_dir, install_dir) print() print("[4/7] Fixing ownership...") chown_extracted(os.path.join(home_dir, install_dir), username) print() print("[5/7] Making scripts executable...") chmod_scripts(home_dir, install_dir) print() print("[6/7] Setting player name...") set_player_name(home_dir, install_dir, player_name) print() print("[7/7] Setting client mode...") set_client_mode(home_dir, install_dir) print() print("Done. Verify with:") print(f" dpkg -l ioquake3") print(f" ls {os.path.join(home_dir, install_dir)}") if __name__ == "__main__": main()