Poke_Transporter_GB/source/text_engine.cpp
2026-02-13 21:25:31 -05:00

482 lines
15 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 "debug_mode.h"
#include "button_menu.h"
#include "sprite_data.h"
#include "fonts.h"
#include "text_data_table.h"
#include "background_engine.h"
#define TEXT_CBB 0
#define TEXT_SBB 10
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(TEXT_CBB) | BG_SBB(TEXT_SBB), // 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();
// tte_set_margins(LEFT, TOP, RIGHT, BOTTOM);
// tte_set_pos(LEFT, TOP);
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)
{
REG_BG1CNT = (REG_BG1CNT && !BG_PRIO_MASK) | BG_PRIO(2); // Show Fennel
show_text_box();
while (true) // This loops through all the connected script objects
{
if (curr_text != NULL && curr_text[char_index] != 0xFF && curr_text[char_index] != 0xFB)
{
tte_set_pos(LEFT, TOP);
tte_erase_rect(LEFT, TOP, RIGHT, BOTTOM);
ptgb_write(curr_text, false);
}
wait_for_user_to_continue(false);
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_text_box();
tte_erase_rect(LEFT, TOP, RIGHT, BOTTOM);
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_identifier = 0;
while (true)
{
bool exit = false;
bool update_text = true;
bool instant_text = false;
key_poll();
while (!exit)
{
if (key_hit(KEY_LEFT))
{
text_identifier = (text_identifier + (text_section_lengths[text_section] - 1)) % text_section_lengths[text_section];
update_text = true;
}
else if (key_hit(KEY_RIGHT))
{
text_identifier = (text_identifier + 1) % text_section_lengths[text_section];
update_text = true;
}
else if (key_hit(KEY_UP))
{
text_section = (text_section + 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;
}
if (update_text)
{
if (text_identifier > text_section_lengths[text_section])
{
text_identifier = text_section_lengths[text_section];
}
if (text_section > NUM_TEXT_SECTIONS)
{
text_section = NUM_TEXT_SECTIONS;
}
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_identifier), true);
ptgb_write_debug(debug_charset, ")", true);
update_text = false;
}
global_next_frame();
}
line_char_index = 0;
curr_text = read_dialogue_text_entry(text_identifier, text_section, diag_entry_text_buffer);
char_index = 0;
if (curr_text != NULL && curr_text[char_index] != 0xFF && curr_text[char_index] != 0xFB)
{
if (text_section == PTGB_INDEX)
{
reset_textbox();
}
else
{
create_textbox(4, 1, 160, 80, true);
}
show_text_box();
tte_erase_rect(0, 0, 240, 160);
ptgb_write(curr_text, instant_text);
}
wait_for_user_to_continue(false);
update_text = true;
hide_text_box();
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 show_text_box()
{
REG_BG2CNT = (REG_BG2CNT & ~BG_PRIO_MASK) | BG_PRIO(1);
}
void hide_text_box()
{
REG_BG2CNT = (REG_BG2CNT & ~BG_PRIO_MASK) | BG_PRIO(3);
}
void set_text_exit()
{
text_exit = true;
key_poll(); // This removes the "A Hit" when exiting the text
}
// Implement a version that just writes the whole string
int ptgb_write(const byte *text, bool instant)
{
return ptgb_write(text, instant, 9999); // 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)
{
instant = instant || INSTANT_TEXT_SPEED;
if (text == NULL)
return 0;
uint ch, gid;
char *str = (char *)text;
TTC *tc = tte_get_context();
TFont *font;
int num = 0;
/*
if (curr_text[char_index] == 0xFB) // This will need to be moved
{
line_char_index += char_index;
line_char_index++;
// Low key kinda scuffed, but it works to split the string
curr_text = &curr_line.get_text()[line_char_index];
}
*/
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 (DISPLAY_CONTROL_CHAR)
{
tc->drawgProc(0x79);
}
wait_for_user_to_continue(false);
scroll_text(instant, tc);
tc->cursorY += tc->font->charH;
tc->cursorX = tc->marginLeft;
break;
case 0xFB:
if (DISPLAY_CONTROL_CHAR)
{
tc->drawgProc(0xB9);
}
wait_for_user_to_continue(true);
break;
case 0xFC:
ch = *str;
str++;
num += 1;
if (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 (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;
// We don't want this tbh- all of the newlines should deal with moving to the next line
/* if (tc->cursorX + charW > tc->marginRight)
{
tc->cursorY += 10; // font->charH;
tc->cursorX = tc->marginLeft;
} */
// Draw and update position
tc->drawgProc(gid);
tc->cursorX += charW;
}
num += 1;
}
if (get_curr_flex_background() == BG_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
{
temp_holding[i] = get_char_from_charset(charset, text[i]);
}
}
return ptgb_write(temp_holding, instant);
}
// Adding this to avoid compiler issues temporarilly
int ptgb_write(const char *text)
{
return 0;
}
void wait_for_user_to_continue(bool clear_text)
{
if (get_curr_flex_background() == BG_FENNEL)
{
if (get_missingno_enabled())
{
fennel_speak(0);
fennel_blink(4);
}
else
{
fennel_speak(0);
}
}
key_poll();
while (!(key_hit(KEY_A) || key_hit(KEY_B) || curr_text == NULL))
{
global_next_frame();
}
if (clear_text)
{
tte_erase_rect(LEFT, TOP, RIGHT, BOTTOM);
tte_set_pos(LEFT, TOP);
}
}
void scroll_text(bool instant, TTC *tc)
{
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[TEXT_CBB][14 + (i * 20)], &tile_mem[TEXT_CBB][16 + (i * 20)], 2 * 2 * 32);
}
// Remove text that went outside of the box and set the position
tte_erase_rect(LEFT, TOP + tc->font->charH, RIGHT, BOTTOM);
tte_set_pos(LEFT, BOTTOM - (8 + (2 * tc->font->charH))); // The newline will trigger after this and move it down a line
}