diff --git a/Makefile b/Makefile index 67ce3a77..b3db89bd 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ SRCS = \ menu/actions.c \ menu/bookkeeping.c \ menu/cart_load.c \ + menu/cheat_load.c \ menu/disk_info.c \ menu/fonts.c \ menu/hdmi.c \ diff --git a/README.md b/README.md index ef70d344..85d80f97 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ This menu aims to support as many N64 flashcarts as possible. The current state * N64 ROM autoload. * ROM information descriptions. * ROM history and favorites (pre-release only). +* ROM cheat file support (pre-release only). ## Documentation diff --git a/docs/00_index.md b/docs/00_index.md index 668784c3..37b68a80 100644 --- a/docs/00_index.md +++ b/docs/00_index.md @@ -5,7 +5,7 @@ - [Initial Setup of an SD Card](./10_getting_started_sd.md) - [Basic Controls](./11_menu_controls.md) - [ROM Configuration](./12_rom_configuration.md) -- [Cheats (Gameshark, etc.)](./13_datel_cheats.md) + - [ROM Patches (Hacks, Fan Translations, etc.)](./14_rom_patches.md) - [Controller PAKs](./15_controller_paks.md) - [Background Images](./16_background_images.md) diff --git a/docs/13_datel_cheats.md b/docs/13_datel_cheats.md index 08ac1612..d60f03f4 100644 --- a/docs/13_datel_cheats.md +++ b/docs/13_datel_cheats.md @@ -1,6 +1,8 @@ [Return to the index](./00_index.md) ## Cheats (Gameshark, etc.) +**THIS FEATURE IS EXPERIMENTAL** + The N64FlashcartMenu supports the cheat code types made popular by the peripherals: - GameShark - Action Replay @@ -10,39 +12,41 @@ Another product by Blaze, called the Xploder64/Xplorer64 also existed in some re **WARNING**: It is not advised to connect a physical cheat cartridge in conjunction with most flashcarts. -The N64FlashcartMenu can only support cheat codes based on Datel carts when also using an Expansion Pak. +The N64FlashcartMenu can only support cheat codes based on Datel carts when **also** using an Expansion Pak. Caveats: - Something about cheats and expansion paks. - -The current code types are supported: -- 80 (description here) -- D0 (description here) -- Fx (description here) -- ... - -The codes XX are not supported, because... - e.g. they rely on the button. +### File parsing support +If a file named the same as the selected rom with the extension `.cht` is found, it will attempt to parse the file for cheat codes and place them in `menu->boot_params->cheat_list` per the cheat backend API. + +The parser ignores lines that start with a `#` or `$`, are under 12 characters or over 15 characters. Every other line needs to be a valid cheat code input with the code on the left, and the value on the right separated by a space. + +Cheat files should be formatted this way: ``` -// Example cheat codes for the game "Majoras Mask USA" -uint32_t cheats[] = { - // Enable code - 0xF1096820, - 0x2400, - 0xFF000220, - 0x0000, - // Inventory Editor (assigned to L) - 0xD01F9B91, - 0x0020, - 0x803FDA3F, - 0x0002, - // Last 2 entries must be 0 - 0, - 0, -}; +# Super mario 64 infinite lives +8033B21D 0064 + +# 120 stars +80207723 0001 +8020770B 00C7 +50001101 0000 +8020770C 00FF ``` -And pass this array as a boot parameter: `menu->boot_params->cheat_list = cheats;` +Another example: +``` +# Example cheat codes for the game "Majoras Mask USA" +# Enable code +F1096820 2400 +FF000220 0000 +# Inventory Editor (assigned to L) +D01F9B91 0020 +803FDA3F 0002 +``` + +The cheat file needs to be enabled for the specific game (press `R` within the Rom Info). + Check the [Pull Requests](https://github.com/Polprzewodnikowy/N64FlashcartMenu/pulls) for work towards GUI editor support. diff --git a/docs/65_experimental.md b/docs/65_experimental.md index f5e6ad07..3349791a 100644 --- a/docs/65_experimental.md +++ b/docs/65_experimental.md @@ -1,6 +1,9 @@ [Return to the index](./00_index.md) ## Experimental Features (Subject to change) +### Cheats +See: [Cheats (Gameshark, etc.)](./13_datel_cheats.md) + ### ROM info descriptions (pre-release only) To show a ROM description in the N64 ROM information screen, add a `.ini` file next to the game ROM file with the same name and the following content: ```ini @@ -15,4 +18,3 @@ Add a `font64` file to the `sd:/menu/` directory called `custom.font64`. You can build a font64 file with `Mkfont`, one of `libdragon`'s tools. At the time of writing, you will need to obtain `libdragon`'s [preview branch artifacts](https://github.com/DragonMinded/libdragon/actions/workflows/build-tool-windows.yml) to find out a copy of the prebuilt Windows executable. [Read its related Wiki page](https://github.com/DragonMinded/libdragon/wiki/Mkfont) for usage information. - diff --git a/src/menu/cart_load.c b/src/menu/cart_load.c index 054102ed..475ffc92 100644 --- a/src/menu/cart_load.c +++ b/src/menu/cart_load.c @@ -6,6 +6,7 @@ #include "path.h" #include "utils/fs.h" #include "utils/utils.h" +#include "cheat_load.h" #ifndef SAVES_SUBDIRECTORY #define SAVES_SUBDIRECTORY "saves" diff --git a/src/menu/cheat_load.c b/src/menu/cheat_load.c new file mode 100644 index 00000000..baabff9c --- /dev/null +++ b/src/menu/cheat_load.c @@ -0,0 +1,197 @@ +/** + * @brief Cheat file support + * + * @authors Mena and XLuma + */ + +#include "cheat_load.h" +#include "../utils/fs.h" + +#include +#include +#include +#include +#include "views/views.h" + + +char *cheat_load_convert_error_message (cheat_load_err_t err) { + switch (err) { + case CHEAT_LOAD_OK: return "Cheats loaded OK"; + case CHEAT_LOAD_ERR_NO_CHEAT_FILE: return "No cheat file found"; + case CHEAT_LOAD_ERR_SIZE_FAILED: return "Error occured acquiring cheat size"; + case CHEAT_LOAD_ERR_CHEAT_EMPTY: return "Cheat file is empty"; + case CHEAT_LOAD_ERR_CHEAT_TOO_LARGE: return "Cheat file is too large (over 128KiB)"; + case CHEAT_LOAD_ERR_MALLOC_FAILED: return "Error occured allocating memory for file"; + case CHEAT_LOAD_ERR_READ_FAILED: return "Error occured during file read"; + case CHEAT_LOAD_ERR_CLOSE_FAILED: return "Error occured during file close"; + default: return "Unknown error [CHEAT_LOAD]"; + } +} + +static int find_str (char const *s, char c) { + int i; + int nb_str; + + i = 0; + nb_str = 0; + if (!s[0]) { + return (0); + } + while (s[i] && s[i] == c) { + i++; + } + while (s[i]) { + if (s[i] == c) { + nb_str++; + while (s[i] && s[i] == c) { + i++; + } + continue; + } + i++; + } + if (s[i - 1] != c) { + nb_str++; + } + return (nb_str); +} + +static void get_next_str (char **next_str, size_t *next_strlen, char c) { + size_t i; + + *next_str += *next_strlen; + *next_strlen = 0; + i = 0; + while (**next_str && **next_str == c) { + (*next_str)++; + } + while ((*next_str)[i]) { + if ((*next_str)[i] == c) { + return; + } + (*next_strlen)++; + i++; + } +} + +static char **free_tab (char **tab) { + int i; + + i = 0; + while (tab[i]) { + free(tab[i]); + i++; + } + free(tab); + return (NULL); +} + +char **ft_split (char const *s, char c) { + char **tab; + char *next_str; + size_t next_strlen; + int i; + + i = -1; + if (!s) { + return (NULL); + } + tab = malloc(sizeof(char *) * (find_str(s, c) + 1)); + if (!tab) { + return (NULL); + } + next_str = (char *)s; + next_strlen = 0; + while (++i < find_str(s, c)) { + get_next_str(&next_str, &next_strlen, c); + tab[i] = (char *)malloc(sizeof(char) * (next_strlen + 1)); + if (!tab[i]) { + return (free_tab(tab)); + } + strlcpy(tab[i], next_str, next_strlen + 1); + } + tab[i] = NULL; + return (tab); +} + +cheat_load_err_t load_cheats (menu_t *menu) { + FILE *cheatsFile; + struct stat st; + size_t cheatsLength; + path_t *path = path_clone(menu->load.rom_path); + + // Parse cheats from file + path_ext_replace(path, "cht"); + if((cheatsFile = fopen(path_get(path), "rb")) == NULL) { + path_free(path); + return CHEAT_LOAD_OK; // no file is not an error. + } + + if (fstat(fileno(cheatsFile), &st)){ + path_free(path); + return CHEAT_LOAD_ERR_SIZE_FAILED; + } + + cheatsLength = st.st_size; + if (cheatsLength <= 0) { + path_free(path); + return CHEAT_LOAD_ERR_CHEAT_EMPTY; + } + if (cheatsLength > KiB(128)) { + path_free(path); + return CHEAT_LOAD_ERR_CHEAT_TOO_LARGE; + } + + char *cheatsContent = NULL; + if((cheatsContent = malloc((cheatsLength + 1) * sizeof(char))) == NULL) { + path_free(path); + return CHEAT_LOAD_ERR_MALLOC_FAILED; + } + if(fread(cheatsContent, cheatsLength, 1, cheatsFile) != 1) { + path_free(path); + return CHEAT_LOAD_ERR_READ_FAILED; + } + + cheatsContent[cheatsLength] = '\0'; + if(fclose(cheatsFile) != 0){ + path_free(path); + return CHEAT_LOAD_ERR_CLOSE_FAILED; + } + cheatsFile = NULL; + + char **tab = ft_split(cheatsContent, '\n'); + size_t lines = 1; + for (size_t i = 0; tab[i] != NULL; i++) { + lines++; + } + + free(cheatsContent); + + uint32_t *cheats = (uint32_t*)malloc(((lines * sizeof(uint32_t)) * 2) + 2); + memset(cheats, 0, ((lines * sizeof(uint32_t)) * 2) + 2); + size_t cheatIndex = 0; + for(size_t i = 0; tab[i] != NULL; i++) { + // ignore titles + if (tab[i][0] == '#' || tab[i][0] == '$') { + continue; + } + // ignore empty, too small or too big lines + if (strlen(tab[i]) < 12 || strlen(tab[i]) > 15) { + continue; + } + char **splitCheat = ft_split(tab[i], ' '); + uint32_t cheatValue1 = strtoul(splitCheat[0], NULL, 16); + uint32_t cheatValue2 = strtoul(splitCheat[1], NULL, 16); + cheats[cheatIndex] = cheatValue1; + cheats[cheatIndex + 1] = cheatValue2; + free_tab(splitCheat); + cheatIndex += 2; + } + free_tab(tab); + + cheats[cheatIndex] = 0; + cheats[cheatIndex + 1] = 0; + menu->boot_params->cheat_list = cheats; + + return CHEAT_LOAD_OK; +} diff --git a/src/menu/cheat_load.h b/src/menu/cheat_load.h new file mode 100644 index 00000000..5d44daf9 --- /dev/null +++ b/src/menu/cheat_load.h @@ -0,0 +1,26 @@ +/*** + * @file cheat_load.h + * @brief Cheat loading functions + */ + +#include "path.h" +#include "utils/fs.h" +#include "utils/utils.h" +#include "menu_state.h" + +/** @brief Cheat code loading enum */ + +typedef enum { + CHEAT_LOAD_OK, + CHEAT_LOAD_ERR_NO_CHEAT_FILE, + CHEAT_LOAD_ERR_SIZE_FAILED, + CHEAT_LOAD_ERR_CHEAT_EMPTY, + CHEAT_LOAD_ERR_CHEAT_TOO_LARGE, + CHEAT_LOAD_ERR_MALLOC_FAILED, + CHEAT_LOAD_ERR_READ_FAILED, + CHEAT_LOAD_ERR_CLOSE_FAILED, + CHEAT_LOAD_ERR_UNKNOWN_ERROR +} cheat_load_err_t; + +cheat_load_err_t load_cheats (menu_t *menu); +char *cheat_load_convert_error_message (cheat_load_err_t err); diff --git a/src/menu/rom_info.c b/src/menu/rom_info.c index fc2d5bfd..c20946ae 100644 --- a/src/menu/rom_info.c +++ b/src/menu/rom_info.c @@ -857,6 +857,8 @@ static rom_err_t save_override (path_t *path, const char *id, int value, int def if (value == default_value) { mini_err = mini_delete_value(rom_info_ini, "custom_boot", id); + } else if (strncmp(id, "cheat_codes", strlen("cheat_codes"))) { + mini_err = mini_set_bool(rom_info_ini, NULL, id, value); } else { mini_err = mini_set_int(rom_info_ini, "custom_boot", id, value); } @@ -962,6 +964,11 @@ rom_err_t rom_info_override_tv_type (path_t *path, rom_info_t *rom_info, rom_tv_ return save_override(path, "tv_type", rom_info->boot_override.tv_type, ROM_TV_TYPE_AUTOMATIC); } +rom_err_t rom_setting_set_cheats (path_t *path, rom_info_t *rom_info, bool enabled) { + rom_info->settings.cheats_enabled = enabled; + return save_override(path, "cheat_codes", enabled, false); +} + rom_err_t rom_info_load (path_t *path, rom_info_t *rom_info) { FILE *f; rom_header_t rom_header; diff --git a/src/menu/rom_info.h b/src/menu/rom_info.h index 26779429..b20d87f9 100644 --- a/src/menu/rom_info.h +++ b/src/menu/rom_info.h @@ -247,4 +247,6 @@ rom_err_t rom_info_override_save_type (path_t *path, rom_info_t *rom_info, rom_s rom_tv_type_t rom_info_get_tv_type (rom_info_t *rom_info); rom_err_t rom_info_override_tv_type (path_t *path, rom_info_t *rom_info, rom_tv_type_t tv_type); +rom_err_t rom_setting_set_cheats (path_t *path, rom_info_t *rom_info, bool enabled); + #endif diff --git a/src/menu/views/file_info.c b/src/menu/views/file_info.c index 1a979426..1b18909f 100644 --- a/src/menu/views/file_info.c +++ b/src/menu/views/file_info.c @@ -15,6 +15,7 @@ static const char *image_extensions[] = { "png", "jpg", "gif", NULL }; static const char *music_extensions[] = { "mp3", "wav", "ogg", "wma", "flac", NULL }; static const char *controller_pak_extensions[] = { "mpk", "pak", NULL }; static const char *emulator_extensions[] = { "nes", "smc", "gb", "gbc", "sms", "gg", "chf", NULL }; +static const char *cheat_extensions[] = {"cht", NULL}; static struct stat st; @@ -44,6 +45,9 @@ static char *format_file_type (char *name, bool is_directory) { } else if (file_has_extensions(name, emulator_extensions)) { return " Type: Emulator ROM file\n"; } + else if (file_has_extensions(name, cheat_extensions)) { + return " Type: Cheats\n"; + } return " Type: Unknown file\n"; } diff --git a/src/menu/views/load_rom.c b/src/menu/views/load_rom.c index 9fb97458..fc18a104 100644 --- a/src/menu/views/load_rom.c +++ b/src/menu/views/load_rom.c @@ -6,6 +6,7 @@ #include #include "utils/fs.h" #include "../bookkeeping.h" +#include "../cheat_load.h" static bool show_extra_info_message = false; static component_boxart_t *boxart; @@ -166,6 +167,23 @@ static void add_favorite (menu_t *menu, void *arg) { bookkeeping_favorite_add(&menu->bookkeeping, menu->load.rom_path, NULL, BOOKKEEPING_TYPE_ROM); } +static void set_cheat_option(menu_t *menu, void *arg) { + bool enabled = (bool)arg; + if (enabled == true) { + cheat_load_err_t err = load_cheats(menu); + if (err != CHEAT_LOAD_OK) { + menu_show_error(menu, cheat_load_convert_error_message(err)); + } + } + if (enabled == false) { + if (menu->boot_params->cheat_list != NULL) { + free(menu->boot_params->cheat_list); + } + } + rom_setting_set_cheats(menu->load.rom_path, &menu->load.rom_info, enabled); + menu->browser.reload = true; +} + static component_context_menu_t set_cic_type_context_menu = { .list = { {.text = "Automatic", .action = set_cic_type, .arg = (void *) (ROM_CIC_TYPE_AUTOMATIC) }, {.text = "CIC-6101", .action = set_cic_type, .arg = (void *) (ROM_CIC_TYPE_6101) }, @@ -204,10 +222,17 @@ static component_context_menu_t set_tv_type_context_menu = { .list = { COMPONENT_CONTEXT_MENU_LIST_END, }}; +static component_context_menu_t set_cheat_options_menu = { .list = { + { .text = "Enable", .action = set_cheat_option, .arg = (void *) (true)}, + { .text = "Disable", .action = set_cheat_option, .arg = (void *) (false)}, + COMPONENT_CONTEXT_MENU_LIST_END, +}}; + static component_context_menu_t options_context_menu = { .list = { { .text = "Set CIC Type", .submenu = &set_cic_type_context_menu }, { .text = "Set Save Type", .submenu = &set_save_type_context_menu }, { .text = "Set TV Type", .submenu = &set_tv_type_context_menu }, + { .text = "Set Cheats", .submenu = &set_cheat_options_menu }, { .text = "Set ROM to autoload", .action = set_autoload_type }, { .text = "Add to favorites", .action = add_favorite }, COMPONENT_CONTEXT_MENU_LIST_END, @@ -367,7 +392,6 @@ static void load (menu_t *menu) { case ROM_TV_TYPE_MPAL: menu->boot_params->tv_type = BOOT_TV_TYPE_MPAL; break; default: menu->boot_params->tv_type = BOOT_TV_TYPE_PASSTHROUGH; break; } - menu->boot_params->cheat_list = NULL; } static void deinit (void) {