Poke_Transporter_GB/source/text_engine.cpp
easyaspi314 f3a164076d Cut VRAM usage roughly in half
Memory manufacturers and AAA game studios hate her!

With some clever rearranging, PTGB now uses only 36 KiB of the 64 KiB
BG VRAM. This means the remaining 28 KiB is free to use in the future.

Caveats: The flex tileset must not use more that 6 KiB. There is a check
for it added.

VRAM Map
  06000000 - 06000800 ( 2K): Eternal backdrop and textbox tileset
  06000800 - 06002000 ( 6K): Flex BG tileset
  06002000 - 06002400 ( 2K): BG0: Backdrop tilemap
  06002800 - 06003000 ( 2K): BG1: Flex BG tilemap
  06003000 - 06003800 ( 2K): BG2: Textbox tilemap
  06003800 - 06004000 ( 2K): BG3: Text tilemap
  06004000 -~06009000 (20K): Text rendering bitmap
  06009000 - 06010000 (28K): Free work RAM
  06010000 - 06018000 (32K): Sprite tileset

Additionally:
- Explicitly clear VRAM on boot
- Dirty workaround to fix box drawing buffer overflow.
- Flex backgrounds use a table instead of a huge switch statement
- Textbox tile IDs are now const u16[] instead of int[]
2026-07-02 16:20:19 -04:00

509 lines
17 KiB
C++

