Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add aoc directory support to botw-patcher #1

Merged
merged 1 commit into from
Sep 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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