Skip to content

Instantly share code, notes, and snippets.

@SamusAranX
Created October 1, 2019 13:03
Show Gist options
  • Save SamusAranX/7b9338a6bfc6bc3b680c75599b5c307e to your computer and use it in GitHub Desktop.
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
#!/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