#include <tonc.h>
#include <cstring>
#include "text_engine.h"
#include "global_frame_controller.h"
#include "pkmn_font.h"
#include "script_array.h"
#include "dbg/debug_mode.h"
#include "button_menu.h"
#include "sprite_data.h"
#include "fonts.h"
#include "text_data_table.h"
#include "background_engine.h"
#include "pokemon_data.h"
script_obj curr_line;
uint char_index;
uint line_char_index;
const byte *curr_text;
bool text_exit;
// This function was separated from text_loop to reduce the scope of the text_decompression_buffer.
// if we didn't do this, the decompression_buffer would be kept on the stack (=IWRAM) for the entire duration of the
// text_loop() call. This is particularly bad because the whole mystery_gift_builder sequence is being triggered from within
// text_loop(). And there we need all the IWRAM we can muster.
// 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)
{
u8 text_decompression_buffer[6144];
const u8 *text_entry;
text_data_table dialogue_table(text_decompression_buffer);
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));
return output_buffer;
}
// This will have to be changed to be dynamic to support nicknamed Pokemon that are in a different language than the current build.
// Maybe combine Japanese and Latin into one larger font?
#if PTGB_BUILD_LANGUAGE == 1
#define BUILD_FONT &japanese_normalFont
#else
#define BUILD_FONT &latin_normalFont
#endif
void init_text_engine()
{
// Load the TTE
// tte_init_se(3, BG_CBB(TEXT_CBB) | BG_SBB(TEXT_SBB) | BG_PRIO(0), 0, CLR_WHITE, 14, &japanese_smallFont, NULL);
tte_init_chr4c(3, // BG 3
BG_CBB(TILESET_TEXT) | BG_SBB(TILEMAP_TEXT), // Charblock 0; screenblock 10
0xF000, // Screen-entry offset
bytes2word( // Color attributes:
13, // Text color
15, // Shadow color
0, // Paper
0), // Special
CLR_WHITE, // White text
BUILD_FONT, // Custom font
NULL // Use default chr4 renderer
);
tte_init_con();
pal_bg_bank[15][INK_WHITE] = CLR_WHITE; // White
pal_bg_bank[15][INK_DARK_GREY] = 0b0000110001100010; // Dark Grey
// 14 will be changed to game color
// Set default variables
char_index = 0;
line_char_index = 0;
text_exit = false;
}
int text_loop(int script)
{
// we have restricted the dialog entries to 1024 bytes in the text_helper main.py
// so we shouldn't run into problems when we only use 1 KB to contain a text entry.
u8 diag_entry_text_buffer[1024];
switch (script)
{
case SCRIPT_DEBUG:
curr_line = transfer_script_params[0];
break;
case SCRIPT_TRANSFER:
curr_line = transfer_script_params[T_SCRIPT_START];
break;
case SCRIPT_EVENT:
curr_line = event_script_params[E_SCRIPT_START];
break;
}
curr_text = (curr_line.has_text()) ? read_dialogue_text_entry(curr_line.get_text_entry_index(), diag_entry_text_buffer) : NULL;
// tte_set_margins(LEFT, TOP, RIGHT, BOTTOM);
if (script != SCRIPT_DEBUG)
{
while (true) // This loops through all the connected script objects
{
if (curr_text != NULL && curr_text[char_index] != 0xFF && curr_text[char_index] != 0xFB)
{
ptgb_write_textbox(curr_text, false, true,
PTGB_INDEX, curr_line.get_text_entry_index(), false);
}
// wait_for_user_to_continue();
line_char_index = 0;
switch (script)
{
case BTN_TRANSFER:
curr_line = transfer_script_params[text_next_obj_id(curr_line)];
break;
case BTN_EVENTS:
curr_line = event_script_params[text_next_obj_id(curr_line)];
break;
}
curr_text = (curr_line.has_text()) ? read_dialogue_text_entry(curr_line.get_text_entry_index(), diag_entry_text_buffer) : NULL;
char_index = 0;
if (text_exit)
{
hide_textbox();
erase_textbox_tiles();
tte_erase_screen();
text_exit = false;
return 0;
}
}
}
else // Debug script loop
{
u16 debug_charset[256];
load_localized_charset(debug_charset, 3, ENGLISH);
int text_section = 0;
int text_key = 0;
while (true)
{
bool exit = false;
bool update_text = true;
bool instant_text = false;
key_poll();
while (!exit)
{
if (key_hit(KEY_LEFT))
{
text_key = (text_key + (text_section_lengths[text_section] - 1)) % text_section_lengths[text_section];
update_text = true;
}
else if (key_hit(KEY_RIGHT))
{
text_key = (text_key + 1) % text_section_lengths[text_section];
update_text = true;
}
else if (key_hit(KEY_UP))
{
if (text_section == 0) {
text_section = NUM_TEXT_SECTIONS - 1;
}
else {
text_section = ((text_section + (NUM_TEXT_SECTIONS - 1)) % NUM_TEXT_SECTIONS);
}
update_text = true;
}
else if (key_hit(KEY_DOWN))
{
text_section = (text_section + (NUM_TEXT_SECTIONS - 1)) % NUM_TEXT_SECTIONS;
update_text = true;
}
else if (key_hit(KEY_START) || key_hit(KEY_SELECT))
{
instant_text = key_hit(KEY_START); // instant with start, not with select
exit = true;
tte_erase_line();
}
if (update_text)
{
if (text_key >= text_section_lengths[text_section])
{
text_key = text_section_lengths[text_section] - 1;
}
if (text_section >= NUM_TEXT_SECTIONS)
{
text_section = NUM_TEXT_SECTIONS - 1;
}
tte_set_pos(0, 0);
tte_erase_rect(0, 0, 240, 160);
ptgb_write_debug(debug_charset, "(", true);
ptgb_write_debug(debug_charset, ptgb::to_string(text_section), true);
ptgb_write_debug(debug_charset, ", ", true);
ptgb_write_debug(debug_charset, ptgb::to_string(text_key), true);
ptgb_write_debug(debug_charset, ")", true);
update_text = false;
}
global_next_frame();
}
line_char_index = 0;
curr_text = read_dialogue_text_entry(text_key, text_section, diag_entry_text_buffer);
char_index = 0;
if (curr_text != NULL && curr_text[char_index] != 0xFF && curr_text[char_index] != 0xFB)
{
ptgb_write_textbox(curr_text, instant_text, true,
text_section, text_key, true);
}
update_text = true;
hide_textbox();
tte_erase_rect(0, 0, H_MAX, V_MAX);
if (text_exit)
{
text_exit = false;
return 0;
}
}
}
}
int text_next_obj_id(script_obj current_line)
{
if (current_line.get_cond_id() == 0)
{
return current_line.get_true_index();
}
else
{
const bool ret = run_conditional(current_line.get_cond_id());
VBlankIntrWait(); // this is needed to handle interrupts
if (ret)
{
return current_line.get_true_index();
}
return current_line.get_false_index();
}
}
void set_text_exit()
{
text_exit = true;
key_poll(); // This removes the "A Hit" when exiting the text
}
// Implement a version that creates the textbox as well
int ptgb_write_textbox(const byte *text, bool instant, bool waitForUser,
int text_section, int text_key, bool eraseMainBox)
{
tte_erase_rect(0, 0, H_MAX, V_MAX);
erase_textbox_tiles();
create_textbox(text_section, text_key, eraseMainBox);
// Set up Fennel if we are in a PTGB dialogue box
if (get_curr_flex_background() == FLEXBG_FENNEL && text_section == PTGB_INDEX)
{
load_flex_background(FLEXBG_FENNEL, 2);
}
int out = ptgb_write(text, instant, 9999, text_box_type_tables[text_section][text_key]); // This is kinda silly but it'll work.
if (waitForUser)
{
int right = H_MAX - 5;
int bottom = V_MAX - 5;
wait_for_user_to_continue(right, bottom);
}
if (eraseMainBox)
{
tte_erase_rect(0, 0, H_MAX, V_MAX);
hide_textbox();
erase_textbox_tiles();
}
return out;
}
// Implement a version that just writes the whole string
int ptgb_write_simple(const byte *text, bool instant)
{
return ptgb_write(text, instant, 9999, -1); // This is kinda silly but it'll work.
}
// Re-implementing TTE's "tte_write" to use the gen 3 character encoding chart
int ptgb_write(const byte *text, bool instant, int length, int box_type)
{
instant = instant || g_debug_options.instant_text_speed;
if (text == NULL)
return 0;
int left, top, right, bottom;
if (box_type == -1)
{
left = 0;
top = 0;
right = H_MAX;
bottom = V_MAX;
}
else
{
left = 8 * (box_type_info[box_type][BOX_TYPE_VAL_START_TILE_X] + 1);
top = 8 * (box_type_info[box_type][BOX_TYPE_VAL_START_TILE_Y] + 1);
right = left + box_type_info[box_type][BOX_TYPE_VAL_PIXELS_PER_LINE];
bottom = top + box_type_info[box_type][BOX_TYPE_VAL_NUM_OF_LINES] * 16;
}
uint ch, gid;
char *str = (char *)text;
TTC *tc = tte_get_context();
TFont *font;
int num = 0;
while ((ch = *str) != 0xFF && num < length)
{
if (get_frame_count() % 2 == 0 || key_held(KEY_B) || key_held(KEY_A) || instant)
{
str++;
switch (ch)
{
case 0xFA:
if (g_debug_options.display_control_char)
{
tc->drawgProc(0x79);
}
wait_for_user_to_continue(right, bottom);
scroll_text(instant, tc, left, top, right, bottom);
break;
case 0xFB:
if (g_debug_options.display_control_char)
{
tc->drawgProc(0xB9);
}
wait_for_user_to_continue(right, bottom);
tte_erase_rect(left, top, right, bottom);
tte_set_pos(left, top);
break;
case 0xFC:
ch = *str;
str++;
num += 1;
if (g_debug_options.display_control_char)
{
for (uint i = 0; i < ch; i++)
{
tc->drawgProc(0xB9);
}
}
else
{
tc->cursorX += tc->font->widths[0xFC] * ch;
}
break;
case 0xFE:
if (g_debug_options.display_control_char)
{
tc->drawgProc(0xEF);
}
tc->cursorY += tc->font->charH;
tc->cursorX = tc->marginLeft;
break;
default:
// Get glyph index and call renderer
font = tc->font;
gid = ch - font->charOffset;
if (tc->charLut)
gid = tc->charLut[gid];
// Character wrap
int charW = font->widths ? font->widths[gid] : font->charW;
// Draw and update position
tc->drawgProc(gid);
tc->cursorX += charW;
}
num += 1;
}
if (get_curr_flex_background() == FLEXBG_FENNEL && !instant)
{
fennel_speak(((num / 4) % 4) + 1);
}
if (!instant)
{
global_next_frame();
}
}
// Return characters used (PONDER: is this really the right thing?)
return 0; // str - text;
}
// This is mostly used for debug stuff, I shouldn't rely it on it much.
int ptgb_write_debug(const u16 *charset, const char *text, bool instant)
{
byte temp_holding[256];
int i;
for (i = 0; i < 256; i++)
{
if (text[i] == '\0')
{
temp_holding[i] = 0xFF;
i = 256;
}
else if (text[i] == '\n')
{
temp_holding[i] = 0xFE;
}
else
{
u16 utf16_char;
// we need to use this conversion function in order to convert char values >= 0x80
// correctly to UTF-16 (which is used by the charset)
convert_utf8_to_utf16_char((const u8*)(text + i), utf16_char);
// WARNING: the conversion of the text characters from u8 to u16 done here, is incorrect
// for every character value >= 0x80. The character set uses UTF-16 to represent characters.
// But the input text is in UTF-8.
temp_holding[i] = get_char_from_charset(charset, utf16_char);
}
}
return ptgb_write_simple(temp_holding, instant);
}
void wait_for_user_to_continue(int right, int bottom)
{
if (get_curr_flex_background() == FLEXBG_FENNEL)
{
if (get_missingno_enabled())
{
fennel_speak(0);
fennel_blink(4);
}
else
{
fennel_speak(0);
}
}
//obj_set_pos(scroll_indicator_red, right-16, bottom-11);
//obj_set_pos(scroll_indicator_gray, right-16, bottom-10);
//obj_unhide(scroll_indicator_red, 0);
key_poll();
while (!(key_hit(KEY_A) || key_hit(KEY_B)))
{
/*
int frameCount = get_frame_count();
if (frameCount % 60 == 0) {
obj_unhide(scroll_indicator_red, 0);
obj_hide(scroll_indicator_gray);
}
else if (frameCount % 30 == 0) {
obj_hide(scroll_indicator_red);
obj_unhide(scroll_indicator_gray, 0);
}
*/
global_next_frame();
}
//obj_hide(scroll_indicator_red);
//obj_hide(scroll_indicator_gray);
}
void scroll_text(bool instant, TTC *tc, int left, int top, int right, int bottom)
{
for (int i = 1; i <= tc->font->charH; i++)
{
REG_BG3VOFS = i;
tte_erase_rect(left, top - tc->font->charH, right, top + i);
if (!instant)
{
global_next_frame();
}
}
REG_BG3VOFS = 0;
// The map starts at tile 0 in the top left, increases by 1 as you go down, and then loops back at the top.
for (int i = 0; i < 30; i++)
{
tonccpy(&tile_mem[TILESET_TEXT][0 + (i * 20)], &tile_mem[TILESET_TEXT][2 + (i * 20)], 20 * 32);
}
// Remove text that went outside of the box and set the position
tte_erase_rect(left, top - tc->font->charH, right, top);
tte_set_pos(left, bottom - (8 + (2 * tc->font->charH))); // The newline will trigger after this and move it down a line
tc->cursorY = bottom - tc->font->charH;
tc->cursorX = left;
}