Add comments, optimize rom space slightly and fix issue in debug_menu_entries.cpp

This commit is contained in:
Philippe Symons 2026-03-10 11:58:43 +01:00
parent 343d4eb83e
commit 07e522fd62
8 changed files with 250 additions and 74 deletions

View File

@ -3,10 +3,15 @@
#include "vertical_menu.h"
// This file contains the implementation of the debug menu.
// But it doesn't actually define the entries or callback functions.
// For the menu entries, check debug_menu_entries.cpp
// For the callback functions, check debug_menu_functions.cpp
typedef void (*on_execute_callback)(void *context, unsigned user_param);
/**
* @brief This struct represents the metadata associated to a single option of a single row in the debug menu.
* @brief This struct represents the metadata associated to a single toggle option of a single row in the debug menu.
*/
typedef struct option_data
{
@ -20,20 +25,61 @@ typedef struct option_data
*/
typedef struct debug_menu_row_data
{
/**
* @brief a charset with which to render the text in this row.
*/
const u16 *charset;
/**
* @brief The text to be shown. This is just a plain old C string,
* because we don't need translations in the debug menu.
*/
const char *labelText;
/**
* @brief Toggle options below (optional):
*/
struct {
/**
* @brief Number of options. If this is 0, then this row will just be a label with no options to select.
*/
u8 num_options;
/**
* @brief The actual array of options alongside their values.
* Note: if you want the widget to free() the array when the widget is getting destroyed,
* set the should_free_on_destruct flag.
*/
const option_data *options;
/**
* @brief This callback will be fired when the user selects an option. (just with LEFT/RIGHT)
*/
on_execute_callback on_option_activate;
/**
* @brief The initial selected option index.
* But this field will get maintained by the widget to track the current selected index.
*/
u8 selected_option_index;
/**
* @brief This flag indicates whether the widget should call free() on the options pointer when the widget is getting destroyed.
*/
bool should_free_on_destruct;
} option_section;
/**
* @brief A callback that you can set to execute when the user selects this row and presses the A button.
* @brief A callback that you can set to execute when the user selects this row and presses the A button. (optional)
*/
on_execute_callback on_execute;
/**
* @brief Context for the on_option-activate and on_execute callbacks. (optional)
*/
void *context;
/**
* @brief This user_param will be passed as an argument to the on_execute callback. (optional)
*/
unsigned user_param;
} debug_menu_row_data;

View File

@ -1,9 +1,30 @@
#ifndef _DEBUG_MENU_FUNCTIONS_H
#define _DEBUG_MENU_FUNCTIONS_H
// Here we define debug menu callback functions, to be used as on_execute_callback or on_option_activate in the debug menu entries.
/**
* @brief This function will open the text debug screen.
*/
void show_text_debug_screen(void *context, unsigned user_param);
/**
* @brief This function will show the debug info screen, which contains various flags and info about the current game state.
*/
void show_debug_info_screen(void *context, unsigned user_param);
/**
* @brief This generic boolean callback function can be used to set/unset any boolean flag.
*
* @param context a (bool *) pointer to the flag you want to set/unset.
* @param user_param the value to set the flag to. 0 -> false, anything else -> true.
*/
void dbg_set_boolean_flag(void *context, unsigned user_param);
/**
* @brief This function will take a song index and play the corresponding song.
* If the index is UINT32_MAX, it will stop the music instead.
*/
void dbg_play_song(void *context, unsigned user_param);
#endif

View File

@ -19,18 +19,78 @@
*/
typedef struct debug_options
{
/** This option will print the link cable data. Pause the transfer with L, resume with R. Skip printing with DOWN. */
bool print_link_data;
/**
* @brief If this option has been set, we won't animate the text, but show the full text immediately.
*/
bool instant_text_speed;
/**
* @brief If this option is set, we won't try to access a game cartridge. Instead, we'll pretend we're using
* a game cartridge with predefined data. (DEBUG_GAME, DEBUG_VERS, DEBUG_LANG)
* We won't show game sprites either.
*
* This is useful for testing our code in an emulator or without a game cartridge.
*/
bool ignore_game_pak;
/**
* @brief If this option is set, we won't try to show sprites that are stored on the game cartridge.
* This is useful for testing our code in an emulator or without a game cartridge.
*/
bool ignore_game_pak_sprites;
/**
* @brief If this flag is set, we won't actually do a link cable connection.
* Instead, we pretend the connection was successful and use gen1_rb_debug_box_data or gen2_debug_box_data.
*/
bool ignore_link_cable;
/**
* @brief If this option is set, we won't block the user from proceeding if he/she hasn't beat the E4 or
* hasn't activated Mystery Gift yet. This is mostly useful for debug purposes.
*/
bool ignore_mg_e4_flags;
/**
* @brief If this option is set, we won't block the transfer from finishing if the player hasn't collected
* the pokémon from previous transfers. Instead those will get overwritten. This does make you lose those pokémon.
* But it can be useful for debugging. (because you wouldn't need to boot into the game everytime between transfer attempts)
*/
bool ignore_unreceived_pkmn;
/**
* @brief This option will make PTGB show the tutorial even if the tutorial completion flag was already set before.
* Useful for debugging purposes.
*/
bool force_tutorial;
/**
* @brief This option will show the pokémon even if not a single one is valid.
* Once again, this can be useful for debugging purposes.
*/
bool dont_hide_invalid_pkmn;
/**
* @brief This option will avoid blocking the transfer if the player hasn't caught enough pokémon yet.
*/
bool ignore_dex_completion;
/**
* @brief This option will pretend as if the player caught all pokémon. This is useful to get past the dex completion check
*/
bool force_all_caught;
/**
* @brief If this option is set, we will write the data received over the link cable to the save data at offset 0x0000.
*/
bool write_cable_data_to_save;
/**
* @brief If this option is enabled, we will show control characters in the text.
*/
bool display_control_char;
} debug_options;

View File

@ -80,14 +80,35 @@ private:
typedef struct vertical_menu_settings
{
// bounds properties
unsigned x;
unsigned y;
unsigned width;
unsigned height;
/**
* @brief The top margin (in pixels) after which we start rendering the first menu item.
*/
unsigned margin_top;
/**
* @brief The bottom margin (in pixels) after which we stop rendering the last menu item.
*/
unsigned margin_bottom;
/**
* @brief The index of the first item to be focused when the menu is shown.
*/
unsigned initial_focus_index;
/**
* @brief The height of each menu item in pixels. Will be used to position the items.
*/
unsigned item_height;
/**
* @brief This index defines which text table to use for the menu entries.
*/
int text_table_index;
/**
@ -220,18 +241,49 @@ private:
bool is_focused_;
};
/**
* @brief Metadata for the simple_item_renderer below.
*/
typedef struct simple_item_widget_data
{
/**
* @brief label text related properties.
*/
struct
{
/**
* @brief The index of the text table (see vertical_menu_settings) entry to use as the label of this item.
*/
unsigned text_table_index;
/**
* @brief The left pixel offset after which we render the label text.
*/
unsigned margin_left;
/**
* @brief The top pixel offset after which we render the label text.
*/
unsigned margin_top;
} text;
/**
* @brief The value this widget represents. This is useful for choice/input handling.
* But it won't be used if an on_execute_callback is set.
*/
unsigned value;
/**
* @brief Optional execute callback. This will be executed when the user presses A while this item is focused.
* If this is nullptr, then MenuInputHandleState::CHOICE_MADE will be returned when the user presses A,
* and the caller can check the value field to know what choice was made.
*/
void (*on_execute_callback)(void *context);
} simple_item_widget_data;
/**
* @brief This is the simplest possible item widget, which will just render a single line of text for a single item
* and allow you to press A to execute a callback. It's useful for simple menus that don't require toggles or multiple options.
*/
class simple_item_renderer : public i_item_widget
{
public:

View File

@ -38,7 +38,7 @@ void show_debug_menu()
.item_height = 10,
.text_table_index = INT32_MAX,
.allow_cancel = true,
.should_delete_item_widgets_on_destruct = false,
.should_delete_item_widgets_on_destruct = true,
.should_hide_state_changed_handler_on_not_focused = true
};

View File

@ -21,7 +21,7 @@ static const option_data toggle_options[2] = {
/**
* @brief This helper function makes it easy to define a multiple choice debug row.
*/
static debug_menu_row_widget __attribute__((noinline)) define_debug_choice_row(const u16 *charset, const char *labelText, on_execute_callback option_activate_callback, const option_data *options_metadata, u8 num_options, bool should_delete_options, u8 default_option_index, void *context)
static debug_menu_row_widget* __attribute__((noinline)) define_debug_choice_row(const u16 *charset, const char *labelText, on_execute_callback option_activate_callback, const option_data *options_metadata, u8 num_options, bool should_delete_options, u8 default_option_index, void *context)
{
const debug_menu_row_data row_data = {
.charset = charset,
@ -37,18 +37,21 @@ static debug_menu_row_widget __attribute__((noinline)) define_debug_choice_row(c
.context = context,
.user_param = 0
};
return debug_menu_row_widget(row_data);
return new debug_menu_row_widget(row_data);
}
/**
* @brief This helper function makes it easy to define a toggle row.
*/
static debug_menu_row_widget define_toggle_row(const u16 *charset, const char* labelText, on_execute_callback on_toggle_callback, bool default_value, void *context)
static debug_menu_row_widget* define_toggle_row(const u16 *charset, const char* labelText, on_execute_callback on_toggle_callback, bool default_value, void *context)
{
return define_debug_choice_row(charset, labelText, on_toggle_callback, toggle_options, 2, false, default_value ? 1 : 0, context);
}
static debug_menu_row_widget __attribute__((noinline)) define_song_row(const u16 *charset, const char* labelText)
/**
* @brief This helper function will define the song row, which will let you toggle between any available maxmod song or None at all.
*/
static debug_menu_row_widget* __attribute__((noinline)) define_song_row(const u16 *charset, const char* labelText)
{
option_data *song_options = (option_data*)malloc(sizeof(option_data) * (MSL_NSONGS + 1));
song_options[0] = {
@ -70,7 +73,7 @@ static debug_menu_row_widget __attribute__((noinline)) define_song_row(const u16
/**
* @brief This helper function makes it easy to define a row that executes a function when you select it and press A.
*/
static debug_menu_row_widget __attribute__((noinline)) define_executable_row(const u16 *charset, const char* labelText, on_execute_callback on_execute, unsigned user_param, void *context)
static debug_menu_row_widget* __attribute__((noinline)) define_executable_row(const u16 *charset, const char* labelText, on_execute_callback on_execute, unsigned user_param, void *context)
{
const debug_menu_row_data row_data = {
.charset = charset,
@ -86,49 +89,31 @@ static debug_menu_row_widget __attribute__((noinline)) define_executable_row(con
.context = context,
.user_param = user_param
};
return debug_menu_row_widget(row_data);
return new debug_menu_row_widget(row_data);
}
// Here we define the actual entries of the debug menu, using the helper functions defined above.
void fill_debug_menu_with_entries(vertical_menu &menu, u16 *charset)
{
#if ENABLE_TEXT_DEBUG_SCREEN
debug_menu_row_widget txt_dbg_screen = define_executable_row(charset, "Text Debug", show_text_debug_screen, 0, nullptr);
#endif
debug_menu_row_widget dbg_info_screen = define_executable_row(charset, "Info", show_debug_info_screen, 0, nullptr);
debug_menu_row_widget row_play_song = define_song_row(charset, "Song");
debug_menu_row_widget row_print_link_data = define_toggle_row(charset, "Print Link", dbg_set_boolean_flag, g_debug_options.print_link_data, &g_debug_options.print_link_data);
debug_menu_row_widget row_instant_text_speed = define_toggle_row(charset, "Instant Text", dbg_set_boolean_flag, g_debug_options.instant_text_speed, &g_debug_options.instant_text_speed);
debug_menu_row_widget row_ignore_game_pak = define_toggle_row(charset, "Ign Cart", dbg_set_boolean_flag, g_debug_options.ignore_game_pak, &g_debug_options.ignore_game_pak);
debug_menu_row_widget row_ignore_game_pak_sprites = define_toggle_row(charset, "Ign Sprites", dbg_set_boolean_flag, g_debug_options.ignore_game_pak_sprites, &g_debug_options.ignore_game_pak_sprites);
debug_menu_row_widget row_ignore_link_cable = define_toggle_row(charset, "Ign Link", dbg_set_boolean_flag, g_debug_options.ignore_link_cable, &g_debug_options.ignore_link_cable);
debug_menu_row_widget row_ignore_mg_e4_flags = define_toggle_row(charset, "Ign MG/E4", dbg_set_boolean_flag, g_debug_options.ignore_mg_e4_flags, &g_debug_options.ignore_mg_e4_flags);
debug_menu_row_widget row_ignore_unreceived_pkmn = define_toggle_row(charset, "Ign Unrec PKMN", dbg_set_boolean_flag, g_debug_options.ignore_unreceived_pkmn, &g_debug_options.ignore_unreceived_pkmn);
debug_menu_row_widget row_force_tutorial = define_toggle_row(charset, "Force Tut", dbg_set_boolean_flag, g_debug_options.force_tutorial, &g_debug_options.force_tutorial);
debug_menu_row_widget row_dont_hide_invalid_pkmn = define_toggle_row(charset, "Show Invalid", dbg_set_boolean_flag, g_debug_options.dont_hide_invalid_pkmn, &g_debug_options.dont_hide_invalid_pkmn);
debug_menu_row_widget row_ignore_dex_completion = define_toggle_row(charset, "Ign Dex Compl", dbg_set_boolean_flag, g_debug_options.ignore_dex_completion, &g_debug_options.ignore_dex_completion);
debug_menu_row_widget row_force_all_caught = define_toggle_row(charset, "Force Caught", dbg_set_boolean_flag, g_debug_options.force_all_caught, &g_debug_options.force_all_caught);
debug_menu_row_widget row_write_cable_data_to_save = define_toggle_row(charset, "Write Cbl Data", dbg_set_boolean_flag, g_debug_options.write_cable_data_to_save, &g_debug_options.write_cable_data_to_save);
debug_menu_row_widget row_display_control_char = define_toggle_row(charset, "Disp CtrlChr", dbg_set_boolean_flag, g_debug_options.display_control_char, &g_debug_options.display_control_char);
i_item_widget* item_widgets[] = {
#if ENABLE_TEXT_DEBUG_SCREEN
&txt_dbg_screen,
define_executable_row(charset, "Text Debug", show_text_debug_screen, 0, nullptr),
#endif
&dbg_info_screen,
&row_play_song,
&row_print_link_data,
&row_instant_text_speed,
&row_ignore_game_pak,
&row_ignore_game_pak_sprites,
&row_ignore_link_cable,
&row_ignore_mg_e4_flags,
&row_ignore_unreceived_pkmn,
&row_force_tutorial,
&row_dont_hide_invalid_pkmn,
&row_ignore_dex_completion,
&row_force_all_caught,
&row_write_cable_data_to_save,
&row_display_control_char
define_executable_row(charset, "Info", show_debug_info_screen, 0, nullptr),
define_song_row(charset, "Song"),
define_toggle_row(charset, "Print Link", dbg_set_boolean_flag, g_debug_options.print_link_data, &g_debug_options.print_link_data),
define_toggle_row(charset, "Instant Text", dbg_set_boolean_flag, g_debug_options.instant_text_speed, &g_debug_options.instant_text_speed),
define_toggle_row(charset, "Ign Cart", dbg_set_boolean_flag, g_debug_options.ignore_game_pak, &g_debug_options.ignore_game_pak),
define_toggle_row(charset, "Ign Sprites", dbg_set_boolean_flag, g_debug_options.ignore_game_pak_sprites, &g_debug_options.ignore_game_pak_sprites),
define_toggle_row(charset, "Ign Link", dbg_set_boolean_flag, g_debug_options.ignore_link_cable, &g_debug_options.ignore_link_cable),
define_toggle_row(charset, "Ign MG/E4", dbg_set_boolean_flag, g_debug_options.ignore_mg_e4_flags, &g_debug_options.ignore_mg_e4_flags),
define_toggle_row(charset, "Ign Unrec PKMN", dbg_set_boolean_flag, g_debug_options.ignore_unreceived_pkmn, &g_debug_options.ignore_unreceived_pkmn),
define_toggle_row(charset, "Force Tut", dbg_set_boolean_flag, g_debug_options.force_tutorial, &g_debug_options.force_tutorial),
define_toggle_row(charset, "Show Invalid", dbg_set_boolean_flag, g_debug_options.dont_hide_invalid_pkmn, &g_debug_options.dont_hide_invalid_pkmn),
define_toggle_row(charset, "Ign Dex Compl", dbg_set_boolean_flag, g_debug_options.ignore_dex_completion, &g_debug_options.ignore_dex_completion),
define_toggle_row(charset, "Force Caught", dbg_set_boolean_flag, g_debug_options.force_all_caught, &g_debug_options.force_all_caught),
define_toggle_row(charset, "Write Cbl Data", dbg_set_boolean_flag, g_debug_options.write_cable_data_to_save, &g_debug_options.write_cable_data_to_save),
define_toggle_row(charset, "Disp CtrlChr", dbg_set_boolean_flag, g_debug_options.display_control_char, &g_debug_options.display_control_char)
};
menu.add_item_widgets(item_widgets, sizeof(item_widgets) / sizeof(item_widgets[0]));

View File

@ -25,14 +25,19 @@ void show_debug_info_screen(void *context, unsigned user_param)
(void)context;
(void)user_param;
char hexBuffer[16];
char flags_hex_str[16];
char def_lang_hex_str[16];
char text_buffer[256];
uint16_t charset[256];
const char *game_code;
u32 pkmn_flags = 0;
load_localized_charset(charset, 3, ENGLISH);
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_GBA_rom.e4_flag);
bool mg_flag = read_flag(curr_GBA_rom.mg_flag);
bool all_collected_flag = read_flag(curr_GBA_rom.all_collected_flag);
@ -44,51 +49,48 @@ void show_debug_info_screen(void *context, unsigned user_param)
bool tutorial = get_tutorial_flag();
int def_lang = get_def_lang_num();
create_textbox(4, 1, 160, 80, true);
show_text_box();
ptgb_write_debug(charset, "Debug info:\n\nG: ", true);
ptgb_write_debug(charset, ptgb::to_string(curr_GBA_rom.language), true);
switch (curr_GBA_rom.gamecode)
{
case RUBY_ID:
ptgb_write_debug(charset, "-R-", true);
game_code = "-R-";
break;
case SAPPHIRE_ID:
ptgb_write_debug(charset, "-S-", true);
game_code = "-S-";
break;
case FIRERED_ID:
ptgb_write_debug(charset, "-F-", true);
game_code = "-F-";
break;
case LEAFGREEN_ID:
ptgb_write_debug(charset, "-L-", true);
game_code = "-L-";
break;
case EMERALD_ID:
ptgb_write_debug(charset, "-E-", true);
game_code = "-E-";
break;
}
ptgb_write_debug(charset, ptgb::to_string(curr_GBA_rom.version), true);
n2hexstr(flags_hex_str, pkmn_flags);
n2hexstr(def_lang_hex_str, def_lang);
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);
create_textbox(4, 1, 160, 80, true);
show_text_box();
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);
npf_snprintf(text_buffer, sizeof(text_buffer),
"Debug info:\n\nG: %d%s%d\nF: %d%d%d-%s\nS: %d-%s\n%s%s",
curr_GBA_rom.language,
game_code,
curr_GBA_rom.version,
e4_flag,
mg_flag,
all_collected_flag,
flags_hex_str,
tutorial,
def_lang_hex_str,
BUILD_INFO,
get_treecko_enabled() ? ".T" : ""
);
ptgb_write_debug(charset, text_buffer, true);
ptgb_write_debug(charset, "\n", true);
ptgb_write_debug(charset, BUILD_INFO, true);
if (get_treecko_enabled())
{
ptgb_write_debug(charset, ".T", true);
}
while (true)
{
if (key_hit(KEY_B))

View File

@ -448,6 +448,15 @@ void load_eternal_sprites()
load_sprite_compressed(button_yes, button_yesTiles, curr_tile_id, BTN_PAL, ATTR0_WIDE, ATTR1_SIZE_64x32, 1);
load_sprite_compressed(button_no, button_noTiles, curr_tile_id, BTN_PAL, ATTR0_WIDE, ATTR1_SIZE_64x32, 1);
load_sprite_compressed(cart_label, Label_GreenTiles, curr_tile_id, GB_CART_PAL, ATTR0_SQUARE, ATTR1_SIZE_32x32, 1);
/**
* @brief Okay, the next lines to load the arrows are a bit confusing.
* Be aware that tempTileBuf is NOT a u8 array, but a u32 array!
* The incoming arrows image is 1bpp, but we convert it into 4bpp in tempTileBuf. So every pixel is a nibble.
*
* Still, every index of tempTileBuf is a full 32 bit integer, yet the third argument of load_sprite is a size_in_bytes.
* This is why those indexes look off. But they are correct.
*/
unsigned int tempTileBuf[48];
BitUnPack(arrowsTiles, tempTileBuf, arrowsTilesLen, 1, 4);
load_sprite(point_arrow, &tempTileBuf[32], 32, curr_tile_id, BTN_PAL, ATTR0_SQUARE, ATTR1_SIZE_8x8, 1);
@ -455,6 +464,7 @@ void load_eternal_sprites()
load_sprite(toggle_arrow_left, &tempTileBuf[40], 32, curr_tile_id, BTN_PAL, ATTR0_SQUARE, ATTR1_SIZE_8x8 | ATTR1_HFLIP, 1);
load_sprite(down_arrow, &tempTileBuf[0], 64, curr_tile_id, BTN_PAL, ATTR0_WIDE, ATTR1_SIZE_16x8, 1);
load_sprite(up_arrow, &tempTileBuf[16], 64, curr_tile_id, BTN_PAL, ATTR0_WIDE, ATTR1_SIZE_16x8, 1);
load_sprite_compressed(link_frame1, link_frame1Tiles, curr_tile_id, LINK_CABLE_PAL, ATTR0_SQUARE, ATTR1_SIZE_32x32, 1);
load_sprite_compressed(link_frame2, link_frame2Tiles, curr_tile_id, LINK_CABLE_PAL, ATTR0_WIDE, ATTR1_SIZE_8x32, 1);
load_sprite_compressed(link_frame3, link_frame3Tiles, curr_tile_id, LINK_CABLE_PAL, ATTR0_WIDE, ATTR1_SIZE_16x32, 1);