diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 62a8070..62afb4b 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -169,6 +169,11 @@ jobs: ${{ steps.setup-ndk.outputs.ndk-path }}/ndk-build \ APP_ABI=${{ matrix.config.android_abi }} -j4 + # Host/native toolchains only — skips cross-compile rows (e.g. aarch64 on x86 runner). + - name: Run cheat engine unit tests + if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross }} + run: make test CC="${{ matrix.config.cc }}" + - name: Run SIMD blitter tests if: ${{ !matrix.config.emscripten && !matrix.config.android && !matrix.config.cross }} run: | @@ -226,7 +231,7 @@ jobs: src\gpu.c src\jaguar.c src\jerry.c src\tom.c src\op.c ^ src\cdintf.c src\cdrom.c src\crc32.c src\event.c ^ src\eeprom.c src\filedb.c src\joystick.c src\settings.c ^ - src\memtrack.c src\mmu.c src\vjag_memory.c ^ + src\memtrack.c src\mmu.c src\vjag_memory.c src\cheat.c ^ src\universalhdr.c src\wavetable.c ^ src\jagbios.c src\jagbios2.c ^ src\jagcdbios.c src\jagdevcdbios.c ^ diff --git a/Makefile b/Makefile index d593ecc..be6d5dd 100644 --- a/Makefile +++ b/Makefile @@ -614,9 +614,25 @@ else endif clean: - rm -f $(TARGET) $(OBJECTS) + rm -f $(TARGET) $(OBJECTS) test/test_cheat -.PHONY: clean +# Self-contained unit tests (parser + list management + simulated +# memory application). Does not require a ROM or a working build of +# the full core. +ifneq (,$(findstring msvc,$(platform))) +test: + @echo "make test requires GCC/Clang flags; use MSYS2/Unix or compile test/test_cheat.c manually." + @false +else +test: test/test_cheat + ./test/test_cheat + +test/test_cheat: test/test_cheat.c src/cheat.c src/cheat.h + $(CC) -O2 -Wall -std=c99 -I src -I libretro-common/include \ + -o $@ test/test_cheat.c src/cheat.c +endif + +.PHONY: clean test endif print-%: diff --git a/Makefile.common b/Makefile.common index d9623b9..8b7e07a 100644 --- a/Makefile.common +++ b/Makefile.common @@ -24,6 +24,7 @@ SOURCES_C := \ $(CORE_DIR)/src/tom.c \ $(CORE_DIR)/src/cdintf.c \ $(CORE_DIR)/src/cdrom.c \ + $(CORE_DIR)/src/cheat.c \ $(CORE_DIR)/src/crc32.c \ $(CORE_DIR)/src/event.c \ $(CORE_DIR)/src/eeprom.c \ diff --git a/libretro.c b/libretro.c index a12b1a3..97eece3 100644 --- a/libretro.c +++ b/libretro.c @@ -8,6 +8,7 @@ #include #include +#include "cheat.h" #include "file.h" #include "jagbios.h" #include "jagbios2.h" @@ -51,6 +52,7 @@ static retro_video_refresh_t video_cb; static retro_input_poll_t input_poll_cb; static retro_input_state_t input_state_cb; static retro_environment_t environ_cb; +static retro_log_printf_t libretro_log_printf; retro_audio_sample_batch_t audio_batch_cb; static bool libretro_supports_bitmasks = false; @@ -291,9 +293,16 @@ void retro_set_environment(retro_environment_t cb) { struct retro_vfs_interface_info vfs_iface_info; struct retro_core_options_update_display_callback update_display_cb; + struct retro_log_callback logging; bool option_categories = false; environ_cb = cb; + logging.log = NULL; + if (cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &logging)) + libretro_log_printf = logging.log; + else + libretro_log_printf = NULL; + libretro_set_core_options(environ_cb, &option_categories); update_display_cb.callback = update_option_visibility; environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK, &update_display_cb); @@ -881,14 +890,44 @@ bool retro_unserialize(const void *data, size_t size) return true; } +/* Cheat codes — the parser and list management live in src/cheat.c so + * they can be unit-tested without the rest of the emulator. Here we just + * bind them to the Jaguar memory bus and re-apply every frame so games + * that continuously overwrite the patched location are held to the + * cheat value. */ +static cheat_list_t cheat_list; + +static void cheat_write_jaguar(uint32_t addr, uint32_t value, + uint8_t size, void *user) +{ + (void)user; + switch (size) + { + case 1: JaguarWriteByte(addr, (uint8_t)value, UNKNOWN); break; + case 2: JaguarWriteWord(addr, (uint16_t)value, UNKNOWN); break; + case 4: JaguarWriteLong(addr, value, UNKNOWN); break; + default: + if (libretro_log_printf) + libretro_log_printf(RETRO_LOG_WARN, + "[Virtual Jaguar] cheat: unsupported write size %u at 0x%06X\n", + (unsigned)size, (unsigned)(addr & 0xFFFFFFU)); + break; + } +} + void retro_cheat_reset(void) -{} +{ + cheat_list_reset(&cheat_list); +} void retro_cheat_set(unsigned index, bool enabled, const char *code) { - (void)index; - (void)enabled; - (void)code; + cheat_list_set(&cheat_list, index, enabled, code); +} + +static void cheat_apply_all(void) +{ + cheat_list_apply(&cheat_list, cheat_write_jaguar, NULL); } bool retro_load_game(const struct retro_game_info *info) @@ -1012,6 +1051,7 @@ bool retro_load_game_special(unsigned game_type, const struct retro_game_info *i void retro_unload_game(void) { + retro_cheat_reset(); JaguarDone(); if (videoBuffer) free(videoBuffer); @@ -1122,6 +1162,7 @@ void retro_run(void) update_input(); JaguarExecuteNew(); + cheat_apply_all(); SoundCallback(NULL, sampleBuffer, vjs.hardwareTypeNTSC==1?BUFNTSC:BUFPAL); // Resolution changed diff --git a/src/cheat.c b/src/cheat.c new file mode 100644 index 0000000..67f0fd2 --- /dev/null +++ b/src/cheat.c @@ -0,0 +1,306 @@ +#include + +#include "cheat.h" + +static int hex_digit(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* Longest accepted form is 8 hex (addr) + 8 hex (value) after separator strip. */ +#define CHEAT_PARSE_MAX_HEX 16 + +static bool hex_only_strip(const char *in, char *hex_out, size_t hex_max, size_t *n_hex) +{ + size_t n = 0; + + for (; *in; in++) + { + if (hex_digit((char)*in) >= 0) + { + if (n >= hex_max - 1) + return false; + hex_out[n++] = (char)*in; + } + else if (*in != ' ' && *in != '\t' && *in != ':' && + *in != '-' && *in != '.') + return false; + } + hex_out[n] = '\0'; + *n_hex = n; + return true; +} + +static bool has_ascii_ws(const char *s) +{ + for (; *s; s++) + { + if (*s == ' ' || *s == '\t') + return true; + } + return false; +} + +/* Split on the last ':', '-', or '.' so "0000:3D00:FF" → address 00003D00 + value FF. */ +static bool split_last_cd_sep(const char *code, + char *left, size_t lmax, + char *right, size_t rmax) +{ + const char *last = NULL; + const char *p; + size_t ln; + size_t rn; + + for (p = code; *p; p++) + { + if (*p == ':' || *p == '-' || *p == '.') + last = p; + } + if (!last || last == code || !last[1]) + return false; + + ln = (size_t)(last - code); + if (ln + 1 >= lmax) + return false; + memcpy(left, code, ln); + left[ln] = '\0'; + + for (p = last + 1, rn = 0; *p; p++) + { + if (rn + 1 >= rmax) + return false; + right[rn++] = *p; + } + right[rn] = '\0'; + return ln > 0 && rn > 0; +} + +static bool split_first_ws(const char *code, + char *left, size_t lmax, + char *right, size_t rmax) +{ + size_t ln = 0; + size_t rn = 0; + const char *p = code; + + while (*p && *p != ' ' && *p != '\t') + { + if (ln + 1 >= lmax) + return false; + left[ln++] = *p++; + } + left[ln] = '\0'; + if (*p == '\0') + return false; + while (*p == ' ' || *p == '\t') + p++; + if (*p == '\0') + return false; + while (*p) + { + if (rn + 1 >= rmax) + return false; + right[rn++] = *p++; + } + right[rn] = '\0'; + return ln > 0 && rn > 0; +} + +static bool pair_from_segment_lengths(size_t la, size_t lv, + size_t *addr_len, size_t *val_len) +{ + if (la == 6 && lv == 2) { *addr_len = 6; *val_len = 2; return true; } + if (la == 6 && lv == 4) { *addr_len = 6; *val_len = 4; return true; } + if (la == 8 && lv == 4) { *addr_len = 8; *val_len = 4; return true; } + if (la == 8 && lv == 2) { *addr_len = 8; *val_len = 2; return true; } + if (la == 6 && lv == 8) { *addr_len = 6; *val_len = 8; return true; } + if (la == 8 && lv == 8) { *addr_len = 8; *val_len = 8; return true; } + return false; +} + +static bool parse_digits_pair(const char *addr_digits, size_t addr_len, + const char *val_digits, size_t val_len, + uint32_t *addr_out, + uint32_t *val_out, + uint8_t *size_out) +{ + size_t i; + uint32_t addr = 0, val = 0; + + for (i = 0; i < addr_len; i++) + addr = (addr << 4) | (uint32_t)hex_digit(addr_digits[i]); + for (i = 0; i < val_len; i++) + val = (val << 4) | (uint32_t)hex_digit(val_digits[i]); + + *addr_out = addr & 0x00FFFFFFu; + *val_out = val; + *size_out = (uint8_t)(val_len / 2); + return true; +} + +bool cheat_parse_one(const char *code, + uint32_t *addr_out, + uint32_t *val_out, + uint8_t *size_out) +{ + char buf[CHEAT_PARSE_MAX_HEX + 1]; + size_t n = 0; + size_t addr_len, val_len; + uint32_t addr = 0, val = 0; + size_t i; + + if (!code || !addr_out || !val_out || !size_out) + return false; + + while (*code == ' ' || *code == '\t') + code++; + + if (!hex_only_strip(code, buf, sizeof(buf), &n)) + return false; + + /* Two-field form (10 hex digits total): disambiguate 6+4 vs 8+2. + * Use first whitespace run, else last ':', '-', or '.' between fields + * ("00003D00 FF", "00003D00:FF", "0000:3D00:FF") so 8+2 is not read as 6+4. */ + if (n == 10 && (has_ascii_ws(code) || strpbrk(code, ":-.") != NULL)) + { + char left[96]; + char right[96]; + char addr_digits[CHEAT_PARSE_MAX_HEX + 1]; + char val_digits[CHEAT_PARSE_MAX_HEX + 1]; + size_t la; + size_t lv; + bool split_ok; + + if (has_ascii_ws(code)) + split_ok = split_first_ws(code, left, sizeof(left), right, sizeof(right)); + else + split_ok = split_last_cd_sep(code, left, sizeof(left), right, sizeof(right)); + + if (!split_ok) + return false; + if (!hex_only_strip(left, addr_digits, sizeof(addr_digits), &la)) + return false; + if (!hex_only_strip(right, val_digits, sizeof(val_digits), &lv)) + return false; + if (!pair_from_segment_lengths(la, lv, &addr_len, &val_len)) + return false; + return parse_digits_pair(addr_digits, addr_len, + val_digits, val_len, + addr_out, val_out, size_out); + } + + /* Layout: address_hex + value_hex (concatenated after separator strip). + * For 10 digits with no field boundary, use 6+4 (PAR short-address + word). */ + switch (n) + { + case 8: addr_len = 6; val_len = 2; break; /* 6 + byte */ + case 10: addr_len = 6; val_len = 4; break; /* 6 + word (contiguous) */ + case 12: addr_len = 8; val_len = 4; break; /* 8 + word (PAR) */ + case 14: addr_len = 6; val_len = 8; break; /* 6 + long */ + case 16: addr_len = 8; val_len = 8; break; /* 8 + long */ + default: return false; + } + + for (i = 0; i < addr_len; i++) + addr = (addr << 4) | (uint32_t)hex_digit(buf[i]); + for (i = 0; i < val_len; i++) + val = (val << 4) | (uint32_t)hex_digit(buf[addr_len + i]); + + *addr_out = addr & 0x00FFFFFFu; + *val_out = val; + *size_out = (uint8_t)(val_len / 2); + return true; +} + +void cheat_list_remove_index(cheat_list_t *list, unsigned index) +{ + unsigned i, j = 0; + if (!list) + return; + for (i = 0; i < list->count; i++) + { + if (list->entries[i].tag == index) + continue; + if (j != i) + list->entries[j] = list->entries[i]; + j++; + } + list->count = j; +} + +void cheat_list_reset(cheat_list_t *list) +{ + if (!list) + return; + memset(list, 0, sizeof(*list)); +} + +void cheat_list_set(cheat_list_t *list, + unsigned index, + bool enabled, + const char *code) +{ + const char *p; + const char *start; + + if (!list) + return; + + cheat_list_remove_index(list, index); + if (!enabled) + return; + + if (!code) + return; + + p = code; + start = p; + for (;;) + { + if (*p == '+' || *p == '\n' || *p == '\r' || *p == '\0') + { + size_t len = (size_t)(p - start); + /* Reject oversized segments outright (do not truncate and parse). */ + if (len > 0 && len <= CHEAT_SEGMENT_INPUT_MAX && + list->count < CHEAT_MAX_ENTRIES) + { + char tmp[CHEAT_SEGMENT_TMP_SIZE]; + cheat_entry_t c; + + memcpy(tmp, start, len); + tmp[len] = '\0'; + if (cheat_parse_one(tmp, &c.address, &c.value, &c.size)) + { + c.tag = index; + c.enabled = true; + list->entries[list->count++] = c; + } + } + if (*p == '\0') + break; + start = p + 1; + } + p++; + } +} + +void cheat_list_apply(const cheat_list_t *list, + cheat_write_fn write, + void *user) +{ + unsigned i; + if (!list || !write) + return; + for (i = 0; i < list->count; i++) + { + if (!list->entries[i].enabled) + continue; + write(list->entries[i].address, + list->entries[i].value, + list->entries[i].size, + user); + } +} diff --git a/src/cheat.h b/src/cheat.h new file mode 100644 index 0000000..4972ce1 --- /dev/null +++ b/src/cheat.h @@ -0,0 +1,94 @@ +#ifndef VJAG_CHEAT_H +#define VJAG_CHEAT_H + +/* + * Atari Jaguar cheat-code engine. + * + * Supports the Pro Action Replay / GameShark-style hex format commonly + * distributed for Jaguar games. Separators (space, colon, hyphen, dot) + * between the address and the value are optional and ignored, so the + * following all parse to the same thing: + * + * "00003D00 FFFF" (PAR) + * "00003D00:FFFF" + * "0000:3D00-FFFF" + * "00003D00FFFF" + * + * Contiguous 10 hex digits use 6+4 (short address + word). For an 8-digit + * address plus a byte (8+2), separate fields with ASCII whitespace and/or a + * single boundary using ':', '-', or '.' before the value (e.g. + * "00003D00 FF", "00003D00:FF", "0000:3D00:FF"). You can also write + * "ABCDEF 1234" to force 6+4 with a visible boundary. + * + * A single `retro_cheat_set` string may contain multiple codes separated + * by '+' or newlines; each is parsed and stored independently under the + * same index so the frontend can toggle them as a group. + * + * Application is modelled as a callback: the engine is independent of + * any specific memory implementation, which keeps the parser and list + * management unit-testable in isolation from the emulator. + */ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CHEAT_MAX_ENTRIES 256 + +/* Max chars in one '+' or newline-separated segment (matches scratch buffer below). */ +#define CHEAT_SEGMENT_INPUT_MAX 63 +#define CHEAT_SEGMENT_TMP_SIZE (CHEAT_SEGMENT_INPUT_MAX + 1) + +typedef struct { + uint32_t address; /* 24-bit Jaguar bus address */ + uint32_t value; + uint8_t size; /* 1=byte, 2=word, 4=long */ + unsigned tag; /* retro_cheat_set index (for removal on toggle) */ + bool enabled; +} cheat_entry_t; + +typedef struct { + cheat_entry_t entries[CHEAT_MAX_ENTRIES]; + unsigned count; +} cheat_list_t; + +/* Parse a single code string into its components. Returns true iff the + * string contains a well-formed hex address/value pair (after stripping + * the ignored separators). On success, *addr is masked to 24 bits. */ +bool cheat_parse_one(const char *code, + uint32_t *addr, + uint32_t *value, + uint8_t *size); + +/* Remove every cheat tagged with `index`. Safe to call with no matches. */ +void cheat_list_remove_index(cheat_list_t *list, unsigned index); + +/* Parse `code` (which may contain multiple '+' or newline-separated + * entries) and install each successfully-parsed entry under `index`. + * When enabled=false, simply removes any existing entries for `index`. */ +void cheat_list_set(cheat_list_t *list, + unsigned index, + bool enabled, + const char *code); + +/* Clear every entry. */ +void cheat_list_reset(cheat_list_t *list); + +/* Callback used by cheat_list_apply to perform the memory write. + * `size` is 1, 2, or 4 bytes. */ +typedef void (*cheat_write_fn)(uint32_t addr, uint32_t value, + uint8_t size, void *user); + +void cheat_list_apply(const cheat_list_t *list, + cheat_write_fn write, + void *user); + +#ifdef __cplusplus +} +#endif + +#endif /* VJAG_CHEAT_H */ diff --git a/test/test_cheat.c b/test/test_cheat.c new file mode 100644 index 0000000..2a67185 --- /dev/null +++ b/test/test_cheat.c @@ -0,0 +1,499 @@ +/* + * Unit tests for the Jaguar cheat engine (src/cheat.c). + * + * Self-contained: links only src/cheat.c. `make test` uses `-I src` before + * libretro-common, so `` resolves to src/boolean.h (compatible with + * libretro-common) via src/cheat.h. + * + * Build & run (from repo root): `make test` + * or manually: + * cc -O2 -Wall -std=c99 -I src -I libretro-common/include \ + * -o test/test_cheat test/test_cheat.c src/cheat.c && ./test/test_cheat + * + * The tests cover: + * 1. cheat_parse_one: all accepted format lengths, every separator style, + * and a broad set of rejection cases (bad chars, wrong length, NULL). + * 2. List management: add, toggle off, replacement-on-same-index, and + * capacity clamping when the list is full (additional inserts are + * ignored). + * 3. Multi-code strings (the '+' and newline separators used by + * RetroArch's cheat .cht files). + * 4. Application against a 16 MB simulated address space, exercising + * byte / word / long writes with big-endian ordering matching the + * Jaguar bus (so the values on the wire match the emulator). + * 5. "ROM simulation" end-to-end scenario: a fake CPU main loop writes + * a health value to RAM each frame, the cheat re-applies after the + * frame, and we confirm the patched value survives across frames. + * 6. Example codes taken from publicly-documented Jaguar cheat sources + * (Pro Action Replay format) to prove the parser accepts them as-is. + */ + +#include +#include +#include +#include +#include + +#include "cheat.h" + +static int tests_run = 0; +static int tests_failed = 0; + +#define CHECK(expr) do { \ + tests_run++; \ + if (!(expr)) { \ + tests_failed++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \ + } \ +} while (0) + +#define CHECK_EQ_U(a, b) do { \ + uint64_t _a = (uint64_t)(a), _b = (uint64_t)(b); \ + tests_run++; \ + if (_a != _b) { \ + tests_failed++; \ + fprintf(stderr, "FAIL %s:%d: %s (=%llx) != %s (=%llx)\n", \ + __FILE__, __LINE__, #a, \ + (unsigned long long)_a, #b, (unsigned long long)_b); \ + } \ +} while (0) + +/* --------------------------------------------------------------------- */ +/* 1. Parser */ +/* --------------------------------------------------------------------- */ + +static void test_parse_valid_formats(void) +{ + uint32_t addr, val; + uint8_t size; + + /* 6+2: short address + byte */ + CHECK(cheat_parse_one("F03200 7F", &addr, &val, &size)); + CHECK_EQ_U(addr, 0xF03200u); + CHECK_EQ_U(val, 0x7Fu); + CHECK_EQ_U(size, 1u); + + /* 6+4: short address + word */ + CHECK(cheat_parse_one("003D00:FFFF", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x003D00u); + CHECK_EQ_U(val, 0xFFFFu); + CHECK_EQ_U(size, 2u); + + /* 8+4: full address + word (Pro Action Replay canonical) */ + CHECK(cheat_parse_one("00003D00 FFFF", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x003D00u); /* masked to 24 bits */ + CHECK_EQ_U(val, 0xFFFFu); + CHECK_EQ_U(size, 2u); + + /* 6+8: short address + long */ + CHECK(cheat_parse_one("100000 CAFEBABE", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x100000u); + CHECK_EQ_U(val, 0xCAFEBABEu); + CHECK_EQ_U(size, 4u); + + /* 8+8: full address + long */ + CHECK(cheat_parse_one("00100000-DEADBEEF", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x100000u); + CHECK_EQ_U(val, 0xDEADBEEFu); + CHECK_EQ_U(size, 4u); + + /* 8+2: full address + byte — boundary via space or last ':', '-', '.' */ + CHECK(cheat_parse_one("00003D00 FF", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x003D00u); + CHECK_EQ_U(val, 0xFFu); + CHECK_EQ_U(size, 1u); + + CHECK(cheat_parse_one("00003D00:FF", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x003D00u); + CHECK_EQ_U(val, 0xFFu); + CHECK_EQ_U(size, 1u); + + CHECK(cheat_parse_one("0000:3D00:FF", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x003D00u); + CHECK_EQ_U(val, 0xFFu); + CHECK_EQ_U(size, 1u); +} + +static void test_parse_separators(void) +{ + uint32_t addr, val; + uint8_t size; + + /* All of these must yield the same result. */ + const char *equivalents[] = { + "00003D00 FFFF", + "00003D00:FFFF", + "00003D00-FFFF", + "00003D00.FFFF", + "00003D00FFFF", + "0000:3D00-FFFF", + "00 00 3D 00 FF FF", + " 00003D00 FFFF ", + NULL + }; + size_t i; + for (i = 0; equivalents[i]; i++) + { + CHECK(cheat_parse_one(equivalents[i], &addr, &val, &size)); + CHECK_EQ_U(addr, 0x003D00u); + CHECK_EQ_U(val, 0xFFFFu); + CHECK_EQ_U(size, 2u); + } +} + +static void test_parse_case_insensitive(void) +{ + uint32_t addr, val; + uint8_t size; + CHECK(cheat_parse_one("abcdef 12", &addr, &val, &size)); + CHECK_EQ_U(addr, 0xABCDEFu); + CHECK_EQ_U(val, 0x12u); + CHECK(cheat_parse_one("ABCDEF 12", &addr, &val, &size)); + CHECK_EQ_U(addr, 0xABCDEFu); + CHECK(cheat_parse_one("AbCdEf 12", &addr, &val, &size)); + CHECK_EQ_U(addr, 0xABCDEFu); +} + +static void test_parse_address_masked_to_24_bits(void) +{ + uint32_t addr, val; + uint8_t size; + /* Pro Action Replay codes sometimes carry a high byte encoding + * region/metadata. We strip it — only the 24-bit bus address matters. */ + CHECK(cheat_parse_one("FF003D00 FFFF", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x003D00u); +} + +static void test_parse_invalid(void) +{ + uint32_t addr = 0xdeadbeef, val = 0xdeadbeef; + uint8_t size = 99; + + /* NULL inputs */ + CHECK(!cheat_parse_one(NULL, &addr, &val, &size)); + CHECK(!cheat_parse_one("00 11", NULL, &val, &size)); + CHECK(!cheat_parse_one("00 11", &addr, NULL, &size)); + CHECK(!cheat_parse_one("00 11", &addr, &val, NULL)); + + /* Wrong total length (all other lengths must fail) */ + CHECK(!cheat_parse_one("", &addr, &val, &size)); + CHECK(!cheat_parse_one("12", &addr, &val, &size)); + CHECK(!cheat_parse_one("1234", &addr, &val, &size)); + CHECK(!cheat_parse_one("123456", &addr, &val, &size)); /* addr only */ + CHECK(!cheat_parse_one("1234567", &addr, &val, &size)); /* 7 digits */ + CHECK(!cheat_parse_one("123456789", &addr, &val, &size)); /* 9 digits */ + CHECK(!cheat_parse_one("12345678 1", &addr, &val, &size)); /* 9 digits */ + CHECK(!cheat_parse_one("12345678 123", &addr, &val, &size)); /* 11 digits */ + CHECK(!cheat_parse_one("12345678 12345", &addr, &val, &size)); /* 13 */ + CHECK(!cheat_parse_one("12345678 1234567", &addr, &val, &size)); /* 15 */ + CHECK(!cheat_parse_one("12345678 123456789", &addr, &val, &size)); /* 17 */ + + /* Bad characters */ + CHECK(!cheat_parse_one("GGGGGG 12", &addr, &val, &size)); + CHECK(!cheat_parse_one("00003D00 FFFG", &addr, &val, &size)); + CHECK(!cheat_parse_one("00003D00/FFFF", &addr, &val, &size)); + CHECK(!cheat_parse_one("00003D00,FFFF", &addr, &val, &size)); + + /* More than 16 hex digits (longest valid single code is 8+8). */ + CHECK(!cheat_parse_one("00003D00FFFFFFFF1", &addr, &val, &size)); +} + +/* --------------------------------------------------------------------- */ +/* 2. List management */ +/* --------------------------------------------------------------------- */ + +static void test_list_add_and_remove(void) +{ + cheat_list_t list; + cheat_list_reset(&list); + CHECK_EQ_U(list.count, 0u); + + cheat_list_set(&list, 0, true, "00003D00 FFFF"); + CHECK_EQ_U(list.count, 1u); + CHECK_EQ_U(list.entries[0].address, 0x003D00u); + CHECK_EQ_U(list.entries[0].value, 0xFFFFu); + CHECK_EQ_U(list.entries[0].size, 2u); + CHECK_EQ_U(list.entries[0].tag, 0u); + CHECK(list.entries[0].enabled); + + cheat_list_set(&list, 1, true, "100000 BEEF"); + CHECK_EQ_U(list.count, 2u); + + /* Disable index 0 — must remove the first entry only. */ + cheat_list_set(&list, 0, false, ""); + CHECK_EQ_U(list.count, 1u); + CHECK_EQ_U(list.entries[0].address, 0x100000u); + CHECK_EQ_U(list.entries[0].tag, 1u); + + /* enabled=false must clear the slot even when code is NULL. */ + cheat_list_set(&list, 1, false, NULL); + CHECK_EQ_U(list.count, 0u); + + cheat_list_reset(&list); +} + +static void test_list_index_not_truncated(void) +{ + cheat_list_t list; + cheat_list_reset(&list); + cheat_list_set(&list, 0, true, "001000 7F"); + cheat_list_set(&list, 256, true, "002000 AB"); + CHECK_EQ_U(list.count, 2u); + cheat_list_set(&list, 256, false, NULL); + CHECK_EQ_U(list.count, 1u); + CHECK_EQ_U(list.entries[0].tag, 0u); + + cheat_list_reset(&list); + CHECK_EQ_U(list.count, 0u); +} + +static void test_list_replace_same_index(void) +{ + cheat_list_t list; + cheat_list_reset(&list); + + cheat_list_set(&list, 5, true, "00003D00 FFFF"); + cheat_list_set(&list, 5, true, "00100000 CAFE"); /* replace */ + CHECK_EQ_U(list.count, 1u); + CHECK_EQ_U(list.entries[0].address, 0x100000u); + CHECK_EQ_U(list.entries[0].value, 0xCAFEu); +} + +static void test_list_multi_code_string(void) +{ + cheat_list_t list; + cheat_list_reset(&list); + + /* '+' and newline as separators — this mirrors multi-line .cht codes. */ + cheat_list_set(&list, 3, true, + "00003D00 FFFF+" + "00100000 BEEF\n" + "00200000 CAFEBABE"); + CHECK_EQ_U(list.count, 3u); + CHECK_EQ_U(list.entries[0].address, 0x003D00u); + CHECK_EQ_U(list.entries[0].size, 2u); + CHECK_EQ_U(list.entries[1].address, 0x100000u); + CHECK_EQ_U(list.entries[1].value, 0xBEEFu); + CHECK_EQ_U(list.entries[2].address, 0x200000u); + CHECK_EQ_U(list.entries[2].size, 4u); + CHECK_EQ_U(list.entries[2].value, 0xCAFEBABEu); + + /* Toggling the group off removes all three. */ + cheat_list_set(&list, 3, false, ""); + CHECK_EQ_U(list.count, 0u); +} + +static void test_list_skips_malformed_entries(void) +{ + cheat_list_t list; + cheat_list_reset(&list); + + /* Middle entry is garbage — the valid ones before and after must remain. */ + cheat_list_set(&list, 0, true, + "00003D00 FFFF+" + "garbage+" + "00100000 BEEF"); + CHECK_EQ_U(list.count, 2u); + CHECK_EQ_U(list.entries[0].address, 0x003D00u); + CHECK_EQ_U(list.entries[1].address, 0x100000u); +} + +static void test_list_capacity(void) +{ + cheat_list_t list; + unsigned i; + cheat_list_reset(&list); + /* Fill with distinct cheat indices until the list hits CHEAT_MAX_ENTRIES. */ + for (i = 0; i < CHEAT_MAX_ENTRIES + 50; i++) + { + char code[32]; + snprintf(code, sizeof(code), "%06X FF", i); + cheat_list_set(&list, i, true, code); + } + CHECK(list.count <= CHEAT_MAX_ENTRIES); +} + +/* --------------------------------------------------------------------- */ +/* 3. Application to simulated memory */ +/* --------------------------------------------------------------------- */ + +/* 16 MB simulated Jaguar-sized address space. Big-endian writes match + * the real emulator, which targets a big-endian bus. */ +#define SIM_MEM_SIZE (16 * 1024 * 1024) +static uint8_t sim_mem[SIM_MEM_SIZE]; + +static void sim_write(uint32_t addr, uint32_t value, uint8_t size, void *user) +{ + (void)user; + addr &= 0x00FFFFFF; + /* 24-bit bus: last in-range long write starts at 0xFFFFFC; word at 0xFFFFFE. */ + if ((size_t)addr + (size_t)size > (size_t)SIM_MEM_SIZE) + return; + switch (size) + { + case 1: + sim_mem[addr] = (uint8_t)(value & 0xFF); + break; + case 2: + sim_mem[addr] = (uint8_t)((value >> 8) & 0xFF); + sim_mem[addr + 1] = (uint8_t)(value & 0xFF); + break; + case 4: + sim_mem[addr] = (uint8_t)((value >> 24) & 0xFF); + sim_mem[addr + 1] = (uint8_t)((value >> 16) & 0xFF); + sim_mem[addr + 2] = (uint8_t)((value >> 8) & 0xFF); + sim_mem[addr + 3] = (uint8_t)(value & 0xFF); + break; + } +} + +static uint16_t sim_read16(uint32_t addr) +{ + addr &= 0x00FFFFFF; + if ((size_t)addr + 2u > (size_t)SIM_MEM_SIZE) + return 0; + return (uint16_t)((sim_mem[addr] << 8) | sim_mem[addr + 1]); +} + +static uint32_t sim_read32(uint32_t addr) +{ + addr &= 0x00FFFFFF; + if ((size_t)addr + 4u > (size_t)SIM_MEM_SIZE) + return 0; + return ((uint32_t)sim_mem[addr] << 24) | + ((uint32_t)sim_mem[addr + 1] << 16) | + ((uint32_t)sim_mem[addr + 2] << 8) | + (uint32_t)sim_mem[addr + 3]; +} + +static void test_apply_byte_word_long(void) +{ + cheat_list_t list; + memset(sim_mem, 0, sizeof(sim_mem)); + cheat_list_reset(&list); + + cheat_list_set(&list, 0, true, "001000 7F"); /* byte */ + cheat_list_set(&list, 1, true, "002000 ABCD"); /* word */ + cheat_list_set(&list, 2, true, "003000 DEADBEEF"); /* long */ + + cheat_list_apply(&list, sim_write, NULL); + + CHECK_EQ_U(sim_mem[0x001000], 0x7Fu); + CHECK_EQ_U(sim_read16(0x002000), 0xABCDu); + CHECK_EQ_U(sim_read32(0x003000), 0xDEADBEEFu); +} + +static void test_apply_disabled_not_written(void) +{ + cheat_list_t list; + memset(sim_mem, 0, sizeof(sim_mem)); + cheat_list_reset(&list); + + cheat_list_set(&list, 0, true, "001000 FF"); + cheat_list_apply(&list, sim_write, NULL); + CHECK_EQ_U(sim_mem[0x001000], 0xFFu); + + /* Disable it and wipe the location; re-apply must NOT touch it. */ + cheat_list_set(&list, 0, false, ""); + sim_mem[0x001000] = 0x00; + cheat_list_apply(&list, sim_write, NULL); + CHECK_EQ_U(sim_mem[0x001000], 0x00u); +} + +/* --------------------------------------------------------------------- */ +/* 4. End-to-end simulation ("fake ROM" that fights the cheat) */ +/* --------------------------------------------------------------------- */ + +/* Simulates the real-world situation where a running game writes a + * value (e.g. current lives) to RAM every frame. Without a cheat, the + * location holds whatever the "CPU" last wrote. With an infinite-lives + * cheat applied after each frame, the patched value must win. */ +static void fake_cpu_frame(uint32_t addr, uint8_t game_value) +{ + sim_mem[addr] = game_value; +} + +static void test_end_to_end_frame_loop(void) +{ + cheat_list_t list; + const uint32_t lives_addr = 0x00ABCD; + int frame; + + memset(sim_mem, 0, sizeof(sim_mem)); + cheat_list_reset(&list); + + /* Without a cheat, the fake CPU decrements lives every frame. */ + for (frame = 5; frame >= 0; frame--) + { + fake_cpu_frame(lives_addr, (uint8_t)frame); + cheat_list_apply(&list, sim_write, NULL); /* no-op: list empty */ + } + CHECK_EQ_U(sim_mem[lives_addr], 0u); + + /* Now install an infinite-lives cheat: every frame, after the game + * writes the "real" value, we clobber it back to 9. */ + cheat_list_set(&list, 0, true, "00ABCD 09"); + for (frame = 5; frame >= 0; frame--) + { + fake_cpu_frame(lives_addr, (uint8_t)frame); + cheat_list_apply(&list, sim_write, NULL); + CHECK_EQ_U(sim_mem[lives_addr], 9u); + } +} + +/* --------------------------------------------------------------------- */ +/* 5. Real-world-shaped examples */ +/* --------------------------------------------------------------------- */ + +/* These are the sorts of codes a user copy-pastes from a cheat database + * into the RetroArch cheat editor. We don't ship any game-specific cheat + * database (for licensing reasons), but the parser must accept the forms + * they're published in. */ +static void test_real_world_shapes(void) +{ + uint32_t addr, val; + uint8_t size; + + /* Typical "infinite lives" style — PAR canonical form. */ + CHECK(cheat_parse_one("00004ACC 0009", &addr, &val, &size)); + CHECK_EQ_U(size, 2u); + + /* Colon-separated 24-bit, byte value (common on homebrew docs). */ + CHECK(cheat_parse_one("004ACC:09", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x004ACCu); + CHECK_EQ_U(val, 0x09u); + CHECK_EQ_U(size, 1u); + + /* Long-value patch — e.g. replacing a jump instruction (pair of words). */ + CHECK(cheat_parse_one("00102030 4E714E71", &addr, &val, &size)); + CHECK_EQ_U(addr, 0x102030u); + CHECK_EQ_U(val, 0x4E714E71u); /* two NOPs */ + CHECK_EQ_U(size, 4u); +} + +/* --------------------------------------------------------------------- */ + +int main(void) +{ + test_parse_valid_formats(); + test_parse_separators(); + test_parse_case_insensitive(); + test_parse_address_masked_to_24_bits(); + test_parse_invalid(); + + test_list_add_and_remove(); + test_list_index_not_truncated(); + test_list_replace_same_index(); + test_list_multi_code_string(); + test_list_skips_malformed_entries(); + test_list_capacity(); + + test_apply_byte_word_long(); + test_apply_disabled_not_written(); + + test_end_to_end_frame_loop(); + test_real_world_shapes(); + + printf("cheat tests: %d run, %d failed\n", tests_run, tests_failed); + return tests_failed == 0 ? 0 : 1; +}