Skip to content

Commit

Permalink
Merge pull request #1 from BravelyPeculiar/master
Browse files Browse the repository at this point in the history
Add aoc directory support to botw-patcher
  • Loading branch information
leoetlino authored Sep 29, 2018
2 parents ca0fc6a + ff38c10 commit c451215
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 24 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
102 changes: 81 additions & 21 deletions botwfstools/botw_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setuptools.setup(
name="botwfstools",
version="1.2.2-1",
version="1.3.0",
author="leoetlino",
author_email="[email protected]",
description="Tools for exploring and editing Breath of the Wild's ROM",
Expand Down

0 comments on commit c451215

Please sign in to comment.