From 1d57c10333b182bb9feddcdb07aee5850980f2dc Mon Sep 17 00:00:00 2001 From: Philippe Symons Date: Wed, 20 May 2026 13:31:28 +0200 Subject: [PATCH] Replace text_data_table with FileContainerReader text_data_table was very similar in concept to FileContainerReader. But FileContainerReader is generally more flexible, as it allows you to split up your table into several chunks. This takes away the worry of these files getting too big to compress. However, we now have a maintenance duty for the text_tables file, which maps a table index to the list of chunks. We also need to be careful about every use of FileContainerReader::getPointerToFileInDecompressionBuffer(). Using it is fine when you're dealing with a single chunk or if you're sure the file doesn't span multiple chunks. But the moment you seek to a different chunk, any pointer obtained from getPointerToFileInDecompressionBuffer() becomes stale as the decompression buffer gets overwritten. Still, this function is necessary for high memory pressure situations, such as mystery_gift_builder, because we can't maintain multiple lineBuffers there. --- Makefile | 6 +- include/FileContainerReader.h | 36 +++++- include/dbg/debug_menu.h | 2 +- include/text_data_table.h | 42 ------- include/text_tables.h | 16 +++ include/vertical_menu.h | 6 +- source/FileContainerReader.cpp | 23 ++++ source/box_menu.cpp | 28 +++-- source/dbg/debug_menu.cpp | 2 +- source/global_frame_controller.cpp | 17 ++- source/link_handler.cpp | 17 ++- source/main.cpp | 116 +++++++++--------- source/multiboot_upload.cpp | 35 +++--- source/mystery_gift_builder.cpp | 53 ++++---- source/pokedex.cpp | 45 +++---- source/select_menu.cpp | 1 - source/text_data_table.cpp | 69 ----------- source/text_engine.cpp | 39 ++---- source/text_tables.cpp | 56 +++++++++ source/vertical_menu.cpp | 18 ++- tools/make-file-container/src/main.cpp | 39 +++++- tools/text_helper/main.py | 160 +++++++++++-------------- 22 files changed, 446 insertions(+), 380 deletions(-) delete mode 100644 include/text_data_table.h create mode 100644 include/text_tables.h delete mode 100644 source/text_data_table.cpp create mode 100644 source/text_tables.cpp diff --git a/Makefile b/Makefile index 7a993a7..34de716 100644 --- a/Makefile +++ b/Makefile @@ -215,7 +215,6 @@ export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) GENERATE_STAMP := $(BUILD)/.generate_data.$(BUILD_LANG).$(BUILD_TYPE).stamp BUILD_STAMP := $(BUILD)/.build.$(BUILD_LANG).$(BUILD_TYPE).stamp -TEXT_HELPER_INPUTS := tools/text_helper/main.py $(wildcard tools/text_helper/fonts/*.png) $(wildcard tools/text_helper/text.xlsx) TEXT_GENERATED_OUTPUTS := \ $(GENERATED_DIR)/translated_text.h \ $(GENERATED_DIR)/translated_text.cpp \ @@ -247,7 +246,7 @@ generated_dir: generate_data: $(GENERATE_STAMP) -$(GENERATE_STAMP): $(TEXT_HELPER_INPUTS) compress_lz10.sh | data to_compress generated_dir +$(GENERATE_STAMP): compress_lz10.sh | data to_compress generated_dir @if [ "$(BUILD_XLSX)" != "remote" ]; then \ $(MAKE) --no-print-directory text_generated BUILD_LANG=$(BUILD_LANG) BUILD_TYPE=$(BUILD_TYPE) BUILD_XLSX=$(BUILD_XLSX); \ fi @@ -259,7 +258,8 @@ $(GENERATE_STAMP): $(TEXT_HELPER_INPUTS) compress_lz10.sh | data to_compress gen @echo @echo "----------------------------------------------------------------" @echo - @find $(FILE_CONTAINERS) -name "*.containerdef" -print0 | xargs -0 -n1 tools/make-file-container/make-file-container -H $(BUILD) to_compress + @find $(FILE_CONTAINERS) -name "*.containerdef" -print0 | xargs -0 -n1 tools/make-file-container/make-file-container -H $(BUILD) to_compress + @find tools/text_helper/build -name "*.containerdef" -print0 | xargs -0 -n1 tools/make-file-container/make-file-container to_compress @echo "Compressing bin files!" @echo -n "[" @find to_compress -name "*.bin" -print0 | xargs -0 -n1 ./compress_lz10.sh diff --git a/include/FileContainerReader.h b/include/FileContainerReader.h index 910dcac..f439dbd 100644 --- a/include/FileContainerReader.h +++ b/include/FileContainerReader.h @@ -4,6 +4,7 @@ #include "typeDefs.h" #define FILE_NAME_LENGTH 16 +#define DEFAULT_CHUNK_SIZE 2048 /** * @brief This class provides functionality to read files from the file container format @@ -19,7 +20,7 @@ class FileContainerReader { public: - FileContainerReader(const u8 **chunkList, u32 chunkCount, u32 chunkSize = 4096); + FileContainerReader(const u8 **chunkList, u32 chunkCount, u32 chunkSize = DEFAULT_CHUNK_SIZE); ~FileContainerReader(); /** @@ -36,6 +37,11 @@ public: */ bool init(u8 *decompressionBuffer, u32 decompressionBufferSize); + /** + * @brief Returns the number of files stored in the file container. + */ + u32 getNumberOfFiles() const; + /** * @brief IF the file container stores the file names, * this function returns a pointer to the file name of the file at the given index. @@ -57,6 +63,34 @@ public: * @brief Read data from the current position in the file container into the provided buffer. */ void read(u8 *buffer, u32 size); + + /** + * @brief Gives you a direct pointer to the specified file in the decompression buffer. (unsafe!) + * + * This is useful to use data directly from the decompression buffer without having to allocate and + * copy another buffer. This is essential in high memory pressure scenarios, such as mystery_gift_builder. + * We can't afford to keep multiple linebuffers in memory in addition to the decompression buffer and all the other variables there. + * + * WARNING: this is only safe if you know the file is stored fully in the current chunk, because files may span multiple chunks. + * Or if the container only consists of a single chunk. + * Pointers acquired this way will become stale whenever a seek is done to a position in a different chunk, + * as that will cause the decompression buffer to be overwritten with the new chunk's data. + */ + u8 *getPointerToFileInDecompressionBuffer(u32 fileIndex); + + /** + * @brief Combines seekToFile and read(). + * Just a convenience function to make our code shorter :-) + */ + void seekAndRead(u32 fileIndex, u8 *buffer, u32 size); + + /** + * @brief Even shorter variant of seekAndRead that reads the entire file at once. + * But it assumes you have provided a large enough buffer to hold the entire file. + * + * (useful for text table reading) + */ + void readFile(u32 fileIndex, u8 *buffer); protected: private: /** diff --git a/include/dbg/debug_menu.h b/include/dbg/debug_menu.h index 48755be..3eaac91 100644 --- a/include/dbg/debug_menu.h +++ b/include/dbg/debug_menu.h @@ -92,7 +92,7 @@ class debug_menu_row_widget : public i_item_widget public: debug_menu_row_widget(const debug_menu_row_data &data); virtual ~debug_menu_row_widget(); - void render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) override; + void render_item(FileContainerReader &text_table, unsigned x, unsigned y, bool is_focused) override; MenuInputHandleState handle_input() override; protected: private: diff --git a/include/text_data_table.h b/include/text_data_table.h deleted file mode 100644 index 3b942e3..0000000 --- a/include/text_data_table.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef _TEXT_DATA_TABLE_H -#define _TEXT_DATA_TABLE_H - -#include - -/** - * This class fully decompresses a text table in the specified decompression_buffer - * and then gives you utility functions to retrieve the text entries - * - * But it requires a buffer large enough to contain the entire decompressed table. - */ -class text_data_table -{ -public: - text_data_table(uint8_t *decompression_buffer); - - /** - * This function will start the full decompression for the specified compressed_table - * and stores it in the decompression_buffer_ - */ - void decompress(const uint8_t *compressed_table); - - /** - * Returns the number of text entries in the decompression_buffer_ - */ - uint16_t get_number_of_text_entries() const; - - /** - * This function returns a pointer to a text entry in the decompression_buffer - */ - const uint8_t* get_text_entry(uint16_t index) const; - - /** - * This function returns the text entry size in bytes at the given index - */ - uint16_t get_text_entry_size(uint16_t index) const; -private: - uint8_t *decompression_buffer_; - uint32_t decompressed_size_; -}; - -#endif \ No newline at end of file diff --git a/include/text_tables.h b/include/text_tables.h new file mode 100644 index 0000000..cea6531 --- /dev/null +++ b/include/text_tables.h @@ -0,0 +1,16 @@ +#ifndef _TEXT_TABLES_H +#define _TEXT_TABLES_H + +#include "typeDefs.h" + +extern "C" +{ + +/** + * @brief This function returns a list of pointers to the text table chunks and the number of chunks. + */ +void get_text_table_chunks(u32 tableIndex, const u8 ***chunkList, u32 *numChunks, u32 *chunkSize); + +} + +#endif \ No newline at end of file diff --git a/include/vertical_menu.h b/include/vertical_menu.h index bb1841d..782b32b 100644 --- a/include/vertical_menu.h +++ b/include/vertical_menu.h @@ -3,7 +3,7 @@ #include "typeDefs.h" #include "libstd_replacements.h" -#include "text_data_table.h" +#include "FileContainerReader.h" enum class MenuInputHandleState { @@ -41,7 +41,7 @@ class i_item_widget { public: virtual ~i_item_widget(); - virtual void render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) = 0; + virtual void render_item(FileContainerReader &text_table, unsigned x, unsigned y, bool is_focused) = 0; virtual MenuInputHandleState handle_input() = 0; protected: private: @@ -292,7 +292,7 @@ public: const simple_item_widget_data& get_data() const; - void render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) override; + void render_item(FileContainerReader &text_table, unsigned x, unsigned y, bool is_focused) override; MenuInputHandleState handle_input() override; protected: private: diff --git a/source/FileContainerReader.cpp b/source/FileContainerReader.cpp index e43b816..c028a3a 100644 --- a/source/FileContainerReader.cpp +++ b/source/FileContainerReader.cpp @@ -56,6 +56,11 @@ bool FileContainerReader::init(u8 *decompressionBuffer, u32 decompressionBufferS return true; } +u32 FileContainerReader::getNumberOfFiles() const +{ + return fileCount_; +} + const char* FileContainerReader::getFileName(u32 fileIndex) { if(!hasNames_) @@ -111,6 +116,24 @@ void FileContainerReader::read(u8 *buffer, u32 size) } } +u8 *FileContainerReader::getPointerToFileInDecompressionBuffer(u32 fileIndex) +{ + seekToFile(fileIndex); + return decompressionBuffer_ + curPos_; +} + +void FileContainerReader::seekAndRead(u32 fileIndex, u8 *buffer, u32 size) +{ + seekToFile(fileIndex); + read(buffer, size); +} + +void FileContainerReader::readFile(u32 fileIndex, u8 *buffer) +{ + const u32 fileSize = getFileSize(fileIndex); + seekAndRead(fileIndex, buffer, fileSize); +} + u32 FileContainerReader::getFileOffset(u32 entryIndex) const { // files data start after: diff --git a/source/box_menu.cpp b/source/box_menu.cpp index 0b7192e..d9cdac2 100644 --- a/source/box_menu.cpp +++ b/source/box_menu.cpp @@ -9,14 +9,21 @@ #include "box_menu.h" #include "text_engine.h" #include "translated_text.h" -#include "text_data_table.h" +#include "FileContainerReader.h" +#include "text_tables.h" Box_Menu::Box_Menu() {}; int Box_Menu::box_main(PokeBox* box) { - u8 names_decompression_buffer[3072]; - text_data_table PKMN_NAMES(names_decompression_buffer); + u8 names_decompression_buffer[2048]; + u8 single_name_buffer[16]; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; + + get_text_table_chunks(PKMN_NAMES_INDEX, &chunkList, &numChunks, &chunkSize); + FileContainerReader namesReader(chunkList, numChunks, chunkSize); tte_erase_screen(); load_flex_background(FLEXBG_BOX, 2); @@ -36,7 +43,7 @@ int Box_Menu::box_main(PokeBox* box) obj_unhide(box_select, 0); int index = 0; - PKMN_NAMES.decompress(get_compressed_text_table(PKMN_NAMES_INDEX)); + namesReader.init(names_decompression_buffer, sizeof(names_decompression_buffer)); while (true) { @@ -133,6 +140,8 @@ int Box_Menu::box_main(PokeBox* box) if (index < box->getNumInBox() && curr_pkmn->isValid) { byte val[11]; + u32 nameEntryIndex = curr_pkmn->getSpeciesIndexNumber(); + tte_set_pos(6, 88); curr_pkmn->externalConvertNickname(val); ptgb_write_simple(val, true); @@ -144,15 +153,14 @@ int Box_Menu::box_main(PokeBox* box) ptgb_write_simple(val, true); } tte_set_pos(14, 98); - if (curr_pkmn->getSpeciesIndexNumber() == MISSINGNO) - { - ptgb_write_simple(PKMN_NAMES.get_text_entry(0), true); - } - else + if(nameEntryIndex == MISSINGNO) { - ptgb_write_simple(PKMN_NAMES.get_text_entry(curr_pkmn->getSpeciesIndexNumber()), true); + nameEntryIndex = 0; } + namesReader.readFile(nameEntryIndex, single_name_buffer); + ptgb_write_simple(single_name_buffer, true); + tte_set_pos(6, 108); val[0] = 0xC6; // L val[1] = 0xEA; // v diff --git a/source/dbg/debug_menu.cpp b/source/dbg/debug_menu.cpp index 3cfb535..b50b6f2 100644 --- a/source/dbg/debug_menu.cpp +++ b/source/dbg/debug_menu.cpp @@ -83,7 +83,7 @@ debug_menu_row_widget::~debug_menu_row_widget() } } -void debug_menu_row_widget::render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) +void debug_menu_row_widget::render_item(FileContainerReader &text_table, unsigned x, unsigned y, bool is_focused) { const int label_margin_left = 12; const int margin_top = 2; diff --git a/source/global_frame_controller.cpp b/source/global_frame_controller.cpp index cd60ab3..c916e3f 100644 --- a/source/global_frame_controller.cpp +++ b/source/global_frame_controller.cpp @@ -7,9 +7,10 @@ #include "text_engine.h" #include "sprite_data.h" #include "string.h" -#include "text_data_table.h" #include "translated_text.h" #include "dbg/debug_mode.h" +#include "FileContainerReader.h" +#include "text_tables.h" int global_frame_count = 0; bool rand_enabled = true; @@ -25,11 +26,17 @@ bool treecko_enabled = false; // the noinline attribute prevents the compiler from inlining this function back into the global_next_frame function static void __attribute__((noinline)) show_pulled_cart_error() { - u8 general_text_table_buffer[2048]; - text_data_table general_text(general_text_table_buffer); + u8 decompression_buffer[2048]; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); - ptgb_write_textbox(general_text.get_text_entry(GENERAL_pulled_cart_error), true, true, + get_text_table_chunks(GENERAL_INDEX, &chunkList, &numChunks, &chunkSize); + + FileContainerReader general_text_reader(chunkList, numChunks, chunkSize); + general_text_reader.init(decompression_buffer, sizeof(decompression_buffer)); + + ptgb_write_textbox(general_text_reader.getPointerToFileInDecompressionBuffer(GENERAL_pulled_cart_error), true, true, GENERAL_INDEX, GENERAL_pulled_cart_error, true); } diff --git a/source/link_handler.cpp b/source/link_handler.cpp index 77f5065..6afbd1a 100644 --- a/source/link_handler.cpp +++ b/source/link_handler.cpp @@ -14,10 +14,11 @@ #include "global_frame_controller.h" #include "background_engine.h" #include "sprite_data.h" -#include "text_data_table.h" #include "libraries/Pokemon-Gen3-to-Gen-X/include/save.h" #include "flash_mem.h" #include "FileContainerReader.h" +#include "text_tables.h" +#include "translated_text.h" #include "GB_Payloads_chunk0_lz10_bin.h" #include "GB_Payloads.h" @@ -64,11 +65,17 @@ void LinkConnection::setup(const u16 *debug_charset) { u8 general_text_table_buffer[2048]; - text_data_table general_text(general_text_table_buffer); + u8 lineBuffer[1024]; // text_helper/main.py restricts the lines to 1024 bytes. + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); - ptgb_write_textbox(general_text.get_text_entry(GENERAL_connecting), true, false, - GENERAL_INDEX, GENERAL_connecting, false); + get_text_table_chunks(GENERAL_INDEX, &chunkList, &numChunks, &chunkSize); + + FileContainerReader general_text_reader(chunkList, numChunks, chunkSize); + general_text_reader.init(general_text_table_buffer, sizeof(general_text_table_buffer)); + general_text_reader.readFile(GENERAL_connecting, lineBuffer); + ptgb_write_textbox(lineBuffer, true, false, GENERAL_INDEX, GENERAL_connecting, false); } create_textbox(0, 0, 138, 128, false); diff --git a/source/main.cpp b/source/main.cpp index 1ebda4f..aec008a 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -27,7 +27,9 @@ #include "multiboot_upload.h" #include "rom_data.h" #include "libraries/Pokemon-Gen3-to-Gen-X/include/save.h" -#include "text_data_table.h" +#include "FileContainerReader.h" +#include "translated_text.h" +#include "text_tables.h" #include "custom_malloc.h" #include "sound.h" @@ -111,18 +113,23 @@ void initialization_script(void) PTGB_MGBA_INFO("Hello from PTGB!"); }; -void game_load_error(void) +// attribute noinline is used to make sure it doesn't get inlined and permanently use IWRAM for the decompression_buffer +void __attribute__((noinline)) game_load_error(void) { + u8 general_text_buffer[2048]; + u8 lineBuffer[1024]; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; + + get_text_table_chunks(GENERAL_INDEX, &chunkList, &numChunks, &chunkSize); + FileContainerReader text_reader(chunkList, numChunks, chunkSize); + + text_reader.init(general_text_buffer, sizeof(general_text_buffer)); + BG_TEXTBOX = (BG_TEXTBOX & ~BG_PRIO_MASK) | BG_PRIO(1); - - { - u8 general_text_table_buffer[2048]; - text_data_table general_text(general_text_table_buffer); - - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); - ptgb_write_textbox(general_text.get_text_entry(GENERAL_cart_load_error), true, false, - GENERAL_INDEX, GENERAL_cart_load_error, false); - } + text_reader.readFile(GENERAL_cart_load_error, lineBuffer); + ptgb_write_textbox(lineBuffer, true, false, GENERAL_INDEX, GENERAL_cart_load_error, false); // key_poll(); do @@ -153,39 +160,28 @@ void game_load_error(void) } } -void first_load_message(void) +// avoid inlining to avoid permanently storing the credits_decompression_buffer in IWRAM +int __attribute__((noinline)) credits() { - tte_set_ink(INK_ROM_COLOR); + u8 credits_decompression_buffer[2048]; + u8 lineBuffer[1024]; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; - { - u8 general_text_table_buffer[2048]; - text_data_table general_text(general_text_table_buffer); + get_text_table_chunks(CREDITS_INDEX, &chunkList, &numChunks, &chunkSize); + FileContainerReader creditsReader(chunkList, numChunks, chunkSize); + u32 curr_credits_num = 0; - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); - ptgb_write_simple(general_text.get_text_entry(GENERAL_intro_first), true); - } - - while (!key_hit(KEY_A)) - { - VBlankIntrWait(); - } -} - -int credits() -{ - u8 text_decompression_buffer[2048]; - text_data_table credits_text_table(text_decompression_buffer); - int curr_credits_num = 0; - - credits_text_table.decompress(get_compressed_text_table(CREDITS_INDEX)); + creditsReader.init(credits_decompression_buffer, sizeof(credits_decompression_buffer)); bool update = true; while (true) { if (update) { - ptgb_write_textbox(credits_text_table.get_text_entry(curr_credits_num), true, false, - CREDITS_INDEX, curr_credits_num, false); + creditsReader.readFile(curr_credits_num, lineBuffer); + ptgb_write_textbox(lineBuffer, true, false, CREDITS_INDEX, curr_credits_num, false); update = false; } @@ -201,7 +197,7 @@ int credits() curr_credits_num--; update = true; } - if (key_hit(KEY_RIGHT) && curr_credits_num < (credits_text_table.get_number_of_text_entries() - 1)) + if (key_hit(KEY_RIGHT) && curr_credits_num < (creditsReader.getNumberOfFiles() - 1)) { curr_credits_num++; update = true; @@ -211,19 +207,24 @@ int credits() } }; -int main_menu_loop() +// attribute noinline is used to avoid permanently storing the general_text_table_buffer in IWRAM +int __attribute__((noinline)) main_menu_loop() { + uint8_t general_text_table_buffer[2048]; + u8 lineBuffer[1024]; #define NUM_MENU_OPTIONS 3 const uint8_t menu_options[NUM_MENU_OPTIONS] = {GENERAL_option_transfer, GENERAL_option_dreamdex, GENERAL_option_credits}; int return_values[NUM_MENU_OPTIONS] = {BTN_TRANSFER, BTN_POKEDEX, BTN_CREDITS}; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; - uint8_t general_text_table_buffer[2048]; - text_data_table general_text(general_text_table_buffer); + get_text_table_chunks(GENERAL_INDEX, &chunkList, &numChunks, &chunkSize); + FileContainerReader text_reader(chunkList, numChunks, chunkSize); bool update = true; - const uint8_t *text_entry; u16 test = 0; - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); + text_reader.init(general_text_table_buffer, sizeof(general_text_table_buffer)); while (true) { @@ -231,8 +232,8 @@ int main_menu_loop() { for (int i = 0; i < NUM_MENU_OPTIONS; i++) { - text_entry = general_text.get_text_entry(menu_options[i]); - int string_length = get_string_length(text_entry); + text_reader.readFile(menu_options[i], lineBuffer); + int string_length = get_string_length(lineBuffer); int x = ((240 - string_length) / 2); tte_set_pos(x, ((i * (16 + 10)) + 70)); if (i == curr_selection) @@ -243,7 +244,7 @@ int main_menu_loop() { tte_set_ink(INK_ROM_COLOR); } - ptgb_write_simple(text_entry, true); + ptgb_write_simple(lineBuffer, true); test++; } } @@ -296,18 +297,21 @@ static void show_gears_of_progress() // this decision was based on the output of build/main.su after adding the -fstack-usage compile flag static void __attribute__((noinline)) show_intro() { - bool start_pressed = false; u8 general_text_table_buffer[2048]; + u8 lineBuffer[1024]; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; - text_data_table general_text(general_text_table_buffer); - const u8 *text_entry; + get_text_table_chunks(GENERAL_INDEX, &chunkList, &numChunks, &chunkSize); + FileContainerReader general_text_reader(chunkList, numChunks, chunkSize); + bool start_pressed = false; - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); - text_entry = general_text.get_text_entry(GENERAL_intro_legal); + general_text_reader.init(general_text_table_buffer, sizeof(general_text_table_buffer)); tte_set_ink(INK_ROM_COLOR); - ptgb_write_textbox(text_entry, true, true, - GENERAL_INDEX, GENERAL_intro_legal, true); + general_text_reader.readFile(GENERAL_intro_legal, lineBuffer); + ptgb_write_textbox(lineBuffer, true, true, GENERAL_INDEX, GENERAL_intro_legal, true); show_gears_of_progress(); @@ -322,12 +326,11 @@ static void __attribute__((noinline)) show_intro() REG_BLDCNT = BLD_BUILD(BLD_BG3, BLD_BG0, 1); - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); - text_entry = general_text.get_text_entry(GENERAL_press_start); + general_text_reader.readFile(GENERAL_press_start, lineBuffer); tte_set_pos(0, 12 * 8); tte_set_ink(INK_DARK_GREY); - ptgb_write_simple(text_entry, true); + ptgb_write_simple(lineBuffer, true); int fade = 0; while (!start_pressed) @@ -347,11 +350,6 @@ int main(void) // Set colors based on current ROM set_background_pal(0, false, false); - /* First load message doesn't really make sense anymore, since you have to load the ROM first. - if (!get_tutorial_flag()) - { - first_load_message(); - }*/ show_intro(); // key_poll(); diff --git a/source/multiboot_upload.cpp b/source/multiboot_upload.cpp index 360c0cc..2d60fd2 100644 --- a/source/multiboot_upload.cpp +++ b/source/multiboot_upload.cpp @@ -5,18 +5,26 @@ #include "libraries/gba-link-connection/LinkCableMultiboot.hpp" #include "text_engine.h" #include "translated_text.h" -#include "text_data_table.h" +#include "FileContainerReader.h" +#include "text_tables.h" void multiboot_upload_screen() { - u8 general_text_table_buffer[2048]; - text_data_table general_text(general_text_table_buffer); + u8 decompression_buffer[2048]; + u8 lineBuffer[1024]; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; + + get_text_table_chunks(GENERAL_INDEX, &chunkList, &numChunks, &chunkSize); + FileContainerReader general_text_reader(chunkList, numChunks, chunkSize); LinkCableMultiboot linkCableMultiboot; - general_text.decompress(get_compressed_text_table(GENERAL_INDEX)); + general_text_reader.init(decompression_buffer, sizeof(decompression_buffer)); // multiboot_show_textbox(); - ptgb_write_textbox(general_text.get_text_entry(GENERAL_send_multiboot_instructions), true, + general_text_reader.readFile(GENERAL_send_multiboot_instructions, lineBuffer); + ptgb_write_textbox(lineBuffer, true, false, GENERAL_INDEX, GENERAL_send_multiboot_instructions, false); // wait for key press @@ -33,7 +41,8 @@ void multiboot_upload_screen() // start upload // multiboot_show_textbox(); - ptgb_write_textbox(general_text.get_text_entry(GENERAL_send_multiboot_wait), true, + general_text_reader.readFile(GENERAL_send_multiboot_wait, lineBuffer); + ptgb_write_textbox(lineBuffer, true, false, GENERAL_INDEX, GENERAL_send_multiboot_wait, false); VBlankIntrWait(); @@ -49,16 +58,10 @@ void multiboot_upload_screen() }); // show result // multiboot_show_textbox(); - if (multibootResult == LinkCableMultiboot::Result::SUCCESS) - { - ptgb_write_textbox(general_text.get_text_entry(GENERAL_send_multiboot_success), true, - false, GENERAL_INDEX, GENERAL_send_multiboot_success, false); - } - else - { - ptgb_write_textbox(general_text.get_text_entry(GENERAL_send_multiboot_failure), true, - false, GENERAL_INDEX, GENERAL_send_multiboot_failure, false); - } + u32 fileIndex = (multibootResult == LinkCableMultiboot::Result::SUCCESS) ? GENERAL_send_multiboot_success : GENERAL_send_multiboot_failure; + general_text_reader.readFile(fileIndex, lineBuffer); + + ptgb_write_textbox(lineBuffer, true, false, GENERAL_INDEX, fileIndex, false); // wait for keypress again. do diff --git a/source/mystery_gift_builder.cpp b/source/mystery_gift_builder.cpp index 978500c..5da8726 100644 --- a/source/mystery_gift_builder.cpp +++ b/source/mystery_gift_builder.cpp @@ -5,7 +5,8 @@ #include "pokemon_data.h" #include "rom_data.h" #include "translated_text.h" -#include "text_data_table.h" +#include "FileContainerReader.h" +#include "text_tables.h" #define MG_SCRIPT false #define S30_SCRIPT true @@ -101,7 +102,15 @@ mystery_gift_script::mystery_gift_script(u8 *save_section_30_buffer) void mystery_gift_script::build_script(PokeBox *box) { decompressed_data_storage_union decompressed_store; - text_data_table decompressed_text_table(decompressed_store.text.buffer); + const u8 **rsefrlgTableChunkList; + u32 rsefrlgNumChunks; + u32 rsefrlgChunkSize; + + // in order to safely use getPointerToFileInDecompressionBuffer() in FileContainerReader + // we must ensure that the table fits inside a single chunk + get_text_table_chunks(RSEFRLG_INDEX, &rsefrlgTableChunkList, &rsefrlgNumChunks, &rsefrlgChunkSize); + FileContainerReader rsefrlgTableReader(rsefrlgTableChunkList, rsefrlgNumChunks, rsefrlgChunkSize); + ptgb::vector mg_variable_list; ptgb::vector sec30_variable_list; @@ -379,39 +388,35 @@ void mystery_gift_script::build_script(PokeBox *box) // this decompresses the ZX0 compressed text table into the buffer inside of the decompressed_store union // thereby reusing the stack (=IWRAM) memory used earlier for the PokemonTables instance we used above - decompressed_text_table.decompress(get_compressed_text_table(RSEFRLG_INDEX)); + rsefrlgTableReader.init(decompressed_store.text.buffer, sizeof(decompressed_store.text.buffer)); switch (curr_GBA_rom.gamecode) { case RUBY_ID: - textGreet.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textGreet_rse)); - textMoveBox.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textMoveBox_rs)); - textWeHere.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textWeHere_r)); - break; case SAPPHIRE_ID: - textGreet.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textGreet_rse)); - textMoveBox.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textMoveBox_rs)); - textWeHere.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textWeHere_s)); + textGreet.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textGreet_rse)); + textMoveBox.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textMoveBox_rs)); + textWeHere.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textWeHere_r)); break; case FIRERED_ID: case LEAFGREEN_ID: - textGreet.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textGreet_frlg)); - textMoveBox.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textMoveBox_frlg)); - textWeHere.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textWeHere_frlg)); + textGreet.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textGreet_frlg)); + textMoveBox.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textMoveBox_frlg)); + textWeHere.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textWeHere_frlg)); break; case EMERALD_ID: - textGreet.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textGreet_rse)); - textMoveBox.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textMoveBox_e)); - textWeHere.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textWeHere_e)); + textGreet.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textGreet_rse)); + textMoveBox.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textMoveBox_e)); + textWeHere.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textWeHere_e)); break; } - textReceived.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textRecieved)); - textYouMustBe.set_text(decompressed_text_table.get_text_entry(first_time ? RSEFRLG_dia_textYouMustBe_first : RSEFRLG_dia_textYouMustBe_second)); - textIAm.set_text(decompressed_text_table.get_text_entry(first_time ? RSEFRLG_dia_textIAm_first : RSEFRLG_dia_textIAm_second)); - textPCConvo.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textPCConvo)); // ȼDon’t worry ƲÀ,Ňyou won’t have to do a thing!"); - textPCThanks.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textPCThanks)); - textThank.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textThank)); - textPCFull.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textPCFull)); - textLookerFull.set_text(decompressed_text_table.get_text_entry(RSEFRLG_dia_textLookerFull)); + textReceived.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textRecieved)); + textYouMustBe.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(first_time ? RSEFRLG_dia_textYouMustBe_first : RSEFRLG_dia_textYouMustBe_second)); + textIAm.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(first_time ? RSEFRLG_dia_textIAm_first : RSEFRLG_dia_textIAm_second)); + textPCConvo.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textPCConvo)); // ȼDon’t worry ƲÀ,Ňyou won’t have to do a thing!"); + textPCThanks.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textPCThanks)); + textThank.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textThank)); + textPCFull.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textPCFull)); + textLookerFull.set_text(rsefrlgTableReader.getPointerToFileInDecompressionBuffer(RSEFRLG_dia_textLookerFull)); textThank.insert_text(decompressed_store.text.gen3_charset, save_section_30); textPCFull.insert_text(decompressed_store.text.gen3_charset, save_section_30); diff --git a/source/pokedex.cpp b/source/pokedex.cpp index b047a90..deae721 100644 --- a/source/pokedex.cpp +++ b/source/pokedex.cpp @@ -10,7 +10,8 @@ #include "button_handler.h" #include "translated_text.h" #include "text_engine.h" -#include "text_data_table.h" +#include "FileContainerReader.h" +#include "text_tables.h" #include "TYPES_lz10_bin.h" Dex dex_array[DEX_MAX]; @@ -29,22 +30,17 @@ bool mew_caught; bool celebi_caught; bool missingno_caught = false; -static void load_text_entry_into_buffer(text_data_table& data_table, u8 *output_buffer, u8 entry_index) +static void load_general_table_text_entries(u8 *decompression_buffer, u32 decompression_buffer_size, u8 *kanto_buffer, u8 *johto_buffer) { - const u8 *entry = data_table.get_text_entry(entry_index); - const u8 *entry_end = (const u8*)strchr((const char*)entry, 0xFF); + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; + get_text_table_chunks(GENERAL_INDEX, &chunkList, &numChunks, &chunkSize); + FileContainerReader text_reader(chunkList, numChunks, chunkSize); + text_reader.init(decompression_buffer, decompression_buffer_size); - // copy the text_entry including the 0xFF at the end - memcpy(output_buffer, entry, entry_end + 1 - entry); -} - -static void load_general_table_text_entries(u8 *decompression_buffer, u8 *kanto_buffer, u8 *johto_buffer) -{ - text_data_table data_table(decompression_buffer); - data_table.decompress(get_compressed_text_table(GENERAL_INDEX)); - - load_text_entry_into_buffer(data_table, kanto_buffer, GENERAL_kanto_name); - load_text_entry_into_buffer(data_table, johto_buffer, GENERAL_johto_name); + text_reader.readFile(GENERAL_kanto_name, kanto_buffer); + text_reader.readFile(GENERAL_johto_name, johto_buffer); } void pokedex_init() @@ -91,21 +87,27 @@ void pokedex_init() #include "gen_3_charsets_lz10_bin.h" #include "libstd_replacements.h" -int pokedex_loop() +// attribute noinline is used to make sure it doesn't get inlined and permanently use IWRAM for the decompression_buffer +int __attribute__((noinline)) pokedex_loop() { u8 TYPES[POKEMON_ARRAY_SIZE][2]; u8 kanto_name[12]; u8 johto_name[12]; - u8 decompression_buffer[3072]; + u8 decompression_buffer[2048]; u16 charset[256]; + u8 name_buffer[16]; + const u8 **namesChunkList; + u32 namesNumChunks; + u32 namesChunkSize; LZ77UnCompWram(TYPES_lz10_bin, (u8*)TYPES); LZ77UnCompWram(gen_3_charsets_lz10_bin, (u8*)charset); - load_general_table_text_entries(decompression_buffer, kanto_name, johto_name); + load_general_table_text_entries(decompression_buffer, sizeof(decompression_buffer), kanto_name, johto_name); - text_data_table PKMN_NAMES(decompression_buffer); - PKMN_NAMES.decompress(get_compressed_text_table(PKMN_NAMES_INDEX)); + get_text_table_chunks(PKMN_NAMES_INDEX, &namesChunkList, &namesNumChunks, &namesChunkSize); + FileContainerReader PKMN_NAMES(namesChunkList, namesNumChunks, namesChunkSize); + PKMN_NAMES.init(decompression_buffer, sizeof(decompression_buffer)); pokedex_init(); pokedex_show(); @@ -229,7 +231,8 @@ int pokedex_loop() ptgb_write_simple(temp_string, true); tte_set_pos(dex_x_cord + (7 * 8), (i * 8 * 2) + 28); - ptgb_write_simple(is_caught(dex_shift + i + 1 + mythic_skip) ? PKMN_NAMES.get_text_entry(dex_shift + i + 1 + mythic_skip) : undiscovered_text, true); + PKMN_NAMES.readFile(dex_shift + i + 1 + mythic_skip, name_buffer); + ptgb_write_simple(is_caught(dex_shift + i + 1 + mythic_skip) ? name_buffer : undiscovered_text, true); } //global_next_frame(); // This is a bit silly, but it works. Makes the types one frame off from the text, but that's 'fine' diff --git a/source/select_menu.cpp b/source/select_menu.cpp index 5e9202c..d4cad16 100644 --- a/source/select_menu.cpp +++ b/source/select_menu.cpp @@ -1,7 +1,6 @@ #include "select_menu.h" #include "sprite_data.h" #include "translated_text.h" -#include "text_data_table.h" #include "global_frame_controller.h" #define TEXT_HEIGHT 10 diff --git a/source/text_data_table.cpp b/source/text_data_table.cpp deleted file mode 100644 index cee4f6b..0000000 --- a/source/text_data_table.cpp +++ /dev/null @@ -1,69 +0,0 @@ -#include "text_data_table.h" -#include -#include - -static uint16_t get_entry_offset_by_index(const uint8_t *text_table, uint16_t index) -{ - return *((uint16_t*)(text_table + 2 + index * 2)); -} - -static uint16_t get_entries_start_offset_of(uint8_t num_text_entries) -{ - // This returns the byte offset to skip the table index and reach the start of the actual entries. - return 2 + (num_text_entries * 2); -} - -static uint16_t get_num_text_entries(const uint8_t *index_buffer) -{ - return *((uint16_t*)index_buffer); -} - -static uint16_t get_entry_size_in_bytes(const uint8_t *index_buffer, uint32_t decompressed_size, uint16_t index) -{ - const uint16_t entry_offset = get_entry_offset_by_index(index_buffer, index); - const uint16_t num_text_entries = get_num_text_entries(index_buffer); - uint16_t entry_size_in_bytes; - - if(index != num_text_entries - 1) - { - const uint16_t next_entry_offset = get_entry_offset_by_index(index_buffer, index + 1); - entry_size_in_bytes = next_entry_offset - entry_offset; - } - else - { - const uint16_t entry_byte_offset = get_entries_start_offset_of(num_text_entries) + entry_offset; - // we don't have a next entry. So we need to consider the end of the file - const uint16_t nDecompressed_size = static_cast(decompressed_size); - entry_size_in_bytes = nDecompressed_size - entry_byte_offset; - } - return entry_size_in_bytes; -} - -text_data_table::text_data_table(uint8_t *decompression_buffer) - : decompression_buffer_(decompression_buffer) - , decompressed_size_(0) -{ -} - -void text_data_table::decompress(const uint8_t *compressed_table) -{ - // byte 2-4 of the compressed data store the decompressed size - decompressed_size_ = compressed_table[1] | (compressed_table[2] << 8) | (compressed_table[3] << 16); - LZ77UnCompWram(compressed_table, decompression_buffer_); -} - -uint16_t text_data_table::get_number_of_text_entries() const -{ - return get_num_text_entries(decompression_buffer_); -} - -const uint8_t* text_data_table::get_text_entry(uint16_t index) const -{ - const uint16_t entry_offset = get_entry_offset_by_index(decompression_buffer_, index); - return decompression_buffer_ + get_entries_start_offset_of(get_number_of_text_entries()) + entry_offset; -} - -uint16_t text_data_table::get_text_entry_size(uint16_t index) const -{ - return get_entry_size_in_bytes(decompression_buffer_, decompressed_size_, index); -} diff --git a/source/text_engine.cpp b/source/text_engine.cpp index 55ef2f0..ed90cda 100644 --- a/source/text_engine.cpp +++ b/source/text_engine.cpp @@ -9,9 +9,10 @@ #include "button_menu.h" #include "sprite_data.h" #include "fonts.h" -#include "text_data_table.h" #include "background_engine.h" #include "pokemon_data.h" +#include "FileContainerReader.h" +#include "text_tables.h" #define TEXT_CBB 0 #define TEXT_SBB 10 @@ -29,32 +30,18 @@ bool text_exit; // Doing it this way does mean that we need to completely restart decompression whenever we switch from dialog entry. // but given that it requires user input to do so, I believe it's worth it and not time-critical. // attribute noinline was used to make sure the compiler doesn't inline this code back into text_loop() -static __attribute__((noinline)) const u8 *read_dialogue_text_entry(uint8_t index, u8 *output_buffer) +static __attribute__((noinline)) const u8 *read_dialogue_text_entry(u32 index, u32 text_section, u8 *output_buffer) { - u8 text_decompression_buffer[6144]; - const u8 *text_entry; + u8 text_decompression_buffer[4096]; + const u8 **chunkList; + u32 numChunks; + u32 chunkSize; - text_data_table dialogue_table(text_decompression_buffer); + get_text_table_chunks(text_section, &chunkList, &numChunks, &chunkSize); + FileContainerReader dialogueTable(chunkList, numChunks, chunkSize); - dialogue_table.decompress(get_compressed_text_table(PTGB_INDEX)); - - text_entry = dialogue_table.get_text_entry(index); - memcpy(output_buffer, text_entry, dialogue_table.get_text_entry_size(index)); - - return output_buffer; -} - -static __attribute__((noinline)) const u8 *read_dialogue_text_entry(uint8_t index, uint8_t text_section, u8 *output_buffer) -{ - u8 text_decompression_buffer[6144]; - const u8 *text_entry; - - text_data_table dialogue_table(text_decompression_buffer); - - dialogue_table.decompress(get_compressed_text_table(text_section)); - - text_entry = dialogue_table.get_text_entry(index); - memcpy(output_buffer, text_entry, dialogue_table.get_text_entry_size(index)); + dialogueTable.init(text_decompression_buffer, sizeof(text_decompression_buffer)); + dialogueTable.readFile(index, output_buffer); return output_buffer; } @@ -117,7 +104,7 @@ int text_loop(int script) break; } - curr_text = (curr_line.has_text()) ? read_dialogue_text_entry(curr_line.get_text_entry_index(), diag_entry_text_buffer) : NULL; + curr_text = (curr_line.has_text()) ? read_dialogue_text_entry(PTGB_INDEX, curr_line.get_text_entry_index(), diag_entry_text_buffer) : NULL; // tte_set_margins(LEFT, TOP, RIGHT, BOTTOM); if (script != SCRIPT_DEBUG) @@ -143,7 +130,7 @@ int text_loop(int script) break; } - curr_text = (curr_line.has_text()) ? read_dialogue_text_entry(curr_line.get_text_entry_index(), diag_entry_text_buffer) : NULL; + curr_text = (curr_line.has_text()) ? read_dialogue_text_entry(PTGB_INDEX, curr_line.get_text_entry_index(), diag_entry_text_buffer) : NULL; char_index = 0; if (text_exit) diff --git a/source/text_tables.cpp b/source/text_tables.cpp new file mode 100644 index 0000000..072a586 --- /dev/null +++ b/source/text_tables.cpp @@ -0,0 +1,56 @@ +#include "text_tables.h" +#include "translated_text.h" +#include "CREDITS_chunk0_lz10_bin.h" +#include "GB_chunk0_lz10_bin.h" +#include "GENERAL_chunk0_lz10_bin.h" +#include "PKMN_NAMES_chunk0_lz10_bin.h" +#include "PKMN_NAMES_chunk1_lz10_bin.h" +#include "PTGB_chunk0_lz10_bin.h" +#include "PTGB_chunk1_lz10_bin.h" +#include "PTGB_chunk2_lz10_bin.h" +#include "RSEFRLG_chunk0_lz10_bin.h" + +static const u8 *CREDITS_chunk_list[] = {CREDITS_chunk0_lz10_bin}; +static const u8 *GB_chunk_list[] = {GB_chunk0_lz10_bin}; +static const u8 *GENERAL_chunk_list[] = {GENERAL_chunk0_lz10_bin}; +static const u8 *PKMN_NAMES_chunk_list[] = {PKMN_NAMES_chunk0_lz10_bin, PKMN_NAMES_chunk1_lz10_bin}; +static const u8 *PTGB_chunk_list[] = {PTGB_chunk0_lz10_bin, PTGB_chunk1_lz10_bin, PTGB_chunk2_lz10_bin}; +static const u8 *RSEFRLG_chunk_list[] = {RSEFRLG_chunk0_lz10_bin}; + +void get_text_table_chunks(u32 tableIndex, const u8 ***chunkList, u32 *numChunks, u32 *chunkSize) +{ + // every table except for RSEFRLG is 2048 bytes per chunk, so we can set that by default and only change it for RSEFRLG + *chunkSize = 2048; + switch(tableIndex) + { + case PTGB_INDEX: + *chunkList = PTGB_chunk_list; + *numChunks = sizeof(PTGB_chunk_list) / sizeof(PTGB_chunk_list[0]); + break; + case RSEFRLG_INDEX: + *chunkList = RSEFRLG_chunk_list; + *numChunks = sizeof(RSEFRLG_chunk_list) / sizeof(RSEFRLG_chunk_list[0]); + *chunkSize = 4096; + break; + case GB_INDEX: + *chunkList = GB_chunk_list; + *numChunks = sizeof(GB_chunk_list) / sizeof(GB_chunk_list[0]); + break; + case GENERAL_INDEX: + *chunkList = GENERAL_chunk_list; + *numChunks = sizeof(GENERAL_chunk_list) / sizeof(GENERAL_chunk_list[0]); + break; + case CREDITS_INDEX: + *chunkList = CREDITS_chunk_list; + *numChunks = sizeof(CREDITS_chunk_list) / sizeof(CREDITS_chunk_list[0]); + break; + case PKMN_NAMES_INDEX: + *chunkList = PKMN_NAMES_chunk_list; + *numChunks = sizeof(PKMN_NAMES_chunk_list) / sizeof(PKMN_NAMES_chunk_list[0]); + break; + default: + *chunkList = NULL; + *numChunks = 0; + break; + } +} \ No newline at end of file diff --git a/source/vertical_menu.cpp b/source/vertical_menu.cpp index eb1e7b2..82b645f 100644 --- a/source/vertical_menu.cpp +++ b/source/vertical_menu.cpp @@ -3,6 +3,7 @@ #include "global_frame_controller.h" #include "text_engine.h" #include "translated_text.h" +#include "text_tables.h" #define TILE_HEIGHT 8 #define TILE_WIDTH 8 @@ -209,13 +210,18 @@ MenuInputHandleState vertical_menu::handle_input() void vertical_menu::update_viewport() { uint8_t decompression_buffer[2048]; - text_data_table text_table(decompression_buffer); + const u8 **textTableChunkList; + u32 textTableNumChunks; + u32 textTableChunkSize; + + get_text_table_chunks(settings_.text_table_index, &textTableChunkList, &textTableNumChunks, &textTableChunkSize); + FileContainerReader text_table(textTableChunkList, textTableNumChunks, textTableChunkSize); clear_viewport(); if (settings_.text_table_index != INT32_MAX) { - text_table.decompress(get_compressed_text_table(settings_.text_table_index)); + text_table.init(decompression_buffer, sizeof(decompression_buffer)); } const unsigned num_visible_items = get_num_visible_items(settings_.height, settings_.margin_top, settings_.margin_bottom, settings_.item_height); @@ -301,10 +307,14 @@ const simple_item_widget_data &simple_item_renderer::get_data() const return data_; } -void simple_item_renderer::render_item(text_data_table &text_table, unsigned x, unsigned y, bool is_focused) +void simple_item_renderer::render_item(FileContainerReader &text_table, unsigned x, unsigned y, bool is_focused) { + // text_helper/main.py restricts each line to 1024 bytes. + u8 lineBuffer[1024]; + + text_table.readFile(data_.text.text_table_index, lineBuffer); tte_set_pos(x + data_.text.margin_left, y + data_.text.margin_top); - ptgb_write_simple(text_table.get_text_entry(data_.text.text_table_index), true); + ptgb_write_simple(lineBuffer, true); } MenuInputHandleState simple_item_renderer::handle_input() diff --git a/tools/make-file-container/src/main.cpp b/tools/make-file-container/src/main.cpp index cc3bf7a..29e1582 100644 --- a/tools/make-file-container/src/main.cpp +++ b/tools/make-file-container/src/main.cpp @@ -13,7 +13,7 @@ #define FILE_RECORD_NAME_LENGTH 16 #define LINE_BUFFER_SIZE 2048 #define PATH_BUFFER_SIZE 4096 -#define DEFAULT_CHUNK_SIZE 4096 +#define DEFAULT_CHUNK_SIZE 2048 typedef struct @@ -38,6 +38,37 @@ static void printUsage() printf(" -H, --header-out Write generated header to this folder\n"); } +/** + * @brief This function parses a directive. + * A directive starts with '@' and is used to set certain parameters for the container generation. + * Right now, we only support @chunkSize + */ +static void parseDirective(ContainerMetadata &meta, char *line) +{ + char * equals = strchr(line, '='); + if(equals == NULL) + { + fprintf(stderr, "Invalid directive (missing '='): %s\n", line); + return; + } + if(strncmp(line + 1, "chunkSize", 9) == 0) + { + char *endptr; + const long value = strtol(equals + 1, &endptr, 10); + if(*endptr != '\0' || value <= 0 || value > 0xFFFF) + { + fprintf(stderr, "Invalid chunk size value: %s\n", equals + 1); + + return; + } + meta.chunkSize = static_cast(value); + } + else + { + fprintf(stderr, "Unknown directive: %s\n", line); + } +} + /** * @brief This function parses a single line from the definition file, extracting the file path and optional alternative name. */ @@ -120,6 +151,12 @@ static bool parseDefinition(const char *defPath, ContainerMetadata &meta) continue; } + if(line[0] == '@') + { + parseDirective(meta, line); + continue; + } + if(!parseDefinitionLine(line, &filePath, &altName)) { fprintf(stderr, "Invalid entry at line %zu\n", line_no); diff --git a/tools/text_helper/main.py b/tools/text_helper/main.py index e6a56e6..7c3f405 100644 --- a/tools/text_helper/main.py +++ b/tools/text_helper/main.py @@ -24,6 +24,20 @@ class Languages(Enum): ChineseTR = 9 PortugueseBR = 10 +LanguageStringMap = { + "japanese": Languages.Japanese, + "english": Languages.English, + "french": Languages.French, + "german": Languages.German, + "italian": Languages.Italian, + "spanishEU": Languages.SpanishEU, + "spanishLA": Languages.SpanishLA, + "korean": Languages.Korean, + "chineseSI": Languages.ChineseSI, + "chineseTR": Languages.ChineseTR, + "portugueseBR": Languages.PortugueseBR, +} + class Font: def __init__(self, fileName, numColors, numChars, numCharsX, numCharsY, cellWidth, cellHeight, charWidth, charHeight): self.fileName = fileName @@ -840,50 +854,35 @@ def convert_item(ogDict, lang, context=None): ogDict["bytes"] = encode_formatted_text(normalized_text, arr, lang, entry_id, context) return ogDict -def write_text_bin_file(filename, dictionary, lang, section, context=None): - MAX_BIN_SIZES = { - "PTGB": 6144, - "RSEFRLG": 3444, - "GB": 9999, - "GENERAL": 2048, - "CREDITS": 2048, - "PKMN_NAMES": 3072, - } +# in order to safely use getPointerToFileInDecompressionBuffer() in FileContainerReader +# we must ensure that a table that is used this way fits inside a single chunk +def get_file_container_chunk_size(section): + if section in ("RSEFRLG"): + return 4096 + else: + return 2048 - Path(filename).parent.mkdir(parents=True, exist_ok=True) - with open(filename, 'wb') as binFile: - # Let the first byte indicate the number of entries - dict_size = len(dictionary) - # We need to store 2 bytes instead of one, because not aligning the data to 16 bits will cause corruption on the gba. - binFile.write(bytes([dict_size & 0xFF, (dict_size >> 8) & 0xFF])) - # After this initial byte, we will read the offset (16 bit) of each line (relative to the last index byte) - index = bytearray(len(dictionary) * 2) - # bindata will contain the binary data of each entry - bindata = bytearray() - current_offset = 0 - num = 0 - # Append every line's binary data to bindata - # keep an index of the binary offset within bindata at which each line starts +def write_text_file_container(filename, dictionary, lang, section, context=None): + buildpath = str(BUILD_DIR) + "/" + Path(buildpath).mkdir(parents=True, exist_ok=True) + defPath = buildpath + filename + ".containerdef" + with open(defPath, 'wb') as defFile: + defLine = "@chunkSize=" + str(get_file_container_chunk_size(section)) + "\n" + defFile.write(defLine.encode("utf-8")) + for key, line in dictionary.items(): - dictionary[key] = convert_item(line, lang, context) - # store the offset of the line in the index as a 16 bit little endian value - index[num * 2] = (current_offset & 0xFF) - index[num * 2 + 1] = (current_offset >> 8) & 0xFF - linedata = bytes.fromhex(dictionary[key]['bytes']) - - bindata.extend(linedata) - current_offset += len(linedata) - - num += 1 - - # Write the index and bindata to the file - binFile.write(index) - binFile.write(bindata) - binFile.seek(0, os.SEEK_END) - if binFile.tell() > MAX_BIN_SIZES[section]: - log_warning_error(lang, "Error", f'Section {section} exceeds the max binary file size by {binFile.tell() - MAX_BIN_SIZES[section]} bytes!', context=context) - binFile.close() + print(f"Processing {section} entry '{key}' for language {lang.name}...") + with open(buildpath + str(key), 'wb') as lineFile: + dictionary[key] = convert_item(line, lang, context) + linedata = bytes.fromhex(dictionary[key]['bytes']) + lineFile.write(linedata) + lineFile.close() + + defLine = buildpath + str(key) + "\n" + defFile.write(defLine.encode("utf-8")) + defFile.close() + def write_enum_to_header_file(hFile, prefix, dictionary): num = 0 @@ -1098,7 +1097,8 @@ def generate_header_file(): sectionEnds = [] index = 0 - lang = Languages.English # This doesn't matter, it just needs to be there. They're all the same + # The lang doesn't matter. They're all the same in this context. + lang = Languages.English for section in textSections: num = write_enum_to_header_file(hFile, section + "_", mainDict[lang.name][section]) hFile.write("#define " + section + "_INDEX " + str(index)) @@ -1136,7 +1136,6 @@ def generate_header_file(): hFile.write(f"\t{{{', '.join(values)}}},\n") hFile.write("};\n\n") - hFile.write("const u8* get_compressed_text_table(int table_index);\n") hFile.write("u8 get_text_box_type(int table_index, int text_index);\n") hFile.write("extern const u8* const text_box_type_tables[NUM_TEXT_SECTIONS];\n") @@ -1144,21 +1143,16 @@ def generate_header_file(): hFile.write("\n#endif") hFile.close() -def generate_text_tables(): +def generate_text_tables(lang): print("\tGenerating text tables") - for lang in Languages: - for section in textSections: - table_file = os.curdir + '/to_compress/' + section + '_' + lang.name.lower() + '.bin' - write_text_bin_file(table_file, mainDict[lang.name][section], lang, section, build_context) + for section in textSections: + table_name = section + write_text_file_container(table_name, mainDict[lang.name][section], lang, section, build_context) def generate_cpp_file(): print("\tGenerating cpp file") with open(TRANSLATED_CPP_PATH, 'w') as cppFile: cppFile.write("// THIS FILE HAS BEEN GENERATED BY text_helper/main.py !\n#include \"translated_text.h\"\n#include \"dbg/debug_mode.h\"\n") - # generate includes for each language - for lang in Languages: - for section in textSections: - cppFile.write("#include \"" + section.upper() + "_" + lang.name.lower() + "_lz10_bin.h\"\n") cppFile.write("\n") for section in textSections: @@ -1177,22 +1171,6 @@ def generate_cpp_file(): cppFile.write(f"\n\t{section_var}_box_types,") cppFile.write("\n};\n") - cppFile.write("\nconst u8* get_compressed_text_table(int table_index)\n") - - for i, lang in enumerate(Languages): - cppFile.write(f"\n#{'el' if i > 0 else ''}if PTGB_BUILD_LANGUAGE == {lang.value + 1}\n") - cppFile.write("{\n") - cppFile.write("\tswitch (table_index)\n\t{\n") - for section in textSections: - cppFile.write("\tcase (" + section + "_INDEX):\n") - if(section == "PTGB"): - cppFile.write("\tdefault:\n") - cppFile.write("\t\treturn " + section + "_" + lang.name.lower() + "_lz10_bin;\n") - cppFile.write("\t\tbreak;\n") - cppFile.write("\t}\n") - cppFile.write("}\n") - cppFile.write(f"#else\n#error \"Unsupported PTGB_BUILD_LANGUAGE\"\n#endif") - cppFile.write("\n\nu8 get_text_box_type(int table_index, int text_index)\n") cppFile.write("{\n") cppFile.write("\tif (text_index < 0)\n") @@ -1210,18 +1188,17 @@ def generate_cpp_file(): cppFile.write("\t}\n") cppFile.write("}\n") -def output_json_file(): +def output_json_file(lang): print("\tOutputting json file") - for lang in Languages: - for section in textSections: - for item in mainDict[lang.name][section]: - arr = get_language_config(lang).char_array["array"] - mainDict[lang.name][section][item]["text"] = render_debug_text( - mainDict[lang.name][section][item]["bytes"], arr - ) + for section in textSections: + for item in mainDict[lang.name][section]: + arr = get_language_config(lang).char_array["array"] + mainDict[lang.name][section][item]["text"] = render_debug_text( + mainDict[lang.name][section][item]["bytes"], arr + ) with open(OUTPUT_JSON_PATH, 'w', encoding='utf-8') as jsonFile: - jsonFile.write(json.dumps(mainDict, ensure_ascii=False, indent=2)) + jsonFile.write(json.dumps(mainDict[lang.name], ensure_ascii=False, indent=2)) def are_generated_files_stale(source_files, generated_files): source_paths = [Path(path) for path in source_files] @@ -1238,15 +1215,15 @@ def are_generated_files_stale(source_files, generated_files): oldest_generated_mtime = min(path.stat().st_mtime for path in generated_paths) return newest_source_mtime > oldest_generated_mtime -def update_generated_files(target_name, source_files, generated_files, generate_function): +def update_generated_files(target_name, source_files, generated_files, generate_function, lang, force_generate): if debugpy.is_client_connected(): print(f"\tDebugger connected, rebuilding {target_name}!") - generate_function() + generate_function(lang) return - if are_generated_files_stale(source_files, generated_files): + if force_generate or are_generated_files_stale(source_files, generated_files): print(f"\t{target_name} outputs are outdated or missing. Rebuilding...") - generate_function() + generate_function(lang) return print(f"\t{target_name} outputs are up to date. Skipping rebuild.") @@ -1373,7 +1350,7 @@ def get_font_source_files(): def get_font_generated_files(): return [FONTS_H_PATH] -def generate_font_files(): +def generate_font_files(lang): print("\tGenerating font tables:") generate_tables() build_h() @@ -1384,6 +1361,8 @@ def update_font_files(): source_files=get_font_source_files(), generated_files=get_font_generated_files(), generate_function=generate_font_files, + lang = Languages.English, + force_generate=False ) def get_text_source_files(): @@ -1396,22 +1375,27 @@ def get_text_generated_files(): generated_files.append(Path(os.curdir) / "to_compress" / f"{section}_{lang.name.lower()}.bin") return generated_files -def generate_text_files(): +def generate_text_files(lang): generate_header_file() - generate_text_tables() + generate_text_tables(lang) generate_cpp_file() - output_json_file() + output_json_file(lang) -def update_text_files(): +def update_text_files(lang): + # force_generate is set to true because we're only generating the BUILD_LANG. + # therefore if we generate another lang, we need to update it again too. + # So we might as well always update it. update_generated_files( target_name="Text", source_files=get_text_source_files(), generated_files=get_text_generated_files(), generate_function=generate_text_files, + lang=lang, + force_generate=True ) def main(): - _, _, build_xlsx_mode = parse_build_args(sys.argv) + lang_name, _, build_xlsx_mode = parse_build_args(sys.argv) print("Running text_helper:") update_font_files() update_xlsx_file(build_xlsx_mode) @@ -1419,7 +1403,7 @@ def main(): print("text_helper finished!\n") return transfer_xlsx_to_dict() - update_text_files() + update_text_files(LanguageStringMap[lang_name]) print("text_helper finished!\n") if __name__ == "__main__":