zvodd
11/29/2017 - 8:20 PM

Save scummer for "Getting Over It with Bennett Foddy"

Save scummer for "Getting Over It with Bennett Foddy"

import winreg
from pprint import pprint
import codecs
import xml.etree.ElementTree as ET
import os
import json
from datetime import datetime
import traceback
import sys


LOCATION_REG_KEY = "SOFTWARE\\Bennett Foddy\\Getting Over It"
IMPORTANT_KEYS = ["SaveGame0_h1867918426",
                  "SaveGame1_h1867918427", "NumSaves_h765021473"]

SAVE_FOLDER = "GoI_Saves"
SAVE_EXTENTION = "goi-scum-json"

MENU_TEXT = (
    """ Type an Option:
    X = exit
    Q = exit
    S = Save Current State
    L = Load last save-file as Current State
    O = Open specific file and load as Current State

    * Current State is what is written in the windows registry.
      i.e. what the game will read.

    * Rember to close the game before saving and loading.
""")


def print_exception(ex, msg, do_print=True):
    tb = ''.join(traceback.format_exception(
        etype=type(ex), value=ex, tb=ex.__traceback__))
    out = "Exception:\n{}\n{}".format(msg, tb)
    if do_print:
        print(out, file=sys.stderr)
    else:
        return out


class CustomException(Exception):
    pass


class BadXMLValues(CustomException):
    pass


class NoSavesFound(CustomException):
    pass


class BadSaveFile(CustomException):
    pass


def read_registry_values(key_location, value_names):
    reg_con = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
    open_key = winreg.OpenKey(reg_con, key_location, 0, winreg.KEY_ALL_ACCESS)
    registry_dict = dict()
    for key_name in value_names:
        val, rtype = winreg.QueryValueEx(open_key, key_name)
        if rtype == 3:
            val = codecs.encode(val, 'hex').decode("utf-8")
        registry_dict[key_name] = (val, rtype)
    return registry_dict


def write_registry_values(key_location, values_dict):
    reg_con = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
    open_key = winreg.OpenKey(reg_con, key_location, 0, winreg.KEY_ALL_ACCESS)
    for key_name, vtuple in values_dict.items():
        val, rtype = vtuple
        if rtype == 3:
            val = codecs.decode(val, 'hex')
        winreg.SetValueEx(open_key, key_name, 0, rtype, val)


def read_and_check():
    registry_dict = read_registry_values(LOCATION_REG_KEY, IMPORTANT_KEYS)
    did_find_saves = False
    for key_name, val_n_type in registry_dict.items():
        value, rtype = val_n_type
        if not key_name.startswith('SaveGame'):
            continue
        else:
            did_find_saves = True

        try:
            xml_string = codecs.decode(value, 'hex').decode('utf-8')[:-1]
            ET.fromstring(xml_string)
        except Exception as ex:
            # print_exception(ex, "OH NOE!")
            raise BadXMLValues(
                "A saved game in registry is malformed, try agian later")
    if not did_find_saves:
        raise NoSavesFound(
            "Hey! You need to play the game first. There are no Saved Games")
    return registry_dict


def generate_save_name():
    ts = datetime.utcfromtimestamp(
        datetime.now().timestamp()).timestamp() * 1000.0
    tstr = [n for n in reversed(str(ts).split('.'))].pop()
    return "{}.{}".format(tstr, SAVE_EXTENTION)


def check_make_dir(dir):
    if not os.path.isdir(dir):
        os.mkdir(dir)


def do_save():
    check_make_dir(SAVE_FOLDER)
    filename = None
    try:
        save_json = json.dumps(read_and_check())
        filename = generate_save_name()
        filepath = os.path.join(SAVE_FOLDER, filename)
        with open(filepath, 'w') as fh:
            fh.write(save_json)

    except CustomException as ex:
        print(ex)

    else:
        print('Saved game to "{}"'.format(filename))


def checked_load(filepath):
    registry_dict = None
    with open(filepath, 'r') as fp:
        registry_dict = json.load(fp)
        # pprint(registry_dict)
    if registry_dict is None:
        raise BadSaveFile()
    if len(list(filter(lambda k: k in IMPORTANT_KEYS, registry_dict))) is not len(IMPORTANT_KEYS):
        raise BadSaveFile()
    write_registry_values(LOCATION_REG_KEY, registry_dict)


def do_load():
    check_make_dir(SAVE_FOLDER)
    files = os.listdir(SAVE_FOLDER)
    known_saves = filter(lambda fn: fn.endswith(SAVE_EXTENTION), files)
    last_save = sorted(known_saves).pop()

    print('Would you like to load "{}"?'.format(last_save))

    selection = input("Y/N > ").upper().lstrip().strip()
    if not selection.startswith("Y"):
        print("Will not load save, due to your selection.")
        return

    filepath = os.path.join(SAVE_FOLDER, last_save)
    try:
        checked_load(filepath)
    except BadSaveFile as ex:
        print_exception(ex, "Whoa, save file is bad.")


def do_open_and_load():
    print("Enter file name (including path):")
    filename = input("> ").lstrip().strip()
    if not os.path.isfile(filename):
        print("File doesnt exist!")
        return
    print('Are you sure you want to load "{}" into registry - making it the current save game?'.format(filename))
    selection = input("Y/N > ").upper().lstrip().strip()
    if not selection.startswith("Y"):
        print("Will not load save, due to your selection.")
        return
    try:
        checked_load(filename)
    except BadSaveFile as ex:
        print_exception(ex, "Whoa, save file is bad.")





def main():
    stay_running = True
    while stay_running:
        print()
        print()
        print(MENU_TEXT)
        selection = input("Enter option> ").upper().lstrip().strip()

        if selection.startswith("X") or selection.startswith("Q"):
            print('bye!')
            stay_running = False
        elif selection.startswith("S"):
            do_save()
        elif selection.startswith("L"):
            do_load()
        elif selection.startswith("O"):
            do_open_and_load()
        print("#################")
        continue


if __name__ == '__main__':
    main()