Ejemplo n.º 1
0
def __patch_engine(original_file: Path, file: Path, engine: str,
                   hex_patches: dict[str, HexPatch]) -> bool:
    """Return True if patch went as expected, False if any warnings happened."""
    data = original_file.read_bytes()
    all_as_expected = True
    for hex_patch_name, hex_patch in hex_patches.items():
        replacement = hex_patch['replacement'] % hex_patch['replacement_args']
        pattern = hex_patch['pattern']
        (data, sub_count) = pattern.subn(replacement, data)
        LOGGER.debug(
            f"Replaced {sub_count} occurrences of '{hex_patch_name}' pattern {pattern.pattern} with {replacement} in '{file}'"
        )
        expected = hex_patch['expected_subs']
        if sub_count == 0 and expected != 0:
            raise LookupError(
                f"Failed to apply '{hex_patch_name}' patch in '{file}' (no occurrences found)"
            )
        elif sub_count != expected:
            LOGGER.warning(
                f"Expected {expected} matches for '{hex_patch_name}' patch in '{file}', found {sub_count}"
            )
            all_as_expected = False
    file.write_bytes(data)
    LOGGER.info(f"Patched '{file}'")
    return all_as_expected
Ejemplo n.º 2
0
def status() -> None:
    if config.HASH_DIR.exists() and any(config.HASH_DIR.iterdir()):
        LOGGER.info(f"Found hashes at '{config.HASH_DIR}'")
        return True
    else:
        LOGGER.info(f"No hashes found at '{config.HASH_DIR}'")
        return False
Ejemplo n.º 3
0
def status() -> None:
    if config.SJSON_DATA_DIR.exists() and any(config.SJSON_DATA_DIR.iterdir()):
        LOGGER.info(f"Found SJSON data at '{config.SJSON_DATA_DIR}'")
        return True
    else:
        LOGGER.info(f"No SJSON data found at '{config.SJSON_DATA_DIR}'")
        return False
Ejemplo n.º 4
0
def uninstall() -> None:
    mod_dir = config.content_dir.joinpath(MOD_TARGET_DIR)
    if mod_dir.exists():
        dir_util.remove_tree(str(mod_dir))
        LOGGER.info(f"Uninstalled Lua mod from '{mod_dir}'")
    else:
        LOGGER.info(f"No Lua mod to uninstall from '{mod_dir}'")
Ejemplo n.º 5
0
def try_get_modimporter() -> Path:
    """Check if modimporter is available in the Content directory."""
    for mod_importer in MOD_IMPORTERS:
        modimporter = config.content_dir.joinpath(mod_importer)
        if modimporter.exists():
            LOGGER.info(f"'modimporter' detected at '{modimporter}'")
            return modimporter
    return None
Ejemplo n.º 6
0
def __patch_hook_file(original_file: Path, file: Path,
                      import_statement: str) -> None:
    source_text = original_file.read_text()
    source_text += f"""

-- Hephaistos hook
{import_statement}
"""
    file.write_text(source_text)
    LOGGER.info(f"Patched '{file}' with hook '{import_statement}'")
Ejemplo n.º 7
0
def patch_lua_status(lua_scripts_dir: Path, import_statement: str) -> None:
    hook_file = lua_scripts_dir.joinpath(HOOK_FILE)
    LOGGER.debug(f"Checking patch status of Lua hook file at '{hook_file}'")
    text = hook_file.read_text()
    if import_statement in text:
        LOGGER.info(f"Found hook '{import_statement}' in '{hook_file}'")
        return True
    else:
        LOGGER.info(f"No hook '{import_statement}' found in '{hook_file}'")
        return False
Ejemplo n.º 8
0
 def __handle_global_args(self, args: list[str]) -> None:
     # logging verbosity level
     level = ParserBase.VERBOSE_TO_LOG_LEVEL[min(args.verbose, 2)]
     LOGGER.setLevel(level)
     # hades_dir
     self.__configure_hades_dir(args.hades_dir)
     # modimporter
     if args.modimporter:
         config.modimporter = helpers.try_get_modimporter()
     else:
         LOGGER.info("Using '--no-modimporter': will not run 'modimporter', even if available")
