diff --git a/Makefile b/Makefile index 8e2f0aee4c..ae763b7ab2 100644 --- a/Makefile +++ b/Makefile @@ -54,14 +54,16 @@ endif # Version-specific settings ifeq ($(VERSION),gc-us) DEBUG := 0 + CPP_DEFINES += -DTEXT_LANGUAGE=TEXT_LANG_US_JP else ifeq ($(VERSION),gc-eu) DEBUG := 0 + CPP_DEFINES += -DTEXT_LANGUAGE=TEXT_LANG_EU else ifeq ($(VERSION),gc-eu-mq) DEBUG := 0 - CPP_DEFINES += -DOOT_MQ + CPP_DEFINES += -DTEXT_LANGUAGE=TEXT_LANG_EU -DOOT_MQ else ifeq ($(VERSION),gc-eu-mq-dbg) DEBUG := 1 - CPP_DEFINES += -DOOT_MQ + CPP_DEFINES += -DTEXT_LANGUAGE=TEXT_LANG_EU -DOOT_MQ else $(error Unsupported version $(VERSION)) endif @@ -380,7 +382,7 @@ setup: venv $(MAKE) -C tools $(PYTHON) tools/decompress_baserom.py $(VERSION) $(PYTHON) tools/extract_baserom.py $(BASEROM_DIR)/baserom-decompressed.z64 --oot-version $(VERSION) -o $(EXTRACTED_DIR)/baserom - $(PYTHON) tools/msgdis.py --oot-version $(VERSION) --text-out $(EXTRACTED_DIR)/text/message_data.h --staff-text-out $(EXTRACTED_DIR)/text/message_data_staff.h + $(PYTHON) tools/msgdis.py $(VERSION) $(PYTHON) extract_assets.py -v $(VERSION) -j$(N_THREADS) disasm: @@ -436,20 +438,30 @@ $(BUILD_DIR)/baserom/%.o: $(EXTRACTED_DIR)/baserom/% $(BUILD_DIR)/data/%.o: data/%.s $(AS) $(ASFLAGS) $< -o $@ -$(BUILD_DIR)/assets/text/%.enc.h: assets/text/%.h $(EXTRACTED_DIR)/text/%.h assets/text/charmap.txt - $(CPP) $(CPPFLAGS) -I$(EXTRACTED_DIR) $< | $(PYTHON) tools/msgenc.py - --output $@ --charmap assets/text/charmap.txt +$(BUILD_DIR)/assets/text/%.enc.jpn.h: assets/text/%.h $(EXTRACTED_DIR)/text/%.h assets/text/charmap.txt + $(CPP) $(CPPFLAGS) -I$(EXTRACTED_DIR) $< | $(PYTHON) tools/msgenc.py --encoding jpn --charmap assets/text/charmap.txt - $@ + +$(BUILD_DIR)/assets/text/%.enc.nes.h: assets/text/%.h $(EXTRACTED_DIR)/text/%.h assets/text/charmap.txt + $(CPP) $(CPPFLAGS) -I$(EXTRACTED_DIR) $< | $(PYTHON) tools/msgenc.py --encoding nes --charmap assets/text/charmap.txt - $@ # Dependencies for files including message data headers # TODO remove when full header dependencies are used. -$(BUILD_DIR)/assets/text/fra_message_data_static.o: $(BUILD_DIR)/assets/text/message_data.enc.h -$(BUILD_DIR)/assets/text/ger_message_data_static.o: $(BUILD_DIR)/assets/text/message_data.enc.h -$(BUILD_DIR)/assets/text/nes_message_data_static.o: $(BUILD_DIR)/assets/text/message_data.enc.h -$(BUILD_DIR)/assets/text/staff_message_data_static.o: $(BUILD_DIR)/assets/text/message_data_staff.enc.h -$(BUILD_DIR)/src/code/z_message_PAL.o: $(BUILD_DIR)/assets/text/message_data.enc.h $(BUILD_DIR)/assets/text/message_data_staff.enc.h +$(BUILD_DIR)/assets/text/jpn_message_data_static.o: $(BUILD_DIR)/assets/text/message_data.enc.jpn.h +$(BUILD_DIR)/assets/text/nes_message_data_static.o: $(BUILD_DIR)/assets/text/message_data.enc.nes.h +$(BUILD_DIR)/assets/text/ger_message_data_static.o: $(BUILD_DIR)/assets/text/message_data.enc.nes.h +$(BUILD_DIR)/assets/text/fra_message_data_static.o: $(BUILD_DIR)/assets/text/message_data.enc.nes.h +$(BUILD_DIR)/assets/text/staff_message_data_static.o: $(BUILD_DIR)/assets/text/message_data_staff.enc.nes.h +$(BUILD_DIR)/src/code/z_message_PAL.o: assets/text/message_data.h assets/text/message_data_staff.h $(BUILD_DIR)/assets/text/%.o: assets/text/%.c +ifneq ($(COMPILER),gcc) +# Preprocess text with modern cpp for varargs macros + $(CPP) -undef -D_LANGUAGE_C -D__sgi $(CPPFLAGS) $(INC) $< -o $(@:.o=.c) + $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -o $@ $(@:.o=.c) +else $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -o $@ $< - $(OBJCOPY) -O binary $@ $@.bin +endif + $(OBJCOPY) -O binary -j.rodata $@ $@.bin $(BUILD_DIR)/assets/%.o: $(EXTRACTED_DIR)/assets/%.c $(CC) -c $(CFLAGS) $(MIPS_VERSION) $(OPTFLAGS) -o $@ $< diff --git a/assets/text/charmap.txt b/assets/text/charmap.txt index e68a67228d..a6c94bee28 100644 --- a/assets/text/charmap.txt +++ b/assets/text/charmap.txt @@ -1,50 +1,58 @@ +# Determines how certain text sequences should be encoded. The text sequence is +# converted to either the first or second tuple element based on whether the +# target encoding is the "wide" encoding. The first element is for the non-wide +# encoding, used for all languages besides JP, while the second element is for +# the wide encoding, used for JP. { - '\n' : 0x01, + '\n' : (0x01, 0x000A), - '‾' : 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, - 'ë' : 0x98, - 'ï' : 0x99, - 'ô' : 0x9A, - 'ö' : 0x9B, - 'ù' : 0x9C, - 'û' : 0x9D, - 'ü' : 0x9E, + '[A]' : (0x9F, 0x839F), + '[B]' : (0xA0, 0x83A0), + '[C]' : (0xA1, 0x83A1), + '[L]' : (0xA2, 0x83A2), + '[R]' : (0xA3, 0x83A3), + '[Z]' : (0xA4, 0x83A4), + '[C-Up]' : (0xA5, 0x83A5), + '[C-Down]' : (0xA6, 0x83A6), + '[C-Left]' : (0xA7, 0x83A7), + '[C-Right]' : (0xA8, 0x83A8), + '▼' : (0xA9, 0x83A9), + '[Control-Pad]' : (0xAA, 0x83AA), + '[D-Pad]' : (0xAB, None), - '[A]' : 0x9F, - '[B]' : 0xA0, - '[C]' : 0xA1, - '[L]' : 0xA2, - '[R]' : 0xA3, - '[Z]' : 0xA4, - '[C-Up]' : 0xA5, - '[C-Down]' : 0xA6, - '[C-Left]' : 0xA7, - '[C-Right]' : 0xA8, - '▼' : 0xA9, - '[Control-Pad]' : 0xAA, - '[D-Pad]' : 0xAB, + # Possibly from a SHIFT-JIS extension, python doesn't have builtin support + '┯' : (None, 0x86D3), + + '‾' : (0x7F, None), + 'À' : (0x80, None), + 'î' : (0x81, None), + 'Â' : (0x82, None), + 'Ä' : (0x83, None), + 'Ç' : (0x84, None), + 'È' : (0x85, None), + 'É' : (0x86, None), + 'Ê' : (0x87, None), + 'Ë' : (0x88, None), + 'Ï' : (0x89, None), + 'Ô' : (0x8A, None), + 'Ö' : (0x8B, None), + 'Ù' : (0x8C, None), + 'Û' : (0x8D, None), + 'Ü' : (0x8E, None), + 'ß' : (0x8F, None), + 'à' : (0x90, None), + 'á' : (0x91, None), + 'â' : (0x92, None), + 'ä' : (0x93, None), + 'ç' : (0x94, None), + 'è' : (0x95, None), + 'é' : (0x96, None), + 'ê' : (0x97, None), + 'ë' : (0x98, None), + 'ï' : (0x99, None), + 'ô' : (0x9A, None), + 'ö' : (0x9B, None), + 'ù' : (0x9C, None), + 'û' : (0x9D, None), + 'ü' : (0x9E, None), } diff --git a/assets/text/fra_message_data_static.c b/assets/text/fra_message_data_static.c index 944d9f03f5..c33ce753b8 100644 --- a/assets/text/fra_message_data_static.c +++ b/assets/text/fra_message_data_static.c @@ -2,9 +2,16 @@ #include "message_data_fmt.h" -#define DEFINE_MESSAGE(textId, type, yPos, nesMessage, gerMessage, fraMessage) \ - const char _message_##textId##_fra[sizeof(fraMessage)] = { fraMessage END }; +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + const char _message_##textId##_fra[] = fraMessage; -#define DEFINE_MESSAGE_NES(textId, type, yPos, nesMessage) +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Present */ const char _message_##textId##_fra[] = fraMessage; +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Not Present */ -#include "assets/text/message_data.enc.h" +// Font Message +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Not Present */ + +#include "assets/text/message_data.enc.nes.h" diff --git a/assets/text/ger_message_data_static.c b/assets/text/ger_message_data_static.c index 9d3e190bce..502b785719 100644 --- a/assets/text/ger_message_data_static.c +++ b/assets/text/ger_message_data_static.c @@ -2,9 +2,16 @@ #include "message_data_fmt.h" -#define DEFINE_MESSAGE(textId, type, yPos, nesMessage, gerMessage, fraMessage) \ - const char _message_##textId##_ger[sizeof(gerMessage)] = { gerMessage END }; +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + const char _message_##textId##_ger[] = gerMessage; -#define DEFINE_MESSAGE_NES(textId, type, yPos, nesMessage) +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Present */ const char _message_##textId##_ger[] = gerMessage; +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Not Present */ -#include "assets/text/message_data.enc.h" +// Font Message +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Not Present */ + +#include "assets/text/message_data.enc.nes.h" diff --git a/assets/text/jpn_message_data_static.c b/assets/text/jpn_message_data_static.c new file mode 100644 index 0000000000..6ccac9b9b9 --- /dev/null +++ b/assets/text/jpn_message_data_static.c @@ -0,0 +1,17 @@ +#define MESSAGE_DATA_STATIC +#define MESSAGE_WCHAR +#include "message_data_fmt.h" + +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + const char _message_##textId##_jpn[] = jpnMessage; + +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Not Present */ +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Present */ const char _message_##textId##_jpn[] = jpnMessage; + +// Font Message +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Present */ const char _message_##textId##_jpn[] = jpnMessage; + +#include "assets/text/message_data.enc.jpn.h" diff --git a/assets/text/message_data.h b/assets/text/message_data.h index 57a0859b56..34fc5e91eb 100644 --- a/assets/text/message_data.h +++ b/assets/text/message_data.h @@ -5,7 +5,23 @@ * Message 0xFFFD must be last to not break the message debugger (see R_MESSAGE_DEBUGGER_TEXTID). * Message 0xFFFC must be immediately before message 0xFFFD to not break Font_LoadOrderedFont. */ -DEFINE_MESSAGE_NES(0xFFFC, TEXTBOX_TYPE_BLACK, TEXTBOX_POS_VARIABLE, +DEFINE_MESSAGE_FFFC(0xFFFC, TEXTBOX_TYPE_BLACK, TEXTBOX_POS_VARIABLE, +MSG( +"0123456789あいうえおかきくけこ\n" +"さしすせそたちつてとなにぬねのはひふへほ\n" +"まみむめもやゆよらりるれろわをんぁぃぅぇ\n" +"ぉっゃゅょがぎぐげござじずぜぞだぢづでど\n" +"ばびぶべぼぱぴぷぺぽアイウエオカキクケコ\n" +"サシスセソタチツテトナニヌネノハヒフヘホ\n" +"マミムメモヤユヨラリルレロワヲンァィゥェ\n" +"ォッャュョガギグゲゴザジズゼゾダヂヅデド\n" +"バビブベボパピプペポヴABCDEFGHI\n" +"JKLMNOPQRSTUVWXYZabc\n" +"defghijklmnopqrstuvw\n" +"xyz ┯?!:−()゛゜,./" +) +, +MSG( "0123456789\n" "ABCDEFGHIJKLMN\n" "OPQRSTUVWXYZ\n" @@ -13,4 +29,9 @@ DEFINE_MESSAGE_NES(0xFFFC, TEXTBOX_TYPE_BLACK, TEXTBOX_POS_VARIABLE, "opqrstuvwxyz\n" " -.\n" ) -DEFINE_MESSAGE(0xFFFD, TEXTBOX_TYPE_BLACK, TEXTBOX_POS_VARIABLE, "", "", "") +, +MSG(/* UNUSED */) +, +MSG(/* UNUSED */) +) +DEFINE_MESSAGE(0xFFFD, TEXTBOX_TYPE_BLACK, TEXTBOX_POS_VARIABLE, MSG(), MSG(), MSG(), MSG()) diff --git a/assets/text/nes_message_data_static.c b/assets/text/nes_message_data_static.c index ff14d66b86..d927ea6522 100644 --- a/assets/text/nes_message_data_static.c +++ b/assets/text/nes_message_data_static.c @@ -2,9 +2,22 @@ #include "message_data_fmt.h" -#define DEFINE_MESSAGE(textId, type, yPos, nesMessage, gerMessage, fraMessage) \ - const char _message_##textId##_nes[sizeof(nesMessage)] = { nesMessage END }; +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + const char _message_##textId##_nes[] = nesMessage; -#define DEFINE_MESSAGE_NES(textId, type, yPos, nesMessage) DEFINE_MESSAGE(textId, type, yPos, nesMessage, , ) +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Present */ const char _message_##textId##_nes[] = nesMessage; +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Not Present */ -#include "assets/text/message_data.enc.h" +#if (TEXT_LANGUAGE == TEXT_LANG_US_JP) +// On US/JP versions, the font message is with JPN +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Not Present */ +#else +// On EU versions, the font message is with NES +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Present */ const char _message_##textId##_nes[] = nesMessage; +#endif + +#include "assets/text/message_data.enc.nes.h" diff --git a/assets/text/staff_message_data_static.c b/assets/text/staff_message_data_static.c index 5ad50b044a..6f028a7e6d 100644 --- a/assets/text/staff_message_data_static.c +++ b/assets/text/staff_message_data_static.c @@ -3,6 +3,6 @@ #include "message_data_fmt.h" #define DEFINE_MESSAGE(textId, type, yPos, staffMessage) \ - const char _message_##textId##_staff[sizeof(staffMessage)] = { staffMessage END }; + const char _message_##textId##_staff[] = staffMessage; -#include "assets/text/message_data_staff.enc.h" +#include "assets/text/message_data_staff.enc.nes.h" diff --git a/baseroms/gc-eu-mq-dbg/config.yml b/baseroms/gc-eu-mq-dbg/config.yml index d4c8f3807d..bd6c9bf6b3 100644 --- a/baseroms/gc-eu-mq-dbg/config.yml +++ b/baseroms/gc-eu-mq-dbg/config.yml @@ -1,4 +1,5 @@ dmadata_start: 0x12F70 +text_lang_pal: true variables: gMtxClear: 0x8012DB20 sNesMessageEntryTable: 0x8014B320 diff --git a/baseroms/gc-eu-mq/config.yml b/baseroms/gc-eu-mq/config.yml index 278bdcf517..e04429d0a2 100644 --- a/baseroms/gc-eu-mq/config.yml +++ b/baseroms/gc-eu-mq/config.yml @@ -1,4 +1,5 @@ dmadata_start: 0x7170 +text_lang_pal: true variables: gMtxClear: 0x800FBC00 sNesMessageEntryTable: 0x801077F0 diff --git a/baseroms/gc-eu/config.yml b/baseroms/gc-eu/config.yml index d2a544ca0a..6f48daec7a 100644 --- a/baseroms/gc-eu/config.yml +++ b/baseroms/gc-eu/config.yml @@ -1,4 +1,5 @@ dmadata_start: 0x7170 +text_lang_pal: true variables: gMtxClear: 0x800FBC20 sNesMessageEntryTable: 0x80107810 diff --git a/baseroms/gc-us/config.yml b/baseroms/gc-us/config.yml index 78ee329db4..0d6acafc34 100644 --- a/baseroms/gc-us/config.yml +++ b/baseroms/gc-us/config.yml @@ -1,7 +1,8 @@ dmadata_start: 0x7170 +text_lang_pal: false variables: gMtxClear: 0x800FE2A0 - sJpnMessageEntryTable: 0x80107810 + sJpnMessageEntryTable: 0x80109E8C sNesMessageEntryTable: 0x8010DFCC sStaffMessageEntryTable: 0x801121EC sShadowTex: 0x80A74130 diff --git a/include/message_data_fmt.h b/include/message_data_fmt.h index 447117d81f..185acf30c1 100644 --- a/include/message_data_fmt.h +++ b/include/message_data_fmt.h @@ -1,180 +1,189 @@ #ifndef MESSAGE_DATA_FMT_H #define MESSAGE_DATA_FMT_H -/* - * Macros to create both a constant and a string literal from a magic value - * The constants are used in code files when parsing text for various purposes - * The strings are used in the message_data_static files themselves, as you can only concat strings with other strings - */ +#include "z64save.h" // For highscores +#include "z64item.h" // For item ids +#include "sfx.h" // For sfx ids -#ifndef GLUE -#define GLUE(a, b) a##b -#endif - -#define STRINGIFY(s) #s -#define EXPAND_AND_STRINGIFY(s) STRINGIFY(s) - -#define HEX(N) GLUE(0x, N) -#define STR(N) EXPAND_AND_STRINGIFY(GLUE(\x, N)) +#define TEXT_LANG_US_JP 0 +#define TEXT_LANG_EU 1 /* * Text control characters */ -// Control character magic values, in 2-digit hex without prefix +// Non-Wide (nes/ger/fra) -#define CTRL_NEWLINE 01 -#define CTRL_END 02 -#define CTRL_BOX_BREAK 04 -#define CTRL_COLOR 05 -#define CTRL_SHIFT 06 -#define CTRL_TEXTID 07 -#define CTRL_QUICKTEXT_ENABLE 08 -#define CTRL_QUICKTEXT_DISABLE 09 -#define CTRL_PERSISTENT 0A -#define CTRL_EVENT 0B -#define CTRL_BOX_BREAK_DELAYED 0C -#define CTRL_AWAIT_BUTTON_PRESS 0D -#define CTRL_FADE 0E -#define CTRL_NAME 0F -#define CTRL_OCARINA 10 -#define CTRL_FADE2 11 -#define CTRL_SFX 12 -#define CTRL_ITEM_ICON 13 -#define CTRL_TEXT_SPEED 14 -#define CTRL_BACKGROUND 15 -#define CTRL_MARATHON_TIME 16 -#define CTRL_RACE_TIME 17 -#define CTRL_POINTS 18 -#define CTRL_TOKENS 19 -#define CTRL_UNSKIPPABLE 1A -#define CTRL_TWO_CHOICE 1B -#define CTRL_THREE_CHOICE 1C -#define CTRL_FISH_INFO 1D -#define CTRL_HIGHSCORE 1E -#define CTRL_TIME 1F +#define MESSAGE_NEWLINE 0x01 +#define MESSAGE_END 0x02 +#define MESSAGE_BOX_BREAK 0x04 +#define MESSAGE_COLOR 0x05 +#define MESSAGE_SHIFT 0x06 +#define MESSAGE_TEXTID 0x07 +#define MESSAGE_QUICKTEXT_ENABLE 0x08 +#define MESSAGE_QUICKTEXT_DISABLE 0x09 +#define MESSAGE_PERSISTENT 0x0A +#define MESSAGE_EVENT 0x0B +#define MESSAGE_BOX_BREAK_DELAYED 0x0C +#define MESSAGE_AWAIT_BUTTON_PRESS 0x0D +#define MESSAGE_FADE 0x0E +#define MESSAGE_NAME 0x0F +#define MESSAGE_OCARINA 0x10 +#define MESSAGE_FADE2 0x11 +#define MESSAGE_SFX 0x12 +#define MESSAGE_ITEM_ICON 0x13 +#define MESSAGE_TEXT_SPEED 0x14 +#define MESSAGE_BACKGROUND 0x15 +#define MESSAGE_MARATHON_TIME 0x16 +#define MESSAGE_RACE_TIME 0x17 +#define MESSAGE_POINTS 0x18 +#define MESSAGE_TOKENS 0x19 +#define MESSAGE_UNSKIPPABLE 0x1A +#define MESSAGE_TWO_CHOICE 0x1B +#define MESSAGE_THREE_CHOICE 0x1C +#define MESSAGE_FISH_INFO 0x1D +#define MESSAGE_HIGHSCORE 0x1E +#define MESSAGE_TIME 0x1F + +// Wide (jpn) + +#define MESSAGE_WIDE_NEWLINE 0x000A +#define MESSAGE_WIDE_END 0x8170 +#define MESSAGE_WIDE_BOX_BREAK 0x81A5 +#define MESSAGE_WIDE_COLOR 0x000B +#define MESSAGE_WIDE_SHIFT 0x86C7 +#define MESSAGE_WIDE_TEXTID 0x81CB +#define MESSAGE_WIDE_QUICKTEXT_ENABLE 0x8189 +#define MESSAGE_WIDE_QUICKTEXT_DISABLE 0x818A +#define MESSAGE_WIDE_PERSISTENT 0x86C8 +#define MESSAGE_WIDE_EVENT 0x819F +#define MESSAGE_WIDE_BOX_BREAK_DELAYED 0x81A3 +#define MESSAGE_WIDE_AWAIT_BUTTON_PRESS 0x81A4 +#define MESSAGE_WIDE_FADE 0x819E +#define MESSAGE_WIDE_NAME 0x874F +#define MESSAGE_WIDE_OCARINA 0x81F0 +#define MESSAGE_WIDE_FADE2 0x81F4 +#define MESSAGE_WIDE_SFX 0x81F3 +#define MESSAGE_WIDE_ITEM_ICON 0x819A +#define MESSAGE_WIDE_TEXT_SPEED 0x86C9 +#define MESSAGE_WIDE_BACKGROUND 0x86B3 +#define MESSAGE_WIDE_MARATHON_TIME 0x8791 +#define MESSAGE_WIDE_RACE_TIME 0x8792 +#define MESSAGE_WIDE_POINTS 0x879B +#define MESSAGE_WIDE_TOKENS 0x86A3 +#define MESSAGE_WIDE_UNSKIPPABLE 0x8199 +#define MESSAGE_WIDE_TWO_CHOICE 0x81BC +#define MESSAGE_WIDE_THREE_CHOICE 0x81B8 +#define MESSAGE_WIDE_FISH_INFO 0x86A4 +#define MESSAGE_WIDE_HIGHSCORE 0x869F +#define MESSAGE_WIDE_TIME 0x81A1 /* * Colors */ -#define COLOR_STR(N) EXPAND_AND_STRINGIFY(GLUE(\x4, N)) +typedef enum { + TEXT_COLOR_DEFAULT, + TEXT_COLOR_RED, + TEXT_COLOR_ADJUSTABLE, + TEXT_COLOR_BLUE, + TEXT_COLOR_LIGHTBLUE, + TEXT_COLOR_PURPLE, + TEXT_COLOR_YELLOW, + TEXT_COLOR_BLACK +} TextColor; -// Color magic values, in single-digit hex without prefix +/* + * Background + */ -#define CTRL_DEFAULT 0 -#define CTRL_RED 1 -#define CTRL_ADJUSTABLE 2 -#define CTRL_BLUE 3 -#define CTRL_LIGHTBLUE 4 -#define CTRL_PURPLE 5 -#define CTRL_YELLOW 6 -#define CTRL_BLACK 7 +typedef enum { + TEXTBOX_BG_X_LEFT, + TEXTBOX_BG_X_RIGHT +} TextboxBackgroundIndex; + +typedef enum { + TEXTBOX_BG_FGCOL_WHITE, + TEXTBOX_BG_FGCOL_DARK_RED, + TEXTBOX_BG_FGCOL_ORANGE, + TEXTBOX_BG_FGCOL_WHITE_3, + TEXTBOX_BG_FGCOL_WHITE_4, + TEXTBOX_BG_FGCOL_WHITE_5, + TEXTBOX_BG_FGCOL_WHITE_6, + TEXTBOX_BG_FGCOL_WHITE_7 +} TextboxBackgroundForegroundColor; + +typedef enum { + TEXTBOX_BG_BGCOL_BLACK, + TEXTBOX_BG_BGCOL_GOLD, + TEXTBOX_BG_BGCOL_BLACK_2, + TEXTBOX_BG_BGCOL_BLACK_3 +} TextboxBackgroundBackgroundColor; + +typedef enum { + TEXTBOX_BG_Y_OFFSET_1, + TEXTBOX_BG_Y_OFFSET_2 +} TextboxBackgroundYOffsetIndex; #ifdef MESSAGE_DATA_STATIC // For use in message_data_static files -#define ARG(x) x +// Encoded text consists of an array of bytes. Since it's in a macro it must be wrapped in a varargs macro so that each +// byte is not treated as a separate macro argument to DEFINE_MESSAGE. IDO doesn't support varargs macros, however we +// preprocess the message_data_static files with modern cpp instead. See the makefile rule for assets/text/ +# define MSG(...) { __VA_ARGS__ END } + +// Encoding helpers +# define ARG2(x) (((x) >> 8) & 0xFF), (((x) >> 0) & 0xFF), +# ifdef MESSAGE_WCHAR +# define ARG1(x) 0x00, (x), +# define ARGC(x) 0x0C, (TEXT_COLOR_ ## x), +# define CTRL_BASE(name) ARG2(MESSAGE_WIDE_ ## name) +# else +# define ARG1(x) (x), +# define ARGC(x) (0x40 | (TEXT_COLOR_ ## x)), +# define CTRL_BASE(name) ARG1(MESSAGE_ ## name) +# endif +# define ARGB1(x) ARG1(TEXTBOX_BG_ ## x) +# define ARGB2(a,b,c,d) (((TEXTBOX_BG_FGCOL_ ## a) << 4) | ((TEXTBOX_BG_BGCOL_ ## b) << 0)), (((TEXTBOX_BG_Y_OFFSET_ ## c) << 4) | ((d) << 0)), + +/* + * Control characters + */ // while a control character, newlines are handled in the charmap conversion // stage to allow normal newline \n usage in message_data_static files -#define NEWLINE STR(CTRL_NEWLINE) -#define END STR(CTRL_END) -#define BOX_BREAK STR(CTRL_BOX_BREAK) -#define COLOR(x) STR(CTRL_COLOR) ARG(x) // 1 -#define SHIFT(x) STR(CTRL_SHIFT) ARG(x) // 1 -#define TEXTID(x) STR(CTRL_TEXTID) ARG(x) // 2 -#define QUICKTEXT_ENABLE STR(CTRL_QUICKTEXT_ENABLE) -#define QUICKTEXT_DISABLE STR(CTRL_QUICKTEXT_DISABLE) -#define PERSISTENT STR(CTRL_PERSISTENT) -#define EVENT STR(CTRL_EVENT) -#define BOX_BREAK_DELAYED(x) STR(CTRL_BOX_BREAK_DELAYED) ARG(x) // 1 -#define AWAIT_BUTTON_PRESS STR(CTRL_AWAIT_BUTTON_PRESS) -#define FADE(x) STR(CTRL_FADE) ARG(x) // 1 -#define NAME STR(CTRL_NAME) -#define OCARINA STR(CTRL_OCARINA) -#define FADE2(x) STR(CTRL_FADE2) ARG(x) // 2 -#define SFX(x) STR(CTRL_SFX) ARG(x) // 2 -#define ITEM_ICON(x) STR(CTRL_ITEM_ICON) ARG(x) // 1 -#define TEXT_SPEED(x) STR(CTRL_TEXT_SPEED) ARG(x) // 1 -#define BACKGROUND(x,y,z) STR(CTRL_BACKGROUND) ARG(x) ARG(y) ARG(z) -#define MARATHON_TIME STR(CTRL_MARATHON_TIME) -#define RACE_TIME STR(CTRL_RACE_TIME) -#define POINTS STR(CTRL_POINTS) -#define TOKENS STR(CTRL_TOKENS) -#define UNSKIPPABLE STR(CTRL_UNSKIPPABLE) -#define TWO_CHOICE STR(CTRL_TWO_CHOICE) -#define THREE_CHOICE STR(CTRL_THREE_CHOICE) -#define FISH_INFO STR(CTRL_FISH_INFO) -#define HIGHSCORE(x) STR(CTRL_HIGHSCORE) ARG(x) // 1 -#define TIME STR(CTRL_TIME) +# define NEWLINE CTRL_BASE(NEWLINE) +# define END CTRL_BASE(END) +# define BOX_BREAK CTRL_BASE(BOX_BREAK) +# define COLOR(color) CTRL_BASE(COLOR) ARGC(color) +# define SHIFT(amount) CTRL_BASE(SHIFT) ARG1(amount) +# define TEXTID(textId) CTRL_BASE(TEXTID) ARG2(textId) +# define QUICKTEXT_ENABLE CTRL_BASE(QUICKTEXT_ENABLE) +# define QUICKTEXT_DISABLE CTRL_BASE(QUICKTEXT_DISABLE) +# define PERSISTENT CTRL_BASE(PERSISTENT) +# define EVENT CTRL_BASE(EVENT) +# define BOX_BREAK_DELAYED(delay) CTRL_BASE(BOX_BREAK_DELAYED) ARG1(delay) +# define AWAIT_BUTTON_PRESS CTRL_BASE(AWAIT_BUTTON_PRESS) +# define FADE(delay) CTRL_BASE(FADE) ARG1(delay) +# define NAME CTRL_BASE(NAME) +# define OCARINA CTRL_BASE(OCARINA) +# define FADE2(delay) CTRL_BASE(FADE2) ARG2(delay) +# define SFX(sfxId) CTRL_BASE(SFX) ARG2(sfxId) +# define ITEM_ICON(itemId) CTRL_BASE(ITEM_ICON) ARG1(itemId) +# define TEXT_SPEED(amount) CTRL_BASE(TEXT_SPEED) ARG1(amount) +# define BACKGROUND(bgIdx, fgColor, bgColor, yOffset, unk) CTRL_BASE(BACKGROUND) ARGB1(bgIdx) ARGB2(fgColor, bgColor, yOffset, unk) +# define MARATHON_TIME CTRL_BASE(MARATHON_TIME) +# define RACE_TIME CTRL_BASE(RACE_TIME) +# define POINTS CTRL_BASE(POINTS) +# define TOKENS CTRL_BASE(TOKENS) +# define UNSKIPPABLE CTRL_BASE(UNSKIPPABLE) +# define TWO_CHOICE CTRL_BASE(TWO_CHOICE) +# define THREE_CHOICE CTRL_BASE(THREE_CHOICE) +# define FISH_INFO CTRL_BASE(FISH_INFO) +# define HIGHSCORE(highscore) CTRL_BASE(HIGHSCORE) ARG1(highscore) +# define TIME CTRL_BASE(TIME) -/* - * Highscore values as strings, for code references the HighScores - * enum should be used. - */ - -#define HS_HORSE_ARCHERY "\x00" -#define HS_POE_POINTS "\x01" -#define HS_LARGEST_FISH "\x02" -#define HS_HORSE_RACE "\x03" -#define HS_MARATHON "\x04" -#define HS_DAMPE_RACE "\x06" - -/* - * Color values as strings - */ - -#define DEFAULT COLOR_STR(CTRL_DEFAULT) -#define RED COLOR_STR(CTRL_RED) -#define ADJUSTABLE COLOR_STR(CTRL_ADJUSTABLE) -#define BLUE COLOR_STR(CTRL_BLUE) -#define LIGHTBLUE COLOR_STR(CTRL_LIGHTBLUE) -#define PURPLE COLOR_STR(CTRL_PURPLE) -#define YELLOW COLOR_STR(CTRL_YELLOW) -#define BLACK COLOR_STR(CTRL_BLACK) - -#else -// For use in code files -#define MSGCOL_DEFAULT HEX(CTRL_DEFAULT) -#define MSGCOL_RED HEX(CTRL_RED) -#define MSGCOL_ADJUSTABLE HEX(CTRL_ADJUSTABLE) -#define MSGCOL_BLUE HEX(CTRL_BLUE) -#define MSGCOL_LIGHTBLUE HEX(CTRL_LIGHTBLUE) -#define MSGCOL_PURPLE HEX(CTRL_PURPLE) -#define MSGCOL_YELLOW HEX(CTRL_YELLOW) -#define MSGCOL_BLACK HEX(CTRL_BLACK) - -#define MESSAGE_NEWLINE HEX(CTRL_NEWLINE) -#define MESSAGE_END HEX(CTRL_END) -#define MESSAGE_BOX_BREAK HEX(CTRL_BOX_BREAK) -#define MESSAGE_COLOR HEX(CTRL_COLOR) -#define MESSAGE_SHIFT HEX(CTRL_SHIFT) -#define MESSAGE_TEXTID HEX(CTRL_TEXTID) -#define MESSAGE_QUICKTEXT_ENABLE HEX(CTRL_QUICKTEXT_ENABLE) -#define MESSAGE_QUICKTEXT_DISABLE HEX(CTRL_QUICKTEXT_DISABLE) -#define MESSAGE_PERSISTENT HEX(CTRL_PERSISTENT) -#define MESSAGE_EVENT HEX(CTRL_EVENT) -#define MESSAGE_BOX_BREAK_DELAYED HEX(CTRL_BOX_BREAK_DELAYED) -#define MESSAGE_AWAIT_BUTTON_PRESS HEX(CTRL_AWAIT_BUTTON_PRESS) -#define MESSAGE_FADE HEX(CTRL_FADE) -#define MESSAGE_NAME HEX(CTRL_NAME) -#define MESSAGE_OCARINA HEX(CTRL_OCARINA) -#define MESSAGE_FADE2 HEX(CTRL_FADE2) -#define MESSAGE_SFX HEX(CTRL_SFX) -#define MESSAGE_ITEM_ICON HEX(CTRL_ITEM_ICON) -#define MESSAGE_TEXT_SPEED HEX(CTRL_TEXT_SPEED) -#define MESSAGE_BACKGROUND HEX(CTRL_BACKGROUND) -#define MESSAGE_MARATHON_TIME HEX(CTRL_MARATHON_TIME) -#define MESSAGE_RACE_TIME HEX(CTRL_RACE_TIME) -#define MESSAGE_POINTS HEX(CTRL_POINTS) -#define MESSAGE_TOKENS HEX(CTRL_TOKENS) -#define MESSAGE_UNSKIPPABLE HEX(CTRL_UNSKIPPABLE) -#define MESSAGE_TWO_CHOICE HEX(CTRL_TWO_CHOICE) -#define MESSAGE_THREE_CHOICE HEX(CTRL_THREE_CHOICE) -#define MESSAGE_FISH_INFO HEX(CTRL_FISH_INFO) -#define MESSAGE_HIGHSCORE HEX(CTRL_HIGHSCORE) -#define MESSAGE_TIME HEX(CTRL_TIME) #endif #endif diff --git a/include/message_data_static.h b/include/message_data_static.h index b888192d58..cb655da6fe 100644 --- a/include/message_data_static.h +++ b/include/message_data_static.h @@ -1,7 +1,7 @@ #ifndef MESSAGE_DATA_STATIC_H #define MESSAGE_DATA_STATIC_H -#include "global.h" +#include "ultra64.h" #include "message_data_fmt.h" typedef enum { @@ -35,6 +35,49 @@ typedef struct { * Message Symbol Declarations */ +/* Non-Credits Messages */ + +#if (TEXT_LANGUAGE == TEXT_LANG_US_JP) +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + extern const char _message_##textId##_nes[]; + +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + extern const char _message_##textId##_jpn[]; + +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) + +#define FONT_MESSAGE_OFFSET (_message_0xFFFC_jpn - (const char*)_jpn_message_data_staticSegmentStart) +#define FONT_MESSAGE_LENGTH (_message_0xFFFD_jpn - _message_0xFFFC_jpn) +#else +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + extern const char _message_##textId##_nes[]; \ + extern const char _message_##textId##_ger[]; \ + extern const char _message_##textId##_fra[]; + +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Empty */ + +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) + +#define FONT_MESSAGE_OFFSET (_message_0xFFFC_nes - (const char*)_nes_message_data_staticSegmentStart) +#define FONT_MESSAGE_LENGTH (_message_0xFFFD_nes - _message_0xFFFC_nes) +#endif + +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) + +#include "assets/text/message_data.h" + +#undef DEFINE_MESSAGE +#undef DEFINE_MESSAGE_NES +#undef DEFINE_MESSAGE_JPN +#undef DEFINE_MESSAGE_FFFC + +/* Credits Messages */ + #define DEFINE_MESSAGE(textId, type, yPos, staffMessage) \ extern const char _message_##textId##_staff[]; @@ -42,17 +85,4 @@ typedef struct { #undef DEFINE_MESSAGE -#define DEFINE_MESSAGE(textId, type, yPos, nesMessage, gerMessage, fraMessage) \ - extern const char _message_##textId##_nes[]; \ - extern const char _message_##textId##_ger[]; \ - extern const char _message_##textId##_fra[]; - -#define DEFINE_MESSAGE_NES(textId, type, yPos, nesMessage) \ - extern const char _message_##textId##_nes[]; - -#include "assets/text/message_data.h" - -#undef DEFINE_MESSAGE -#undef DEFINE_MESSAGE_NES - #endif diff --git a/src/code/z_kanfont.c b/src/code/z_kanfont.c index 5b3bb2e670..83686a02ed 100644 --- a/src/code/z_kanfont.c +++ b/src/code/z_kanfont.c @@ -38,8 +38,8 @@ void Font_LoadOrderedFont(Font* font) { s32 fontBufIndex; u32 offset; - font->msgOffset = _message_0xFFFC_nes - (const char*)_nes_message_data_staticSegmentStart; - len = font->msgLength = _message_0xFFFD_nes - _message_0xFFFC_nes; + font->msgOffset = FONT_MESSAGE_OFFSET; + len = font->msgLength = FONT_MESSAGE_LENGTH; DMA_REQUEST_SYNC(font->msgBuf, (uintptr_t)_nes_message_data_staticSegmentRomStart + font->msgOffset, len, "../z_kanfont.c", 122); diff --git a/src/code/z_message_PAL.c b/src/code/z_message_PAL.c index 027e66c217..2a66a754a1 100644 --- a/src/code/z_message_PAL.c +++ b/src/code/z_message_PAL.c @@ -28,30 +28,49 @@ s16 sMessageHasSetSfx = false; u16 sOcarinaSongBitFlags = 0; // ocarina bit flags MessageTableEntry sNesMessageEntryTable[] = { -#define DEFINE_MESSAGE(textId, type, yPos, nesMessage, gerMessage, fraMessage) \ +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ { textId, (_SHIFTL(type, 4, 8) | _SHIFTL(yPos, 0, 8)), _message_##textId##_nes }, -#define DEFINE_MESSAGE_NES(textId, type, yPos, nesMessage) DEFINE_MESSAGE(textId, type, yPos, nesMessage, , ) +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) /* Not Present */ +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + /* Present */ DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) #include "assets/text/message_data.h" #undef DEFINE_MESSAGE #undef DEFINE_MESSAGE_NES +#undef DEFINE_MESSAGE_JPN +#undef DEFINE_MESSAGE_FFFC { 0xFFFF, 0, NULL }, }; const char* sGerMessageEntryTable[] = { -#define DEFINE_MESSAGE(textId, type, yPos, nesMessage, gerMessage, fraMessage) _message_##textId##_ger, -#define DEFINE_MESSAGE_NES(textId, type, yPos, nesMessage) +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) _message_##textId##_ger, +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) /* Not Present */ +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) /* Not Present */ #include "assets/text/message_data.h" #undef DEFINE_MESSAGE #undef DEFINE_MESSAGE_NES +#undef DEFINE_MESSAGE_JPN +#undef DEFINE_MESSAGE_FFFC NULL, }; const char* sFraMessageEntryTable[] = { -#define DEFINE_MESSAGE(textId, type, yPos, nesMessage, gerMessage, fraMessage) _message_##textId##_fra, -#define DEFINE_MESSAGE_NES(textId, type, yPos, nesMessage) +#define DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) _message_##textId##_fra, +#define DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) /* Not Present */ +#define DEFINE_MESSAGE(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_NES(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) \ + DEFINE_MESSAGE_JPN(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) +#define DEFINE_MESSAGE_FFFC(textId, type, yPos, jpnMessage, nesMessage, gerMessage, fraMessage) /* Not Present */ #include "assets/text/message_data.h" #undef DEFINE_MESSAGE #undef DEFINE_MESSAGE_NES +#undef DEFINE_MESSAGE_JPN +#undef DEFINE_MESSAGE_FFFC NULL, }; @@ -390,7 +409,7 @@ void Message_FindCreditsMessage(PlayState* play, u16 textId) { void Message_SetTextColor(MessageContext* msgCtx, u16 colorParameter) { switch (colorParameter) { - case MSGCOL_RED: + case TEXT_COLOR_RED: if (msgCtx->textBoxType == TEXTBOX_TYPE_WOODEN) { msgCtx->textColorR = 255; msgCtx->textColorG = 120; @@ -401,7 +420,7 @@ void Message_SetTextColor(MessageContext* msgCtx, u16 colorParameter) { msgCtx->textColorB = 60; } break; - case MSGCOL_ADJUSTABLE: + case TEXT_COLOR_ADJUSTABLE: if (msgCtx->textBoxType == TEXTBOX_TYPE_WOODEN) { msgCtx->textColorR = R_TEXT_ADJUST_COLOR_1_R; msgCtx->textColorG = R_TEXT_ADJUST_COLOR_1_G; @@ -412,7 +431,7 @@ void Message_SetTextColor(MessageContext* msgCtx, u16 colorParameter) { msgCtx->textColorB = R_TEXT_ADJUST_COLOR_2_B; } break; - case MSGCOL_BLUE: + case TEXT_COLOR_BLUE: if (msgCtx->textBoxType == TEXTBOX_TYPE_WOODEN) { msgCtx->textColorR = 80; msgCtx->textColorG = 110; @@ -423,7 +442,7 @@ void Message_SetTextColor(MessageContext* msgCtx, u16 colorParameter) { msgCtx->textColorB = 255; } break; - case MSGCOL_LIGHTBLUE: + case TEXT_COLOR_LIGHTBLUE: if (msgCtx->textBoxType == TEXTBOX_TYPE_WOODEN) { msgCtx->textColorR = 90; msgCtx->textColorG = 180; @@ -438,7 +457,7 @@ void Message_SetTextColor(MessageContext* msgCtx, u16 colorParameter) { msgCtx->textColorB = 255; } break; - case MSGCOL_PURPLE: + case TEXT_COLOR_PURPLE: if (msgCtx->textBoxType == TEXTBOX_TYPE_WOODEN) { msgCtx->textColorR = 210; msgCtx->textColorG = 100; @@ -449,7 +468,7 @@ void Message_SetTextColor(MessageContext* msgCtx, u16 colorParameter) { msgCtx->textColorB = 180; } break; - case MSGCOL_YELLOW: + case TEXT_COLOR_YELLOW: if (msgCtx->textBoxType == TEXTBOX_TYPE_WOODEN) { msgCtx->textColorR = 255; msgCtx->textColorG = 255; @@ -460,10 +479,10 @@ void Message_SetTextColor(MessageContext* msgCtx, u16 colorParameter) { msgCtx->textColorB = 50; } break; - case MSGCOL_BLACK: + case TEXT_COLOR_BLACK: msgCtx->textColorR = msgCtx->textColorG = msgCtx->textColorB = 0; break; - case MSGCOL_DEFAULT: + case TEXT_COLOR_DEFAULT: default: if (msgCtx->textBoxType == TEXTBOX_TYPE_NONE_NO_SHADOW) { msgCtx->textColorR = msgCtx->textColorG = msgCtx->textColorB = 0; diff --git a/tools/msgdis.py b/tools/msgdis.py index 87e5c1f90f..aaa26c9cf8 100755 --- a/tools/msgdis.py +++ b/tools/msgdis.py @@ -1,459 +1,853 @@ #!/usr/bin/env python3 # -# message_data_static disassembler/decompiler +# message_data_static disassembler/decompiler # -import re, struct -from os import path -import argparse +import argparse, re, struct +from typing import Callable, Dict, List, Optional, Tuple import version_config -# =================================================== -# Util -# =================================================== - -def as_hword_list(b): - if len(b) % 2 != 0: - return None - return [h[0] for h in struct.iter_unpack(">H", b)] - -def as_word_list(b): - if len(b) % 4 != 0: - return None - return [i[0] for i in struct.iter_unpack(">I", b)] - -def read_message_table(b, offset): - table = [] - while True: - e = struct.unpack(">HBxI", b[offset:offset+8]) - entry = (e[0], e[1]>>0x4&0xF, e[1]&0xF, e[2]) - table.append(entry) - offset += 8 - if entry[0] == 0xFFFF: - break - return table - -def segmented_to_physical(address): - return address & ~0x07000000 - -# =================================================== -# Decode message_data_static encoded strings -# =================================================== - -""" -Special characters conversion -""" -extraction_charmap = { - 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: 'ê', - 0x98: 'ë', - 0x99: 'ï', - 0x9A: 'ô', - 0x9B: 'ö', - 0x9C: 'ù', - 0x9D: 'û', - 0x9E: 'ü', - 0x9F: '[A]', - 0xA0: '[B]', - 0xA1: '[C]', - 0xA2: '[L]', - 0xA3: '[R]', - 0xA4: '[Z]', - 0xA5: '[C-Up]', - 0xA6: '[C-Down]', - 0xA7: '[C-Left]', - 0xA8: '[C-Right]', - 0xA9: '▼', - 0xAA: '[Control-Pad]', - 0xAB: '[D-Pad]', +item_ids = { + 0x00 : "ITEM_DEKU_STICK", + 0x01 : "ITEM_DEKU_NUT", + 0x02 : "ITEM_BOMB", + 0x03 : "ITEM_BOW", + 0x04 : "ITEM_ARROW_FIRE", + 0x05 : "ITEM_DINS_FIRE", + 0x06 : "ITEM_SLINGSHOT", + 0x07 : "ITEM_OCARINA_FAIRY", + 0x08 : "ITEM_OCARINA_OF_TIME", + 0x09 : "ITEM_BOMBCHU", + 0x0A : "ITEM_HOOKSHOT", + 0x0B : "ITEM_LONGSHOT", + 0x0C : "ITEM_ARROW_ICE", + 0x0D : "ITEM_FARORES_WIND", + 0x0E : "ITEM_BOOMERANG", + 0x0F : "ITEM_LENS_OF_TRUTH", + 0x10 : "ITEM_MAGIC_BEAN", + 0x11 : "ITEM_HAMMER", + 0x12 : "ITEM_ARROW_LIGHT", + 0x13 : "ITEM_NAYRUS_LOVE", + 0x14 : "ITEM_BOTTLE_EMPTY", + 0x15 : "ITEM_BOTTLE_POTION_RED", + 0x16 : "ITEM_BOTTLE_POTION_GREEN", + 0x17 : "ITEM_BOTTLE_POTION_BLUE", + 0x18 : "ITEM_BOTTLE_FAIRY", + 0x19 : "ITEM_BOTTLE_FISH", + 0x1A : "ITEM_BOTTLE_MILK_FULL", + 0x1B : "ITEM_BOTTLE_RUTOS_LETTER", + 0x1C : "ITEM_BOTTLE_BLUE_FIRE", + 0x1D : "ITEM_BOTTLE_BUG", + 0x1E : "ITEM_BOTTLE_BIG_POE", + 0x1F : "ITEM_BOTTLE_MILK_HALF", + 0x20 : "ITEM_BOTTLE_POE", + 0x21 : "ITEM_WEIRD_EGG", + 0x22 : "ITEM_CHICKEN", + 0x23 : "ITEM_ZELDAS_LETTER", + 0x24 : "ITEM_MASK_KEATON", + 0x25 : "ITEM_MASK_SKULL", + 0x26 : "ITEM_MASK_SPOOKY", + 0x27 : "ITEM_MASK_BUNNY_HOOD", + 0x28 : "ITEM_MASK_GORON", + 0x29 : "ITEM_MASK_ZORA", + 0x2A : "ITEM_MASK_GERUDO", + 0x2B : "ITEM_MASK_TRUTH", + 0x2C : "ITEM_SOLD_OUT", + 0x2D : "ITEM_POCKET_EGG", + 0x2E : "ITEM_POCKET_CUCCO", + 0x2F : "ITEM_COJIRO", + 0x30 : "ITEM_ODD_MUSHROOM", + 0x31 : "ITEM_ODD_POTION", + 0x32 : "ITEM_POACHERS_SAW", + 0x33 : "ITEM_BROKEN_GORONS_SWORD", + 0x34 : "ITEM_PRESCRIPTION", + 0x35 : "ITEM_EYEBALL_FROG", + 0x36 : "ITEM_EYE_DROPS", + 0x37 : "ITEM_CLAIM_CHECK", + 0x38 : "ITEM_BOW_FIRE", + 0x39 : "ITEM_BOW_ICE", + 0x3A : "ITEM_BOW_LIGHT", + 0x3B : "ITEM_SWORD_KOKIRI", + 0x3C : "ITEM_SWORD_MASTER", + 0x3D : "ITEM_SWORD_BIGGORON", + 0x3E : "ITEM_SHIELD_DEKU", + 0x3F : "ITEM_SHIELD_HYLIAN", + 0x40 : "ITEM_SHIELD_MIRROR", + 0x41 : "ITEM_TUNIC_KOKIRI", + 0x42 : "ITEM_TUNIC_GORON", + 0x43 : "ITEM_TUNIC_ZORA", + 0x44 : "ITEM_BOOTS_KOKIRI", + 0x45 : "ITEM_BOOTS_IRON", + 0x46 : "ITEM_BOOTS_HOVER", + 0x47 : "ITEM_BULLET_BAG_30", + 0x48 : "ITEM_BULLET_BAG_40", + 0x49 : "ITEM_BULLET_BAG_50", + 0x4A : "ITEM_QUIVER_30", + 0x4B : "ITEM_QUIVER_40", + 0x4C : "ITEM_QUIVER_50", + 0x4D : "ITEM_BOMB_BAG_20", + 0x4E : "ITEM_BOMB_BAG_30", + 0x4F : "ITEM_BOMB_BAG_40", + 0x50 : "ITEM_STRENGTH_GORONS_BRACELET", + 0x51 : "ITEM_STRENGTH_SILVER_GAUNTLETS", + 0x52 : "ITEM_STRENGTH_GOLD_GAUNTLETS", + 0x53 : "ITEM_SCALE_SILVER", + 0x54 : "ITEM_SCALE_GOLDEN", + 0x55 : "ITEM_GIANTS_KNIFE", + 0x56 : "ITEM_ADULTS_WALLET", + 0x57 : "ITEM_GIANTS_WALLET", + 0x58 : "ITEM_DEKU_SEEDS", + 0x59 : "ITEM_FISHING_POLE", + 0x5A : "ITEM_SONG_MINUET", + 0x5B : "ITEM_SONG_BOLERO", + 0x5C : "ITEM_SONG_SERENADE", + 0x5D : "ITEM_SONG_REQUIEM", + 0x5E : "ITEM_SONG_NOCTURNE", + 0x5F : "ITEM_SONG_PRELUDE", + 0x60 : "ITEM_SONG_LULLABY", + 0x61 : "ITEM_SONG_EPONA", + 0x62 : "ITEM_SONG_SARIA", + 0x63 : "ITEM_SONG_SUN", + 0x64 : "ITEM_SONG_TIME", + 0x65 : "ITEM_SONG_STORMS", + 0x66 : "ITEM_MEDALLION_FOREST", + 0x67 : "ITEM_MEDALLION_FIRE", + 0x68 : "ITEM_MEDALLION_WATER", + 0x69 : "ITEM_MEDALLION_SPIRIT", + 0x6A : "ITEM_MEDALLION_SHADOW", + 0x6B : "ITEM_MEDALLION_LIGHT", + 0x6C : "ITEM_KOKIRI_EMERALD", + 0x6D : "ITEM_GORON_RUBY", + 0x6E : "ITEM_ZORA_SAPPHIRE", + 0x6F : "ITEM_STONE_OF_AGONY", + 0x70 : "ITEM_GERUDOS_CARD", + 0x71 : "ITEM_SKULL_TOKEN", + 0x72 : "ITEM_HEART_CONTAINER", + 0x73 : "ITEM_HEART_PIECE", + 0x74 : "ITEM_DUNGEON_BOSS_KEY", + 0x75 : "ITEM_DUNGEON_COMPASS", + 0x76 : "ITEM_DUNGEON_MAP", + 0x77 : "ITEM_SMALL_KEY", + 0x78 : "ITEM_MAGIC_JAR_SMALL", + 0x79 : "ITEM_MAGIC_JAR_BIG", + 0x7A : "ITEM_HEART_PIECE_2", + 0x7B : "ITEM_INVALID_1", + 0x7C : "ITEM_INVALID_2", + 0x7D : "ITEM_INVALID_3", + 0x7E : "ITEM_INVALID_4", + 0x7F : "ITEM_INVALID_5", + 0x80 : "ITEM_INVALID_6", + 0x81 : "ITEM_INVALID_7", + 0x82 : "ITEM_MILK", + 0x83 : "ITEM_RECOVERY_HEART", + 0x84 : "ITEM_RUPEE_GREEN", + 0x85 : "ITEM_RUPEE_BLUE", + 0x86 : "ITEM_RUPEE_RED", + 0x87 : "ITEM_RUPEE_PURPLE", + 0x88 : "ITEM_RUPEE_GOLD", + 0x89 : "ITEM_INVALID_8", + 0x8A : "ITEM_DEKU_STICKS_5", + 0x8B : "ITEM_DEKU_STICKS_10", + 0x8C : "ITEM_DEKU_NUTS_5", + 0x8D : "ITEM_DEKU_NUTS_10", + 0x8E : "ITEM_BOMBS_5", + 0x8F : "ITEM_BOMBS_10", + 0x90 : "ITEM_BOMBS_20", + 0x91 : "ITEM_BOMBS_30", + 0x92 : "ITEM_ARROWS_5", + 0x93 : "ITEM_ARROWS_10", + 0x94 : "ITEM_ARROWS_30", + 0x95 : "ITEM_DEKU_SEEDS_30", + 0x96 : "ITEM_BOMBCHUS_5", + 0x97 : "ITEM_BOMBCHUS_20", + 0x98 : "ITEM_DEKU_STICK_UPGRADE_20", + 0x99 : "ITEM_DEKU_STICK_UPGRADE_30", + 0x9A : "ITEM_DEKU_NUT_UPGRADE_30", + 0x9B : "ITEM_DEKU_NUT_UPGRADE_40", + 0xFC : "ITEM_SWORD_CS", + 0xFE : "ITEM_NONE_FE", + 0xFF : "ITEM_NONE", } -control_codes = { - '\x01': "NEWLINE", - '\x02': "END", - '\x04': "BOX_BREAK", - '\x05': "COLOR", - '\x06': "SHIFT", - '\x07': "TEXTID", - '\x08': "QUICKTEXT_ENABLE", - '\x09': "QUICKTEXT_DISABLE", - '\x0A': "PERSISTENT", - '\x0B': "EVENT", - '\x0C': "BOX_BREAK_DELAYED", - '\x0D': "AWAIT_BUTTON_PRESS", - '\x0E': "FADE", - '\x0F': "NAME", - '\x10': "OCARINA", - '\x11': "FADE2", - '\x12': "SFX", - '\x13': "ITEM_ICON", - '\x14': "TEXT_SPEED", - '\x15': "BACKGROUND", - '\x16': "MARATHON_TIME", - '\x17': "RACE_TIME", - '\x18': "POINTS", - '\x19': "TOKENS", - '\x1A': "UNSKIPPABLE", - '\x1B': "TWO_CHOICE", - '\x1C': "THREE_CHOICE", - '\x1D': "FISH_INFO", - '\x1E': "HIGHSCORE", - '\x1F': "TIME", -} - -colors = { - 0x40 : "DEFAULT", - 0x41 : "RED", - 0x42 : "ADJUSTABLE", - 0x43 : "BLUE", - 0x44 : "LIGHTBLUE", - 0x45 : "PURPLE", - 0x46 : "YELLOW", - 0x47 : "BLACK", -} - -highscores = { - 0x00 : "HS_HORSE_ARCHERY", - 0x01 : "HS_POE_POINTS", - 0x02 : "HS_LARGEST_FISH", - 0x03 : "HS_HORSE_RACE", - 0x04 : "HS_MARATHON", - 0x06 : "HS_DAMPE_RACE", -} - -def format_char(byte): - return f"\\x{byte:02X}" - -def decode(read_bytes, box_type): - next_is_color = False - next_is_highscore = False - - next_is_byte_mod = False - next_is_box_break_delayed = False - next_is_hword_mod = 0 - next_is_background = 0 - - buf = [] - for byte in read_bytes: - if next_is_byte_mod: - #buf.append(format_hex(byte,1) + ") \"") - value = "\"" + format_char(byte) + "\"" - if next_is_highscore: - value = highscores[byte] - next_is_highscore = False - elif next_is_color: - """ - if box_type == 5: - value = color_type_5[byte] - elif box_type == 1: - value = color_type_1[byte] - else: - value = color_type_default[byte] - """ - value = colors[byte] - next_is_color = False - buf.append(value + ") \"") # + ("\n" if next_is_box_break_delayed else "") - next_is_byte_mod = False - next_is_box_break_delayed = False - elif next_is_hword_mod == 1: - #buf.append(format_hex(byte,1)) - buf.append("\"" + format_char(byte)) - next_is_hword_mod = 2 - elif next_is_hword_mod == 2: - #buf.append(format_hex(byte,1).replace("0x","") + ") \"") - buf.append(format_char(byte) + "\") \"") - next_is_hword_mod = 0 - elif next_is_background == 1: - #buf.append(format_hex(byte,1) + ", ") - buf.append("\"" + format_char(byte) + "\", ") - next_is_background = 2 - elif next_is_background == 2: - #buf.append(format_hex(byte,1) + ", ") - buf.append("\"" + format_char(byte) + "\", ") - next_is_background = 3 - elif next_is_background == 3: - #buf.append(format_hex(byte,1) + ") \"") - buf.append("\"" + format_char(byte) + "\") \"") - next_is_background = 0 +# From https://stackoverflow.com/questions/241327/remove-c-and-c-comments-using-python +def remove_comments(text : str) -> str: + def replacer(match : re.Match) -> str: + string : str = match.group(0) + if string.startswith("/"): + return " " # note: a space and not an empty string else: - for ctrl in list(control_codes.keys()): - if byte == ord(ctrl): - name = control_codes[ctrl] - # single bytes - if (name == "COLOR" or name == "SHIFT" or name == "BOX_BREAK_DELAYED" or - name == "FADE" or name == "ITEM_ICON" or name == "TEXT_SPEED" or - name == "HIGHSCORE"): - buf.append("\" " + name + "(") - if name == "HIGHSCORE": - next_is_highscore = True - elif name == "COLOR": - next_is_color = True - elif name == "BOX_BREAK_DELAYED": - next_is_box_break_delayed = True - else: - next_is_box_break_delayed = False - next_is_byte_mod = True - # single halfwords - elif (name == "TEXTID" or name == "FADE2" or name == "SFX"): - buf.append("\" " + name + "(") - next_is_hword_mod = 1 - # multiple bytes - elif (name == "BACKGROUND"): - buf.append("\" " + name + "(") - next_is_background = 1 - elif (name == "BOX_BREAK"): - buf.append("\"" + name + "\"") - else: - if byte == 0x01: # real newlines - buf.append("\n") - elif byte == 0x02: # omit END ctrl code - buf.append("") - else: - buf.append("\" " + name + " \"") + return string + + pattern = re.compile( + r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE + ) + return re.sub(pattern, replacer, text) + +def read4(data : bytes, p : int) -> int: + return struct.unpack(">I", data[p:p+4])[0] + +def read_baserom_segment(version : str, name : str) -> bytes: + data = None + with open(f"extracted/{version}/baserom/{name}", "rb") as infile: + data = infile.read() + return data + +def write_output_file(version : str, name : str, contents : str): + with open(f"extracted/{version}/text/{name}", "w") as outfile: + outfile.write(contents) + +def read_sfx_ids(): + sfx_tables = ( + (0x0000, "playerbank_table.h"), + (0x1000, "itembank_table.h"), + (0x2000, "environmentbank_table.h"), + (0x3000, "enemybank_table.h"), + (0x4000, "systembank_table.h"), + (0x5000, "ocarinabank_table.h"), + (0x6000, "voicebank_table.h"), + ) + + sfx_ids = {} + + for base,header_name in sfx_tables: + contents = None + with open("include/tables/sfx/" + header_name, "r") as infile: + contents = infile.read() + contents = remove_comments(contents).replace("\\\n", "").strip() + value = base + for line in contents.split("\n"): + line = line.strip() + assert line.startswith("DEFINE_SFX(") and line.endswith(")") + line = line[len("DEFINE_SFX("):-len(")")] + args = [a.strip() for a in line.split(",")] + assert len(args) == 5 + sfx_ids[value] = args[0] + value += 1 + + return sfx_ids + +class MessageDecoder: + def __init__(self, sfx_ids : Dict[int,str], control_end : int, control_codes : Dict[int, Tuple[str, str, Optional[Tuple[Callable[[int], str]]]]], extraction_charmap : Dict[int, str]) -> None: + self.sfx_ids : Dict[int,str] = sfx_ids + self.control_end : int = control_end + self.control_codes : Dict[int, Tuple[str, str, Optional[Tuple[Callable[[int], str]]]]] = control_codes + self.extraction_charmap : Dict[int, str] = extraction_charmap + self.msg : Optional[bytes] = None + + def pop_char(self) -> int: + # Implement in subclass + raise NotImplementedError() + + def pop_char_end(self) -> int: + # Implement in subclass + raise NotImplementedError() + + def decode_char(self, c : int) -> str: + # Implement in subclass + raise NotImplementedError() + + def pop_byte(self) -> int: + c, self.msg = self.msg[0], self.msg[1:] + return c + + def pop_byte_end(self) -> int: + c, self.msg = self.msg[-1], self.msg[:-1] + return c + + def pop_2byte(self) -> int: + u = self.pop_byte() + l = self.pop_byte() + return (u << 8) | l + + def pop_2byte_end(self) -> int: + l = self.pop_byte_end() + u = self.pop_byte_end() + return (u << 8) | l + + def format_sfx_id(self, c : int) -> str: + if c & 0x800: + return self.sfx_ids[c & ~0x800] + else: + return f"{self.sfx_ids[c]} - SFX_FLAG" + + def format_item_id(self, c : int) -> str: + return item_ids[c] + + def format_decimal(self, c : int) -> str: + return str(c) + + def format_text_id(self, c : int) -> str: + return f"0x{c:04X}" + + def format_bg_arg(self, c : int) -> str: + return { + 0 : "X_LEFT", + 1 : "X_RIGHT", + }[c] + + def format_bg_bits1(self, c : int) -> str: + c1 = (c >> 4) & 0xF + c2 = (c >> 0) & 0xF + + fgcol = { + 0 : "WHITE", + 1 : "DARK_RED", + 2 : "ORANGE", + 3 : "WHITE_3", + 4 : "WHITE_4", + 5 : "WHITE_5", + 6 : "WHITE_6", + 7 : "WHITE_7", + }[c1] + bgcol = { + 0 : "BLACK", + 1 : "GOLD", + 2 : "BLACK_2", + 3 : "BLACK_3", + }[c2] + + return f"{fgcol}, {bgcol}" + + def format_bg_bits2(self, c : int) -> str: + c1 = (c >> 4) & 0xF + c2 = (c >> 0) & 0xF + + y_offset = { + 0 : "1", + 1 : "2", + }[c1] + + return f"{y_offset}, {c2}" + + def emit_tokens(self, tokens : List[Tuple[str, str]]) -> str: + if len(tokens) == 0: + return "\"\"" + + out = "" + + q_state = False + s_state = False + nindentlines = 0 + + def maybe_enter_q(): + nonlocal out, q_state + if not q_state: + # If we're not in quotes, open quotes + out += "\"" + q_state = True + + def maybe_exit_q(space=False): + nonlocal out, q_state + if q_state: + # If we're in quotes, close quotes + out += "\"" + if space: + # If the caller asked for a trailing space following closing quotes, also add it + out += " " + q_state = False + + for tok_type,tok_dat in tokens: + if tok_type in ("BOX_BREAK", "BOX_BREAK_DELAYED"): + # Box break has special handling since it is preceded by a newline and followed by two newlines + + # Close quotes since we're about to newline + maybe_exit_q() + nindentlines = 0 + s_state = False + + # Emit box break control character surrounded by real newlines + out += "\n" + tok_dat + "\n\n" + continue + + if s_state: + # Add a leading space before this token + out += " " + s_state = False + + if tok_type == "NEWLINE": + # Coming up on a newline + maybe_enter_q() + # Add the escaped newline character and a real newline + out += "\\n\"\n" + # Always closes quotes since we're at EOL + q_state = False + # Indent the line if requested + if nindentlines != 0: + out += " " + nindentlines -= 1 + elif tok_type == "TEXT": + # Coming up on text + maybe_enter_q() + # Emit text + out += tok_dat + else: + # Control characters + maybe_exit_q(space=True) + + # Emit the control character + out += tok_dat + + if tok_type == "TWO_CHOICE": + # Start a new line and indent next two lines + nindentlines = 2-1 + out += "\n " + elif tok_type == "THREE_CHOICE": + # Start a new line and indent next three lines + nindentlines = 3-1 + out += "\n " + else: + # No particular special handling + # Signal to next token to add a trailing space + s_state = True + + # If the message ended with quotes open, close them + maybe_exit_q() + + if out[-1] == "\n": + out = out[:-1] + + return out + + def decode_ctrl(self, name : str, argfmt : str, formatters : Tuple[Callable[[int], str]]) -> str: + if argfmt == "": + # No args to handle, just return the control char name + return name + + # Read and format args + args : List[int] = [] + for a in argfmt: + if a == "x": + assert self.pop_byte() == 0 + else: + args.append({ + "b" : self.pop_byte, + "h" : self.pop_2byte, + }[a]()) + return f"{name}({', '.join(formatters[i](a) for i,a in enumerate(args))})" + + def decode(self, msg : bytes) -> str: + if len(msg) == 0: + # Empty message without even an END? + return "None" + + # Strip trailing 0 bytes (assumed padding) + while msg[-1] == 0: + msg = msg[:-1] + + self.msg = msg + + # Must end in an END control code + assert self.pop_char_end() == self.control_end, msg + + tokens : List[Tuple[str, str]] = [] + token_run = "" + def flush_text(): + nonlocal tokens, token_run + if token_run != "": + tokens.append(("TEXT", token_run)) + token_run = "" + + # Consume the message, transforming it into tokens + while len(self.msg) != 0: + c = self.pop_char() + # print(f"{c:02X}", self.control_codes.get(c,None)) + + if c in self.control_codes: + # Hit a control character, flush current run of text + flush_text() + # Add a token for the control character + tokens.append((self.control_codes[c][0], self.decode_ctrl(*self.control_codes[c]))) + else: + # Not a control character, accumulate a run of text + if c in self.extraction_charmap: + token_run += self.extraction_charmap[c] + else: + token_run += self.decode_char(c) + + # Flush any remaining text + flush_text() + + self.msg = None + + # Convert tokens to final decoded text + return self.emit_tokens(tokens) + + def format_highscore(self, c : int) -> str: + return { + 0 : "HS_HBA", + 1 : "HS_POE_POINTS", + 2 : "HS_FISHING", + 3 : "HS_HORSE_RACE", + 4 : "HS_MARATHON", + 5 : "HS_UNK_05", + 6 : "HS_DAMPE_RACE", + }[c] + +class MessageDecoderJPN(MessageDecoder): + def __init__(self, sfx_ids : Dict[int, str]) -> None: + control_end = 0x8170 + control_codes = { + 0x000A : ("NEWLINE", "", None), + 0x8170 : ("END", "", None), + 0x81A5 : ("BOX_BREAK", "", None), + 0x000B : ("COLOR", "h", (self.format_color,)), + 0x86C7 : ("SHIFT", "xb", (self.format_decimal,)), + 0x81CB : ("TEXTID", "h", (self.format_text_id,)), + 0x8189 : ("QUICKTEXT_ENABLE", "", None), + 0x818A : ("QUICKTEXT_DISABLE", "", None), + 0x86C8 : ("PERSISTENT", "", None), + 0x819F : ("EVENT", "", None), + 0x81A3 : ("BOX_BREAK_DELAYED", "xb", (self.format_decimal,)), + 0x81A4 : ("AWAIT_BUTTON_PRESS", "", None), + 0x819E : ("FADE", "xb", (self.format_decimal,)), + 0x874F : ("NAME", "", None), + 0x81F0 : ("OCARINA", "", None), + 0x81F4 : ("FADE2", "h", (self.format_decimal,)), + 0x81F3 : ("SFX", "h", (self.format_sfx_id,)), + 0x819A : ("ITEM_ICON", "xb", (self.format_item_id,)), + 0x86C9 : ("TEXT_SPEED", "xb", (self.format_decimal,)), + 0x86B3 : ("BACKGROUND", "xbbb", (self.format_bg_arg, self.format_bg_bits1, self.format_bg_bits2)), + 0x8791 : ("MARATHON_TIME", "", None), + 0x8792 : ("RACE_TIME", "", None), + 0x879B : ("POINTS", "", None), + 0x86A3 : ("TOKENS", "", None), + 0x8199 : ("UNSKIPPABLE", "", None), + 0x81BC : ("TWO_CHOICE", "", None), + 0x81B8 : ("THREE_CHOICE", "", None), + 0x86A4 : ("FISH_INFO", "", None), + 0x869F : ("HIGHSCORE", "xb", (self.format_highscore,)), + 0x81A1 : ("TIME", "", None), + } + extraction_charmap = { + 0x839F : "[A]", + 0x83A0 : "[B]", + 0x83A1 : "[C]", + 0x83A2 : "[L]", + 0x83A3 : "[R]", + 0x83A4 : "[Z]", + 0x83A5 : "[C-Up]", + 0x83A6 : "[C-Down]", + 0x83A7 : "[C-Left]", + 0x83A8 : "[C-Right]", + 0x83A9 : "▼", + 0x83AA : "[Control-Pad]", + + # Possibly from a SHIFT-JIS extension, python doesn't have builtin support + 0x86D3 : "┯", + } + super().__init__(sfx_ids, control_end, control_codes, extraction_charmap) + self.pop_char = self.pop_2byte + self.pop_char_end = self.pop_2byte_end + + def decode_char(self, c): + assert c not in range(0x8440, 0x847F), "Hylian codepage unimplemented" + return bytes([c>>8, c &0xFF]).decode("SHIFT-JIS") + + def format_color(self, c): + c1 = c & 0xF + c2 = c & ~0xF + assert c2 == 0x0C00 + return { + 0 : "DEFAULT", + 1 : "RED", + 2 : "ADJUSTABLE", + 3 : "BLUE", + 4 : "LIGHTBLUE", + 5 : "PURPLE", + 6 : "YELLOW", + 7 : "BLACK", + }[c1] + +class MessageDecoderNES(MessageDecoder): + def __init__(self, sfx_ids : Dict[int, str]) -> None: + control_end = 0x02 + control_codes = { + 0x01 : ("NEWLINE", "", None), + 0x02 : ("END", "", None), + 0x04 : ("BOX_BREAK", "", None), + 0x05 : ("COLOR", "b", (self.format_color,)), + 0x06 : ("SHIFT", "b", (self.format_decimal,)), + 0x07 : ("TEXTID", "h", (self.format_text_id,)), + 0x08 : ("QUICKTEXT_ENABLE", "", None), + 0x09 : ("QUICKTEXT_DISABLE", "", None), + 0x0A : ("PERSISTENT", "", None), + 0x0B : ("EVENT", "", None), + 0x0C : ("BOX_BREAK_DELAYED", "b", (self.format_decimal,)), + 0x0D : ("AWAIT_BUTTON_PRESS", "", None), + 0x0E : ("FADE", "b", (self.format_decimal,)), + 0x0F : ("NAME", "", None), + 0x10 : ("OCARINA", "", None), + 0x11 : ("FADE2", "h", (self.format_decimal,)), + 0x12 : ("SFX", "h", (self.format_sfx_id,)), + 0x13 : ("ITEM_ICON", "b", (self.format_item_id,)), + 0x14 : ("TEXT_SPEED", "b", (self.format_decimal,)), + 0x15 : ("BACKGROUND", "bbb", (self.format_bg_arg, self.format_bg_bits1, self.format_bg_bits2)), + 0x16 : ("MARATHON_TIME", "", None), + 0x17 : ("RACE_TIME", "", None), + 0x18 : ("POINTS", "", None), + 0x19 : ("TOKENS", "", None), + 0x1A : ("UNSKIPPABLE", "", None), + 0x1B : ("TWO_CHOICE", "", None), + 0x1C : ("THREE_CHOICE", "", None), + 0x1D : ("FISH_INFO", "", None), + 0x1E : ("HIGHSCORE", "b", (self.format_highscore,)), + 0x1F : ("TIME", "", None), + } + extraction_charmap = { + 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 : 'ê', + 0x98 : 'ë', + 0x99 : 'ï', + 0x9A : 'ô', + 0x9B : 'ö', + 0x9C : 'ù', + 0x9D : 'û', + 0x9E : 'ü', + 0x9F : '[A]', + 0xA0 : '[B]', + 0xA1 : '[C]', + 0xA2 : '[L]', + 0xA3 : '[R]', + 0xA4 : '[Z]', + 0xA5 : '[C-Up]', + 0xA6 : '[C-Down]', + 0xA7 : '[C-Left]', + 0xA8 : '[C-Right]', + 0xA9 : '▼', + 0xAA : '[Control-Pad]', + 0xAB : '[D-Pad]', + } + super().__init__(sfx_ids, control_end, control_codes, extraction_charmap) + self.pop_char = self.pop_byte + self.pop_char_end = self.pop_byte_end + + def decode_char(self, c : int) -> str: + decoded = bytes([c]).decode("ASCII") + # Escape quotes within the text itself + if decoded == "\"": + decoded = "\\\"" + return decoded + + def format_color(self, c : int) -> str: + return { + 0x40 : "DEFAULT", + 0x41 : "RED", + 0x42 : "ADJUSTABLE", + 0x43 : "BLUE", + 0x44 : "LIGHTBLUE", + 0x45 : "PURPLE", + 0x46 : "YELLOW", + 0x47 : "BLACK", + }[c] + +class MessageTableDesc: + def __init__(self, table_name : str, seg_name : str, decoder : MessageDecoder, parent : Optional[int]) -> None: + self.table_name : str = table_name + self.seg_name : str = seg_name + self.decoder : MessageDecoder = decoder + self.parent : Optional[int] = parent + +class MessageTableEntry: + SIZE = 8 + + def __init__(self, text_id : int, box_type : int, box_pos : int, addr : int) -> None: + self.text_id, self.box_type, self.box_pos, self.addr = text_id, box_type, box_pos, addr + + @staticmethod + def from_bin(data : bytes) -> "MessageTableEntry": + text_id,info,addr = struct.unpack(">HBxI", data) + box_type = (info >> 4) & 0xF + box_pos = (info >> 0) & 0xF + return MessageTableEntry(text_id, box_type, box_pos, addr) + +class MessageEntry: + def __init__(self, message_tables : List[Optional[MessageTableDesc]], text_id : int, box_type : int, box_pos : int) -> None: + self.text_id : int = text_id + self.box_type : int = box_type + self.box_pos : int = box_pos + self.data : List[Tuple[Optional[MessageDecoder], Optional[bytes]]] = [(None,None) for _ in message_tables] + self.select = tuple(tbl is not None for tbl in message_tables) + + def box_type_str(self) -> str: + return { + 0: "TEXTBOX_TYPE_BLACK", + 1: "TEXTBOX_TYPE_WOODEN", + 2: "TEXTBOX_TYPE_BLUE", + 3: "TEXTBOX_TYPE_OCARINA", + 4: "TEXTBOX_TYPE_NONE_BOTTOM", + 5: "TEXTBOX_TYPE_NONE_NO_SHADOW", + 0xB: "TEXTBOX_TYPE_CREDITS", + }[self.box_type] + + def box_pos_str(self) -> str: + return { + 0: "TEXTBOX_POS_VARIABLE", + 1: "TEXTBOX_POS_TOP", + 2: "TEXTBOX_POS_MIDDLE", + 3: "TEXTBOX_POS_BOTTOM", + }[self.box_pos] + + def decode(self) -> str: + selection = tuple(not (select and data == (None,None)) for select,data in zip(self.select,self.data)) + assert any(sel for sel in selection) + + defn = "" + if all(sel for sel in selection): + # Valid for all languages + defn = "DEFINE_MESSAGE" + elif selection == (True,False,True,True): + # JPN only + defn = "DEFINE_MESSAGE_JPN" + elif selection == (False,True,True,True): + # NES only + defn = "DEFINE_MESSAGE_NES" + else: + # Other unimplemented cases + assert False + + out = f"{defn}(0x{self.text_id:04X}, {self.box_type_str()}, {self.box_pos_str()},\n" + out += "\n,\n".join(f"MSG(\n{decoder.decode(data)}\n)" if decoder is not None else "MSG(/* MISSING */)" for decoder,data in self.data) + out += "\n)\n" + return out + +def collect_messages(message_tables : List[Optional[MessageTableDesc]], version : str, + config : version_config.VersionConfig, code_vram : int, code_bin : bytes): + + messages : Dict[int,MessageEntry] = {} + + all_text_ids : List[Optional[List[int]]] = [None for _ in range(len(message_tables))] + + for lang_num,desc in enumerate(message_tables): + if desc is None: + continue + + baserom_seg = read_baserom_segment(version, desc.seg_name) + code_offset = config.variables[desc.table_name] - code_vram + + if desc.parent is None: + # Complete table + + table_entries : List[MessageTableEntry] = [] + text_ids : List[int] = [] + + while True: + end = code_offset + MessageTableEntry.SIZE + entry = MessageTableEntry.from_bin(code_bin[code_offset:end]) + code_offset = end + + table_entries.append(entry) + text_ids.append(entry.text_id) + + if entry.text_id == 0xFFFF: break - else: - if byte in extraction_charmap: - buf.append(extraction_charmap[byte]) - else: - decoded = bytes([byte]).decode("ASCII") - if decoded == "\"": - decoded = "\\\"" - buf.append(decoded) - return "".join(buf) + all_text_ids[lang_num] = text_ids -# =================================================== -# message entry tables -# =================================================== + for i in range(len(table_entries) - 1): + curr = table_entries[i + 0] + next = table_entries[i + 1] -textbox_type = { - 0: "TEXTBOX_TYPE_BLACK", - 1: "TEXTBOX_TYPE_WOODEN", - 2: "TEXTBOX_TYPE_BLUE", - 3: "TEXTBOX_TYPE_OCARINA", - 4: "TEXTBOX_TYPE_NONE_BOTTOM", - 5: "TEXTBOX_TYPE_NONE_NO_SHADOW", - 0xB: "TEXTBOX_TYPE_CREDITS", -} + curr_offset = curr.addr & ~0x0F000000 + next_offset = (next.addr & ~0x0F000000) if next.text_id != 0xFFFF else len(baserom_seg) + size = next_offset - curr_offset -textbox_ypos = { - 0: "TEXTBOX_POS_VARIABLE", - 1: "TEXTBOX_POS_TOP", - 2: "TEXTBOX_POS_MIDDLE", - 3: "TEXTBOX_POS_BOTTOM", -} - -# Global variables for baserom version and message entry table vrom addresses, -# set based on command line arguments in main() -version = None -nes_message_entry_table_addr = None -ger_message_entry_table_addr = None -fra_message_entry_table_addr = None -staff_message_entry_table_addr = None - -nes_message_entry_table = [] -ger_message_entry_table = [] -fra_message_entry_table = [] - -combined_message_entry_table = [] - -staff_message_entry_table = [] - -def read_tables(): - global nes_message_entry_table - global ger_message_entry_table - global fra_message_entry_table - - global combined_message_entry_table - global staff_message_entry_table - - baserom = None - with open(f"extracted/{version}/baserom/code","rb") as infile: - baserom = infile.read() - - nes_message_entry_table = read_message_table(baserom, nes_message_entry_table_addr) - - ids = [entry[0] for entry in nes_message_entry_table if entry[0] != 0xFFFC] - ger_message_entry_table = list(zip(ids,as_word_list(baserom[ger_message_entry_table_addr: fra_message_entry_table_addr]))) - fra_message_entry_table = list(zip(ids,as_word_list(baserom[fra_message_entry_table_addr:staff_message_entry_table_addr]))) - - for entry in nes_message_entry_table: - if entry[0] != 0xFFFC: - combined_message_entry_table.append((*entry, dict(ger_message_entry_table)[entry[0]], dict(fra_message_entry_table)[entry[0]])) + if curr.text_id not in messages: + messages[curr.text_id] = MessageEntry(message_tables, curr.text_id, curr.box_type, curr.box_pos) + messages[curr.text_id].data[lang_num] = (desc.decoder, baserom_seg[curr_offset : curr_offset+size]) else: - combined_message_entry_table.append((*entry, None, None)) + # Addresses only - staff_message_entry_table = read_message_table(baserom, staff_message_entry_table_addr) + for text_id in all_text_ids[desc.parent][:-1]: # Exclude text id 0xFFFF + if text_id in (0xFFFC,): + continue -# =================================================== -# Run -# =================================================== + curr = read4(code_bin, code_offset + 0) + next = read4(code_bin, code_offset + 4) + code_offset += 4 -### TODO this is all awful -def cvt(m): - return m.group(0).replace("\"\"","") + curr_offset = curr & ~0x0F000000 + next_offset = next & ~0x0F000000 if text_id != 0xFFFD else len(baserom_seg) + size = next_offset - curr_offset -doubles = re.compile(r"(? Dict[str,str]: with open(path) as infile: charmap = infile.read() - charmap = ast.literal_eval(charmap) - charmap = { repr(k)[1:-1] : chr(v) for k,v in charmap.items() } - return charmap + out_charmap = {} + for k,v in charmap.items(): + v = v[wchar] + if v is None: + v = 0 + assert isinstance(k, str) + assert v in (range(0xFFFF + 1) if wchar else range(0xFF + 1)) + + k = repr(k)[1:-1] + + if wchar: + u = (v >> 8) & 0xFF + l = (v >> 0) & 0xFF + out_charmap[k] = f"0x{u:02X}, 0x{l:02X}," + else: + out_charmap[k] = f"0x{v:02X}," + + return out_charmap # From https://stackoverflow.com/questions/241327/remove-c-and-c-comments-using-python -def remove_comments(text): - def replacer(match): - s = match.group(0) - if s.startswith('/'): - return " " # note: a space and not an empty string +def remove_comments(text : str) -> str: + def replacer(match : re.Match) -> str: + string : str = match.group(0) + if string.startswith("/"): + return " " # note: a space and not an empty string else: - return s + return string pattern = re.compile( - r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', - re.DOTALL | re.MULTILINE + r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', re.DOTALL | re.MULTILINE ) return re.sub(pattern, replacer, text) -def convert_text(text, charmap): - def cvt_str(m): - string = m.group(0) +def convert_text(text : str, encoding : str, charmap : Dict[str, str]) -> str: + def cvt_str(match : re.Match) -> str: + string : str = match.group(0) - for orig,char in charmap.items(): - string = string.replace(orig, char) + # strip quotes + string = string[1:-1] - return string + def cvt_escape(s : str): + # Convert escape sequences such as "\\\"" to "\"" + return s.encode("ascii").decode("unicode-escape") + + run_start = 0 + + def emit(text : Optional[str], advance : int): + nonlocal out, string, i, run_start + # flush text + to_flush = string[run_start:i] + if len(string[run_start:i]) != 0: + out += ",".join(f"0x{b:02X}" for b in to_flush.encode(encoding)) + out += "," + if text is None: + return + # emit + advance source pos + out += text + i += advance + # start new run + run_start = i + + out = "" + + i = 0 + while i != len(string): + # check charmap + for k in charmap.keys(): + if string.startswith(k, i): + # is in charmap, emit the mapped sequence + emit(charmap[k], len(k)) + break + else: + if string[i] == "\\" and string[i + 1] != "\\": + # is already escaped, emit the escape sequence verbatim + if string[i + 1] == "x": + # \x** + emit("0" + string[i + 1 : i + 4] + ",", 4) + else: + # \* + e = cvt_escape(string[i : i + 2]).encode(encoding) + assert len(e) == 1 + emit(f"0x{e[0]:02X},", 2) + else: + # increment pos, accumulating text that requires encoding + i += 1 + + # emit remaining accumulated text + emit(None, 0) + + return out # Naive string matcher, assumes single line strings and no comments, handles escaped quotations string_regex = re.compile(r'"((?:[^\\"\n]|\\.)*)"') @@ -50,16 +113,23 @@ def convert_text(text, charmap): return text def main(): - parser = argparse.ArgumentParser(description="Encode message_data_static text headers") + parser = argparse.ArgumentParser( + description="Encode message_data_static text headers" + ) parser.add_argument( "input", help="path to file to be encoded, or - for stdin", ) parser.add_argument( - "--output", - "-o", + "output", help="path to write encoded file, or - for stdout", + ) + parser.add_argument( + "--encoding", + help="encoding (jpn or nes)", required=True, + type=str, + choices=("jpn", "nes"), ) parser.add_argument( "--charmap", @@ -68,7 +138,12 @@ def main(): ) args = parser.parse_args() - charmap = read_charmap(args.charmap) + wchar,encoding = { + "jpn" : (True, "SHIFT-JIS"), + "nes" : (False, "raw-unicode-escape"), + }[args.encoding] + + charmap = read_charmap(args.charmap, wchar) text = "" if args.input == "-": @@ -78,12 +153,12 @@ def main(): text = infile.read() text = remove_comments(text) - text = convert_text(text, charmap) + text = convert_text(text, encoding, charmap) if args.output == "-": - sys.stdout.buffer.write(text.encode("raw_unicode_escape")) + sys.stdout.buffer.write(text.encode("utf-8")) else: - with open(args.output, "w", encoding="raw_unicode_escape") as outfile: + with open(args.output, "w") as outfile: outfile.write(text) if __name__ == "__main__": diff --git a/tools/version_config.py b/tools/version_config.py index eab12b2add..6ca203fd59 100644 --- a/tools/version_config.py +++ b/tools/version_config.py @@ -22,6 +22,8 @@ class VersionConfig: version: str # ROM offset to start of DMA table dmadata_start: int + # Whether the languages are PAL (EN/DE/FR) or not (JP/EN) + text_lang_pal: bool # DMA segment information, in ROM order dmadata_segments: OrderedDict[str, SegmentInfo] # Addresses of important variables needed for asset extraction @@ -70,6 +72,7 @@ def load_version_config(version: str) -> VersionConfig: return VersionConfig( version=version, dmadata_start=config["dmadata_start"], + text_lang_pal=config["text_lang_pal"], dmadata_segments=load_dmadata_segments(version), variables=config["variables"], assets=assets,