diff --git a/workspace/all/minarch/ma_cheats.c b/workspace/all/minarch/ma_cheats.c index 289a2a303..b80cac6be 100644 --- a/workspace/all/minarch/ma_cheats.c +++ b/workspace/all/minarch/ma_cheats.c @@ -13,12 +13,6 @@ struct Cheats cheatcodes; #define CHEAT_MAX_LINE_LEN 52 #define CHEAT_MAX_LINES 3 -static size_t parse_count(FILE *file) { - size_t count = 0; - fscanf(file, " cheats = %lu\n", (unsigned long *)&count); - return count; -} - static const char *find_val(const char *start) { start--; while(!isspace(*++start)) @@ -33,9 +27,36 @@ static const char *find_val(const char *start) { while(isspace(*++start)) ; + // newer cheat files quote every value, older ones quote only strings + if (*start == '"') + start++; + return start; } +// the count sits on the first line in older cheat files and on the last line in +// newer ones, so scan the whole file and keep the last match +static size_t parse_count(FILE *file) { + char line[512]; + size_t count = 0; + + while (fgets(line, sizeof(line), file)) { + const char *ptr = line; + while (isspace(*ptr)) + ptr++; + + // anchored, so a description mentioning "cheats = " can't hijack us + if (strncmp(ptr, "cheats", 6)) + continue; + + if ((ptr = find_val(ptr))) + count = strtoul(ptr, NULL, 10); + } + + rewind(file); + return count; +} + static int parse_bool(const char *ptr, int *out) { if (!strncasecmp(ptr, "true", 4)) { *out = 1; @@ -53,9 +74,7 @@ static int parse_string(const char *ptr, char *buf, size_t len) { buf[0] = '\0'; - if (*ptr++ != '"') - return -1; - + // find_val already consumed the opening quote; the closing one is checked below while (*ptr != '\0' && *ptr != '"' && index < len - 1) { if (*ptr == '\\' && index < input_len - 1) { ptr++; @@ -100,10 +119,14 @@ static int parse_cheats(struct Cheats *cheats, FILE *file) { size_t len; sscanf(ptr, "cheat%d", &index); - if (index >= cheats->count) + if (index < 0 || (size_t)index >= cheats->count) continue; cheat = &cheats->cheats[index]; + // these would otherwise match the _value and _address tests below + if (strstr(ptr, "_rumble") || strstr(ptr, "_repeat")) + continue; + if (strstr(ptr, "_desc")) { ptr = find_val(ptr); if (!ptr || parse_string(ptr, buf, sizeof(buf))) { @@ -153,6 +176,27 @@ static int parse_cheats(struct Cheats *cheats, FILE *file) { continue; } } + // the rest describe a memory write, see Cheats_apply + // note: _address_bit_position must be tested before _address + else if (strstr(ptr, "_address_bit_position")) { + if ((ptr = find_val(ptr))) + cheat->bit_position = strtoul(ptr, NULL, 10); + } else if (strstr(ptr, "_address")) { + if ((ptr = find_val(ptr))) + cheat->address = strtoul(ptr, NULL, 10); + } else if (strstr(ptr, "_value")) { + if ((ptr = find_val(ptr))) + cheat->value = strtoul(ptr, NULL, 10); + } else if (strstr(ptr, "_cheat_type")) { + if ((ptr = find_val(ptr))) + cheat->type = strtoul(ptr, NULL, 10); + } else if (strstr(ptr, "_memory_search_size")) { + if ((ptr = find_val(ptr))) + cheat->size = strtoul(ptr, NULL, 10); + } else if (strstr(ptr, "_big_endian")) { + if ((ptr = find_val(ptr))) + parse_bool(ptr, &cheat->big_endian); + } } } while(1); @@ -249,7 +293,7 @@ void Cheat_getPaths(char paths[CHEAT_MAX_PATHS][MAX_PATH], int* count) { void Cheats_free() { size_t i; - for (i = 0; i < cheatcodes.count; i++) { + for (i = 0; cheatcodes.cheats && i < cheatcodes.count; i++) { struct Cheat *cheat = &cheatcodes.cheats[i]; if (cheat) { free((char *)cheat->name); @@ -258,6 +302,7 @@ void Cheats_free() { } } free(cheatcodes.cheats); + cheatcodes.cheats = NULL; cheatcodes.count = 0; } @@ -320,7 +365,7 @@ bool Cheats_load() { } cheatcodes.count = parse_count(file); - if (cheatcodes.count <= 0) { + if (cheatcodes.count == 0 || cheatcodes.count > CHEAT_MAX_COUNT) { LOG_error("Couldn't read cheat count\n"); goto finish; } @@ -336,7 +381,9 @@ bool Cheats_load() { goto finish; } - LOG_info("Found %i cheats for the current game.\n", cheatcodes.count); + // memory-write cheats are useless without system ram, so report what we got + LOG_info("Found %i cheats for the current game (system ram: %zu bytes).\n", + cheatcodes.count, core.get_memory_size ? core.get_memory_size(RETRO_MEMORY_SYSTEM_RAM) : 0); success = 1; finish: @@ -346,4 +393,58 @@ bool Cheats_load() { if (file) fclose(file); + + return success; +} + +// Cheats from RetroArch's newer format carry no code for the core to interpret, +// they name a memory location to hold at a value. Those have to be rewritten +// every frame, otherwise the game just overwrites them again. +// Addresses are treated as offsets into system RAM; cores that publish a memory +// map (RETRO_ENVIRONMENT_SET_MEMORY_MAPS) may expect a different layout. +void Cheats_apply(void) { + static const unsigned char widths[] = { 0, 0, 0, 1, 2, 4 }; // by memory_search_size + unsigned char *ram; + size_t ram_size, i; + + if (!core.get_memory_data || !core.get_memory_size) + return; + + ram_size = core.get_memory_size(RETRO_MEMORY_SYSTEM_RAM); + ram = core.get_memory_data(RETRO_MEMORY_SYSTEM_RAM); + if (!ram || !ram_size) + return; + + for (i = 0; i < cheatcodes.count; i++) { + const struct Cheat *cheat = &cheatcodes.cheats[i]; + unsigned char width; + unsigned value; + int shift; + + // code means the core applies it, see Core_applyCheats + if (!cheat->enabled || cheat->code || cheat->type != CHEAT_TYPE_SET_TO_VALUE) + continue; + + if (cheat->size < 3) { + // sub-byte cheat: mask into the byte at bit_position + unsigned char mask = (1 << (1 << cheat->size)) - 1; + if (cheat->address >= ram_size || cheat->bit_position > 7) + continue; + + shift = cheat->bit_position; + ram[cheat->address] = (ram[cheat->address] & ~(mask << shift)) | ((cheat->value & mask) << shift); + continue; + } + + // written this way round so address + width can't overflow + width = cheat->size < sizeof(widths) ? widths[cheat->size] : 0; + if (!width || width > ram_size || cheat->address > ram_size - width) + continue; + + value = cheat->value; + for (shift = 0; shift < width; shift++) { + int byte = cheat->big_endian ? width - 1 - shift : shift; + ram[cheat->address + byte] = (value >> (shift * 8)) & 0xff; + } + } } diff --git a/workspace/all/minarch/ma_cheats.h b/workspace/all/minarch/ma_cheats.h index 7e865a63f..cde52d8eb 100644 --- a/workspace/all/minarch/ma_cheats.h +++ b/workspace/all/minarch/ma_cheats.h @@ -5,6 +5,8 @@ #include "defines.h" +#define CHEAT_TYPE_SET_TO_VALUE 1 +#define CHEAT_MAX_COUNT 4096 #define CHEAT_MAX_PATHS 48 #define CHEAT_MAX_DISPLAY_PATHS 8 #define CHEAT_MAX_LIST_LENGTH (CHEAT_MAX_DISPLAY_PATHS * MAX_PATH) @@ -13,7 +15,16 @@ struct Cheat { const char *name; const char *info; int enabled; - const char *code; + const char *code; // classic format, handed to retro_cheat_set() + + // RetroArch's newer format describes cheats as a memory write instead of a + // code string (handler = 1). Those have no code and are applied by us. + unsigned address; + unsigned value; + unsigned char type; // cheat_type, 1 = set to value (the only one we apply) + unsigned char size; // memory_search_size, 0-5 = 1,2,4,8,16,32 bits + unsigned char bit_position; // address_bit_position, sub-byte sizes only + int big_endian; }; struct Cheats { @@ -27,3 +38,4 @@ extern struct Cheats cheatcodes; void Cheat_getPaths(char paths[CHEAT_MAX_PATHS][MAX_PATH], int* count); void Cheats_free(void); bool Cheats_load(void); +void Cheats_apply(void); diff --git a/workspace/all/minarch/ma_core.c b/workspace/all/minarch/ma_core.c index 771c8fb10..3572cdbe7 100644 --- a/workspace/all/minarch/ma_core.c +++ b/workspace/all/minarch/ma_core.c @@ -104,7 +104,8 @@ void Core_applyCheats(struct Cheats *cheats) core.cheat_reset(); for (int i = 0; i < cheats->count; i++) { - if (cheats->cheats[i].enabled) { + // no code means it describes a memory write, applied by Cheats_apply + if (cheats->cheats[i].enabled && cheats->cheats[i].code) { core.cheat_set(i, cheats->cheats[i].enabled, cheats->cheats[i].code); } } diff --git a/workspace/all/minarch/ma_runframe.c b/workspace/all/minarch/ma_runframe.c index 75984da90..32369bcf3 100644 --- a/workspace/all/minarch/ma_runframe.c +++ b/workspace/all/minarch/ma_runframe.c @@ -1,5 +1,6 @@ #include #include "ma_internal.h" +#include "ma_cheats.h" #include "ma_rewind.h" #include "ma_input.h" #include "ma_config.h" @@ -46,6 +47,8 @@ static void limitFF(void) { } void run_frame(void) { + Cheats_apply(); + // if rewind is toggled, fast-forward toggle must stay off; fast-forward hold pauses rewind int do_rewind = (rewind_pressed || rewind_toggle) && !(rewind_toggle && ff_hold_active); if (do_rewind) {