Ejemplo n.º 9
0
def status() -> None:
    mod_dir = config.content_dir.joinpath(MOD_TARGET_DIR)
    if mod_dir.exists() and any(mod_dir.iterdir()):
        LOGGER.info(f"Found Lua mod at '{mod_dir}'")
        (mod_dir, lua_scripts_dir, relative_path_to_mod,
         _) = __prepare_variables()
        return patchers.patch_lua_status(
            lua_scripts_dir, relative_path_to_mod + MOD_ENTRY_POINT)
    else:
        LOGGER.info(f"No Lua mod found at '{mod_dir}'")
        return False
Ejemplo n.º 10
0
def patch_sjsons() -> None:
    LOGGER.info(
        "Reading SJSON data (this operation can take time, please be patient)")
    sjson_dir = config.content_dir.joinpath(SJSON_DIR)
    for dirname, files in SJON_PATCHES.items():
        sub_dir = sjson_dir.joinpath(dirname)
        for filename, patches in files.items():
            file = sub_dir.joinpath(filename)
            LOGGER.debug(f"Patching SJSON file at '{file}'")
            with safe_patch_file(file) as (source_sjson, file):
                __patch_sjson_file(source_sjson, file, patches)
Ejemplo n.º 11
0
def install() -> None:
    LOGGER.debug(f"Installing Lua mod from '{config.MOD_SOURCE_DIR}'")
    (mod_dir, lua_scripts_dir, relative_path_to_mod,
     import_statement) = __prepare_variables()
    dir_util.copy_tree(str(config.MOD_SOURCE_DIR), str(mod_dir))
    LOGGER.debug(f"Copied '{config.MOD_SOURCE_DIR}' to '{mod_dir}'")
    __configure(mod_dir, relative_path_to_mod)
    LOGGER.info(f"Installed Lua mod to '{mod_dir}'")
    # run modimporter (if available) to register Hephaistos
    if config.modimporter:
        LOGGER.info(f"Running 'modimporter' to register Hephaistos")
        helpers.run_modimporter(config.modimporter)
    # otherwise register manually
    else:
        patchers.patch_lua(lua_scripts_dir, import_statement)
Ejemplo n.º 12
0
def patch_profile_sjsons() -> None:
    if config.custom_resolution:
        profile_sjsons = helpers.try_get_profile_sjson_files()
        if not profile_sjsons:
            msg = """Cannot patch custom resolution to 'ProfileX.sjson'.
This is a non-blocking issue but might prevent you from running Hades at the resolution of your choice."""
            LOGGER.warning(msg)
            return
        edited_list = []
        for file in profile_sjsons:
            LOGGER.debug(f"Analyzing '{file}'")
            data = sjson.loads(file.read_text())
            for key in ['X', 'WindowWidth']:
                data[key] = config.resolution.width
            for key in ['Y', 'WindowHeight']:
                data[key] = config.resolution.height
            # we manually set WindowX/Y in ProfileX.sjson configuration files as a
            # safeguard against WindowX/Y values overflowing when switching to
            # windowed mode while using a custom resolution larger than officially
            # supported by the main monitor, ensuring Hades will not be drawn
            # offscreen and can then be repositioned by the user
            for key in ['WindowX', 'WindowY']:
                if not key in data:
                    data[key] = WINDOW_XY_DEFAULT_OFFSET
                    LOGGER.debug(
                        f"'{key}' not found in '{file.name}', inserted '{key} = {WINDOW_XY_DEFAULT_OFFSET}'"
                    )
                elif data[key] >= WINDOW_XY_OVERFLOW_THRESHOLD:
                    data[key] = WINDOW_XY_DEFAULT_OFFSET
                    LOGGER.debug(
                        f"'{key}' found in '{file.name}' but with overflowed value, reset to '{key} = {WINDOW_XY_DEFAULT_OFFSET}'"
                    )
            file.write_text(sjson.dumps(data))
            edited_list.append(file)
        if edited_list:
            edited_list = '\n'.join(f"  - {file}" for file in edited_list)
            msg = f"""Applied custom resolution to:
{edited_list}"""
            LOGGER.info(msg)
