Poke_Transporter_GB/source/main.cpp
Philippe Symons eef173b0d2 Fix crash + unrelated buffer overflow + some optimizations
There was a crash happening with ptgb::vector when you'd press A on the CONFIRM button of the box screen. It only occurred on actual gba hardware and
was a real heisenbug: as soon as you'd add code to display logs on screen, the problem would disappear. So it was very difficult to figure this one
out. We're not even entirely sure why, but it looks like the malloc/realloc/free use in ptgb::vector would cause issues.

Maybe it was alignment, but after messing with the code we also saw a warning appear in the terminal telling us that realloc wouldn't properly
deal with non-POD types. It complained about this very thing while referring to the add_track() function, which stores ptgb::vectors inside another
ptgb::vector. We also didn't have a custom copy constructor yet to actually copy the buffer instead of its pointer.
All of these could potentially have led to the crash. But debugging during the link cable flow was difficult, so we were never able to confirm it in
a debugger, log or dump.

Because I suspected the high IWRAM consumption (especially now with ZX0 decompression) for a while, I also did an optimization in mystery_gift_builder
to pass global_memory_buffer as its section_30_data buffer instead. This reduces IWRAM consumption by 4 KB.

There was another problem I discovered during my crash hunt: the out_array (now payload_buffer) was allocated as a 672 byte array, but the payloads
were actually 707 bytes. Therefore writing this to the buffer caused a buffer overflow, thereby corrupting the global variables appearing after it in
IWRAM. It turned out eventually that none of these variables were really critical, but it could explain some minor bugs GearsProgress has seen.

I also did a few performance optimizations:

- At various stages in the code, for loops were used to copy data from one buffer into another byte-by-byte. This was far from optimal because the gba
cpu can load/copy 4 bytes at a time if you ask it to. So I replaced those with memcpy(), which is a hand-optimized assembly function to copy data
using this principle.

- generate_payload was being called twice: once at start_link and once at continue_link, giving the exact same result, even though it was already
being stored in a global buffer allocated in IWRAM. This was also a fairly heavy function. So I optimized the code to only initialize it once in
the script chain and then just retrieve the buffer.

- generate_payload was constructing the eventual payload twice even within the same call. That's because it first merged z80_rng_seed, z80_payload
and z80_patchlist into a full_data ptgb::vector, after which it then copied the data again to out_array (now called payload_buffer). I eliminated the
full_data vector now.
2025-06-18 10:23:03 +02:00

635 lines
15 KiB
C++

