parent
9e14a02ec9
commit
bfedb4506a
3 changed files with 332 additions and 0 deletions
@ -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)) |
@ -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" |
||||||
|
} |
||||||
|
} |
@ -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) |
Loading…
Reference in new issue