Ejemplo n.º 13
0
 def handler(self, **kwargs) -> None:
     """Restore backups, discard hashes and SJSON data, uninstall Lua mod."""
     # run 'modimporter --clean' (if available) to unregister Hephaistos
     if config.modimporter:
         LOGGER.info(f"Running 'modimporter --clean' to unregister Hephaistos")
         helpers.run_modimporter(config.modimporter, clean_only=True)
     backups.restore()
     hashes.discard()
     sjson_data.discard()
     lua_mod.uninstall()
     # clean up Hephaistos data dir if empty (using standalone executable)
     if not any(config.HEPHAISTOS_DATA_DIR.iterdir()):
         dir_util.remove_tree(str(config.HEPHAISTOS_DATA_DIR))
         LOGGER.info(f"Cleaned up empty directory '{config.HEPHAISTOS_DATA_DIR}'")
     # re-run modimporter (if available) to re-register other mods
     if config.modimporter:
         LOGGER.info(f"Running 'modimporter' to re-register other mods")
         helpers.run_modimporter(config.modimporter)
Ejemplo n.º 14
0
def patch_engines_status() -> None:
    status = True
    for engine, filepath in ENGINES[config.platform].items():
        file = config.hades_dir.joinpath(filepath)
        LOGGER.debug(
            f"Checking patch status of '{engine}' backend at '{file}'")
        try:
            with safe_patch_file(file,
                                 store_backup=False) as (original_file, file):
                if original_file is not None:
                    LOGGER.info(f"'{file}' looks patched")
                else:
                    status = False
                    LOGGER.info(f"'{file}' is not patched")
        except hashes.HashMismatch:
            status = False
            LOGGER.info(
                f"'{file}' has been modified since last backup: probably not patched"
            )
    return status
Ejemplo n.º 15
0
    def handler(self, width: int, height: int, scaling: Scaling, hud: HUD, custom_resolution: bool, force: bool, **kwargs) -> None:
        """Compute viewport depending on arguments, then patch all needed files and install Lua mod.
        If using '--force', discard backups, hashes and SJSON data, and uninstall Lua mod."""
        helpers.configure_screen_variables(width, height, scaling)
        LOGGER.info(f"Using resolution: {config.resolution.width, config.resolution.height}")
        LOGGER.info(f"Using '--scaling={scaling}': computed patch viewport {config.new_screen.width, config.new_screen.height}")

        config.center_hud = True if hud == HUD.CENTER else False
        msg = f"Using '--hud={hud}': HUD will be kept in the center of the screen" if config.center_hud else f"Using '--hud={hud}': HUD will be expanded horizontally"
        LOGGER.info(msg)

        if not custom_resolution:
            LOGGER.info("Using '--no-custom-resolution': will not bypass monitor resolution detection")
        config.custom_resolution = custom_resolution

        if force:
            LOGGER.info("Using '--force': will repatch on top of existing files in case of hash mismatch and store new backups / hashes")
            config.force = True

        # run 'modimporter --clean' (if available) to restore everything before patching
        if config.modimporter:
            LOGGER.info(f"Running 'modimporter --clean' to restore original state before patching")
            helpers.run_modimporter(config.modimporter, clean_only=True) 

        try:
            patchers.patch_engines()
            patchers.patch_sjsons()
            patchers.patch_profile_sjsons()
            lua_mod.install()
        except hashes.HashMismatch as e:
            LOGGER.error(e)
            if config.interactive_mode:
                LOGGER.error("It looks like the game was updated. Do you wish to discard previous backups and re-patch Hades from its current state?")
                choice = interactive.pick(options=['Yes', 'No',], add_option=None)
                if choice == 'Yes':
                    self.handler(width, height, scaling, hud, custom_resolution, force=True)
            else:
                LOGGER.error("Was the game updated? Re-run with '--force' to discard previous backups and re-patch Hades from its current state.")
        except (LookupError, FileExistsError) as e:
            LOGGER.error(e)
Ejemplo n.º 16
0
def discard() -> None:
    if config.HASH_DIR.exists():
        dir_util.remove_tree(str(config.HASH_DIR))
        LOGGER.info(f"Discarded hashes at '{config.HASH_DIR}'")
Ejemplo n.º 17
0
def discard() -> None:
    if config.SJSON_DATA_DIR.exists():
        dir_util.remove_tree(str(config.SJSON_DATA_DIR))
        LOGGER.info(f"Discarded SJSON data at '{config.SJSON_DATA_DIR}'")
Ejemplo n.º 18
0
def __patch_sjson_file(source_sjson: SJSON, file: Path,
                       patches: SJSONPatch) -> None:
    patched_sjson = __patch_sjson_data(source_sjson, patches)
    file.write_text(sjson.dumps(patched_sjson))
    LOGGER.info(f"Patched '{file}'")