From d6dbbc3ed2d1d111843cc2bd28aacbb11989ee53 Mon Sep 17 00:00:00 2001 From: Nessy Date: Thu, 10 Aug 2023 21:19:13 +0100 Subject: [PATCH 1/2] Create helpers for making custom messages --- code/include/rnd/custom_message.h | 19 ++++ code/source/game/message.cpp | 5 +- code/source/main.cpp | 2 + code/source/rnd/custom_messages.cpp | 136 ++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 code/include/rnd/custom_message.h create mode 100644 code/source/rnd/custom_messages.cpp diff --git a/code/include/rnd/custom_message.h b/code/include/rnd/custom_message.h new file mode 100644 index 00000000..df39edc9 --- /dev/null +++ b/code/include/rnd/custom_message.h @@ -0,0 +1,19 @@ +#ifndef _RND_CUSTOM_MESSAGE_H_ +#define _RND_CUSTOM_MESSAGE_H_ + +#include "common/utils.h" + +#define MAX_MSG_SIZE 512 + +void CustomMessage_Init(void); + +typedef struct { + char data[MAX_MSG_SIZE]; + u16 size; +} CustomMessage; + +/*----- Custom Message Declarations -----*/ +extern CustomMessage iceTrapMsg; +/*----- Custom Message Declarations -----*/ + +#endif diff --git a/code/source/game/message.cpp b/code/source/game/message.cpp index 54fd94b6..7e8d836f 100644 --- a/code/source/game/message.cpp +++ b/code/source/game/message.cpp @@ -7,6 +7,7 @@ * Brought in from the Project Restoration libraries. Modified for custom messages. */ #include "game/message.h" +#include "rnd/custom_message.h" #include "common/utils.h" @@ -67,8 +68,8 @@ namespace game { customIceMessage.flags = 0xFF0000; // customIceMessage.texts[0].offset = "Hmmph... I've been made a // \x7f:\x00\x01\x00\x46OOL\x7f\x00:\x00\x0b\x00 of!\x7f\x00\x31"; - customIceMessage.texts[0].offset = " \x7f:\x00\x01\x00\x46OOL!\x7f:\x00\x0b\x00\x7f\x00\x31"; - customIceMessage.texts[0].length = 22; + customIceMessage.texts[0].offset = iceTrapMsg.data; + customIceMessage.texts[0].length = iceTrapMsg.size; entry = &customIceMessage; isCustom = true; } else if (id == 0x0037) { diff --git a/code/source/main.cpp b/code/source/main.cpp index 89ba5b5b..8d8dc8a1 100644 --- a/code/source/main.cpp +++ b/code/source/main.cpp @@ -6,6 +6,7 @@ #include "game/sound.h" #include "game/states/state.h" #include "game/ui.h" +#include "rnd/custom_message.h" #include "rnd/extdata.h" #include "rnd/icetrap.h" #include "rnd/item_override.h" @@ -28,6 +29,7 @@ namespace rnd { rHeap_Init(); ItemOverride_Init(); + CustomMessage_Init(); // SaveFile_LoadExtSaveData(1); // TODO: Maybe make this an option? link::FixSpeedIssues(); diff --git a/code/source/rnd/custom_messages.cpp b/code/source/rnd/custom_messages.cpp new file mode 100644 index 00000000..b35cf21a --- /dev/null +++ b/code/source/rnd/custom_messages.cpp @@ -0,0 +1,136 @@ +#include "rnd/custom_message.h" + +/*----- Custom Message Declarations -----*/ +CustomMessage iceTrapMsg; +/*----- Custom Message Declarations -----*/ + +typedef enum { + blank = 0x00, + aButton, + bButton, + xButton, + yButton, + lButton, + rButton, + startButton, + homeButton, + powerButton, + dPad, + dPadUp, + dPadRight, + dPadDown, + dPadLeft, + dPadVertical, + dPadHorizontal, + cPad, + cPadUp, + cPadRight, + cPadDown, + cPadLeft, + cPadVertical, + cPadHorizontal, + touchI, + touchX, + touchY, + touchII, + touchItems, + + touchMasks = 0x1E, + touchOcarina, + touchPictobox, + targetReticle, + + sun = 0x28, + moon, + oneBlock, + twoBlock, + threeBlock, + touchGear, + touchMap, + flag, + tatl, + tilde, + notebook, + firstPerson, + zlButton, + zrButton, + majora, +} iconList; + +typedef enum { + WHITE, + RED, + GREEN, + BLUE, + YELLOW, + CYAN, + MAGENTA, + GREY, + ORANGE, + DARK_GREY, + BLACK, + DEFAULT, +} colList; + +class MsgBuilder { + public: + char* data; + u16* size; + + MsgBuilder* set(CustomMessage* msg) { + data = &msg->data[0]; + size = &msg->size; + return this; + } + + MsgBuilder* addChr(char chr, bool txt = false) { + if (*size < MAX_MSG_SIZE) + data[(*size)++] = chr; + return this; + } + + MsgBuilder* text(const char* txt) { + for (u16 idx = 0; txt[idx]; idx++) + addChr(txt[idx], true); + return this; + } + + MsgBuilder* addCom(char com) { + addChr(0x7f); + if (*size % 2) + addChr(0x00); + addChr(com); + return addChr(0x00); + } + + MsgBuilder* addCom(char com, char arg) { + addCom(com); + addChr(arg); + return addChr(0x00); + } + + MsgBuilder* col(colList c) { return addCom(0x3a, c); } // Change text colour until used again or new box + MsgBuilder* icon(iconList img) { return addCom(0x25, img); } // Insert an icon. See: https://cdn.discordapp.com/attachments/896879784162918400/1138941632675328010/image.png + MsgBuilder* delay(char time) { return addCom(0x29, time); } // Time measured in 20ths of a second + MsgBuilder* top() { return addCom(0x28); } // Applies to all boxes in message + MsgBuilder* instant() { return addCom(0x27); } // Applies only to current box + MsgBuilder* newline() { return addCom(0x01); } + MsgBuilder* newBox() { addCom(0x31, 0x01); return addCom(0x02); } + MsgBuilder* end() { addCom(0x31, 0x00); return addCom(0x00); } +}; + +MsgBuilder builder; +bool init = false; + +void CustomMessage_Init(void) { + if (!init) { + // ----- Custom Message Definitions ----- + builder.set(&iceTrapMsg)->instant()->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->text("You")->delay(10)->newline() + ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->text("are")->delay(10)->newline() + ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->text(" a")->delay(20)->newline() + ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->col(RED)->text(" FOOL")->col(WHITE)->text("!")->newBox() + + ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->col(YELLOW)->text("<3")->end(); + } + init = true; +} From 15cf9c38fadf51a00ba2ed4f3d0b4cf8c673f48f Mon Sep 17 00:00:00 2001 From: Nessy Date: Wed, 16 Aug 2023 18:54:48 +0100 Subject: [PATCH 2/2] Dynamic custom message formatting --- code/include/rnd/custom_message.h | 19 -- code/include/rnd/custom_messages.h | 92 ++++++ code/source/game/message.cpp | 67 +--- code/source/main.cpp | 2 - code/source/rnd/custom_messages.cpp | 466 ++++++++++++++++++++++------ 5 files changed, 468 insertions(+), 178 deletions(-) delete mode 100644 code/include/rnd/custom_message.h create mode 100644 code/include/rnd/custom_messages.h diff --git a/code/include/rnd/custom_message.h b/code/include/rnd/custom_message.h deleted file mode 100644 index df39edc9..00000000 --- a/code/include/rnd/custom_message.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef _RND_CUSTOM_MESSAGE_H_ -#define _RND_CUSTOM_MESSAGE_H_ - -#include "common/utils.h" - -#define MAX_MSG_SIZE 512 - -void CustomMessage_Init(void); - -typedef struct { - char data[MAX_MSG_SIZE]; - u16 size; -} CustomMessage; - -/*----- Custom Message Declarations -----*/ -extern CustomMessage iceTrapMsg; -/*----- Custom Message Declarations -----*/ - -#endif diff --git a/code/include/rnd/custom_messages.h b/code/include/rnd/custom_messages.h new file mode 100644 index 00000000..ecf1b84f --- /dev/null +++ b/code/include/rnd/custom_messages.h @@ -0,0 +1,92 @@ +#ifndef _RND_CUSTOM_MESSAGES_H_ +#define _RND_CUSTOM_MESSAGES_H_ + +#include "common/utils.h" +#include "game/message.h" + +#define MAX_MSG_SIZE 512 +#define MAX_UNFORMATTED_SIZE 354 + +bool SetCustomMessage(u16, game::MessageResEntry*); + +typedef struct { + u16 id; + u16 field_2; + u32 field_4; + u32 flags; + char text[MAX_UNFORMATTED_SIZE]; + char cols[4]; + char icons[6]; + char delays[6]; + u16 sfxAndFlags; +} UnformattedMessage; + +typedef enum { + BLANK = 0x00, + A_BUTTON, + B_BUTTON, + X_BUTTON, + Y_BUTTON, + L_BUTTON, + R_BUTTON, + START_BUTTON, + HOME_BUTTON, + POWER_BUTTON, + D_PAD, + D_PAD_UP, + D_PAD_RIGHT, + D_PAD_DOWN, + D_PAD_LEFT, + P_PAD_VERTICAL, + P_PAD_HORIZONTAL, + C_PAD, + C_PAD_UP, + C_PAD_RIGHT, + C_PAD_DOWN, + C_PAD_LEFT, + C_PAD_VERTICAL, + C_PAD_HORIZONTAL, + I_TOUCH, + X_TOUCH, + Y_TOUCH, + II_TOUCH, + ITEMS_TOUCH, + + MASKS_TOUCH = 0x1E, + OCARINA_TOUCH, + PICTOBOX_TOUCH, + TARGET_RETICLE, + + SUN = 0x28, + MOON, + ONE_BLOCK, + TWO_BLOCK, + THREE_BLOCK, + GEAR_TOUCH, + MAP_TOUCH, + FLAG, + TATL, + TILDE, + NOTEBOOK, + FIRST_PERSON, + ZL_BUTTON, + ZR_BUTTON, + MAJORA, +} iconType; + +typedef enum { + WHITE, + RED, + GREEN, + BLUE, + YELLOW, + CYAN, + MAGENTA, + GREY, + ORANGE, + DARK_GREY, + BLACK, + DEFAULT, +} colType; + +#endif diff --git a/code/source/game/message.cpp b/code/source/game/message.cpp index 7e8d836f..3ced24a9 100644 --- a/code/source/game/message.cpp +++ b/code/source/game/message.cpp @@ -7,7 +7,7 @@ * Brought in from the Project Restoration libraries. Modified for custom messages. */ #include "game/message.h" -#include "rnd/custom_message.h" +#include "rnd/custom_messages.h" #include "common/utils.h" @@ -19,84 +19,27 @@ extern "C" { #endif namespace game { -#ifdef ENABLE_DEBUG - static bool declareTestMessage = false; - static MessageResEntry ptrCustomMessageEntries[1] = {0}; - volatile const u32 numCustomMessageEntries = 1; -#else - volatile const MessageResEntry* ptrCustomMessageEntries = {0}; - volatile const u32 numCustomMessageEntries = {0}; -#endif - MessageResEntry customIceMessage = {0}; - MessageResEntry customSwordMessage = {0}; + MessageResEntry customEntry = {0}; MessageMgr& MessageMgr::Instance() { return rnd::util::GetPointer(0x1C51D0)(); } bool MessageData::Get(u32 id, Message* msg) const { -#ifdef ENABLE_DEBUG - if (!declareTestMessage) { - ptrCustomMessageEntries[0].id = 0x6133; - ptrCustomMessageEntries[0].field_2 = 0xFFFF; - ptrCustomMessageEntries[0].field_4 = 0x3FFFFFFF; - ptrCustomMessageEntries[0].flags = 0xFF0000; - ptrCustomMessageEntries[0].texts[0].offset = - "Hmmph... I've been made a \x7f:\x00\x01\x00\x46OOL\x7f\x00:\x00\x0b\x00 " - "of!\x7f\x00\x31"; - ptrCustomMessageEntries[0].texts[0].length = 55; - declareTestMessage = true; - } -#endif int start = 0; int end = res_header->num_msgs - 1; - int customEnd = numCustomMessageEntries - 1; bool isCustom = false; const auto get_entry = [this](size_t idx) { return reinterpret_cast((const u8*)res_entries + res_entry_size * idx); }; - const auto get_custom_entry = [this](size_t idx) { - return reinterpret_cast((const u8*)ptrCustomMessageEntries + idx); - }; const MessageResEntry* entry = nullptr; - - if (id == 0x0012) { - customIceMessage.id = 0x0012; - customIceMessage.field_2 = 0xFFFF; - customIceMessage.field_4 = 0x3FFFFFFF; - customIceMessage.flags = 0xFF0000; - // customIceMessage.texts[0].offset = "Hmmph... I've been made a - // \x7f:\x00\x01\x00\x46OOL\x7f\x00:\x00\x0b\x00 of!\x7f\x00\x31"; - customIceMessage.texts[0].offset = iceTrapMsg.data; - customIceMessage.texts[0].length = iceTrapMsg.size; - entry = &customIceMessage; - isCustom = true; - } else if (id == 0x0037) { - customSwordMessage.id = 0x0037; - customSwordMessage.field_2 = 0xFFFF; - customSwordMessage.field_4 = 0x3FFFFFF; - customSwordMessage.flags = 0x4D0000; - customSwordMessage.texts[0].offset = "\x7f\x00'\x00You got the \x7f\x00:\x00\x01\x00Kokiri " - "sword!\x7f:\x00\x00\x00\x7f\x00\x31\x00"; - customSwordMessage.texts[0].length = 48; - entry = &customSwordMessage; + if (SetCustomMessage(id, &customEntry)) { + entry = &customEntry; isCustom = true; } - while (!entry && start <= customEnd) { - const int current_entry_idx = (start + customEnd) / 2; - const auto* candidate = get_custom_entry(current_entry_idx); - if (candidate->id < id) - start = current_entry_idx + 1; - else if (candidate->id > id) - customEnd = current_entry_idx - 1; - else { - entry = candidate; - isCustom = true; - } - } - if (!entry) { + if (!isCustom) { start = 0; while (!entry && start <= end) { const int current_entry_idx = (start + end) / 2; diff --git a/code/source/main.cpp b/code/source/main.cpp index 8d8dc8a1..89ba5b5b 100644 --- a/code/source/main.cpp +++ b/code/source/main.cpp @@ -6,7 +6,6 @@ #include "game/sound.h" #include "game/states/state.h" #include "game/ui.h" -#include "rnd/custom_message.h" #include "rnd/extdata.h" #include "rnd/icetrap.h" #include "rnd/item_override.h" @@ -29,7 +28,6 @@ namespace rnd { rHeap_Init(); ItemOverride_Init(); - CustomMessage_Init(); // SaveFile_LoadExtSaveData(1); // TODO: Maybe make this an option? link::FixSpeedIssues(); diff --git a/code/source/rnd/custom_messages.cpp b/code/source/rnd/custom_messages.cpp index b35cf21a..739e562a 100644 --- a/code/source/rnd/custom_messages.cpp +++ b/code/source/rnd/custom_messages.cpp @@ -1,76 +1,85 @@ -#include "rnd/custom_message.h" - -/*----- Custom Message Declarations -----*/ -CustomMessage iceTrapMsg; -/*----- Custom Message Declarations -----*/ - -typedef enum { - blank = 0x00, - aButton, - bButton, - xButton, - yButton, - lButton, - rButton, - startButton, - homeButton, - powerButton, - dPad, - dPadUp, - dPadRight, - dPadDown, - dPadLeft, - dPadVertical, - dPadHorizontal, - cPad, - cPadUp, - cPadRight, - cPadDown, - cPadLeft, - cPadVertical, - cPadHorizontal, - touchI, - touchX, - touchY, - touchII, - touchItems, - - touchMasks = 0x1E, - touchOcarina, - touchPictobox, - targetReticle, - - sun = 0x28, - moon, - oneBlock, - twoBlock, - threeBlock, - touchGear, - touchMap, - flag, - tatl, - tilde, - notebook, - firstPerson, - zlButton, - zrButton, - majora, -} iconList; - -typedef enum { - WHITE, - RED, - GREEN, - BLUE, - YELLOW, - CYAN, - MAGENTA, - GREY, - ORANGE, - DARK_GREY, - BLACK, - DEFAULT, -} colList; +#include "rnd/custom_messages.h" + +#if defined ENABLE_DEBUG || defined DEBUG_PRINT +#include "common/debug.h" +extern "C" { +#include <3ds/svc.h> +} +#endif + +#define LINE_WIDTH 260 +#define ICON_WIDTH 16 +#define MAJORA_ICON_WIDTH 32 +#define MAX_CHAR 0x220 +#define DEFAULT_WIDTH 8 +#define INSTANT_FLAG 0x8000 +#define REPEAT_FLAG 0x4000 + +// Pixel widths of printable characters (many are replaced by * which is 8 pixels) +char width[MAX_CHAR] = { + 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 00x + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 01x + 4, 4, 6, 10, 9, 12, 10, 4, 5, 5, 8, 9, 5, 7, 4, 7, // 02x + 9, 6, 9, 9, 10, 9, 9, 9, 9, 9, 4, 5, 8, 8, 8, 8, // 03x + 11, 10, 10, 10, 11, 9, 9, 10, 10, 4, 6, 10, 8, 13, 11, 10, // 04x + 10, 10, 9, 9, 9, 10, 10, 13, 9, 9, 10, 5, 7, 5, 8, 8, // 05x + 5, 8, 9, 8, 9, 9, 5, 8, 9, 4, 5, 8, 4, 12, 9, 9, // 06x + 9, 8, 6, 7, 5, 9, 8, 11, 8, 8, 8, 5, 4, 5, 6, 0, // 07x + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 08x + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 09x + 0, 4, 8, 10, 8, 9, 4, 9, 6, 10, 5, 8, 7, 7, 10, 7, // 0Ax + 4, 10, 6, 5, 5, 10, 8, 4, 4, 4, 5, 8, 9, 9, 10, 8, // 0Bx + 10, 10, 10, 10, 11, 10, 12, 9, 9, 9, 9, 10, 5, 5, 5, 5, // 0Cx + 11, 10, 10, 10, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 8, 8, // 0Dx + 8, 8, 9, 9, 8, 9, 11, 8, 9, 9, 9, 9, 5, 5, 5, 5, // 0Ex + 9, 8, 9, 9, 9, 9, 9, 11, 9, 9, 9, 9, 9, 8, 9, 9, // 0Fx + 10, 8, 10, 8, 11, 10, 10, 8, 8, 8, 10, 8, 10, 8, 11, 12, // 10x + 8, 8, 9, 9, 8, 8, 9, 9, 9, 9, 9, 9, 8, 8, 10, 8, // 11x + 10, 8, 10, 8, 8, 8, 11, 9, 8, 8, 6, 6, 8, 8, 5, 5, // 12x + 4, 4, 10, 8, 8, 8, 10, 8, 8, 9, 6, 8, 5, 8, 8, 8, // 13x + 8, 9, 5, 11, 9, 11, 9, 11, 9, 8, 8, 8, 8, 8, 8, 8, // 14x + 10, 9, 11, 12, 10, 6, 8, 8, 10, 6, 9, 7, 8, 8, 9, 7, // 15x + 9, 7, 8, 8, 9, 7, 8, 8, 8, 8, 10, 9, 8, 8, 10, 9, // 16x + 10, 9, 10, 10, 8, 8, 8, 8, 8, 10, 8, 10, 8, 10, 8, 8, // 17x + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 18x + 8, 8, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 19x + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1Ax + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1Bx + 8, 8, 8, 8, 8, 15, 14, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1Cx + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1Dx + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1Ex + 8, 8, 15, 14, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 1Fx + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, // 20x + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 5, 8, 8, 8, 8, // 21x +}; + +typedef struct { + u16 id; + char data[MAX_MSG_SIZE]; + u16 size; +} CustomMessage; + +// Extract data less than 8 bits long from a char array without wasting bits +u8 unpackData(u8 size, u8 idx, char* data) { + u8 i, unpacked = 0; + for (i = 0; i < size; i++) { + unpacked <<= 1; + if ((data[(idx * size + i) / 8] << ((idx * size + i) % 8)) & 0x80) + unpacked += 1; + } + return unpacked; +} + +// Pack data less than 8 bits long into a char array without wasting bits +void packData(u8 size, u8 idx, char* data, u8 unpacked) { + for (u8 i = size; i--; ) { + if (unpacked & 1) + data[(idx * size + i) / 8] |= 0x80 >> ((idx * size + i) % 8); + else + data[(idx * size + i) / 8] &= ~(0x80 >> ((idx * size + i) % 8)); + unpacked >>= 1; + } +} class MsgBuilder { public: @@ -83,22 +92,35 @@ class MsgBuilder { return this; } - MsgBuilder* addChr(char chr, bool txt = false) { + MsgBuilder* addChr(char chr) { if (*size < MAX_MSG_SIZE) data[(*size)++] = chr; +#if defined ENABLE_DEBUG || defined DEBUG_PRINT + else + rnd::util::Print("Error building message: MAX_MSG_SIZE exceeded\n"); +#endif return this; } MsgBuilder* text(const char* txt) { + // UTF8 encoding excluding 0x00 and 0x7F + // Many characters, especially control characters, print as `*` for (u16 idx = 0; txt[idx]; idx++) - addChr(txt[idx], true); + addChr(txt[idx]); return this; } - MsgBuilder* addCom(char com) { - addChr(0x7f); + MsgBuilder* pad(bool always) { + if (always) + addChr(0x00); if (*size % 2) addChr(0x00); + return this; + } + + MsgBuilder* addCom(char com) { + addChr(0x7f); + pad(false); addChr(com); return addChr(0x00); } @@ -109,28 +131,282 @@ class MsgBuilder { return addChr(0x00); } - MsgBuilder* col(colList c) { return addCom(0x3a, c); } // Change text colour until used again or new box - MsgBuilder* icon(iconList img) { return addCom(0x25, img); } // Insert an icon. See: https://cdn.discordapp.com/attachments/896879784162918400/1138941632675328010/image.png - MsgBuilder* delay(char time) { return addCom(0x29, time); } // Time measured in 20ths of a second - MsgBuilder* top() { return addCom(0x28); } // Applies to all boxes in message - MsgBuilder* instant() { return addCom(0x27); } // Applies only to current box - MsgBuilder* newline() { return addCom(0x01); } - MsgBuilder* newBox() { addCom(0x31, 0x01); return addCom(0x02); } - MsgBuilder* end() { addCom(0x31, 0x00); return addCom(0x00); } + MsgBuilder* sound(u16 id) { + // Must have a sufficient delay if used multiple times in one box + addCom(0x32); + // Has 2 extra padding bytes if not the first command in a message + pad(*size != 4); + addChr(id & 0xFF); + addChr(id >> 0x8); + addChr(0x00); + return addChr(0x01); + } + + MsgBuilder* filename() { return addCom(0x03); } // 1 to 8 UTF8 characters so size highly variable + MsgBuilder* col(char c) { return addCom(0x3a, c); } // Change text colour until used again or new box + MsgBuilder* icon(char img) { return addCom(0x25, img); } // Insert an icon. See: https://cdn.discordapp.com/attachments/896879784162918400/1138941632675328010/image.png + MsgBuilder* delay(char time) { return addCom(0x29, time); } // Time measured in 20ths of a second + MsgBuilder* top() { return addCom(0x28); } // Applies to all boxes in message + MsgBuilder* instant() { return addCom(0x27); } // Applies only to current box + MsgBuilder* newline() { return addCom(0x01); } + MsgBuilder* newBox() { addCom(0x31, 0x01); return addCom(0x02); } + MsgBuilder* end() { addCom(0x31, 0x00); return addCom(0x00); } + + void format(UnformattedMessage msg) { + // @ - filename min: 4px max: 120px + // # - colour change + // & - newline + // ^ - new box + // $ - icon + // % - delay + u16 idx = 0xFFFF, lastSpaceIdx = 0; + u16 colIdx = 0, colIdxAtLastSpace = 0; + u16 iconIdx = 0, iconIdxAtLastSpace = 0; + u16 delayIdx = 0, delayIdxAtLastSpace = 0; + u16 sizeAtLastSpace = 0, resolvedChar = 0, lineLen = 0; + bool inCol = false, inColAtLastSpace = false; + u16 sfx = msg.sfxAndFlags & 0x3FFF; + u8 resolvedCol = 0, resolvedIcon = 0, resolvedDelay = 0; + *size = 0; + + if (sfx) + sound(sfx); + if (msg.sfxAndFlags & INSTANT_FLAG) + instant(); + + while (++idx < MAX_UNFORMATTED_SIZE && msg.text[idx]) { + resolvedChar = msg.text[idx]; + + switch (resolvedChar) { + case '@': // Player file name + if (!lastSpaceIdx) + sizeAtLastSpace = *size; + filename(); + lineLen += 120; // TODO: dynamically fit to actual filename? + break; + + case '#': // Colour marker + if (inCol) + col(DEFAULT); + else { + // Abort if out of colours + if (colIdx >= 8) { + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + rnd::util::Print("Error formatting message, out of colours: %s\n", msg.text); + #endif + + return; + } + // Get next colour + resolvedCol = unpackData(4, colIdx++, msg.cols); + + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + // Could be caused by not providing enough colours so provide debug message + if (!resolvedCol) + rnd::util::Print("Warning formatting message, colour %i is 0x00: %s\n", colIdx, msg.text); + #endif + + col(resolvedCol); + } + inCol = !inCol; + break; + + case '&': // Newline + newline(); + lastSpaceIdx = 0; + lineLen = 0; + break; + + case '^': // Newbox + newBox(); + // New boxes have default colours so re-apply colour + if (inCol) + col(resolvedCol); + // If set to repeat sfx then add to this box + if ((msg.sfxAndFlags & REPEAT_FLAG) && sfx) + sound(sfx); + // New boxes lose the instant text property so re-set it + if (msg.sfxAndFlags & INSTANT_FLAG) + instant(); + + lastSpaceIdx = 0; + lineLen = 0; + break; + + case '$': // Icon marker + if (!lastSpaceIdx) + sizeAtLastSpace = *size; + // Abort if out of icons + if (iconIdx >= 8) { + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + rnd::util::Print("Error formatting message, out of icons: %s\n", msg.text); + #endif + + return; + } + // Get next icon + resolvedIcon = unpackData(6, iconIdx++, msg.icons); + + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + // Could be caused by not providing enough icons so provide debug message + if (!resolvedIcon) + rnd::util::Print("Warning formatting message, icon %i is 0x00: %s\n", iconIdx, msg.text); + #endif + // Majora icon is wider than all other icons + lineLen += (resolvedIcon == MAJORA) ? MAJORA_ICON_WIDTH : ICON_WIDTH; + icon(resolvedIcon); + break; + + case '%': // Delay marker + // Abort if out of delays + if (delayIdx >= 8) { + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + rnd::util::Print("Error formatting message, out of delays: %s\n", msg.text); + #endif + + return; + } + + resolvedDelay = unpackData(6, delayIdx++, msg.delays); + // Don't bother inserting a delay of 0 as is a waste of characters + if (resolvedDelay) + delay(resolvedDelay); + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + // Could be caused by not providing enough delays so provide debug message + else + rnd::util::Print("Warning formatting message, delay %i is 0x00: %s\n", delayIdx, msg.text); + #endif + break; + + case ' ': + // Keep track of spaces for inserting line breaks + lastSpaceIdx = idx; + colIdxAtLastSpace = colIdx; + iconIdxAtLastSpace = iconIdx; + delayIdxAtLastSpace = delayIdx; + inColAtLastSpace = inCol; + sizeAtLastSpace = *size; + // Don't break so can still be processed as a normal character + default: + if (!lastSpaceIdx) + sizeAtLastSpace = *size; + // Uses UTF8 encoding so convert multi-byte representations to a single number + if (resolvedChar > 0x7F) { + // Abort if char requires 3 or more bytes to represent in UTF8 + if (resolvedChar > 0xDF) { + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + rnd::util::Print("Error formatting message, unsupported character: %s\n", msg.text); + #endif + + return; + } + // Extract data from 2 byte UTF8 char. Also add the first half to the message + resolvedChar = ((msg.text[idx] & 0x1F) << 6) | (msg.text[idx + 1] & 0x3F); + addChr(msg.text[idx++]); + } + + addChr(msg.text[idx]); + // Assumes all further chars will be represented by * as many up to MAX_CHAR already are + lineLen += (resolvedChar < MAX_CHAR) ? width[resolvedChar] : DEFAULT_WIDTH; + break; + } + + // Replace last space with newline if necessary + // If no space available use start of last item with width + if (lineLen > LINE_WIDTH) { + if (lastSpaceIdx) { + idx = lastSpaceIdx; + colIdx = colIdxAtLastSpace; + iconIdx = iconIdxAtLastSpace; + delayIdx = delayIdxAtLastSpace; + inCol = inColAtLastSpace; + + lastSpaceIdx = 0; + } else + idx -= (resolvedChar > 0x7F) ? 2 : 1; + + *size = sizeAtLastSpace; + newline(); + lastSpaceIdx = 0; + lineLen = 0; + } + } + + // Add debug message if the last colour didn't have an end marker + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + if (inCol) + rnd::util::Print("Warning formatting message, colour not closed: %s\n", msg.text); + #endif + end(); + } }; MsgBuilder builder; -bool init = false; +CustomMessage customMsg; + +#ifdef ENABLE_DEBUG + static bool declareTestMessage = false; + static UnformattedMessage ptrCustomMessageEntries[1] = {0}; + volatile const u32 numCustomMessageEntries = 1; +#else + volatile const UnformattedMessage* ptrCustomMessageEntries = {0}; + volatile const u32 numCustomMessageEntries = {0}; +#endif -void CustomMessage_Init(void) { - if (!init) { - // ----- Custom Message Definitions ----- - builder.set(&iceTrapMsg)->instant()->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->text("You")->delay(10)->newline() - ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->text("are")->delay(10)->newline() - ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->text(" a")->delay(20)->newline() - ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->col(RED)->text(" FOOL")->col(WHITE)->text("!")->newBox() +bool SetCustomMessage(u16 id, game::MessageResEntry* msgResEntry) { + #if defined ENABLE_DEBUG || defined DEBUG_PRINT + static u16 lastId; + if (id && id != lastId) + rnd::util::Print("Message ID is %x\n", id); + lastId = id; - ->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->icon(blank)->col(YELLOW)->text("<3")->end(); + if (!declareTestMessage) { + ptrCustomMessageEntries[0].id = 0x0224; + ptrCustomMessageEntries[0].field_2 = 0xFFFF; + ptrCustomMessageEntries[0].field_4 = 0x3FFFFFFF; + ptrCustomMessageEntries[0].flags = 0xFF0000; + std::strcpy(ptrCustomMessageEntries[0].text, + "Ayo what's poppin'? It's #ya gal# Tatl $^This door's a right jerk innit. " + "I bet you could just #smash through# if you was moving fast enough." + "&%That'd show it who's $oss."); + packData(4, 0, ptrCustomMessageEntries[0].cols, YELLOW); + packData(4, 1, ptrCustomMessageEntries[0].cols, RED); + packData(6, 0, ptrCustomMessageEntries[0].icons, TATL); + packData(6, 1, ptrCustomMessageEntries[0].icons, B_BUTTON); + packData(6, 0, ptrCustomMessageEntries[0].delays, 20); + ptrCustomMessageEntries[0].sfxAndFlags = 0x0000; + + declareTestMessage = true; + } + #endif + + UnformattedMessage customMsgData; + s32 start = 0, end = numCustomMessageEntries - 1, current; + + while (start <= end) { + current = (start + end) / 2; + // Compiler isn't happy with assigning volatile const to not so reference/dereference to get data + customMsgData = *(UnformattedMessage*)&ptrCustomMessageEntries[current]; + if (customMsgData.id < id) + start = current + 1; + else if (customMsgData.id > id) + end = current - 1; + else { + // Only reformat message if it's different to the current formatted message + // Message get function called multiple times per message so this is important + if (id != customMsg.id) + builder.set(&customMsg)->format(customMsgData); + // Populate message entry with data from app side and the formatted message + msgResEntry->id = customMsg.id = id; + msgResEntry->field_2 = customMsgData.field_2; + msgResEntry->field_4 = customMsgData.field_4; + msgResEntry->flags = customMsgData.flags; + msgResEntry->texts[0].offset = customMsg.data; + msgResEntry->texts[0].length = customMsg.size; + return true; + } } - init = true; + + // Didn't find id in custom messages + return false; }