#include <tonc.h>
#include <cstdlib>
// #include <maxmod.h> //Music
#include "libstd_replacements.h"
#include "flash_mem.h"
#include "interrupt.h"
#include "gb_link.h"
#include "gameboy_colour.h"
#include "pokemon.h"
#include "random.h"
#include "text_engine.h"
#include "background_engine.h"
#include "pokemon_party.h"
#include "pokemon_data.h"
#include "script_array.h"
#include "sprite_data.h"
#include "button_handler.h"
#include "button_menu.h"
#include "debug_mode.h"
// #include "soundbank.h"
// #include "soundbank_bin.h"
#include "dex_handler.h"
#include "pokedex.h"
#include "global_frame_controller.h"
#include "pkmn_font.h"
#include "save_data_manager.h"
#include "mystery_gift_injector.h"
#include "mystery_gift_builder.h"
#include "multiboot_upload.h"
#include "rom_data.h"
#include "libraries/Pokemon-Gen3-to-Gen-X/include/save.h"
#include "text_data_table.h"
/*
TODO:
- Add in putting item from Pkmn to bag in gen 2
- Determine if transfered Shiny Pokemon are square/star sparkles
- Music and sound effects
- Wii Channel
- Events
- MissingNo/Enigma Berry
- Text translations
- Add support for other languages
- Add in dolls for gen 2/3
- Doxygen generation
--------
*/
int delay_counter = 0;
int curr_selection = 0;
bool skip = true;
rom_data curr_rom;
Button_Menu yes_no_menu(1, 2, 40, 24, false);
/*
int test_main(void) Music
{
irq_init(NULL);
// Initialize maxmod with default settings
// pass soundbank address, and allocate 8 channels.
irq_set(II_VBLANK, mmVBlank, 0);
irq_enable(II_VBLANK);
mmInitDefault((mm_addr)soundbank_bin, 8);
mmStart(MOD_FLATOUTLIES, MM_PLAY_LOOP);
// Song is playing now (well... almost)
while (1)
{
// ..process game logic..
// Update Maxmod
mmFrame();
// Wait for new frame (SWI 5)
VBlankIntrWait();
// ..update graphical data..
}
}
*/
// (R + G*32 + B*1024)
#define RGB(r, g, b) (r + (g * 32) + (b * 1024))
// make sure outBuffer is large enough! Should be at least hex_len + 1
template <typename I>
void n2hexstr(char* outBuffer, I w, size_t hex_len = sizeof(I) << 1)
{
static const char *digits = "0123456789ABCDEF";
memset(outBuffer, '0', hex_len);
outBuffer[hex_len] = '\0'; // we must make sure to terminate the string
for (size_t i = 0, j = (hex_len - 1) * 4; i < hex_len; ++i, j -= 4)
outBuffer[i] = digits[(w >> j) & 0x0f];
}
void load_graphics()
{
tte_erase_rect(0, 0, H_MAX, V_MAX);
// Load opening background first so it hides everything else
load_flex_background(BG_OPENING, 1);
load_background();
load_textbox_background();
load_eternal_sprites();
// Set up global yes no button
yes_no_menu.clear_vector();
yes_no_menu.add_button(Button(button_yes), true);
yes_no_menu.add_button(Button(button_no), false);
}
void initalization_script(void)
{
// Initalizations
REG_DISPCNT = DCNT_BLANK | DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_BG2 | DCNT_BG3 | DCNT_OBJ | DCNT_OBJ_1D;
irq_init(NULL);
irq_enable(II_VBLANK);
// Disable for save data read/write
REG_IME = 0;
REG_IE = 0;
// Sound bank init
irq_init(NULL);
irq_enable(II_VBLANK);
// irq_set(II_VBLANK, mmVBlank, 0); //Music
// mmInitDefault((mm_addr)soundbank_bin, 8); //Music
// mmStart(MOD_FLATOUTLIES, MM_PLAY_LOOP); //Music
// Graphics init
oam_init(obj_buffer, 128);
load_graphics();
// Prepare text engine for dialogue
init_text_engine();
// Set the random seed
rand_set_seed(0x1216);
// Clean up the main screen quick
tte_erase_rect(0, 0, 240, 160);
VBlankIntrWait();
REG_DISPCNT &= ~DCNT_BLANK;
};
void game_load_error(void)
{
REG_BG2CNT = (REG_BG2CNT & ~BG_PRIO_MASK) | BG_PRIO(1);
create_textbox(4, 1, 152, 100, true);
{
u8 general_text_table_buffer[2048];
text_data_table general_text(general_text_table_buffer);
general_text.decompress(get_compressed_general_table());
ptgb_write(general_text.get_text_entry(GENERAL_cart_load_error), true);
}
key_poll();
do
{
global_next_frame();
} while (!key_hit(KEY_A) && !key_hit(KEY_SELECT));
tte_erase_rect(0, 0, H_MAX, V_MAX);
if (key_hit(KEY_SELECT))
{
// We also want to give the option in this screen to upload the multiboot rom to another GBA.
// This can be useful when the user wants to work with a flashcart in single rom mode.
// The EZ Flash Omega (DE) for instance, triggers a reset of the gba if you insert it while the GBA is turned on.
// So the only way to work with it, is to boot Poke Transporter GB over multiboot and have the flashcart already inserted.
// It would be a shame not to support this flashcart, because it's awesome for pokémon fans. After all: it supports ds transfer
// and should support connecting with the gamecube games.
multiboot_upload_screen();
return;
}
delay_counter = 0;
while (delay_counter < 60)
{
delay_counter++;
global_next_frame();
}
}
void first_load_message(void)
{
tte_set_margins(8, 8, H_MAX - 8, V_MAX);
tte_set_pos(8, 8);
tte_set_ink(INK_ROM_COLOR);
{
u8 general_text_table_buffer[2048];
text_data_table general_text(general_text_table_buffer);
general_text.decompress(get_compressed_general_table());
ptgb_write(general_text.get_text_entry(GENERAL_intro_first), true);
}
while (!key_hit(KEY_A))
{
global_next_frame();
}
tte_erase_rect(0, 0, H_MAX, V_MAX);
}
#define TIMER_ENABLE 0x80
#define TIMER_CASCADE 0x4
#define TIMER_FREQ_1 0x0 // 16.78 MHz
#define TIMER_FREQ_64 0x1 // 262,144 Hz
#define TIMER_FREQ_256 0x2 // 65,536 Hz
#define TIMER_FREQ_1024 0x3 // 16,384 Hz
int test_decompress()
{
uint16_t charset[256];
// Reset both timers
REG_TM0CNT = 0;
REG_TM1CNT = 0;
REG_TM0D = 0;
REG_TM1D = 0;
// Set up TIMER0: count with no prescaler
REG_TM0CNT = TIMER_ENABLE | TIMER_FREQ_1;
// Set up TIMER1: cascade mode (increment when TIMER0 overflows)
REG_TM1CNT = TIMER_ENABLE | TIMER_CASCADE;
load_localized_charset(charset, 3, ENG_ID);
// Read combined 32-bit timer value
const u32 ticks = ((u32)REG_TM1D << 16) | REG_TM0D;
// Stop timers
REG_TM0CNT = 0;
REG_TM1CNT = 0;
create_textbox(4, 1, 160, 80, true);
ptgb_write_debug(charset, "Test results:\n\nDecompress: ", true);
ptgb_write_debug(charset, ptgb::to_string(static_cast<unsigned>(ticks * 1000 / 16777)), true);
ptgb_write_debug(charset, " usec\n", true);
while (true)
{
if (key_hit(KEY_B))
{
hide_text_box();
reset_textbox();
return 0;
}
global_next_frame();
}
return 0;
}
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_credits_table());
bool update = true;
global_next_frame();
while (true)
{
if (update)
{
create_textbox(4, 1, 160, 80, true);
show_text_box();
ptgb_write(credits_text_table.get_text_entry(curr_credits_num), true);
update = false;
}
if (key_hit(KEY_B))
{
hide_text_box();
reset_textbox();
return 0;
}
if (key_hit(KEY_LEFT) && curr_credits_num > 0)
{
curr_credits_num--;
update = true;
}
if (key_hit(KEY_RIGHT) && curr_credits_num < (credits_text_table.get_number_of_text_entries() - 1))
{
curr_credits_num++;
update = true;
}
if(key_hit(KEY_SELECT))
{
return test_decompress();
}
#if 0
if (ENABLE_DEBUG_SCREEN && key_hit(KEY_SELECT))
{
char hexBuffer[16];
uint16_t charset[256];
load_localized_charset(charset, 3, ENG_ID);
if (key_held(KEY_UP) && key_held(KEY_L) && key_held(KEY_R))
{
set_treecko(true);
}
u32 pkmn_flags = 0;
bool e4_flag = read_flag(curr_rom.e4_flag);
bool mg_flag = read_flag(curr_rom.mg_flag);
bool all_collected_flag = read_flag(curr_rom.all_collected_flag);
for (int i = 0; i < 30; i++)
{
pkmn_flags |= (read_flag(curr_rom.pkmn_collected_flag_start + i) << i);
}
bool tutorial = get_tutorial_flag();
int def_lang = get_def_lang_num();
create_textbox(4, 1, 160, 80, true);
ptgb_write_debug(charset, "Debug info:\n\nG: ", true);
ptgb_write_debug(charset, ptgb::to_string(curr_rom.language), true);
switch (curr_rom.gamecode)
{
case RUBY_ID:
ptgb_write_debug(charset, "-R-", true);
break;
case SAPPHIRE_ID:
ptgb_write_debug(charset, "-S-", true);
break;
case FIRERED_ID:
ptgb_write_debug(charset, "-F-", true);
break;
case LEAFGREEN_ID:
ptgb_write_debug(charset, "-L-", true);
break;
case EMERALD_ID:
ptgb_write_debug(charset, "-E-", true);
break;
}
ptgb_write_debug(charset, ptgb::to_string(curr_rom.version), true);
ptgb_write_debug(charset, "\nF: ", true);
ptgb_write_debug(charset, ptgb::to_string(e4_flag), true);
ptgb_write_debug(charset, ptgb::to_string(mg_flag), true);
ptgb_write_debug(charset, ptgb::to_string(all_collected_flag), true);
ptgb_write_debug(charset, "-", true);
n2hexstr(hexBuffer, pkmn_flags);
ptgb_write_debug(charset, hexBuffer, true);
ptgb_write_debug(charset, "\nS: ", true);
ptgb_write_debug(charset, ptgb::to_string(tutorial), true);
ptgb_write_debug(charset, "-", true);
n2hexstr(hexBuffer, def_lang);
ptgb_write_debug(charset, hexBuffer, true);
ptgb_write_debug(charset, "\n", true);
ptgb_write_debug(charset, VERSION, true);
if (get_treecko_enabled())
{
ptgb_write_debug(charset, ".T", true);
}
while (true)
{
if (key_hit(KEY_B))
{
hide_text_box();
reset_textbox();
return 0;
}
global_next_frame();
}
}
#endif
global_next_frame();
}
};
#define NUM_MENU_OPTIONS 3
int main_menu_loop()
{
uint8_t general_text_table_buffer[2048];
text_data_table general_text(general_text_table_buffer);
bool update = true;
const uint8_t menu_options[NUM_MENU_OPTIONS] = {GENERAL_option_transfer, GENERAL_option_dreamdex, GENERAL_option_credits};
const uint8_t *text_entry;
int return_values[NUM_MENU_OPTIONS] = {BTN_TRANSFER, BTN_POKEDEX, BTN_CREDITS};
u16 test = 0;
general_text.decompress(get_compressed_general_table());
while (true)
{
if (update)
{
tte_erase_rect(0, 80, 240, 160);
for (int i = 0; i < NUM_MENU_OPTIONS; i++)
{
text_entry = general_text.get_text_entry(menu_options[i]);
int size = get_string_length(text_entry);
int char_width = (PTGB_BUILD_LANGUAGE == JPN_ID ? 8 : 6);
int x = ((240 - (size * char_width)) / 2);
tte_set_pos(x, ((i * 17) + 80));
if (i == curr_selection)
{
tte_set_ink(INK_WHITE);
}
else
{
tte_set_ink(INK_ROM_COLOR);
}
ptgb_write(text_entry, true);
test++;
}
}
update = true;
if (key_hit(KEY_DOWN))
{
curr_selection = ((curr_selection + 1) % NUM_MENU_OPTIONS);
}
else if (key_hit(KEY_UP))
{
curr_selection = ((curr_selection + (NUM_MENU_OPTIONS - 1)) % NUM_MENU_OPTIONS);
}
else if (key_hit(KEY_A))
{
tte_erase_rect(0, test, H_MAX, V_MAX);
ptgb_write("#{cx:0xF000}");
return return_values[curr_selection];
}
else
{
update = false;
}
global_next_frame();
}
}
// Legal mumbo jumbo
static void show_legal_text(const u8* intro_text)
{
tte_set_margins(8, 8, H_MAX - 8, V_MAX - 8);
tte_set_pos(8, 8);
tte_set_ink(INK_ROM_COLOR);
ptgb_write(intro_text, true);
bool wait = true;
while (wait)
{
global_next_frame();
if (key_hit(KEY_A))
{
wait = false;
}
}
}
// Gears of Progress
static void show_gears_of_progress()
{
tte_erase_rect(0, 0, 240, 160);
REG_BG1VOFS = 0;
delay_counter = 0;
while (delay_counter < (15 * 60))
{
global_next_frame();
delay_counter++;
if (key_hit(KEY_A))
{
delay_counter = (15 * 60);
}
}
}
// split off from the main function in order to keep the scope of the variables limited to the execution of this function
// otherwise they stick around for the entire runtime of the program
// the attribute noinline is used to prevent the compiler from inlining this function back into the main function
// 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 press_start_text[32];
u8 press_start_text_length;
text_data_table general_text(general_text_table_buffer);
const u8 *text_entry;
general_text.decompress(get_compressed_general_table());
text_entry = general_text.get_text_entry(GENERAL_press_start);
press_start_text_length = get_string_length(text_entry);
memcpy(press_start_text, text_entry, press_start_text_length + 1);
text_entry = general_text.get_text_entry(GENERAL_intro_legal);
show_legal_text(text_entry);
show_gears_of_progress();
REG_BG1CNT = REG_BG1CNT | BG_PRIO(3);
key_poll(); // Reset the keys
curr_rom.load_rom();
obj_set_pos(ptgb_logo_l, 56, 12);
obj_set_pos(ptgb_logo_r, 56 + 64, 12);
obj_unhide_multi(ptgb_logo_l, 1, 2);
REG_BLDCNT = BLD_BUILD(BLD_BG3, BLD_BG0, 1);
int char_width = (PTGB_BUILD_LANGUAGE == JPN_ID ? 8 : 6);
int x = ((240 - (press_start_text_length * char_width)) / 2);
tte_set_pos(x, 12 * 8);
tte_set_ink(INK_DARK_GREY);
ptgb_write(press_start_text, true);
int fade = 0;
while (!start_pressed)
{
fade = abs(((get_frame_count() / 6) % 24) - 12);
global_next_frame();
start_pressed = key_hit(KEY_START) | key_hit(KEY_A);
REG_BLDALPHA = BLDA_BUILD(0b10000, fade);
};
}
int main(void)
{
initalization_script();
// 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();
tte_erase_rect(0, 0, H_MAX, V_MAX);
REG_BLDALPHA = BLDA_BUILD(0b10000, 0); // Reset fade
// Check if the game has been loaded correctly.
while (!curr_rom.load_rom())
{
obj_hide_multi(ptgb_logo_l, 2);
global_next_frame();
game_load_error();
// initalization_script();
}
// Initalize memory and save data after loading the game
reset_textbox();
REG_BG2CNT = REG_BG2CNT | BG_PRIO(3);
init_bank();
initalize_memory_locations();
load_custom_save_data();
set_background_pal(curr_rom.gamecode, false, true);
if (!IGNORE_MG_E4_FLAGS && (!get_tutorial_flag() || FORCE_TUTORIAL))
{
obj_hide_multi(ptgb_logo_l, 2);
text_loop(BTN_TRANSFER);
initalize_save_data();
}
obj_unhide_multi(ptgb_logo_l, 1, 2);
// MAIN LOOP
while (true)
{
if (DEBUG_MODE && false) // This isn't really needed anymore
{
print_mem_section();
curr_rom.print_rom_info();
}
load_flex_background(BG_MAIN_MENU, 2);
obj_unhide_multi(ptgb_logo_l, 1, 2);
obj_set_pos(ptgb_logo_l, 56, 12);
obj_set_pos(ptgb_logo_r, 56 + 64, 12);
switch (main_menu_loop())
{
case (BTN_TRANSFER):
tte_set_ink(INK_DARK_GREY);
obj_hide_multi(ptgb_logo_l, 2);
load_flex_background(BG_FENNEL, 3);
text_loop(BTN_TRANSFER);
break;
case (BTN_POKEDEX):
if (get_tutorial_flag())
{
obj_hide_multi(ptgb_logo_l, 2);
global_next_frame();
load_flex_background(BG_DEX, 2);
set_background_pal(curr_rom.gamecode, true, false);
pokedex_loop();
load_flex_background(BG_DEX, 3);
set_background_pal(curr_rom.gamecode, false, false);
}
break;
case (BTN_CREDITS):
tte_set_ink(INK_DARK_GREY);
// create_textbox(0, 0, 160, 80, true);
// show_text_box();
REG_BG1CNT = (REG_BG1CNT & ~BG_PRIO_MASK) | BG_PRIO(3);
obj_set_pos(ptgb_logo_l, 56, 108);
obj_set_pos(ptgb_logo_r, 56 + 64, 108);
credits();
break;
case (BTN_EVENTS):
obj_hide_multi(ptgb_logo_l, 2);
text_loop(BTN_EVENTS);
break;
default:
global_next_frame();
}
}
}