From 76134074aba42e8da70f7743971c7644f549c7f2 Mon Sep 17 00:00:00 2001 From: Ethan Roseman Date: Wed, 11 Nov 2020 16:21:25 -0500 Subject: [PATCH] n64splat extensions --- tools/n64splat | 2 +- tools/splat.yaml | 1 + tools/splat_ext/PaperMarioMapFS.py | 67 ++++ tools/splat_ext/PaperMarioMessages.py | 430 ++++++++++++++++++++++++++ tools/splat_ext/__init__.py | 0 5 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 tools/splat_ext/PaperMarioMapFS.py create mode 100644 tools/splat_ext/PaperMarioMessages.py create mode 100644 tools/splat_ext/__init__.py diff --git a/tools/n64splat b/tools/n64splat index 6106762b05..8e424ae728 160000 --- a/tools/n64splat +++ b/tools/n64splat @@ -1 +1 @@ -Subproject commit 6106762b0561e40a640c11852a17bb87963c7ba8 +Subproject commit 8e424ae728e9f8c05ea3d81af85a4f995bb29915 diff --git a/tools/splat.yaml b/tools/splat.yaml index 79dbc8d832..3b973d06b1 100644 --- a/tools/splat.yaml +++ b/tools/splat.yaml @@ -6,6 +6,7 @@ options: mnemonic_ljust: 10 ld_o_replace_extension: False ld_addrs_header: include/ld_addrs.h + extensions: splat_ext segments: - name: header type: header diff --git a/tools/splat_ext/PaperMarioMapFS.py b/tools/splat_ext/PaperMarioMapFS.py new file mode 100644 index 0000000000..2fc070e7c9 --- /dev/null +++ b/tools/splat_ext/PaperMarioMapFS.py @@ -0,0 +1,67 @@ +import os +from segtypes.segment import N64Segment +from pathlib import Path +from util import Yay0decompress + + +def decode_null_terminated_ascii(data): + length = 0 + for byte in data: + if byte == 0: + break + length += 1 + + return data[:length].decode('ascii') + + +class N64SegPaperMarioMapFS(N64Segment): + def __init__(self, segment, next_segment, options): + super().__init__(segment, next_segment, options) + + def split(self, rom_bytes, base_path): + bin_dir = self.create_split_dir(base_path, "bin/assets") + + data = rom_bytes[self.rom_start: self.rom_end] + + asset_idx = 0 + while True: + asset_data = data[0x20 + asset_idx * 0x1C:] + + name = decode_null_terminated_ascii(asset_data[0:]) + offset = int.from_bytes(asset_data[0x10:0x14], byteorder="big") + size = int.from_bytes(asset_data[0x14:0x18], byteorder="big") + decompressed_size = int.from_bytes( + asset_data[0x18:0x1C], byteorder="big") + + is_compressed = size != decompressed_size + + if offset == 0: + path = None + else: + path = "{}.bin".format(name) + self.create_parent_dir(bin_dir, path) + + if name == "end_data": + break + + with open(os.path.join(bin_dir, path), "wb") as f: + bytes = rom_bytes[self.rom_start + 0x20 + + offset: self.rom_start + 0x20 + offset + size] + + if is_compressed: + self.log(f"Decompressing {name}...") + bytes = Yay0decompress.decompress_yay0(bytes) + + f.write(bytes) + self.log(f"Wrote {name} to {Path(bin_dir, path)}") + + asset_idx += 1 + + + def get_ld_files(self): + return [("bin/assets", self.name, ".data")] + + + @staticmethod + def get_default_name(addr): + return "assets" diff --git a/tools/splat_ext/PaperMarioMessages.py b/tools/splat_ext/PaperMarioMessages.py new file mode 100644 index 0000000000..6a52397584 --- /dev/null +++ b/tools/splat_ext/PaperMarioMessages.py @@ -0,0 +1,430 @@ +from segtypes.segment import N64Segment +from pathlib import Path + +CHARSET = { + 0x00: "𝅘𝅥𝅮", + 0x01: "!", + 0x02: '"', + 0x03: "#", + 0x04: "$", + 0x05: "%", + 0x06: "&", + 0x07: "'", + 0x08: "(", + 0x09: ")", + 0x0A: "*", + 0x0B: "+", + 0x0C: ",", + 0x0D: "-", + 0x0E: ".", + 0x0F: "/", + 0x10: "0", + 0x11: "1", + 0x12: "2", + 0x13: "3", + 0x14: "4", + 0x15: "5", + 0x16: "6", + 0x17: "7", + 0x18: "8", + 0x19: "9", + 0x1A: ":", + 0x1B: ";", + 0x1C: "<", + 0x1D: "=", + 0x1E: ">", + 0x1F: "?", + 0x20: "@", + 0x21: "A", + 0x22: "B", + 0x23: "C", + 0x24: "D", + 0x25: "E", + 0x26: "F", + 0x27: "G", + 0x28: "H", + 0x29: "I", + 0x2A: "J", + 0x2B: "K", + 0x2C: "L", + 0x2D: "M", + 0x2E: "N", + 0x2F: "O", + 0x30: "P", + 0x31: "Q", + 0x32: "R", + 0x33: "S", + 0x34: "T", + 0x35: "U", + 0x36: "V", + 0x37: "W", + 0x38: "X", + 0x39: "Y", + 0x3A: "Z", + 0x3B: "\\[", + 0x3C: "¥", + 0x3D: "]", + 0x3E: "^", + 0x3F: "_", + 0x40: "`", + 0x41: "a", + 0x42: "b", + 0x43: "c", + 0x44: "d", + 0x45: "e", + 0x46: "f", + 0x47: "g", + 0x48: "h", + 0x49: "i", + 0x4A: "j", + 0x4B: "k", + 0x4C: "l", + 0x4D: "m", + 0x4E: "n", + 0x4F: "o", + 0x50: "p", + 0x51: "q", + 0x52: "r", + 0x53: "s", + 0x54: "t", + 0x55: "u", + 0x56: "v", + 0x57: "w", + 0x58: "x", + 0x59: "y", + 0x5A: "z", + 0x5B: "{", + 0x5C: "|", + 0x5D: "}", + 0x5E: "~", + 0x5F: "°", + 0x60: "À", + 0x61: "Á", + 0x62: "Â", + 0x63: "Ä", + 0x64: "Ç", + 0x65: "È", + 0x66: "É", + 0x67: "Ê", + 0x68: "Ë", + 0x69: "Ì", + 0x6A: "Í", + 0x6B: "Î", + 0x6C: "Ï", + 0x6D: "Ñ", + 0x6E: "Ò", + 0x6F: "Ó", + 0x70: "Ô", + 0x71: "Ö", + 0x72: "Ù", + 0x73: "Ú", + 0x74: "Û", + 0x75: "Ü", + 0x76: "ß", + 0x77: "à", + 0x78: "á", + 0x79: "â", + 0x7A: "ä", + 0x7B: "ç", + 0x7C: "è", + 0x7D: "é", + 0x7E: "ê", + 0x7F: "ë", + 0x80: "ì", + 0x81: "í", + 0x82: "î", + 0x83: "ï", + 0x84: "ñ", + 0x85: "ò", + 0x86: "ó", + 0x87: "ô", + 0x88: "ö", + 0x89: "ù", + 0x8A: "ú", + 0x8B: "û", + 0x8C: "ü", + 0x8D: "¡", + 0x8E: "¿", + 0x8F: "ª", + 0x90: "♥", + 0x91: "★", + 0x92: "↑", + 0x93: "↓", + 0x94: "←", + 0x95: "→", + 0x96: "●", + 0x97: "✖", + 0xA2: "“", + 0xA3: "”", + 0xA4: "‘", + 0xA5: "’", + 0xF7: " ", + 0xF0: "[br]\n", + 0xF1: "[prompt]", + 0xF2: {None: lambda d: (f"[sleep {d[0]}]", 1)}, + 0xFB: "[next]\n", + 0xFC: { + 0x01: "[style=right]\n", + 0x02: "[style=left]\n", + 0x03: "[style=center]\n", + 0x04: "[style=tattle]\n", + 0x05: {None: lambda d: (f"[style=choice x={d[1]} y={d[3]} w={d[0]} h={d[2]}]\n", 4)}, + 0x06: "[style=inspect]\n", + 0x07: "[style=sign]\n", + 0x08: "[style=lamppost]\n", + 0x09: "[style=postcard]\n", + 0x0A: "[style=popup]\n", + 0x0C: {None: lambda d: (f"[style=upgrade x={d[1]} y={d[3]} w={d[0]} h={d[2]}]\n", 4)}, + 0x0D: "[style=narrate]\n", + 0x0E: "[style=epilogue]\n", + }, + 0xFF: { + 0x00: { + 0: "[font=normal]", + 3: "[font=title]\n", + 4: "[font=subtitle]\n", + }, + 0x05: { + 0x0A: "[color=normal]", + 0x20: "[color=red]", + 0x21: "[color=pink]", + 0x22: "[color=purple]", + 0x23: "[color=blue]", + 0x24: "[color=cyan]", + 0x25: "[color=green]", + 0x26: "[color=yellow]", + + 0x00: "[color=normal ctx=diary]", + 0x07: "[color=red ctx=diary]", + + 0x17: "[color=dark ctx=inspect]", + + 0x18: "[color=normal ctx=sign]", + 0x19: "[color=red ctx=sign]", + 0x1A: "[color=blue ctx=sign]", + 0x1B: "[color=green ctx=sign]", + + 0x28: "[color=red ctx=popup]", + 0x29: "[color=pink ctx=popup]", + 0x2A: "[color=purple ctx=popup]", + 0x2B: "[color=blue ctx=popup]", + 0x2C: "[color=teal ctx=popup]", + 0x2D: "[color=green ctx=popup]", + 0x2E: "[color=yellow ctx=popup]", + 0x2F: "[color=normal ctx=popup]", + }, + 0x07: "[noskip]\n", + 0x08: "[/noskip]\n", + 0x09: "[instant]\n", + 0x0A: "[/instant]\n", + 0x0B: {None: lambda d: (f"[kerning={d[0]}]", 1)}, + 0x0C: {None: lambda d: (f"[scroll {d[0]}]", 1)}, + 0x0D: {None: lambda d: (f"[size x={d[0]} y={d[0]}]\n", 2)}, + 0x0E: "[/size]\n", + 0x0F: {None: lambda d: (f"[speed delay={d[0]} chars={d[1]}]", 2)}, + 0x10: {None: lambda d: (f"[pos x={d[0]} y={d[1]}]", 2)}, + 0x11: {None: lambda d: (f"[pos y={d[0]}]", 1)}, + 0x12: {None: lambda d: (f"[indent {d[0]}]", 1)}, + 0x13: {None: lambda d: (f"[down {d[0]}]", 1)}, + 0x14: {None: lambda d: (f"[up {d[0]}]", 1)}, + 0x15: {None: lambda d: (f"[image {d[0]}]\n", 1)}, + 0x16: {None: lambda d: (f"[sprite {d[0]} {d[1]} {d[2]}]\n", 3)}, + 0x17: {None: lambda d: (f"[item {d[0]} {d[1]}]\n", 2)}, + 0x18: {None: lambda d: (f"[image {d[0]} {d[1]} {d[2]} {d[3]} {d[4]} {d[5]} {d[6]}]\n", 7)}, + 0x1E: {None: lambda d: (f"[cursor {d[0]}]", 1)}, + 0x1F: {None: lambda d: (f"[choicecount={d[0]}]", 1)}, + 0x20: {None: lambda d: (f"[cancel={d[0]}]", 1)}, + 0x21: {None: lambda d: (f"[option {d[0]}]", 1)}, + 0x24: {0xFF: {0x05: { + 0x10: {0x98: {0xFF: {0x25: "Ⓐ"}}}, + 0x11: {0x99: {0xFF: {0x25: "Ⓑ"}}}, + 0x12: {0xA1: {0xFF: {0x25: "Ⓢ"}}}, + 0x13: { + 0x9D: {0xFF: {0x25: "▲"}}, + 0x9E: {0xFF: {0x25: "▼"}}, + 0x9F: {0xFF: {0x25: "◀"}}, + 0xA0: {0xFF: {0x25: "▶"}}, + }, + 0x14: {0x9C: {0xFF: {0x25: "Ⓩ"}}}, + }}}, + 0x26: { + 0x00: "[shaky]", + 0x01: "[wavy]", + 0x03: {None: lambda d: (f"[noise fade={d[0]}]", 1)}, + 0x05: {None: lambda d: (f"[faded-shaky fade={d[0]}]", 1)}, + 0x07: {None: lambda d: (f"[fade={d[0]}]", 1)}, + 0x0A: "[shout]", + 0x0B: "[whisper]", + 0x0C: "[scream]", + 0x0D: "[chortle]", + 0x0E: "[shadow]", + }, + 0x27: { + 0x00: "[/shaky]", + 0x01: "[/wavy]", + 0x03: "[/noise]", + 0x05: "[/faded-shaky]", + 0x07: "[/fade]", + 0x0A: "[/shout]", + 0x0B: "[/whisper]", + 0x0C: "[/scream]", + 0x0D: "[/chortle]", + 0x0E: "[/shadow]", + }, + 0x28: {None: lambda d: (f"[var {d[0]}]", 1)}, + 0x29: {None: lambda d: (f"[center {d[0]}]", 1)}, + 0x2E: {None: lambda d: (f"[volume={d[0]}]", 1)}, + 0x2F: { + 1: "[sound=bowser]\n", + 2: "[sound=spirit]\n", + None: lambda d: (f"[sound={d[0]}]\n", 1), + }, + None: lambda d: (f"[func 0x{d[0]:X}]", 1), + }, + None: lambda d: (f"[raw 0x{d[0]:02X}]", 1), +} + +CHARSET_CREDITS = { + **CHARSET, + 0x00: "A", + 0x01: "B", + 0x02: "C", + 0x03: "D", + 0x04: "E", + 0x05: "F", + 0x06: "G", + 0x07: "H", + 0x08: "I", + 0x09: "J", + 0x0A: "K", + 0x0B: "L", + 0x0C: "M", + 0x0D: "N", + 0x0E: "O", + 0x0F: "P", + 0x10: "Q", + 0x11: "R", + 0x12: "S", + 0x13: "T", + 0x14: "U", + 0x15: "V", + 0x16: "W", + 0x17: "X", + 0x18: "Y", + 0x19: "Z", + 0x1A: "'", + 0x1B: ".", + 0x1C: ",", + 0x1D: "0", + 0x1E: "1", + 0x1F: "2", + 0x20: "3", + 0x21: "4", + 0x22: "5", + 0x23: "6", + 0x24: "7", + 0x25: "8", + 0x26: "9", + 0x27: "©", + 0x28: "&", + 0xF7: " ", +} + +class N64SegPaperMarioMessages(N64Segment): + def __init__(self, segment, next_segment, options): + super().__init__(segment, next_segment, options) + self.files = segment.get("files", []) if type(segment) is dict else [] + + def split(self, rom_bytes, base_path): + data = rom_bytes[self.rom_start: self.rom_end] + + section_offsets = [] + pos = 0 + while True: + offset = int.from_bytes(data[pos:pos+4], byteorder="big") + + if offset == 0: + break + + section_offsets.append(offset) + pos += 4 + + for i, section_offset in enumerate(section_offsets): + name = f"{i:02X}" + if len(self.files) >= i: + name = self.files[i] + + msg_offsets = [] + pos = section_offset + while True: + offset = int.from_bytes(data[pos:pos+4], byteorder="big") + + if offset == section_offset: + break + + msg_offsets.append(offset) + pos += 4 + + self.log(f"Reading {len(msg_offsets)} messages in section {name} (0x{i:02X})") + + path = Path(base_path, self.name, name + ".msg") + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w") as f: + for j, msg_offset in enumerate(msg_offsets): + if j != 0: + f.write("\n") + f.write(f"[message section=0x{i:02X} index={j}]\n") + self.write_message_markup(data[msg_offset:], f) + f.write("\n[/message]\n") + + def get_ld_files(self): + return [("", self.name, ".data")] + + @staticmethod + def get_default_name(addr): + return "msg" + + def write_message_markup(self, data, f): + pos = 0 + font = CHARSET + + while data[pos] != 0xFD: + markup, delta = self.char_to_markup(data[pos:], charset=font) + + f.write(markup) + pos += delta + + if markup == "[font=title]\n" or markup == "[font=subtitle]\n": + font = CHARSET_CREDITS + elif markup == "[font=normal]": + font = CHARSET + + def char_to_markup(self, data, charset=CHARSET): + value = None + char = int(data[0]) + + if char in charset: + value = charset[char] + elif None in charset: + value = charset[None] + + if type(value) is str: + return value, 1 + if callable(value): + return value(data) + if type(value) is dict: + markup, delta = self.char_to_markup(data[1:], charset=value) + + if markup is None: + if None in charset: + value = charset[None] + + if callable(value): + return value(data) + + return value, 1 + else: + return markup, delta + 1 + + + return None, 0 diff --git a/tools/splat_ext/__init__.py b/tools/splat_ext/__init__.py new file mode 100644 index 0000000000..e69de29bb2