diff --git a/.gitignore b/.gitignore index 3600fdfb40..349671fabe 100644 --- a/.gitignore +++ b/.gitignore @@ -9,17 +9,17 @@ venv/ ctx.c expected/ settings.mk +.vscode/launch.json # Build artifacts *.ld *.z64 -*.bin -*.i *.Yay0 -bin/ -img/ -build/ -docs/doxygen/ -include/ld_addrs.h +/build/ +/docs/doxygen/ +/include/ld_addrs.h -.vscode/launch.json +# Assets +/bin +/img +/msg diff --git a/Makefile b/Makefile index 9058c9f924..cf93519fb9 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,8 @@ ELF := $(BUILD_DIR)/$(TARGET).elf LD_SCRIPT := $(TARGET).ld LD_MAP := $(BUILD_DIR)/$(TARGET).map ASSETS_BIN := $(BUILD_DIR)/bin/assets/assets.bin - +MSG_BIN := $(BUILD_DIR)/msg.bin +GENERATED_HEADERS := include/ld_addrs.h ### Tools ### @@ -91,7 +92,7 @@ clean: clean-code: rm -rf $(BUILD_DIR)/src -setup: clean submodules split +setup: clean submodules split $(LD_SCRIPT) make -C tools submodules: @@ -99,10 +100,10 @@ submodules: split: rm -rf bin img - $(SPLAT) --modes ld bin Yay0 PaperMarioMapFS img + $(SPLAT) --modes bin Yay0 PaperMarioMapFS PaperMarioMessages img split-%: - $(SPLAT) --modes ld $* + $(SPLAT) --modes $* --verbose split-all: rm -rf bin img @@ -130,7 +131,7 @@ $(BUILD_DIR)/%.Yay0.o: $(BUILD_DIR)/%.bin.Yay0 $(LD) -r -b binary -o $@ $< # Compile C files -$(BUILD_DIR)/%.c.o: %.c $(MDEPS) | include/ld_addrs.h +$(BUILD_DIR)/%.c.o: %.c $(MDEPS) | $(GENERATED_HEADERS) @mkdir -p $(shell dirname $@) $(CPP) $(CPPFLAGS) -o - $(CPPMFLAGS) $< | iconv --from UTF-8 --to SHIFT-JIS | $(CC) $(CFLAGS) -o - | $(OLD_AS) $(OLDASFLAGS) -o $@ - @@ -178,22 +179,30 @@ $(BUILD_DIR)/%.i8.png: %.png @mkdir -p $(shell dirname $@) $(PYTHON) tools/convert_image.py i8 $< $@ $(IMG_FLAGS) +# Assets ASSET_FILES := $(foreach asset, $(ASSETS), $(BUILD_DIR)/bin/assets/$(asset)) YAY0_ASSET_FILES := $(foreach asset, $(filter-out %_tex, $(ASSET_FILES)), $(asset).Yay0) - $(BUILD_DIR)/bin/assets/%: bin/assets/%.bin @mkdir -p $(shell dirname $@) @cp $< $@ - $(ASSETS_BIN): $(ASSET_FILES) $(YAY0_ASSET_FILES) sources.mk @mkdir -p $(shell dirname $@) @echo "building $@" @$(PYTHON) tools/build_assets_bin.py $@ $(ASSET_FILES) - $(ASSETS_BIN:.bin=.o): $(ASSETS_BIN) $(LD) -r -b binary -o $@ $< +# Messages +$(MSG_BIN): $(MESSAGES) + @mkdir -p $(shell dirname $@) + @echo "building $@" + @$(PYTHON) tools/compile_messages.py $@ /dev/null $(MESSAGES) +$(MSG_BIN:.bin=.o): $(MSG_BIN) + @mkdir -p $(shell dirname $@) + $(LD) -r -b binary -o $@ $< + $(LD_SCRIPT): $(SPLAT_YAML) + @mkdir -p $(shell dirname $@) $(SPLAT) --modes ld $(BUILD_DIR)/$(LD_SCRIPT): $(LD_SCRIPT) diff --git a/docs/message-colors.jpg b/docs/message-colors.jpg new file mode 100644 index 0000000000..65f3ab8190 Binary files /dev/null and b/docs/message-colors.jpg differ diff --git a/docs/messages.md b/docs/messages.md new file mode 100644 index 0000000000..11c8e01da8 --- /dev/null +++ b/docs/messages.md @@ -0,0 +1,214 @@ +# `.msg` syntax + +## Character Set + +`[font=normal]`: 𝅘𝅥𝅮!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ\[¥]^_`abcdefghijklmnopqrstuvwxyz{|}~°ÀÁÂÄÇÈÉÊËÌÍÎÏÑÒÓÔÖÙÚÛÜßàáâäçèéêëìíîïñòóôöùúûü¡¿ª♥★↑↓←→●✖“”‘’ ⒶⒷⓈ▲▼◀▶ⓁⓇⓏ + +`[font=title]` and `[font=subtitle]`: ABCDEFGHIJKLMNOPQRSTUVWXYZ'.,0123456789©& + +## Tags + +Similar to BBCode, tags begin with `[` and end with `]`. They may take positional arguments (`value`) and named arguments (`arg=value`). The order of named arguments does not matter. Values are parsed as integers if possible, and hexidecimal integers are supported. Tag names and argument names are case-insensitive. + +To write a literal `[` character (and not start a tag), prefix it with a backslash; i.e. `\[`. + +### `[message name section= index=]...[/message]` + +Marks the beginning and end of a message. + +All parameters on the opening tag are optional. +If `section` and/or `index` are omitted, the message will be allocated a section and/or index respectively. + +### `[br]` +Line break. + +### `[prompt]` +Waits for the A button to be pressed before continuing. + +### `[next]` +Scrolls down the message box to begin a new 'paragraph.' + +### `[sleep n]` +Waits for `n` frames before continuing. + +### `[color= ctx=]` + +Supported color names are: + +`ctx=normal` (default): normal red pink purple blue cyan green yellow + +`ctx=diary`: normal red + +`ctx=inspect`: dark + +`ctx=button`: blue green red yellow gray grey + +`ctx=popup`: normal red pink purple blue teal green yellow + +`ctx=sign`: normal red blue green + +To use other colors, provide `color` as an integer (e.g. `[color=0x10]`). Here are all the colors supported by the engine: +![Message colors](message-colors.jpg) + +### `[/color]` +Resets the color to what it was before the most recent `[color=]` tag. The default color at the start of the message is assumed to be `[color=normal]`, which is true for most messages. + +### `[a]` `[b]` `[l]` `[r]` `[z]` `[start]` `[c-up]` `[c-down]` `[c-left]` `[c-right]` + +Shorthand for the button prompt characters ⒶⒷⓁⓇⓏⓈ▲▼◀▶ respectively. You can override the color used with a named parameter `[a color=]`. + +Supported color names: blue green red yellow gray grey + +### `[style=]` + +Sets the box style to use for this message. Supported styles are: + +`[style=right]` `[style=left]` `[style=center]` - Standard speech bubble with the speaker coming from the given direction + +`[style=tattle]` - Small bubble used for overworld tattles + +`[style=choice x= y= w= h=]` - Box for multiple-choice options + +`[style=inspect]` - Internal narration box, often used when inspecting objects by pressing A + +`[style=sign]` `[style=lamppost]` `[style=postcard]` - Boxes with custom backgrounds + +`[style=popup]` - Box in center of screen that grows dynamically depending on how long the message is + +`[style=upgrade x= y= w= h=]` - Super Block box + +`[style=narrate]` - Narration; used when you obtain new partners + +`[style=epilogue]` - Used for post-chapter descriptions of what Mario and party did + +### `[font=]` + +Supported fonts: normal title subtitle + +Note that the `title` and `subtitle` fonts use a different character set to `normal`. + +### `[/font]` + +Resets the font to what it was before the most recent `[font=]` tag. + +### `[noskip]...[/noskip]` + +Disables the B button from skipping text within. + +### `[instant]...[/instant]` + +Causes all text within to appear instantly. + +### `[kerning=]` + +Modifies the spacing between letters. + +### `[scroll n]` + +Scrolls down `n` lines. + +### `[size x= y=]` + +Changes font size. + +### `[/size]` + +Resets the size back to x=10 y=10 (the default). + +### `[speed delay= chars=]` + +Changes text printing speed. `delay` is the number of frames between each print, and `chars` is the number of characters to print at once. For example, `[speed delay=5 chars=3]` would print 3 characters every 5 frames. + +### `[pos x= y=]` + +Overrides the current text printing position. + +`x` is optional. If only `y` is provided, the x position will not change. + +### `[indent n]` + +Indents the following text by `n` tabs. + +### `[up n]` and `[down n]` + +Moves the text printing position up/down by `n` pixels respectively. + +### `[image id]` + +Displays the given image. This requires extra setup when printing the message. + +There is also a 7-parameter variant of `image` that is not yet understood. + +### `[sprite unknown id raster]` + +Displays the given sprite. + +### `[item a b]` + +Displays the given world icon. + +### `[cursor n]` and `[option n]` + +Denotes the position for the hand cursor to appear and the start of the option text for choice `n` of a `[style=choice]` box. + +### `[choicecount=]` + +Sets the number of options given in this `[style=choice]` box. + +### `[cancel=]` + +Sets the option number to be selected if the user presses B. +If this is not provided, pressing B does nothing. + +### Effects + +- `[shaky]...[/shaky]` +- `[noise fade=]...[/noise]` +- `[faded-shaky fade=]...[/faded-shaky]` +- `[fade=]...[/fade]` +- `[shout]...[/shout]` or `[shrinking]...[/shrinking]` +- `[whisper]...[/whisper]` or `[growing]...[/growing]` +- `[scream]...[/scream]` or `[shaky-size]...[/shaky-size]` +- `[chortle]...[/chortle]` or `[wavy-size]...[/wavy-size]` +- `[shadow]...[/shadow]` + +### `[var n]` + +Replaced with the value of message variable `n`. Must be set before the message is printed. + +### `[center=]` + +Centers the following text. Parameter purpose is unknown. + +### `[volume=]` + +Changes the volume of the following text. + +### `[sound=]` + +Changes the speech sound for the following text. + +Supported sound names: normal bowser spirit + +You can also use an integer for the parameter. + +### `[/sound]` + +Resets the speech sound to what it was before the most recent `[sound=]` tag. + +### `[note]` `[heart]` `[star]` `[circle]` `[cross]` `[arrow-up]` `[arrow-down]` `[arrow-left]` `[arrow-right]` + +Equivalent to the characters 𝅘𝅥𝅮♥★●✖↑↓←→ respectively. + +### `[raw ...]` + +Outputs all arguments provided as bytes, without modification. + +### `[func ...]` + +Same as `raw`, but prefixed with 0xFF. + +## Comments + +`//` line comments are allowed anywhere outside of a `[message]...[/message]` block and will be ignored. +Block comments are not supported. diff --git a/include/messages.h b/include/messages.h index a7fea9666f..f580a18400 100644 --- a/include/messages.h +++ b/include/messages.h @@ -7,60 +7,10 @@ typedef s32 MessageID; #define MESSAGE_ID(section, index) ((section << 0x10) + index) -// 00 Introduction -// 01 Postgame Celebration -// 02 Toad Town Gate Sector -// 03 Toad Town Castle Sector -// 04 Toad Town Bridge Sector -// 05 Toad Town Train Sector -// 06 Toad Town Warehouse Sector -// 07 Toad Town Docks Sector -// 08 Minigames -// 09 Castle Grounds -// 0A Shooting Star Summit -// 0B Chapter 0 -// 0C Chapter 1 -// 0D Chapter 2 -// 0E Chapter 3 -// 0F Chapter 4 -// 10 Chapter 5 -// 11 Chapter 6 -// 12 Chapter 7 -// 13 Bowser's Castle -// 14 Peach Segments -// 15 Koopa Koot Favors -// 16 Russ T Advice -// 17 News Bulletin -// 18 Gossip Bulletin - -// 19 Map Tattles #define MessageID_TATTLE_KMR_03 MESSAGE_ID(0x19, 0x3B) #define MessageID_TATTLE_KMR_12 MESSAGE_ID(0x19, 0x40) -// 1A NPC Tattles -// 1B Entity Tattles -// 1C Enemy Tattles - -// 1D Menus I #define MessageID_SIGN_MUSHROOM_GOOMBA_TRAP MESSAGE_ID(0x1D, 0x167) #define MessageID_SIGN_GOOMBA_KINGS_FORTRESS_AHEAD MESSAGE_ID(0x1D, 0x168) -// 1E Choices -// 1F Menus II -// 20 Party Letters + Luigi's Diary -// 21 Advice Fortunes -// 22 Treasure Fortunes -// 23 Item Descriptions I -// 24 Item Descriptions II -// 25 Item Descriptions III -// 26 Item Names -// 27 Shop Messages -// 28 Partner Descriptions -// 29 Enemy Names -// 2A Mario Moves -// 2B Partner Moves -// 2C Quiz Questions -// 2D Quiz Options -// 2E Credits - #endif diff --git a/include/types.h b/include/types.h index ac7a5dcf3d..298e297e4d 100644 --- a/include/types.h +++ b/include/types.h @@ -11,6 +11,6 @@ #define UNK_ARGS typedef s32 FormationID; -#define FORMATION_ID(section, stage, index) (section << 16) + (stage << 8) + index +#define FORMATION_ID(section, stage, index) ((section << 16) + (stage << 8) + index) #endif diff --git a/sources.mk b/sources.mk index f2c0e86888..2d3efdad4e 100644 --- a/sources.mk +++ b/sources.mk @@ -56,5 +56,7 @@ ASSETS := \ title_data \ party_kurio party_kameki party_pinki party_pareta party_resa party_akari party_opuku party_pokopi +MESSAGES := $(shell find msg -type f -name "*.msg") + # Image settings $(BUILD_DIR)/img/battle/text_action_command_ratings.ia4.png: IMG_FLAGS = --flip-y diff --git a/tools/compile_messages.py b/tools/compile_messages.py new file mode 100755 index 0000000000..e92d716376 --- /dev/null +++ b/tools/compile_messages.py @@ -0,0 +1,823 @@ +#! /usr/bin/python3 + +from sys import argv +from collections import OrderedDict +import re + +class Message: + def __init__(self, name, section, index): + self.name = name + self.section = section + self.index = index + + self.bytes = [] + +def try_convert_int(s): + try: + return int(s, base=0) + except: + return s + +def parse_command(source): + if source[0] != "[": + return None, [], {}, source + source = source[1:] # "[" + + inside_brackets = "" + while source[0] != "]": + if source[0] == "\n": + return None, [], {}, source + + inside_brackets += source[0] + source = source[1:] + source = source[1:] # "]" + + command, *args = inside_brackets.split(" ") + + positional_args = [] + named_args = {} + + if "=" in command: + key, value = command.split("=", 1) + command = key + named_args[key] = try_convert_int(value) + + for arg in args: + if "=" in arg: + key, value = arg.split("=", 1) + named_args[key.lower()] = try_convert_int(value.lower()) + else: + positional_args.append(try_convert_int(arg)) + + return command.lower(), positional_args, named_args, source + +def color_to_code(color, ctx="normal"): + COLORS = { + "normal": { + "normal": 0x0A, + "red": 0x20, + "pink": 0x21, + "purple": 0x22, + "blue": 0x23, + "cyan": 0x24, + "green": 0x25, + "yellow": 0x26, + }, + "diary": { + "normal": 0x00, + "red": 0x07, + }, + "inspect": { + "dark": 0x17, + }, + "button": { + "blue": 0x10, + "green": 0x11, + "red": 0x12, + "yellow": 0x13, + "gray": 0x14, + "grey": 0x14, + }, + "popup": { + "red": 0x28, + "pink": 0x29, + "purple": 0x2A, + "blue": 0x2B, + "teal": 0x2C, + "green": 0x2D, + "yellow": 0x2E, + "normal": 0x2F, + }, + "sign": { + "normal": 0x18, + "red": 0x19, + "blue": 0x1A, + "green": 0x1B, + } + } + + if type(color) is int: + return color + + return COLORS.get(ctx, {}).get(color) + +CHARSET = { + "𝅘𝅥𝅮": 0x00, + "!": 0x01, + '"': 0x02, + "#": 0x03, + "$": 0x04, + "%": 0x05, + "&": 0x06, + "'": 0x07, + "(": 0x08, + ")": 0x09, + "*": 0x0A, + "+": 0x0B, + ",": 0x0C, + "-": 0x0D, + ".": 0x0E, + "/": 0x0F, + "0": 0x10, + "1": 0x11, + "2": 0x12, + "3": 0x13, + "4": 0x14, + "5": 0x15, + "6": 0x16, + "7": 0x17, + "8": 0x18, + "9": 0x19, + ":": 0x1A, + ";": 0x1B, + "<": 0x1C, + "=": 0x1D, + ">": 0x1E, + "?": 0x1F, + "@": 0x20, + "A": 0x21, + "B": 0x22, + "C": 0x23, + "D": 0x24, + "E": 0x25, + "F": 0x26, + "G": 0x27, + "H": 0x28, + "I": 0x29, + "J": 0x2A, + "K": 0x2B, + "L": 0x2C, + "M": 0x2D, + "N": 0x2E, + "O": 0x2F, + "P": 0x30, + "Q": 0x31, + "R": 0x32, + "S": 0x33, + "T": 0x34, + "U": 0x35, + "V": 0x36, + "W": 0x37, + "X": 0x38, + "Y": 0x39, + "Z": 0x3A, + "[": 0x3B, + "¥": 0x3C, + "]": 0x3D, + "^": 0x3E, + "_": 0x3F, + "`": 0x40, + "a": 0x41, + "b": 0x42, + "c": 0x43, + "d": 0x44, + "e": 0x45, + "f": 0x46, + "g": 0x47, + "h": 0x48, + "i": 0x49, + "j": 0x4A, + "k": 0x4B, + "l": 0x4C, + "m": 0x4D, + "n": 0x4E, + "o": 0x4F, + "p": 0x50, + "q": 0x51, + "r": 0x52, + "s": 0x53, + "t": 0x54, + "u": 0x55, + "v": 0x56, + "w": 0x57, + "x": 0x58, + "y": 0x59, + "z": 0x5A, + "{": 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, + "Ⓐ": [0xFF, 0x24, 0xFF, 0x05, 0x10, 0x98, 0xFF, 0x25], + "Ⓑ": [0xFF, 0x24, 0xFF, 0x05, 0x11, 0x99, 0xFF, 0x25], + "Ⓢ": [0xFF, 0x24, 0xFF, 0x05, 0x12, 0xA1, 0xFF, 0x25], + "▲": [0xFF, 0x24, 0xFF, 0x05, 0x13, 0x9D, 0xFF, 0x25], + "▼": [0xFF, 0x24, 0xFF, 0x05, 0x13, 0x9E, 0xFF, 0x25], + "◀": [0xFF, 0x24, 0xFF, 0x05, 0x13, 0x9F, 0xFF, 0x25], + "▶": [0xFF, 0x24, 0xFF, 0x05, 0x13, 0xA0, 0xFF, 0x25], + "Ⓛ": [0xFF, 0x24, 0xFF, 0x05, 0x14, 0x9A, 0xFF, 0x25], + "Ⓡ": [0xFF, 0x24, 0xFF, 0x05, 0x14, 0x9B, 0xFF, 0x25], + "Ⓩ": [0xFF, 0x24, 0xFF, 0x05, 0x14, 0x9C, 0xFF, 0x25], +} + +CHARSET_CREDITS = { + "A": 0x00, + "B": 0x01, + "C": 0x02, + "D": 0x03, + "E": 0x04, + "F": 0x05, + "G": 0x06, + "H": 0x07, + "I": 0x08, + "J": 0x09, + "K": 0x0A, + "L": 0x0B, + "M": 0x0C, + "N": 0x0D, + "O": 0x0E, + "P": 0x0F, + "Q": 0x10, + "R": 0x11, + "S": 0x12, + "T": 0x13, + "U": 0x14, + "V": 0x15, + "W": 0x16, + "X": 0x17, + "Y": 0x18, + "Z": 0x19, + "'": 0x1A, + ".": 0x1B, + ",": 0x1C, + "0": 0x1D, + "1": 0x1E, + "2": 0x1F, + "3": 0x20, + "4": 0x21, + "5": 0x22, + "6": 0x23, + "7": 0x24, + "8": 0x25, + "9": 0x26, + "©": 0x27, + "&": 0x28, + " ": 0xF7, +} + +if __name__ == "__main__": + if len(argv) < 4: + print("usage: compile_messages.py [OUTBIN] [OUTHEADER] [INFILES]") + exit(1) + + _, outfile, outheader, *infiles = argv + + messages = [] + + for filename in infiles: + message = None + with open(filename, "r") as f: + source = f.read() + lineno = 1 + + charset = CHARSET + font_stack = [0] + sound_stack = [0] + color_stack = [0x0A] + + while len(source) > 0: + if source.startswith("\n"): + lineno += 1 + source = source[1:] + continue + + if message is None: + if source.startswith("//"): + while source[0] != "\n": + source = source[1:] + else: + command, positional_args, named_args, source = parse_command(source) + + if not command: + print(f"{filename}:{lineno}: expected [message]") + exit(1) + + name = positional_args[0] if len(positional_args) > 0 else None + message = Message(name, named_args.get("section"), named_args.get("index")) + messages.append(message) + else: + command, positional_args, named_args, source = parse_command(source) + + if command: + if command == "/message": + message.bytes += [0xFD] + + # padding + while len(message.bytes) % 4 != 0: + message.bytes += [0x00] + + message = None + elif command == "raw": + message.bytes += [*positional_args] + elif command == "func": + message.bytes += [0xFF, *positional_args] + elif command == "br": + message.bytes += [0xF0] + elif command == "prompt": + message.bytes += [0xF1] + elif command == "sleep": + if len(positional_args) == 0: + print(f"{filename}:{lineno}: {command} command requires a positional parameter") + exit(1) + + message.bytes += [0xF2, positional_args[0]] + elif command == "next": + message.bytes += [0xFB] + elif command == "color": + if "color" not in named_args: + print(f"{filename}:{lineno}: color command requires a 'color' parameter") + exit(1) + + color = color_to_code(**named_args) + + if color is None: + print(f"{filename}:{lineno}: unknown color combination {named_args}") + exit(1) + + message.bytes += [0xFF, 0x05, color] + color_stack.append(color) + elif command == "/color": + color_stack.pop() + message.bytes += [0xFF, 0x05, color_stack[0]] + elif command == "style": + if "style" not in named_args: + print(f"{filename}:{lineno}: style command requires a 'style' parameter") + exit(1) + + message.bytes += [0xFC] + + style = named_args["style"] + if type(style) is int: + message.bytes += [style, *positional_args] + else: + if style == "right": + message.bytes += [0x01] + elif style == "left": + message.bytes += [0x02] + elif style == "center": + message.bytes += [0x03] + elif style == "tattle": + message.bytes += [0x04] + elif style == "choice": + if "w" not in named_args or "h" not in named_args or "x" not in named_args or "y" not in named_args: + print(f"{filename}:{lineno}: 'choice' style requires parameters: x, y, w, h") + exit(1) + + message.bytes += [0x05, named_args["w"], named_args["x"], named_args["h"], named_args["y"]] + elif style == "inspect": + message.bytes += [0x06] + elif style == "sign": + message.bytes += [0x07] + elif style == "lamppost": + message.bytes += [0x08] + elif style == "postcard": + message.bytes += [0x09] + elif style == "popup": + message.bytes += [0x0A] + elif style == "upgrade": + if "w" not in named_args or "h" not in named_args or "x" not in named_args or "y" not in named_args: + print(f"{filename}:{lineno}: 'upgrade' style requires parameters: x, y, w, h") + exit(1) + + message.bytes += [0x0C, named_args["w"], named_args["x"], named_args["h"], named_args["y"]] + elif style == "narrate": + message.bytes += [0x0D] + elif style == "epilogue": + message.bytes += [0x0E] + elif command == "font": + if "font" not in named_args: + print(f"{filename}:{lineno}: font command requires a 'font' parameter") + exit(1) + + font = named_args["font"] + + if font == "normal": + font = 0 + elif font == "title": + font = 3 + elif font == "subtitle": + font = 4 + + if type(font) is not int: + print(f"{filename}:{lineno}: unknown font '{font}'") + exit(1) + + message.bytes += [0xFF, 0x00, font] + font_stack.append(font) + + if font == 3 or font == 4: + charset = CHARSET_CREDITS + else: + charset = CHARSET + elif command == "/font": + font_stack.pop() + message.bytes += [0xFF, 0x00, font_stack[0]] + + if font == 3 or font == 4: + charset = CHARSET_CREDITS + else: + charset = CHARSET + elif command == "noskip": + message.bytes += [0xFF, 0x07] + elif command == "/noskip": + message.bytes += [0xFF, 0x08] + elif command == "instant": + message.bytes += [0xFF, 0x09] + elif command == "/instant": + message.bytes += [0xFF, 0x0A] + elif command == "kerning": + if "kerning" not in named_args: + print(f"{filename}:{lineno}: kerning command requires a 'kerning' parameter") + exit(1) + + message.bytes += [0xFF, 0x0B, named_args["kerning"]] + elif command == "scroll": + if len(positional_args) == 0: + print(f"{filename}:{lineno}: scroll command requires a positional parameter") + exit(1) + + message.bytes += [0xFF, 0x0C, positional_args[0]] + elif command == "size": + if "x" not in named_args or "y" not in named_args: + print(f"{filename}:{lineno}: size command requires parameters: x, y") + exit(1) + + message.bytes += [0xFF, 0x0D, named_args["x"], named_args["y"]] + elif command == "/size": + message.bytes += [0xFF, 0x0E] + elif command == "speed": + if "delay" not in named_args or "chars" not in named_args: + print(f"{filename}:{lineno}: speed command requires parameters: delay, chars") + exit(1) + + message.bytes += [0xFF, 0x0F, named_args["delay"], named_args["chars"]] + elif command == "pos": + if "y" not in named_args: + print(f"{filename}:{lineno}: pos command requires parameter: y (x is optional)") + exit(1) + + if "x" in named_args: + message.bytes += [0xFF, 0x10, named_args["x"], named_args["y"]] + else: + message.bytes += [0xFF, 0x11, named_args["y"]] + elif command == "indent": + if len(positional_args) == 0: + print(f"{filename}:{lineno}: indent command requires a positional parameter") + exit(1) + + message.bytes += [0xFF, 0x12, positional_args[0]] + elif command == "down": + if len(positional_args) == 0: + print(f"{filename}:{lineno}: down command requires a positional parameter") + exit(1) + + message.bytes += [0xFF, 0x13, positional_args[0]] + elif command == "up": + if len(positional_args) == 0: + print(f"{filename}:{lineno}: up command requires a positional parameter") + exit(1) + + message.bytes += [0xFF, 0x14, positional_args[0]] + elif command == "image": + if len(positional_args) == 1: + message.bytes += [0xFF, 0x15, positional_args[0]] + elif len(positional_args) == 7: + message.bytes += [0xFF, 0x18, *positional_args] + else: + print(f"{filename}:{lineno}: image command requires 1 or 7 positional parameters") + exit(1) + elif command == "sprite": + if len(positional_args) != 3: + print(f"{filename}:{lineno}: sprite command requires 3 positional parameters") + exit(1) + + message.bytes += [0xFF, 0x16, *positional_args] + elif command == "item": + if len(positional_args) != 2: + print(f"{filename}:{lineno}: item command requires 2 positional parameters") + exit(1) + + message.bytes += [0xFF, 0x17, *positional_args] + elif command == "cursor": + if len(positional_args) != 1: + print(f"{filename}:{lineno}: cursor command requires 1 positional parameter") + exit(1) + + message.bytes += [0xFF, 0x1E, *positional_args] + elif command == "option": + if len(positional_args) != 1: + print(f"{filename}:{lineno}: option command requires 1 positional parameter") + exit(1) + + message.bytes += [0xFF, 0x21, *positional_args] + elif command == "choice": + if len(positional_args) != 1: + print(f"{filename}:{lineno}: choice command requires 1 positional parameter") + exit(1) + + message.bytes += [0xFF, 0x1E, positional_args[0], 0xFF, 0x21, positional_args[0]] + elif command == "choicecount": + if "choicecount" not in named_args: + print(f"{filename}:{lineno}: choicecount command requires a 'choicecount' parameter") + exit(1) + + message.bytes += [0xFF, 0x1F, named_args["choicecount"]] + elif command == "cancel": + if "cancel" not in named_args: + print(f"{filename}:{lineno}: cancel command requires a 'cancel' parameter") + exit(1) + + message.bytes += [0xFF, 0x20, named_args["cancel"]] + elif command == "shaky": + message.bytes += [0xFF, 0x26, 0x00] + elif command == "/shaky": + message.bytes += [0xFF, 0x27, 0x00] + elif command == "wavy": + message.bytes += [0xFF, 0x26, 0x01] + elif command == "/wavy": + message.bytes += [0xFF, 0x27, 0x01] + elif command == "shaky": + if "opacity" in named_args: + print(f"{filename}:{lineno}: shaky command doesn't accept parameter 'fade' (hint: did you mean 'faded-shaky'?)") + exit(1) + message.bytes += [0xFF, 0x26, 0x00] + elif command == "/shaky": + message.bytes += [0xFF, 0x27, 0x00] + elif command == "noise": + message.bytes += [0xFF, 0x26, 0x03, named_args.get("fade", 3)] + elif command == "/noise": + message.bytes += [0xFF, 0x27, 0x03] + elif command == "faded-shaky": + message.bytes += [0xFF, 0x26, 0x05, named_args.get("fade", 5)] + elif command == "/faded-shaky": + message.bytes += [0xFF, 0x27, 0x05] + elif command == "fade": + message.bytes += [0xFF, 0x26, 0x07, named_args.get("fade", 7)] + elif command == "/fade": + message.bytes += [0xFF, 0x27, 0x07] + elif command == "shout" or command == "shrinking": + message.bytes += [0xFF, 0x26, 0x0A] + elif command == "/shout" or command == "/shrinking": + message.bytes += [0xFF, 0x27, 0x0A] + elif command == "whisper" or command == "growing": + message.bytes += [0xFF, 0x26, 0x0B] + elif command == "/whisper" or command == "/growing": + message.bytes += [0xFF, 0x27, 0x0B] + elif command == "scream" or command == "shaky-size": + message.bytes += [0xFF, 0x26, 0x0C] + elif command == "/scream" or command == "/shaky-size": + message.bytes += [0xFF, 0x27, 0x0C] + elif command == "chortle" or command == "wavy-size": + message.bytes += [0xFF, 0x26, 0x0D] + elif command == "/chortle" or command == "/wavy-size": + message.bytes += [0xFF, 0x27, 0x0D] + elif command == "shadow": + message.bytes += [0xFF, 0x26, 0x0E] + elif command == "/shadow": + message.bytes += [0xFF, 0x27, 0x0E] + elif command == "var": + if len(positional_args) != 1: + print(f"{filename}:{lineno}: var command requires 1 positional parameter") + exit(1) + + message.bytes += [0xFF, 0x28, *positional_args] + elif command == "center": + if len(positional_args) != 1: + print(f"{filename}:{lineno}: center command requires 1 positional parameter") + exit(1) + + message.bytes += [0xFF, 0x29, *positional_args] + elif command == "volume": + if "volume" not in named_args: + print(f"{filename}:{lineno}: volume command requires a 'volume' parameter") + exit(1) + + message.bytes += [0xFF, 0x2E, named_args["volume"]] + elif command == "sound": + if "sound" not in named_args: + print(f"{filename}:{lineno}: sound command requires a 'sound' parameter") + exit(1) + + sound = named_args["sound"] + + if sound == "normal": + sound = 0 + elif sound == "bowser": + sound = 1 + elif sound == "spirit": + sound = 2 + + if type(sound) is not int: + print(f"{filename}:{lineno}: unknown sound '{sound}'") + exit(1) + + message.bytes += [0xFF, 0x2F, sound] + sound_stack.append(sound) + elif command == "/sound": + sound_stack.pop() + message.bytes += [0xFF, 0x2F, sound_stack[0]] + elif command == "a": + color_code = color_to_code(named_args.get("color", "blue"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x98, 0xFF, 0x25] + elif command == "b": + color_code = color_to_code(named_args.get("color", "green"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x99, 0xFF, 0x25] + elif command == "l": + color_code = color_to_code(named_args.get("color", "gray"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x9A, 0xFF, 0x25] + elif command == "r": + color_code = color_to_code(named_args.get("color", "gray"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x9B, 0xFF, 0x25] + elif command == "z": + color_code = color_to_code(named_args.get("color", "gray"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x9C, 0xFF, 0x25] + elif command == "c-up": + color_code = color_to_code(named_args.get("color", "yellow"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x9D, 0xFF, 0x25] + elif command == "c-down": + color_code = color_to_code(named_args.get("color", "yellow"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x9E, 0xFF, 0x25] + elif command == "c-left": + color_code = color_to_code(named_args.get("color", "yellow"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0x9F, 0xFF, 0x25] + elif command == "c-right": + color_code = color_to_code(named_args.get("color", "yellow"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0xA0, 0xFF, 0x25] + elif command == "start": + color_code = color_to_code(named_args.get("color", "red"), named_args.get("ctx", "button")) + message.bytes += [0xFF, 0x24, 0xFF, 0x05, color_code, 0xA1, 0xFF, 0x25] + elif command == "note": + message.bytes += [0x00] + elif command == "heart": + message.bytes += [0x90] + elif command == "star": + message.bytes += [0x91] + elif command == "arrow-up": + message.bytes += [0x92] + elif command == "arrow-down": + message.bytes += [0x93] + elif command == "arrow-left": + message.bytes += [0x94] + elif command == "arrow-right": + message.bytes += [0x95] + elif command == "circle": + message.bytes += [0x96] + elif command == "cross": + message.bytes += [0x97] + elif command == "wait": + print(f"{filename}:{lineno}: unknown command 'wait' (hint: did you mean 'prompt'?)") + exit(1) + elif command == "pause": + print(f"{filename}:{lineno}: unknown command 'pause' (hint: did you mean 'sleep'?)") + exit(1) + else: + print(f"{filename}:{lineno}: unknown command '{command}'") + exit(1) + else: + if source[0] == "\\": + source = source[1:] + + if source[0] in charset: + data = charset[source[0]] + + if type(data) is int: + message.bytes.append(data) + else: + message.bytes += data + + source = source[1:] + else: + print(f"{filename}:{lineno}: unsupported character '{source[0]}' for current font") + exit(1) + + if message != None: + print(f"{filename}: missing [/message]") + exit(1) + + with open(outfile, "wb") as f: + messages.sort(key=lambda msg: bool(msg.section) + bool(msg.index)) + + names = OrderedDict() + + sections = [] * 0x2E + for message in messages: + if message.section is None: + # allocate a section + for section_idx, section in enumerate(sections): + if len(section) < 0xFFF: + break + else: + section_idx = message.section + while len(sections) <= section_idx: + sections.append([]) + section = sections[section_idx] + + index = message.index if message.index is not None else len(section) + + if message.name: + if message.name in names: + print(f"warning: multiple messages with name '{message.name}'") + + names[message.name] = (section_idx, index) + + section.append(bytes(message.bytes)) + + f.seek((len(sections) + 1) * 4) # skip past table of contents + + section_offsets = [] + for section in sections: + message_offsets = [] + for message in section: + message_offsets.append(f.tell()) + f.write(message) + + section_offset = f.tell() + section_offsets.append(section_offset) + for offset in message_offsets: + f.write(offset.to_bytes(4, byteorder="big")) + f.write(section_offset.to_bytes(4, byteorder="big")) + + # padding + while f.tell() % 0x10 != 0: + f.write(b'\0\0\0\0') + + f.seek(0) + for offset in section_offsets: + f.write(offset.to_bytes(4, byteorder="big")) + f.write(b'\0\0\0\0') + + with open(outheader, "w") as f: + f.write( + "#ifndef _MESSAGE_IDS_H_\n" + "#define _MESSAGE_IDS_H_\n" + "\n" + '#include "messages.h"\n' + "\n" + ) + + for name, i in names.items(): + section, index = i + f.write(f"#define MessageID_{name} MESSAGE_ID({section}, {index})\n") + + f.write("\n#endif\n") diff --git a/tools/n64splat b/tools/n64splat index 79f47d6951..6106762b05 160000 --- a/tools/n64splat +++ b/tools/n64splat @@ -1 +1 @@ -Subproject commit 79f47d69514c39d902b7b3aee41d713670d88934 +Subproject commit 6106762b0561e40a640c11852a17bb87963c7ba8 diff --git a/tools/splat.yaml b/tools/splat.yaml index a689c0e23e..c378e9bc56 100644 --- a/tools/splat.yaml +++ b/tools/splat.yaml @@ -7257,6 +7257,57 @@ segments: - [0x1B81E88, "Yay0"] - [0x1B82058, "Yay0"] - [0x1B82202, "bin"] - - [0x1E40000, "PaperMarioMapFS"] + - start: 0x1B83000 + type: PaperMarioMessages + files: + - intro + - end/postgame + - toad_town/gate + - toad_town/castle + - toad_town/bridge + - toad_town/train + - toad_town/warehouse + - toad_town/docks + - toad_town/minigames + - castle_grounds + - shooting_star_summit + - prologue + - chapter1 + - chapter2 + - chapter3 + - chapter4 + - chapter5 + - chapter6 + - chapter7 + - chapter8 + - peach_interludes + - koopa_koot_quests + - advice/russ_t + - toad_town/bulletin_news + - toad_town/bulletin_gossip + - world/map_tattles + - world/npc_tattles + - world/entity_tattles + - battle/enemy_tattles + - ui/misc + - ui/choices + - ui/pause + - diary_letters + - advice/merlon + - advice/merluvlee + - item/descriptions_23 # TODO: difference between 23,24,25 + - item/descriptions_24 + - item/descriptions_25 + - item/names + - shops + - partner_descriptions + - battle/enemy_names + - battle/mario_moves + - battle/partner_moves + - quiz/questions + - quiz/options + - end/credits + - [0x1C84D30, bin] + - [0x1E40000, PaperMarioMapFS] - [0x27FEE22, "bin"] - [0x2800000]