Created
October 1, 2019 13:03
-
-
Save SamusAranX/7b9338a6bfc6bc3b680c75599b5c307e to your computer and use it in GitHub Desktop.
Extractor for the ARC format used in Ultimate Board Game Collection for the Wii
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
import argparse | |
from sys import exit | |
from os import makedirs | |
from os.path import getsize, basename, splitext, join | |
from struct import unpack, calcsize | |
from time import sleep | |
class Structure: | |
FORMAT = "" | |
@classmethod | |
def parse(cls, f): | |
data = f.read(calcsize(cls.FORMAT)) | |
return cls(*unpack(cls.FORMAT, data)) | |
class Header(Structure): | |
FORMAT = ">4s4I" | |
def __init__(self, magic, file_length, file_num, u1, z1): | |
self.magic = magic | |
self.file_length = file_length | |
self.file_num = file_num | |
self.u1 = u1 | |
self.z1 = z1 | |
assert self.magic == b"wii\x00", "Not a valid Wii ARC file" | |
assert self.u1 in [0, 2], "header u1 assertion failed" | |
assert self.z1 == 0, "header zero assertion failed" | |
class HeaderTOCEntry(Structure): | |
FORMAT = ">64s" | |
def __init__(self, fname): | |
self.fname = fname.split(b"\x00", 1)[0].decode("ascii") | |
class TOCEntry(Structure): | |
FORMAT = ">64s7I" | |
EXTENSIONS = { | |
22: "dsp" | |
} | |
def __init__(self, fname, type, length, offset, idx, z1, u1, z2): | |
self.fname = fname.split(b"\x00", 1)[0].decode("ascii") | |
self.type = type | |
self.length = length | |
self.offset = offset | |
self.index = idx | |
self.z1 = z1 | |
self.u1 = u1 | |
self.z2 = z2 | |
assert self.z1 == self.z2 == 0, "toc zero assertion failed" | |
def extract_arc(infile, debug): | |
print(f"Extracting {infile}…") | |
with open(infile, "rb") as arc_file: | |
header = Header.parse(arc_file) | |
if getsize(infile) != header.file_length: | |
# one-off scripts for weird-ass games, woo! | |
raise Exception("Compressed archives are not supported") | |
for i in range(3): | |
header_toc = HeaderTOCEntry.parse(arc_file) | |
# print(header_toc.fname) | |
if debug: | |
print(f"Archive contains {header.file_num} files:") | |
toc = [] | |
types = set() | |
for i in range(header.file_num): | |
entry = TOCEntry.parse(arc_file) | |
toc.append(entry) | |
types.add(entry.type) | |
if debug: | |
print(f"Name: {entry.fname}") | |
print(f"Type: {entry.type}") | |
print(f"Offset: {entry.offset:08X}") | |
print(f"Length: {entry.length}") | |
print("-----") | |
entry_dir, _ = splitext(basename(infile)) | |
if len(types) > 1: | |
for t in types: | |
makedirs(join(entry_dir, str(t)), exist_ok=True) | |
else: | |
makedirs(entry_dir, exist_ok=True) | |
for entry in toc: | |
arc_file.seek(entry.offset) | |
entry_fname, entry_ext = splitext(entry.fname) | |
entry_new_ext = TOCEntry.EXTENSIONS.get(entry.type) | |
if not entry_ext and entry_new_ext: | |
entry_fname += f".{entry_new_ext}" | |
elif entry_ext: | |
entry_fname += f".{entry_ext[1:]}" | |
if len(types) > 1: | |
entry_file = join(entry_dir, str(entry.type), entry_fname) | |
else: | |
entry_file = join(entry_dir, entry_fname) | |
with open(entry_file, "wb") as out_file: | |
out_file.write(arc_file.read(entry.length)) | |
if debug: | |
print("End Offset", arc_file.tell()) | |
def main(args): | |
for f in args.infiles: | |
try: | |
extract_arc(f, args.debug) | |
except Exception as e: | |
print(e) | |
sleep(2.5) | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser(description="Wii ARC extractor") | |
parser.add_argument("infiles", type=str, nargs="*", help="archive file") | |
parser.add_argument("--debug", action="store_true", help="Debug mode") | |
args = parser.parse_args() | |
main(args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment