Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ jobs:
- name: Build
run: make -j4 CC=${{ matrix.config.cc }} CXX=${{ matrix.config.cxx }}

- name: Run cheat engine unit tests
run: |
${{ matrix.config.cc }} -O2 -Wall -std=c99 \
-I src -I libretro-common/include \
-o test_cheat test/test_cheat.c src/cheat.c
./test_cheat
Comment on lines +76 to +77
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CI step builds the test binary as ./test_cheat in the repo root, while the Makefile target builds test/test_cheat (and .gitignore ignores that path). To keep things consistent and avoid stray artifacts, consider invoking make test here, or at least outputting to test/test_cheat and running that.

Suggested change
-o test_cheat test/test_cheat.c src/cheat.c
./test_cheat
-o test/test_cheat test/test_cheat.c src/cheat.c
./test/test_cheat

Copilot uses AI. Check for mistakes.

- name: Run SIMD blitter tests
run: |
# Detect which SIMD impl to test based on runner architecture
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
.build
/.claude
test/test_blitter_simd
test/test_cheat
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -609,9 +609,19 @@ 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.
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

.PHONY: clean test
endif

print-%:
Expand Down
1 change: 1 addition & 0 deletions Makefile.common
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
35 changes: 31 additions & 4 deletions libretro.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <compat/posix_string.h>
#include <compat/strl.h>

#include "cheat.h"
#include "file.h"
#include "jagbios.h"
#include "jagbios2.h"
Expand Down Expand Up @@ -881,14 +882,38 @@ 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;
}
}

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)
Expand Down Expand Up @@ -1012,6 +1037,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);
Expand Down Expand Up @@ -1122,6 +1148,7 @@ void retro_run(void)
update_input();

JaguarExecuteNew();
cheat_apply_all();
SoundCallback(NULL, sampleBuffer, vjs.hardwareTypeNTSC==1?BUFNTSC:BUFPAL);

// Resolution changed
Expand Down
144 changes: 144 additions & 0 deletions src/cheat.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#include <string.h>

#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;
}

bool cheat_parse_one(const char *code,
uint32_t *addr_out,
uint32_t *val_out,
uint8_t *size_out)
{
char buf[32];
size_t n = 0;
size_t addr_len, val_len, i;
uint32_t addr = 0, val = 0;

if (!code || !addr_out || !val_out || !size_out)
return false;

for (; *code && n < sizeof(buf) - 1; code++)
{
if (hex_digit((char)*code) >= 0)
buf[n++] = *code;
else if (*code != ' ' && *code != '\t' && *code != ':' &&
*code != '-' && *code != '.')
return false;
}
buf[n] = '\0';
Comment on lines +26 to +34
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cheat_parse_one() silently truncates input once it has collected 31 hex digits (buf size limit) without checking whether the original string still has additional hex digits. That can cause overlong inputs to be accepted after truncation (e.g., the first 16 digits form a valid code but extra digits follow). Consider detecting overflow (if the loop exits because n hit the limit and there are still characters left) and returning false, or cap the number of hex digits at 16 and reject anything beyond.

Copilot uses AI. Check for mistakes.

/* Layout: address_hex + value_hex (concatenated after separator strip).
* Accepted pairings cover byte/word/long values under 24- or 32-bit
* nominal addresses; the address is always masked to 24 bits below. */
switch (n)
{
case 8: addr_len = 6; val_len = 2; break; /* 6 + byte */
case 10: addr_len = 6; val_len = 4; break; /* 6 + word */
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 == (uint8_t)(index & 0xFF))
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 || !code)
return;

cheat_list_remove_index(list, index);
if (!enabled)
Comment on lines +91 to +95
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cheat_list_set()'s header contract says that when enabled=false it should remove existing entries for index, but the implementation returns early when code == NULL before calling cheat_list_remove_index(). This means callers that pass NULL (or disable without a code string) will be unable to turn cheats off. Consider removing the !code early-return (or moving the NULL check after the enabled branch) so removal always happens regardless of code.

Suggested change
if (!list || !code)
return;
cheat_list_remove_index(list, index);
if (!enabled)
if (!list)
return;
cheat_list_remove_index(list, index);
if (!enabled || !code)

Copilot uses AI. Check for mistakes.
return;

p = code;
start = p;
for (;;)
{
if (*p == '+' || *p == '\n' || *p == '\r' || *p == '\0')
{
size_t len = (size_t)(p - start);
if (len > 0 && list->count < CHEAT_MAX_ENTRIES)
{
char tmp[64];
cheat_entry_t c;
if (len >= sizeof(tmp))
len = sizeof(tmp) - 1;
memcpy(tmp, start, len);
tmp[len] = '\0';
if (cheat_parse_one(tmp, &c.address, &c.value, &c.size))
{
c.tag = (uint8_t)(index & 0xFF);
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);
}
}
84 changes: 84 additions & 0 deletions src/cheat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#ifndef __CHEAT_H__
#define __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"
*
* 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 <stdint.h>
#include <stddef.h>
#include <boolean.h>

#ifdef __cplusplus
extern "C" {
#endif

#define CHEAT_MAX_ENTRIES 256

typedef struct {
uint32_t address; /* 24-bit Jaguar bus address */
uint32_t value;
uint8_t size; /* 1 byte, 2 word, 4 long */
uint8_t tag; /* retro cheat 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 /* __CHEAT_H__ */
Loading
Loading