diff --git a/bin/prepare_steam b/bin/prepare_steam new file mode 100755 index 0000000..c0da4ec --- /dev/null +++ b/bin/prepare_steam @@ -0,0 +1,120 @@ +#!/usr/bin/python +# Use Steam with external HDDs +# (c) 2015 Taeyeon Mori CC-BY-SA + +import sys +import os +import argparse +import posixpath +import ntpath + +import vdfparser + + +CONFIG = os.path.expanduser("~/.files/etc/prepare_steam.vdf") + + +def parse_args(argv): + parser = argparse.ArgumentParser(prog=argv[0]) + parser.add_argument("-p", "--platform", "--profile", default=sys.platform, help="Platform profile (%(default)s)") + parser.add_argument("--config", default=CONFIG, help="Use alternate config file") + return parser.parse_args(argv[1:]) + + +def take_sorted_list(dct, pred): + keys = list(filter(pred, dct.keys())) + lst = [dct[k] for k in sorted(keys)] + for k in keys: + del dct[k] + return lst + + +def detect_steam_platformpath(steamroot): + if os.path.exists(os.path.join(steamroot, "Steam.exe")): + return ntpath + else: + return posixpath + + +def main(argv): + args = parse_args(argv) + + vdf = vdfparser.VdfParser() + + with open(args.config) as f: + config = vdf.parse(f) + + profile = config[args.platform] + steamroot = os.path.expanduser(profile["steamroot"]) + + # Read steam libraryfolders.vdf + libsvdf = os.path.join(steamroot, "steamapps", "libraryfolders.vdf") + + with open(libsvdf) as f: + libs_config = vdf.parse(f) + + library_folders = take_sorted_list(libs_config["LibraryFolders"], str.isdigit) + + # Read steam config.vdf + steamvdf = os.path.join(steamroot, "config", "config.vdf") + + with open(steamvdf) as f: + steam_config = vdf.parse(f) + + client_config = steam_config["InstallConfigStore"]["Software"]["Valve"]["Steam"] + + take_sorted_list(client_config, lambda k: k.startswith("BaseInstallFolder_")) + + # Fix Library Folders + steampath = detect_steam_platformpath(steamroot) + do_normpath = profile.get("SanitizeLibraryPaths", "1") != "0" + + if do_normpath: + orig_library_folders = library_folders + library_folders = [] + + for f in orig_library_folders: + f = steampath.normpath(f) + if f not in library_folders: + library_folders.append(f) + + for path, steam_path in profile.get("Libraries", {}).items(): + if not steampath: + steam_path = path + + if do_normpath: + steam_path = steampath.normpath(steam_path) + + if os.path.exists(path): + if steam_path not in library_folders: + library_folders.append(steam_path) + print ("Added Library Folder at %s%s" % (path, (" (%s)" % steam_path) if steam_path != path else "")) + elif steam_path in library_folders: + print ("Removing unavailable Library Folder %s" % steam_path) + library_folders.remove(steam_path) + + for path in profile.get("LibraryBlacklist", {}).values(): + if path in library_folders: + print ("Removing blacklisted Library Folder %s" % path) + library_folders.remove(path) + + for i, path in enumerate(library_folders): + libs_config["LibraryFolders"][str(i + 1)] = path + client_config["BaseInstallFolder_%i" % (i + 1)] = path + + print("Available Libraries: %s" % ("\"%s\"" % "\", \"".join(library_folders)) if library_folders else "(none)") + + # Save new vdfs + os.rename(libsvdf, libsvdf + ".bak") + with open(libsvdf, "w") as f: + vdf.write(f, libs_config) + + os.rename(steamvdf, steamvdf + ".bak") + with open(steamvdf, "w") as f: + vdf.write(f, steam_config) + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/etc/prepare_steam.vdf b/etc/prepare_steam.vdf new file mode 100644 index 0000000..6a6e166 --- /dev/null +++ b/etc/prepare_steam.vdf @@ -0,0 +1,38 @@ +// prepare_steam config file +// In Valve VDF format +// Top-level keys are platform profiles (-p/--platform/--profile) +"linux" +{ + // Steam Install path + "steamroot" "~/.local/share/Steam/" + // Allow prepare_steam to sanitize library paths? + //"SanitizeLibraryPaths" "1" + // Libraries that prepare_steam should manage + "Libraries" + { + // "Actual Path" "Path to tell Steam" + // The latter may be empty, in which case the former is used + "/media/Data/SteamLibrary" "" + "/media/DumpStore/Steam/Library" "" + } + // Library folders that shouldn't be used by this profile + "LibraryBlacklist" + { + // "whatever" "Path Steam shouldn't use" + // The former value doesn't matter, must however be unique + "0" "/media/DumpStore/Steam/Windows Library" + } +} +"wine32" +{ + "steamroot" "~/.local/share/wineprefixes/Steam32/drive_c/Program Files/Steam/" + "Libraries" + { + "/media/Data/SteamLibrary" "E:\\SteamLibrary" + "/media/DumpStore/Steam/Windows Library" "F:\\Steam\\Windows Library" + } + "LibraryBlacklist" + { + "0" "F:\\Steam\\Library" + } +} \ No newline at end of file diff --git a/lib/python/vdfparser.py b/lib/python/vdfparser.py new file mode 100644 index 0000000..0ce4a35 --- /dev/null +++ b/lib/python/vdfparser.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# Parse Steam/Source VDF Files +# Reference: https://developer.valvesoftware.com/wiki/KeyValues#File_Format +# (c) 2015 Taeyeon Mori; CC-BY-SA + +from __future__ import unicode_literals + +import io + + +class VdfParser: + """ + Simple Steam/Source VDF parser + """ + # Special Characters + quote_char = "\"" + escape_char = "\\" + begin_char = "{" + end_char = "}" + whitespace_chars = " \t\n" + comment_char = "/" + newline_char = "\n" + + def __init__(self, *, encoding=False, factory=dict, strict=True): + """ + @brief Construct a VdfParser instance + @param encoding Encoding for bytes operations. Pass None to use unicode strings + @param factory A factory function creating a mapping type from an iterable of key/value tuples. + """ + self.encoding = encoding + if encoding: + self.empty_string = self.empty_string.encode(encoding) + self.quote_char = self.quote_char.encode(encoding) + self.escape_char = self.escape_char.encode(encoding) + self.begin_char = self.begin_char.encode(encoding) + self.end_char = self.end_char.encode(encoding) + self.whitespace_chars = self.whitespace_chars.encode(encoding) + self.comment_char = self.comment_char.encode(encoding) + self.newline_char = self.newline_char.encode(encoding) + self.factory = factory + self.strict = strict + + def _make_map(self, tokens): + return self.factory(zip(tokens[::2], tokens[1::2])) + + def _parse_map(self, fd, inner=False): + tokens = [] + current = [] + escape = False + quoted = False + comment = False + + if self.encoding: + make_string = b"".join + else: + make_string = "".join + + def finish(override=False): + if current or override: + tokens.append(make_string(current)) + current.clear() + + while True: + c = fd.read(1) + + if not c: + finish() + if len(tokens) / 2 != len(tokens) // 2: + raise ValueError("Unexpected EOF: Last pair incomplete") + elif self.strict and (escape or quoted or inner): + raise ValueError("Unexpected EOF: EOF encountered while not processing outermost mapping") + return self._make_map(tokens) + + if escape: + current.append(c) + escape = False + + elif quoted: + if c == self.escape_char: + escape = True + elif c == self.quote_char: + quoted = False + finish(override=True) + else: + current.append(c) + + elif comment: + if c == self.newline_char: + comment = False + + else: + if c == self.escape_char: + escape = True + elif c == self.begin_char: + finish() + if len(tokens) / 2 == len(tokens) // 2 and (self.strict or self.factory == dict): + raise ValueError("Sub-dictionary cannot be a key") + tokens.append(self._parse_map(fd, True)) + elif c == self.end_char: + finish() + if len(tokens) / 2 != len(tokens) // 2: + raise ValueError("Unexpected close: Missing last value (Unbalanced tokens)") + return self._make_map(tokens) + elif c in self.whitespace_chars: + finish() + elif c == self.quote_char: + finish() + quoted = True + elif c == self.comment_char and current and current[-1] == self.comment_char: + del current[-1] + finish() + comment = True + else: + current.append(c) + + def parse(self, fd): + """ + Parse a VDF file into a python dictionary + """ + return self._parse_map(fd) + + def parse_string(self, content): + """ + Parse the content of a VDF file + """ + if self.encoding: + return self.parse(io.BytesIO(content)) + else: + return self.parse(io.StringIO(content)) + + def _make_literal(self, lit): + # TODO + return "\"%s\"" % (str(lit).replace("\\", "\\\\").replace("\"", "\\\"")) + + def _write_map(self, fd, dictionary, indent): + if indent is None: + def write(str=None, i=False, d=False, nl=False): + if str: + fd.write(str) + if delim: + fd.write(" ") + + else: + def write(str=None, i=False, d=False, nl=False): + if not str and nl: + fd.write("\n") + else: + if i: + fd.write("\t" * indent) + if str: + fd.write(str) + if nl: + fd.write("\n") + elif d: + fd.write("\t\t") + + for k, v in dictionary.items(): + if isinstance(v, dict): + write(self._make_literal(k), i=1, d=1, nl=1) + write("{", i=1, nl=1) + self._write_map(fd, v, indent + 1 if indent is not None else None) + write("}", i=1) + else: + write(self._make_literal(k), i=1, d=1) + write(self._make_literal(v)) + write(d=1, nl=1) + + def write(self, fd, dictionary, *, pretty=True): + """ + Write a dictionary to a file in VDF format + """ + if self.encoding: + raise NotImplementedError("Writing in binary mode is not implemented yet.") # TODO (maybe) + self._write_map(fd, dictionary, 0 if pretty else None)