summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xtf2sgu259
1 files changed, 259 insertions, 0 deletions
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"<Profile path={self.path!r}>"
+
+ @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)