From ff38c10c405dcdcd61066c8f5916db41249fe12b Mon Sep 17 00:00:00 2001 From: Joshua Walton Date: Thu, 20 Sep 2018 22:22:51 +0100 Subject: [PATCH] Added support for aoc directory to botw-patcher. --- README.md | 8 ++- botwfstools/botw_patcher.py | 102 ++++++++++++++++++++++++++++-------- setup.py | 2 +- 3 files changed, 88 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b2b7289..e5b8d34 100644 --- a/README.md +++ b/README.md @@ -83,11 +83,15 @@ Converts an extracted content patch directory into a loadable content layer. This tool will repack any extracted archives and update the file sizes in the Resource Size Table automatically. - patcher ORIGINAL_CONTENT_DIR MOD_DIR TARGET_DIR --target {wiiu,switch} + patcher ORIGINAL_CONTENT_DIR MOD_DIR TARGET_DIR --target {wiiu,switch} + [--aoc_dir ORIGINAL_AOC_DIR --aoc_patch_dir AOC_MOD_DIR --aoc_target_dir AOC_TARGET_DIR] -Usage example: +Usage examples: patcher botw/merged/ botw/mod-files/ botw/patched-files/ + + patcher botw/merged/ botw/mod/content/ botw/patched/content/ --target switch + --aoc_dir botw/aoc/ --aoc_patch_dir botw/mod/aoc/ --aoc_target_dir botw/patched/aoc/ The patched files can be used on console or with botw-overlayfs. diff --git a/botwfstools/botw_patcher.py b/botwfstools/botw_patcher.py index 992654d..3ec4a65 100755 --- a/botwfstools/botw_patcher.py +++ b/botwfstools/botw_patcher.py @@ -15,10 +15,32 @@ import sys import typing import wszst_yaz0 +import re ARCHIVE_EXTS = {'sarc', 'pack', 'bactorpack', 'bmodelsh', 'beventpack', 'stera', 'stats', 'ssarc', 'spack', 'sbactorpack', 'sbmodelsh', 'sbeventpack', 'sstera', 'sstats', 'blarc', 'sblarc', 'genvb', 'sgenvb'} +AOC_PREFIX = 'Aoc/0010/' +AOC_PREFIX_LIST = [ + 'Terrain/A/AocField', + 'UI/StaffRollDLC/', + 'Map/MainField/', + 'Map/MainFieldDungeon/', + 'Map/AocField/', + 'Physics/StaticCompound/AocField/', + 'Physics/StaticCompound/MainFieldDungeon/', + 'Movie/Demo6', + 'Game/AocField/', + 'NavMesh/AocField/', + 'NavMesh/MainFieldDungeon/', + 'Physics/TeraMeshRigidBody/AocField/', + 'System/AocVersion.txt', + 'Pack/RemainsWind.pack', + 'Pack/RemainsElectric.pack', + 'Pack/RemainsWater.pack', + 'Pack/RemainsFire.pack', + 'Pack/FinalTrial.pack'] +AOC_VOICE_PATTERN = re.compile('^Voice/.*/Stream_Demo6.*/.*\.bfstm$') def _is_archive_filename(path: Path) -> bool: return path.suffix[1:] in ARCHIVE_EXTS @@ -67,7 +89,7 @@ def _get_parents_and_path(path: Path): def _find_sarc(path: Path) -> typing.Optional[sarc.SARC]: archive: typing.Optional[sarc.SARC] = None - archive_path: str = "" + archive_path: str = '' for i, p in enumerate(_get_parents_and_path(path)): if _exists(p) and _is_dir(p): continue @@ -90,7 +112,7 @@ def _find_sarc(path: Path) -> typing.Optional[sarc.SARC]: return archive def repack_archive(content_dir: Path, archive_path: Path, rel_archive_dir: Path) -> bool: - temp_archive_dir = archive_path.with_name(archive_path.name + ".PATCHER_TEMP") + temp_archive_dir = archive_path.with_name(archive_path.name + '.PATCHER_TEMP') os.rename(archive_path, temp_archive_dir) archive = _find_sarc(content_dir / rel_archive_dir) @@ -124,6 +146,7 @@ def repack_archive(content_dir: Path, archive_path: Path, rel_archive_dir: Path) _RSTB_BLACKLIST = {'Actor/ActorInfo.product.byml'} _RSTB_BLACKLIST_ARCHIVE_EXT = {'.blarc', '.sblarc', '.genvb', '.sgenvb', '.bfarc', '.sbfarc'} _RSTB_BLACKLIST_SUFFIXES = {'.pack', '.yml', '.yaml', '.aamp', '.xml'} + size_calculator = rstb.SizeCalculator() def _should_be_listed_in_rstb(resource_path: Path, rel_path: Path) -> bool: @@ -134,8 +157,8 @@ def _should_be_listed_in_rstb(resource_path: Path, rel_path: Path) -> bool: return False return resource_path.suffix not in _RSTB_BLACKLIST_SUFFIXES -def _fix_rstb_resource_size(path: Path, rel_path: Path, table: rstb.ResourceSizeTable, wiiu: bool): - resource_path = _get_resource_path_for_rstb(rel_path) +def _fix_rstb_resource_size(path: Path, rel_path: Path, table: rstb.ResourceSizeTable, wiiu: bool, is_aoc: bool): + resource_path = _get_resource_path_for_rstb(rel_path, is_aoc) if not _should_be_listed_in_rstb(Path(resource_path), rel_path=rel_path): sys.stderr.write(f'{Fore.WHITE}{rel_path}{Style.RESET_ALL} ({resource_path})\n') return @@ -156,18 +179,44 @@ def _fix_rstb_resource_size(path: Path, rel_path: Path, table: rstb.ResourceSize % (prev_resource_size, resource_size, ' '.join(notes))) table.set_size(resource_path, resource_size) -def _get_resource_path_for_rstb(rel_path: Path) -> str: +def _get_resource_path_for_rstb(rel_path: Path, is_aoc: bool) -> str: """Get the RSTB resource path for a resource file.""" rel_path = rel_path.with_suffix(rel_path.suffix.replace('.s', '.')) for parent in rel_path.parents: if _is_archive_filename(parent): - return rel_path.relative_to(parent).as_posix() + return get_path(rel_path.relative_to(parent).as_posix(), is_aoc) # File is not in any archive, so just return the path relative to the content root. - return rel_path.as_posix() + return get_path(rel_path.as_posix(), is_aoc) -def make_loadable_layer(content_dir: Path, patch_dir: Path, target_dir: Path, wiiu: bool): +def get_path(path: str, is_aoc: bool) -> str: + """Add aoc prefix to resource path if necessary""" + + if path.startswith('001'): + new_path = path[5:] + else: + new_path = path + + if is_aoc: + for prefix in AOC_PREFIX_LIST: + if new_path.startswith(prefix): + return AOC_PREFIX + new_path + if AOC_VOICE_PATTERN.match(new_path): + return AOC_PREFIX + new_path + + dungeon_num_str = re.search('Dungeon\((.+?)\)', new_path) + if dungeon_num_str: + dungeon_num = int(dungeon_num_str) + if dungeon_num > 119: + if new_path.startswith('Pack/') and new_path.endswith('.pack'): + return AOC_PREFIX + new_path + if new_path.startswith(('Map/CDungeon/', 'Physics/StaticCompound/', 'NavMesh/CDungeon/')): + return AOC_PREFIX + new_path + + return path + +def make_loadable_layer(content_dir: Path, patch_dir: Path, target_dir: Path, wiiu: bool, table: rstb.ResourceSizeTable, is_aoc: bool): """Converts an extracted content patch view into a loadable content layer. Directories that have an SARC extension in their name will be recursively repacked as archives. @@ -185,8 +234,7 @@ def make_loadable_layer(content_dir: Path, patch_dir: Path, target_dir: Path, wi for file_name in dirs: full_path = os.path.join(root, file_name) files_by_depth[full_path.count(os.path.sep)].append(Path(full_path)) - - table = rstb.util.read_rstb(str(content_dir / _RSTB_PATH_IN_CONTENT), be=wiiu) + size_calculator = rstb.SizeCalculator() for depth in sorted(files_by_depth.keys(), reverse=True): @@ -204,18 +252,12 @@ def make_loadable_layer(content_dir: Path, patch_dir: Path, target_dir: Path, wi # Fix the size in the RSTB *before* compression. - _fix_rstb_resource_size(path=file, rel_path=rel_path, table=table, wiiu=wiiu) + _fix_rstb_resource_size(path=file, rel_path=rel_path, table=table, wiiu=wiiu, is_aoc=is_aoc) # TODO: automatically compress file types that are managed by the resource system # and that are not already in a compressed archive (excluding pack, bfevfl # bcamanim and barslist). - sys.stderr.write('writing new RSTB...\n') - table.set_size(_RSTB_PATH_IN_CONTENT.replace('.srsizetable', '.rsizetable'), table.get_buffer_size()) - final_rstb_path = target_dir / _RSTB_PATH_IN_CONTENT - os.makedirs(final_rstb_path.parent, exist_ok=True) - rstb.util.write_rstb(table, str(final_rstb_path), be=wiiu) - def _fail_if_not_dir(path: Path): if not path.is_dir(): sys.stderr.write('error: %s is not a directory\n' % path) @@ -230,8 +272,12 @@ def cli_main() -> None: parser.add_argument('target_dir', type=Path, help='Path to the target directory') parser.add_argument('-f', '--force', action='store_true', help='Clean up the target directory if it exists') parser.add_argument('-t', '--target', choices=['wiiu', 'switch'], help='Target platform', required=True) + parser.add_argument('--aoc_dir', type=Path, help='Path to the game add-on-content directory') + parser.add_argument('--aoc_patch_dir', type=Path, help='Path to the extracted add-on-content patch directory') + parser.add_argument('--aoc_target_dir', type=Path, help='Path to the target add-on-content directory') + args = parser.parse_args() - + # These would always fail on Windows because of WinFsp. if os.name != 'nt': _fail_if_not_dir(args.content_dir) @@ -243,9 +289,23 @@ def cli_main() -> None: if os.path.exists(args.target_dir) and len(os.listdir(args.target_dir)) != 0: sys.stderr.write('error: target dir is not empty. please remove all the files inside it\n') sys.exit(1) - - make_loadable_layer(args.content_dir, args.patch_dir, args.target_dir, - wiiu=(args.target == 'wiiu')) + + wiiu = args.target == 'wiiu' + + table = rstb.util.read_rstb(args.content_dir / _RSTB_PATH_IN_CONTENT, be=wiiu) + + make_loadable_layer(args.content_dir, args.patch_dir, args.target_dir, wiiu, table, is_aoc=False) + if args.aoc_dir or args.aoc_patch_dir or args.aoc_target_dir: + if args.aoc_dir and args.aoc_patch_dir and args.aoc_target_dir: + make_loadable_layer(args.aoc_dir, args.aoc_patch_dir, args.aoc_target_dir, wiiu, table, is_aoc=True) + else: + sys.stderr.write('Not all aoc arguments were specified - ignoring aoc files\n') + + sys.stderr.write('writing new RSTB...\n') + table.set_size(_RSTB_PATH_IN_CONTENT.replace('.srsizetable', '.rsizetable'), table.get_buffer_size()) + final_rstb_path = args.target_dir / _RSTB_PATH_IN_CONTENT + os.makedirs(final_rstb_path.parent, exist_ok=True) + rstb.util.write_rstb(table, str(final_rstb_path), be=wiiu) if __name__ == '__main__': cli_main() diff --git a/setup.py b/setup.py index 5ab0491..1f787ee 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setuptools.setup( name="botwfstools", - version="1.2.2-1", + version="1.3.0", author="leoetlino", author_email="leo@leolam.fr", description="Tools for exploring and editing Breath of the Wild's ROM",