From 4f549de2e9b38bf81be891446cd3e2c296c9b92c Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 26 May 2022 21:07:59 +0200 Subject: Initial commit --- tf2sgu | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100755 tf2sgu diff --git a/tf2sgu b/tf2sgu new file mode 100755 index 0000000..fac02c8 --- /dev/null +++ b/tf2sgu @@ -0,0 +1,259 @@ +#!/usr/bin/python +"""A script to move savegames from the Transport Fever 2 directory to a cloud +directory. + +Aims to replace the missing Steam Cloud functionality for Transport Fever 2. +""" +import argparse +import logging +import sys +import shutil +import datetime +from collections import namedtuple +from pathlib import Path + +GAME_ID = "1066780" +STEAM_PATH = Path.home() / ".local" / "share" / "Steam" +SAVEGAME_SUFFIX = ".sav" +SUFFIXES = [".sav", ".jpg", ".sav.lua"] + +logger = logging.getLogger("tf2sgu") + + +class SaveGame(namedtuple("SaveGame", "name date base")): + """Represents a savegame on disk.""" + def __str__(self): + return f"{self.name} - {self.date}" + + def copy_to(self, dest_dir): + """Copies this savegame to the given directory. + + Note that this overrides existing savegames with this name! + """ + for suffix in SUFFIXES: + src = self.base.with_suffix(suffix) + dst = dest_dir / src.name + logging.debug(f"Copying {src!r} -> {dst!r}") + shutil.copy2(src, dst) + + +class UserException(Exception): + """Exception that shouldbe displayed to the user.""" + def __init__(self, msg): + super().__init__() + self.msg = msg + + +class Profile: + """An on-disk Steam profile.""" + def __init__(self, path): + self.path = path + + def __repr__(self): + return f"" + + @property + def user_id(self): + """Gives the Steam user ID of the profile.""" + return self.path.name + + @property + def save_dir(self): + """Gives the folder (as Path) for the savegames.""" + return self.path / GAME_ID / "local" / "save" + + def saves(self): + """Returns a list of all saves available in this profile.""" + return read_saves(self.save_dir) + + +def read_saves(path): + """Read the savegames in the given folder.""" + saves = [] + for item in path.iterdir(): + if item.suffix == SAVEGAME_SUFFIX: + name = item.stem + base = item.parent / name + date = datetime.datetime.fromtimestamp(item.stat().st_mtime) + saves.append(SaveGame(name, date, base)) + return saves + + +def guess_remote(): + """Guess where the cloud savegames are stored.""" + if (Path.home() / "ownCloud").is_dir(): + return Path.home() / "ownCloud" / "Transport Fever 2 Savegames" + return Path.home() / "Nextcloud" / "Transport Fever 2 Savegames" + + +def ask_choice(items, labels=None): + """Ask for a user choice in the CLI.""" + if labels is None: + labels = [str(item) for item in items] + for i, label in enumerate(labels, 1): + print(f"{i}:", label) + while True: + choice = input("? ") + try: + choice = int(choice) + return items[choice - 1] + except (ValueError, IndexError): + print("Invalid choice") + + +def get_steam_profiles(): + """Returns steam profiles that have data for Transport Fever 2 assigned.""" + profiles = [] + userdatas = STEAM_PATH / "userdata" + for user in userdatas.iterdir(): + if (user / GAME_ID).is_dir(): + profiles.append(Profile(user)) + return profiles + + +def find(iterable, **kwargs): + """Find an item with the given attributes in the given iterable.""" + for item in iterable: + if all(getattr(item, k) == v for (k, v) in kwargs.items()): + return item + return None + + +def select_profile(args): + """Lets the user select the right steam profile.""" + profiles = get_steam_profiles() + logger.debug(f"Found {len(profiles)} profiles") + if args.profile_id is not None: + profile = find(profiles, user_id=args.profile_id) + if profile is None: + raise UserException(f"Steam profile {args.profile_id} not found") + return profile + elif not profiles: + raise UserException("No profiles found") + elif len(profiles) == 1: + logger.debug(f"Automatically choosing {profiles[0]}") + return profiles[0] + else: + labels = [ + f"{profile.user_id}: {len(profile.saves())} savegames" + for profile in profiles + ] + print("Select your profile:") + return ask_choice(profiles, labels) + + +def cmd_tui(args): + """Load the user interface.""" + raise NotImplementedError() + + +def cmd_list_profiles(_args): + """List available Steam profiles (with Transport Fever 2 installed).""" + profiles = get_steam_profiles() + for profile in profiles: + print(profile.user_id, len(profile.saves()), "savegames") + + +def cmd_list_locals(args): + """List all available local savegames.""" + profile = select_profile(args) + for save in profile.saves(): + print(save) + + +def cmd_list_remotes(args): + """List all available remote savegames.""" + saves = read_saves(args.remote_dir) + for save in saves: + print(save) + + +def cmd_download(args): + """Download the given savegame from the remote folder to the local one.""" + profile = select_profile(args) + saves = read_saves(args.remote_dir) + if args.name is not None: + save = find(saves, name=args.name) + if save is None: + raise UserException(f"Savegame '{args.name}' not found") + else: + print("The following save games are available:") + save = ask_choice(saves) + logger.debug(f"Chose {save}") + save.copy_to(profile.save_dir) + + +def cmd_upload(args): + """Uploads the given savegame from the local profile to the remote folder.""" + profile = select_profile(args) + saves = profile.saves() + if args.name is not None: + save = find(saves, name=args.name) + if save is None: + raise UserException(f"Savegame '{args.name}' not found") + else: + print("The following save games are available:") + save = ask_choice(saves) + logger.debug(f"Chose {save}") + save.copy_to(args.remote_dir) + + +def get_parser(): + """Build the argument parser.""" + parser = argparse.ArgumentParser() + parser.add_argument("--profile-id", "-p", help="Specify the Steam user ID.") + parser.add_argument( + "--remote-dir", + "-r", + help="Specify the remote (cloud) directory.", + type=Path, + default=guess_remote(), + ) + parser.set_defaults(func=cmd_tui) + + subparsers = parser.add_subparsers() + + parser_list_profiles = subparsers.add_parser( + "list-profiles", help=cmd_list_profiles.__doc__ + ) + parser_list_profiles.set_defaults(func=cmd_list_profiles) + + parser_list_locals = subparsers.add_parser( + "list-local-saves", help=cmd_list_locals.__doc__ + ) + parser_list_locals.set_defaults(func=cmd_list_locals) + + parser_list_remotes = subparsers.add_parser( + "list-remote-saves", help=cmd_list_remotes.__doc__ + ) + parser_list_remotes.set_defaults(func=cmd_list_remotes) + + parser_download = subparsers.add_parser("download", help=cmd_download.__doc__) + parser_download.add_argument( + "name", nargs="?", help="Name of the savegame to download." + ) + parser_download.set_defaults(func=cmd_download) + + parser_upload = subparsers.add_parser("upload", help=cmd_upload.__doc__) + parser_upload.add_argument( + "name", nargs="?", help="Name of the savegame to upload." + ) + parser_upload.set_defaults(func=cmd_upload) + + return parser + + +def main(): + """Main entry point.""" + logging.basicConfig(level=logging.DEBUG) + parser = get_parser() + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + try: + main() + except UserException as exc: + print("ERROR:", exc.msg, file=sys.stderr) + sys.exit(1) -- cgit v1.2.3