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
def try_get_profile_sjson_files() -> list[Path]: """Try to detect save directory and list all Profile*.sjson files.""" save_dirs = TRY_SAVE_DIR[config.platform]() for save_dir in save_dirs: if save_dir.exists(): LOGGER.debug(f"Found save directory at '{save_dir}'") if config.platform == Platform.MS_STORE: # Microsoft Store save files are not actually named # `Profile*.sjson` and instead use random hexadecimal names with # no file extensions, so we need to list them by trying to parse # them as SJSON profiles = __find_sjsons(save_dir) else: profiles = [item for item in save_dir.glob('Profile*.sjson')] if profiles: return profiles save_dirs_list = '\n'.join(f" - {save_dir}" for save_dir in save_dirs) msg = f"""Did not find any 'ProfileX.sjson' in save directories: {save_dirs_list}""" LOGGER.warning(msg) return []
def patch_engines() -> None: HEX_PATCHES['viewport']['replacement_args'] = (__int_to_bytes( config.new_screen.width), __int_to_bytes(config.new_screen.height)) HEX_PATCHES['fullscreen_vector']['replacement_args'] = (__float_to_bytes( config.new_screen.width), __float_to_bytes(config.new_screen.height)) HEX_PATCHES['width_height_floats']['replacement_args'] = (__float_to_bytes( config.new_screen.height), __float_to_bytes(config.new_screen.width)) HEX_PATCHES['screencenter_vector']['replacement_args'] = ( __float_to_bytes(config.new_screen.center_x), __float_to_bytes(config.new_screen.center_y)) for engine, filepath in ENGINES[config.platform].items(): hex_patches = __get_engine_specific_hex_patches(engine) file = config.hades_dir.joinpath(filepath) LOGGER.debug(f"Patching '{engine}' backend at '{file}'") got_any_warnings = False with safe_patch_file(file) as (original_file, file): if not __patch_engine(original_file, file, engine, hex_patches): got_any_warnings = True if got_any_warnings: LOGGER.warning( "Hephaistos managed to apply all hex patches but did not patch everything exactly as expected." ) LOGGER.warning("This is most probably due to a game update.") LOGGER.warning( "In most cases this is inconsequential and Hephaistos will work anyway, but Hephaistos might need further changes to work properly with the new version of the game." )
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)