mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
1019 lines
30 KiB
C
1019 lines
30 KiB
C
#include "cheats.h"
|
|
|
|
#include "psx.h"
|
|
#include "log.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Portability shims */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* Same shape (and the same reasoning) as psx/state.c: C11 atomics where the toolchain has
|
|
them, volatile where it does not. The values crossing threads here are a single int flag
|
|
and a single pointer, both written by one producer and consumed by one consumer. */
|
|
#if !defined(__STDC_NO_ATOMICS__) && defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)
|
|
#include <stdatomic.h>
|
|
#define PSX_CHEATS_ATOMIC_INT _Atomic int
|
|
#define PSX_CHEATS_LOAD(p) atomic_load_explicit(&(p), memory_order_acquire)
|
|
#define PSX_CHEATS_STORE(p, v) atomic_store_explicit(&(p), (v), memory_order_release)
|
|
static atomic_flag g_cheats_lock = ATOMIC_FLAG_INIT;
|
|
#define PSX_CHEATS_LOCK() \
|
|
while (atomic_flag_test_and_set_explicit(&g_cheats_lock, memory_order_acquire)) { \
|
|
}
|
|
#define PSX_CHEATS_UNLOCK() atomic_flag_clear_explicit(&g_cheats_lock, memory_order_release)
|
|
#else
|
|
#define PSX_CHEATS_ATOMIC_INT volatile int
|
|
#define PSX_CHEATS_LOAD(p) (p)
|
|
#define PSX_CHEATS_STORE(p, v) ((p) = (v))
|
|
#define PSX_CHEATS_LOCK() ((void)0)
|
|
#define PSX_CHEATS_UNLOCK() ((void)0)
|
|
#endif
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Code types */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
#define CHEAT_T_WRITE8 0x30u
|
|
#define CHEAT_T_WRITE16 0x80u
|
|
#define CHEAT_T_SLIDE 0x50u
|
|
#define CHEAT_T_IF16_EQ 0xD0u
|
|
#define CHEAT_T_IF16_NE 0xD1u
|
|
#define CHEAT_T_IF16_LT 0xD2u
|
|
#define CHEAT_T_IF16_GT 0xD3u
|
|
#define CHEAT_T_IF8_EQ 0xE0u
|
|
#define CHEAT_T_IF8_NE 0xE1u
|
|
#define CHEAT_T_IF8_LT 0xE2u
|
|
#define CHEAT_T_IF8_GT 0xE3u
|
|
|
|
/* Not a GameShark type. Separates one armed cheat from the next inside the flat program so
|
|
a dangling conditional or an unconsumed slide can never leak across the boundary. */
|
|
#define CHEAT_T_BOUNDARY 0xFFu
|
|
|
|
static int psx_cheat_type_supported(uint32_t type) {
|
|
switch (type) {
|
|
case CHEAT_T_WRITE8:
|
|
case CHEAT_T_WRITE16:
|
|
case CHEAT_T_SLIDE:
|
|
case CHEAT_T_IF16_EQ:
|
|
case CHEAT_T_IF16_NE:
|
|
case CHEAT_T_IF16_LT:
|
|
case CHEAT_T_IF16_GT:
|
|
case CHEAT_T_IF8_EQ:
|
|
case CHEAT_T_IF8_NE:
|
|
case CHEAT_T_IF8_LT:
|
|
case CHEAT_T_IF8_GT:
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Catalogue */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
typedef struct {
|
|
char* name; /* owned */
|
|
char* description; /* owned, "" when the file gave none */
|
|
uint32_t* words; /* owned; 2 per code line: code, value */
|
|
int line_count;
|
|
int armed;
|
|
int has_unsupported;
|
|
} psx_cheat_entry_t;
|
|
|
|
static psx_cheat_entry_t* g_entries = NULL;
|
|
static int g_entry_count = 0;
|
|
static char* g_source_path = NULL; /* owned */
|
|
static char* g_unsupported = NULL; /* owned */
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Program (what the emulation thread walks) */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
typedef struct {
|
|
int word_count; /* number of uint32_t, always even */
|
|
int cheat_count;
|
|
uint32_t words[1]; /* over-allocated */
|
|
} psx_cheat_program_t;
|
|
|
|
/* Parked by the arming thread, adopted by the emulation thread. Both transitions happen
|
|
under the lock, so the pointer can never be freed while the other side holds it. */
|
|
static psx_cheat_program_t* g_pending = NULL;
|
|
static PSX_CHEATS_ATOMIC_INT g_publish_pending = 0;
|
|
|
|
/* Emulation thread ONLY. Never read or written anywhere else — that is what makes the
|
|
per-frame fast path a plain load with no synchronisation at all. */
|
|
static psx_cheat_program_t* g_active = NULL;
|
|
|
|
static PSX_CHEATS_ATOMIC_INT g_armed_count = 0;
|
|
static PSX_CHEATS_ATOMIC_INT g_inhibited = 0;
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Small helpers */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* Every string this module keeps is its own copy. A borrowed pointer from a caller that
|
|
later frees (or, worse, is a mapped file) is how a previous device path in this codebase
|
|
ended up truncating a user's disc image. */
|
|
static char* psx_cheat_strdup(const char* s, size_t max) {
|
|
size_t len;
|
|
char* out;
|
|
|
|
if (!s)
|
|
s = "";
|
|
|
|
len = strlen(s);
|
|
|
|
if (len > max)
|
|
len = max;
|
|
|
|
out = (char*)malloc(len + 1);
|
|
|
|
if (!out)
|
|
return NULL;
|
|
|
|
memcpy(out, s, len);
|
|
out[len] = '\0';
|
|
|
|
return out;
|
|
}
|
|
|
|
static void psx_cheat_trim(char* s) {
|
|
size_t len;
|
|
size_t start = 0;
|
|
|
|
if (!s)
|
|
return;
|
|
|
|
len = strlen(s);
|
|
|
|
while (len > start && (unsigned char)s[len - 1] <= ' ')
|
|
len--;
|
|
|
|
while (start < len && (unsigned char)s[start] <= ' ')
|
|
start++;
|
|
|
|
if (start)
|
|
memmove(s, s + start, len - start);
|
|
|
|
s[len - start] = '\0';
|
|
}
|
|
|
|
/* The [start, end) of `s` with leading and trailing whitespace removed. */
|
|
static void psx_cheat_span(const char* s, const char** start, const char** end) {
|
|
const char* first = s ? s : "";
|
|
const char* last;
|
|
|
|
while (*first && (unsigned char)*first <= ' ')
|
|
first++;
|
|
|
|
last = first + strlen(first);
|
|
|
|
while (last > first && (unsigned char)*(last - 1) <= ' ')
|
|
last--;
|
|
|
|
*start = first;
|
|
*end = last;
|
|
}
|
|
|
|
/* Case-insensitive, leading/trailing space ignored — so a name typed by hand into
|
|
settings.toml matches the one the file declares. */
|
|
static int psx_cheat_name_matches(const char* a, const char* b) {
|
|
const char* a_start;
|
|
const char* a_end;
|
|
const char* b_start;
|
|
const char* b_end;
|
|
|
|
if (!a || !b)
|
|
return 0;
|
|
|
|
psx_cheat_span(a, &a_start, &a_end);
|
|
psx_cheat_span(b, &b_start, &b_end);
|
|
|
|
if ((a_end - a_start) != (b_end - b_start))
|
|
return 0;
|
|
|
|
while (a_start < a_end) {
|
|
if (tolower((unsigned char)*a_start) != tolower((unsigned char)*b_start))
|
|
return 0;
|
|
|
|
a_start++;
|
|
b_start++;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Parsing */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* A run of hex digits, at least `min` and at most `max` of them. Returns the count consumed
|
|
(0 = no match) and leaves *out holding the value. */
|
|
static int psx_cheat_hex(const char* s, int min, int max, uint32_t* out) {
|
|
uint32_t value = 0;
|
|
int used = 0;
|
|
|
|
while (used < max) {
|
|
int c = (unsigned char)s[used];
|
|
int digit;
|
|
|
|
if (c >= '0' && c <= '9')
|
|
digit = c - '0';
|
|
else if (c >= 'a' && c <= 'f')
|
|
digit = c - 'a' + 10;
|
|
else if (c >= 'A' && c <= 'F')
|
|
digit = c - 'A' + 10;
|
|
else
|
|
break;
|
|
|
|
value = (value << 4) | (uint32_t)digit;
|
|
used++;
|
|
}
|
|
|
|
if (used < min)
|
|
return 0;
|
|
|
|
*out = value;
|
|
|
|
return used;
|
|
}
|
|
|
|
/*
|
|
One code line -> (code, value).
|
|
|
|
Accepts the three shapes that actually appear in the wild:
|
|
|
|
800AB3C4 0063 the usual one
|
|
800AB3C4:0063 colon-separated, as some sites render it
|
|
800AB3C40063 run together, as some copy/paste loses the space
|
|
|
|
Returns 0 when the line is not a code line at all.
|
|
*/
|
|
static int psx_cheat_parse_code(const char* line, uint32_t* code, uint32_t* value) {
|
|
const char* p = line;
|
|
uint32_t hi = 0;
|
|
uint32_t lo = 0;
|
|
int used;
|
|
|
|
while (*p && (unsigned char)*p <= ' ')
|
|
p++;
|
|
|
|
used = psx_cheat_hex(p, 8, 8, &hi);
|
|
|
|
if (!used)
|
|
return 0;
|
|
|
|
p += used;
|
|
|
|
/* Run-together form: the value is glued to the address. */
|
|
if (*p && ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) {
|
|
used = psx_cheat_hex(p, 1, 8, &lo);
|
|
|
|
if (!used)
|
|
return 0;
|
|
|
|
p += used;
|
|
} else {
|
|
while (*p && ((unsigned char)*p <= ' ' || *p == ':' || *p == ','))
|
|
p++;
|
|
|
|
used = psx_cheat_hex(p, 1, 8, &lo);
|
|
|
|
if (!used)
|
|
return 0;
|
|
|
|
p += used;
|
|
}
|
|
|
|
/* Anything after the two words that is not blank or a comment means this was prose that
|
|
merely started with hex, not a code. Refusing it is what keeps a description line out
|
|
of the program. */
|
|
while (*p && (unsigned char)*p <= ' ')
|
|
p++;
|
|
|
|
if (*p && *p != '#' && *p != ';' && !(p[0] == '/' && p[1] == '/'))
|
|
return 0;
|
|
|
|
*code = hi;
|
|
*value = lo;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static void psx_cheat_free_entries(psx_cheat_entry_t* entries, int count) {
|
|
int i;
|
|
|
|
if (!entries)
|
|
return;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
free(entries[i].name);
|
|
free(entries[i].description);
|
|
free(entries[i].words);
|
|
}
|
|
|
|
free(entries);
|
|
}
|
|
|
|
/* Append `type` to the "types this build does not implement" set, once each. */
|
|
static void psx_cheat_note_unsupported(char* buffer, size_t capacity, uint32_t type) {
|
|
char token[8];
|
|
size_t len;
|
|
|
|
snprintf(token, sizeof(token), "%02X", (unsigned)(type & 0xFFu));
|
|
|
|
if (strstr(buffer, token))
|
|
return;
|
|
|
|
len = strlen(buffer);
|
|
|
|
if (len + strlen(token) + 2 >= capacity)
|
|
return;
|
|
|
|
if (len) {
|
|
buffer[len++] = ' ';
|
|
buffer[len] = '\0';
|
|
}
|
|
|
|
memcpy(buffer + len, token, strlen(token) + 1);
|
|
}
|
|
|
|
int psx_cheats_load_file(const char* path) {
|
|
FILE* file;
|
|
char line[1024];
|
|
psx_cheat_entry_t* entries = NULL;
|
|
int count = 0;
|
|
int capacity = 0;
|
|
int total_lines = 0;
|
|
int truncated = 0;
|
|
int first_line = 1;
|
|
char unsupported[128];
|
|
char* new_path;
|
|
char* new_unsupported;
|
|
psx_cheat_entry_t* old_entries;
|
|
int old_count;
|
|
char* old_path;
|
|
char* old_unsupported;
|
|
|
|
unsupported[0] = '\0';
|
|
|
|
/* No file for this game is the NORMAL case, not a failure. Clearing here rather than
|
|
leaving the previous game's catalogue in place is what stops a cheat list following
|
|
the user from one disc to the next. */
|
|
if (!path || !path[0]) {
|
|
PSX_CHEATS_LOCK();
|
|
old_entries = g_entries;
|
|
old_count = g_entry_count;
|
|
old_path = g_source_path;
|
|
old_unsupported = g_unsupported;
|
|
g_entries = NULL;
|
|
g_entry_count = 0;
|
|
g_source_path = NULL;
|
|
g_unsupported = NULL;
|
|
PSX_CHEATS_UNLOCK();
|
|
|
|
psx_cheat_free_entries(old_entries, old_count);
|
|
free(old_path);
|
|
free(old_unsupported);
|
|
|
|
return 0;
|
|
}
|
|
|
|
file = fopen(path, "rb");
|
|
|
|
if (!file) {
|
|
log_info("cheats: no cheat file at %s", path);
|
|
return -1;
|
|
}
|
|
|
|
while (fgets(line, (int)sizeof(line), file)) {
|
|
char* p = line;
|
|
uint32_t code = 0;
|
|
uint32_t value = 0;
|
|
|
|
/* Strip the newline and any UTF-8 BOM the first line may carry. Without the BOM strip
|
|
the first `[Name]` is not recognised and the whole file's codes end up in a single
|
|
"Unnamed codes" group — Windows editors write that BOM by default. */
|
|
p[strcspn(p, "\r\n")] = '\0';
|
|
|
|
if (first_line) {
|
|
first_line = 0;
|
|
|
|
if ((unsigned char)p[0] == 0xEF && (unsigned char)p[1] == 0xBB &&
|
|
(unsigned char)p[2] == 0xBF) {
|
|
p += 3;
|
|
}
|
|
}
|
|
|
|
while (*p && (unsigned char)*p <= ' ')
|
|
p++;
|
|
|
|
if (!*p || *p == '#' || *p == ';' || (p[0] == '/' && p[1] == '/'))
|
|
continue;
|
|
|
|
if (*p == '[') {
|
|
char* end = strrchr(p, ']');
|
|
psx_cheat_entry_t* entry;
|
|
|
|
if (end)
|
|
*end = '\0';
|
|
|
|
if (count >= PSX_CHEAT_MAX_ENTRIES) {
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
|
|
if (count == capacity) {
|
|
int next = capacity ? capacity * 2 : 32;
|
|
psx_cheat_entry_t* grown =
|
|
(psx_cheat_entry_t*)realloc(entries, (size_t)next * sizeof(psx_cheat_entry_t));
|
|
|
|
if (!grown) {
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
|
|
entries = grown;
|
|
capacity = next;
|
|
}
|
|
|
|
entry = &entries[count];
|
|
memset(entry, 0, sizeof(*entry));
|
|
|
|
psx_cheat_trim(p + 1);
|
|
entry->name = psx_cheat_strdup(p + 1, PSX_CHEAT_NAME_MAX);
|
|
entry->description = psx_cheat_strdup("", PSX_CHEAT_DESC_MAX);
|
|
|
|
if (!entry->name || !entry->description) {
|
|
free(entry->name);
|
|
free(entry->description);
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
|
|
/* An unnamed group is still a group: it just gets a name a user can select. */
|
|
if (!entry->name[0]) {
|
|
free(entry->name);
|
|
entry->name = psx_cheat_strdup("(unnamed)", PSX_CHEAT_NAME_MAX);
|
|
|
|
if (!entry->name) {
|
|
free(entry->description);
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
count++;
|
|
continue;
|
|
}
|
|
|
|
if (p[0] == '@') {
|
|
/* `@desc <text>` — the one piece of metadata this reads. Anything else beginning
|
|
with @ is ignored so the format can grow without breaking old builds. */
|
|
if (count > 0 && strncmp(p + 1, "desc", 4) == 0) {
|
|
char* text = p + 5;
|
|
char* merged;
|
|
size_t have;
|
|
size_t add;
|
|
|
|
while (*text && (unsigned char)*text <= ' ')
|
|
text++;
|
|
|
|
have = strlen(entries[count - 1].description);
|
|
add = strlen(text);
|
|
|
|
if (have + add + 2 <= PSX_CHEAT_DESC_MAX) {
|
|
merged = (char*)realloc(entries[count - 1].description, have + add + 2);
|
|
|
|
if (merged) {
|
|
if (have)
|
|
merged[have++] = ' ';
|
|
|
|
memcpy(merged + have, text, add + 1);
|
|
entries[count - 1].description = merged;
|
|
}
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!psx_cheat_parse_code(p, &code, &value))
|
|
continue;
|
|
|
|
if (total_lines >= PSX_CHEAT_MAX_LINES) {
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
|
|
/* Codes before the first [Name]. Rather than dropping them, give them a group so the
|
|
user can see and switch them on — a plain list of codes with no header is a real
|
|
thing people paste. */
|
|
if (count == 0) {
|
|
psx_cheat_entry_t* entry;
|
|
|
|
if (count == capacity) {
|
|
int next = capacity ? capacity * 2 : 32;
|
|
psx_cheat_entry_t* grown =
|
|
(psx_cheat_entry_t*)realloc(entries, (size_t)next * sizeof(psx_cheat_entry_t));
|
|
|
|
if (!grown) {
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
|
|
entries = grown;
|
|
capacity = next;
|
|
}
|
|
|
|
entry = &entries[count];
|
|
memset(entry, 0, sizeof(*entry));
|
|
entry->name = psx_cheat_strdup("Unnamed codes", PSX_CHEAT_NAME_MAX);
|
|
entry->description = psx_cheat_strdup("", PSX_CHEAT_DESC_MAX);
|
|
|
|
if (!entry->name || !entry->description) {
|
|
free(entry->name);
|
|
free(entry->description);
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
|
|
count++;
|
|
}
|
|
|
|
{
|
|
psx_cheat_entry_t* entry = &entries[count - 1];
|
|
uint32_t* grown =
|
|
(uint32_t*)realloc(entry->words, (size_t)(entry->line_count + 1) * 2 * sizeof(uint32_t));
|
|
|
|
if (!grown) {
|
|
truncated = 1;
|
|
break;
|
|
}
|
|
|
|
entry->words = grown;
|
|
entry->words[entry->line_count * 2 + 0] = code;
|
|
entry->words[entry->line_count * 2 + 1] = value;
|
|
entry->line_count++;
|
|
total_lines++;
|
|
|
|
if (!psx_cheat_type_supported(code >> 24)) {
|
|
entry->has_unsupported = 1;
|
|
psx_cheat_note_unsupported(unsupported, sizeof(unsupported), code >> 24);
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose(file);
|
|
|
|
if (truncated) {
|
|
log_warn("cheats: %s is malformed or too large; stopped after %d entries", path, count);
|
|
}
|
|
|
|
new_path = psx_cheat_strdup(path, 4095);
|
|
new_unsupported = psx_cheat_strdup(unsupported, sizeof(unsupported));
|
|
|
|
if (!new_path || !new_unsupported) {
|
|
free(new_path);
|
|
free(new_unsupported);
|
|
psx_cheat_free_entries(entries, count);
|
|
log_error("cheats: out of memory loading %s", path);
|
|
return -1;
|
|
}
|
|
|
|
PSX_CHEATS_LOCK();
|
|
old_entries = g_entries;
|
|
old_count = g_entry_count;
|
|
old_path = g_source_path;
|
|
old_unsupported = g_unsupported;
|
|
g_entries = entries;
|
|
g_entry_count = count;
|
|
g_source_path = new_path;
|
|
g_unsupported = new_unsupported;
|
|
PSX_CHEATS_UNLOCK();
|
|
|
|
psx_cheat_free_entries(old_entries, old_count);
|
|
free(old_path);
|
|
free(old_unsupported);
|
|
|
|
if (unsupported[0]) {
|
|
log_info("cheats: loaded %d entries from %s (unimplemented code types present: %s)",
|
|
count, path, unsupported);
|
|
} else {
|
|
log_info("cheats: loaded %d entries from %s", count, path);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Catalogue accessors */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
const char* psx_cheats_source_path(void) {
|
|
return g_source_path ? g_source_path : "";
|
|
}
|
|
|
|
int psx_cheats_count(void) {
|
|
return g_entry_count;
|
|
}
|
|
|
|
const char* psx_cheats_name(int index) {
|
|
if (index < 0 || index >= g_entry_count || !g_entries)
|
|
return "";
|
|
|
|
return g_entries[index].name ? g_entries[index].name : "";
|
|
}
|
|
|
|
const char* psx_cheats_description(int index) {
|
|
if (index < 0 || index >= g_entry_count || !g_entries)
|
|
return "";
|
|
|
|
return g_entries[index].description ? g_entries[index].description : "";
|
|
}
|
|
|
|
int psx_cheats_line_count(int index) {
|
|
if (index < 0 || index >= g_entry_count || !g_entries)
|
|
return 0;
|
|
|
|
return g_entries[index].line_count;
|
|
}
|
|
|
|
int psx_cheats_has_unsupported(int index) {
|
|
if (index < 0 || index >= g_entry_count || !g_entries)
|
|
return 0;
|
|
|
|
return g_entries[index].has_unsupported;
|
|
}
|
|
|
|
const char* psx_cheats_unsupported_types(void) {
|
|
return g_unsupported ? g_unsupported : "";
|
|
}
|
|
|
|
char* psx_cheats_describe(void) {
|
|
size_t needed = 1;
|
|
char* out;
|
|
size_t used = 0;
|
|
int i;
|
|
|
|
PSX_CHEATS_LOCK();
|
|
|
|
for (i = 0; i < g_entry_count; i++) {
|
|
/* name + SEP + up to 10 digits + SEP + 1 + SEP + description + record separator */
|
|
needed += strlen(g_entries[i].name ? g_entries[i].name : "") +
|
|
strlen(g_entries[i].description ? g_entries[i].description : "") + 24;
|
|
}
|
|
|
|
out = (char*)malloc(needed);
|
|
|
|
if (!out) {
|
|
PSX_CHEATS_UNLOCK();
|
|
return NULL;
|
|
}
|
|
|
|
out[0] = '\0';
|
|
|
|
for (i = 0; i < g_entry_count; i++) {
|
|
const char* name = g_entries[i].name ? g_entries[i].name : "";
|
|
const char* desc = g_entries[i].description ? g_entries[i].description : "";
|
|
int written;
|
|
|
|
if (used)
|
|
out[used++] = '\x1E';
|
|
|
|
written = snprintf(out + used, needed - used, "%s\x1F%d\x1F%d\x1F%s", name,
|
|
g_entries[i].line_count, g_entries[i].has_unsupported ? 1 : 0, desc);
|
|
|
|
if (written < 0 || (size_t)written >= needed - used) {
|
|
/* Cannot happen with the sizing above; truncate rather than run off the end. */
|
|
out[used] = '\0';
|
|
break;
|
|
}
|
|
|
|
used += (size_t)written;
|
|
}
|
|
|
|
PSX_CHEATS_UNLOCK();
|
|
|
|
return out;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Arming */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
int psx_cheats_arm(int master, const char* const* names, int count, int* missing) {
|
|
psx_cheat_program_t* program = NULL;
|
|
psx_cheat_program_t* replaced = NULL;
|
|
int armed = 0;
|
|
int words = 0;
|
|
int i;
|
|
int j;
|
|
|
|
if (missing)
|
|
*missing = 0;
|
|
|
|
/* Hardcore. Refuse rather than arm-and-hope: a cheat that armed here and was only
|
|
stopped further down would be one refactor away from reaching the game. */
|
|
if (PSX_CHEATS_LOAD(g_inhibited)) {
|
|
master = 0;
|
|
}
|
|
|
|
if (!names)
|
|
count = 0;
|
|
|
|
/*
|
|
Held across the WHOLE walk, not just the publish.
|
|
|
|
This runs on the UI thread through JNI and on the emulation thread once at boot, and it
|
|
reads g_entries end to end — so a psx_cheats_load_file() landing mid-walk would free the
|
|
array under it. Everything inside is a comparison or a memcpy of a few hundred bytes;
|
|
the file read that would actually be worth not holding a lock across happens in
|
|
psx_cheats_load_file(), which parses into locals and only takes the lock to swap.
|
|
*/
|
|
PSX_CHEATS_LOCK();
|
|
|
|
for (i = 0; i < g_entry_count; i++) {
|
|
int wanted = 0;
|
|
|
|
for (j = 0; j < count; j++) {
|
|
if (psx_cheat_name_matches(g_entries[i].name, names[j])) {
|
|
wanted = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_entries[i].armed = (master && wanted) ? 1 : 0;
|
|
|
|
if (g_entries[i].armed) {
|
|
armed++;
|
|
/* +1 line for this cheat's boundary marker. */
|
|
words += (g_entries[i].line_count + 1) * 2;
|
|
}
|
|
}
|
|
|
|
if (missing) {
|
|
for (j = 0; j < count; j++) {
|
|
int found = 0;
|
|
|
|
for (i = 0; i < g_entry_count; i++) {
|
|
if (psx_cheat_name_matches(g_entries[i].name, names[j])) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
(*missing)++;
|
|
}
|
|
}
|
|
|
|
if (armed > 0 && words > 0) {
|
|
program = (psx_cheat_program_t*)malloc(sizeof(psx_cheat_program_t) +
|
|
(size_t)(words - 1) * sizeof(uint32_t));
|
|
|
|
if (!program) {
|
|
PSX_CHEATS_UNLOCK();
|
|
log_error("cheats: out of memory arming %d entries", armed);
|
|
return 0;
|
|
}
|
|
|
|
program->word_count = 0;
|
|
program->cheat_count = armed;
|
|
|
|
for (i = 0; i < g_entry_count; i++) {
|
|
if (!g_entries[i].armed)
|
|
continue;
|
|
|
|
memcpy(&program->words[program->word_count], g_entries[i].words,
|
|
(size_t)g_entries[i].line_count * 2 * sizeof(uint32_t));
|
|
program->word_count += g_entries[i].line_count * 2;
|
|
|
|
program->words[program->word_count++] = (uint32_t)CHEAT_T_BOUNDARY << 24;
|
|
program->words[program->word_count++] = 0;
|
|
}
|
|
}
|
|
|
|
replaced = g_pending;
|
|
g_pending = program;
|
|
PSX_CHEATS_STORE(g_publish_pending, 1);
|
|
PSX_CHEATS_UNLOCK();
|
|
|
|
/* Whatever we displaced was never handed to the emulation thread (it took it out of
|
|
g_pending under the same lock, or it is still sitting there), so this is ours to free. */
|
|
free(replaced);
|
|
|
|
PSX_CHEATS_STORE(g_armed_count, armed);
|
|
|
|
log_info("cheats: %d of %d entries armed%s", armed, g_entry_count,
|
|
PSX_CHEATS_LOAD(g_inhibited) ? " (hardcore: forced off)" : "");
|
|
|
|
return armed;
|
|
}
|
|
|
|
int psx_cheats_armed_count(void) {
|
|
return PSX_CHEATS_LOAD(g_armed_count);
|
|
}
|
|
|
|
void psx_cheats_set_inhibited(int inhibited) {
|
|
const int want = inhibited ? 1 : 0;
|
|
|
|
if (PSX_CHEATS_LOAD(g_inhibited) == want)
|
|
return;
|
|
|
|
PSX_CHEATS_STORE(g_inhibited, want);
|
|
|
|
if (want) {
|
|
/* Publish an empty program NOW. The selection itself is untouched in the catalogue,
|
|
so clearing hardcore and re-arming restores exactly what the user had. */
|
|
psx_cheat_program_t* replaced;
|
|
|
|
PSX_CHEATS_LOCK();
|
|
replaced = g_pending;
|
|
g_pending = NULL;
|
|
PSX_CHEATS_STORE(g_publish_pending, 1);
|
|
PSX_CHEATS_UNLOCK();
|
|
|
|
free(replaced);
|
|
PSX_CHEATS_STORE(g_armed_count, 0);
|
|
|
|
log_info("cheats: disabled — RetroAchievements hardcore mode is active");
|
|
}
|
|
}
|
|
|
|
int psx_cheats_inhibited(void) {
|
|
return PSX_CHEATS_LOAD(g_inhibited);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Per-frame application (emulation thread) */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static void psx_cheat_write(psx_t* psx, uint32_t address, uint32_t value, int width) {
|
|
psx_ram_t* ram = psx->ram;
|
|
|
|
/* Refuse rather than alias. psx_ram_write*() masks with (size - 1), so a code written
|
|
for an 8 MB debug unit would otherwise land somewhere real on a 2 MB console. */
|
|
if ((size_t)address + (size_t)width > ram->size)
|
|
return;
|
|
|
|
if (width == 1)
|
|
psx_ram_write8(ram, address, (uint8_t)value);
|
|
else
|
|
psx_ram_write16(ram, address, (uint16_t)value);
|
|
|
|
/* The cached interpreter caches decoded words by physical address. A cheat that patches
|
|
code (a `jr $ra` over a check is a common one) would otherwise keep running the old
|
|
instruction until the entry was evicted. No-op when the plain interpreter is selected. */
|
|
psx_cpu_invalidate_range(psx->cpu, address, (uint32_t)width);
|
|
}
|
|
|
|
void psx_cheats_apply(struct psx_t* psx_opaque) {
|
|
psx_t* psx = (psx_t*)psx_opaque;
|
|
psx_cheat_program_t* program;
|
|
int index;
|
|
int skip_next = 0;
|
|
uint32_t slide_count = 0;
|
|
uint32_t slide_addr_step = 0;
|
|
uint32_t slide_value_step = 0;
|
|
|
|
/* THE fast path. One predictable branch on a thread-local pointer plus one atomic load,
|
|
once per frame, for every session that never arms a cheat. */
|
|
if (!g_active && !PSX_CHEATS_LOAD(g_publish_pending))
|
|
return;
|
|
|
|
if (PSX_CHEATS_LOAD(g_publish_pending)) {
|
|
psx_cheat_program_t* adopted;
|
|
|
|
PSX_CHEATS_LOCK();
|
|
adopted = g_pending;
|
|
g_pending = NULL;
|
|
PSX_CHEATS_STORE(g_publish_pending, 0);
|
|
PSX_CHEATS_UNLOCK();
|
|
|
|
free(g_active);
|
|
g_active = adopted;
|
|
}
|
|
|
|
program = g_active;
|
|
|
|
if (!program || !psx || !psx->ram || !psx->cpu)
|
|
return;
|
|
|
|
for (index = 0; index + 1 < program->word_count; index += 2) {
|
|
const uint32_t code = program->words[index];
|
|
const uint32_t value = program->words[index + 1];
|
|
const uint32_t type = code >> 24;
|
|
const uint32_t address = code & 0x00FFFFFFu;
|
|
|
|
if (type == CHEAT_T_BOUNDARY) {
|
|
skip_next = 0;
|
|
slide_count = 0;
|
|
continue;
|
|
}
|
|
|
|
if (skip_next) {
|
|
skip_next = 0;
|
|
continue;
|
|
}
|
|
|
|
switch (type) {
|
|
case CHEAT_T_SLIDE:
|
|
/* 5000ccii vvvv — arms the NEXT write. Held rather than applied. */
|
|
slide_count = (code >> 8) & 0xFFu;
|
|
slide_addr_step = code & 0xFFu;
|
|
slide_value_step = value;
|
|
break;
|
|
|
|
case CHEAT_T_WRITE8:
|
|
case CHEAT_T_WRITE16: {
|
|
const int width = (type == CHEAT_T_WRITE8) ? 1 : 2;
|
|
uint32_t repeats = slide_count ? slide_count : 1;
|
|
uint32_t step = slide_count ? slide_addr_step : 0;
|
|
uint32_t delta = slide_count ? slide_value_step : 0;
|
|
uint32_t current_address = address;
|
|
uint32_t current_value = value;
|
|
uint32_t n;
|
|
|
|
for (n = 0; n < repeats; n++) {
|
|
psx_cheat_write(psx, current_address, current_value, width);
|
|
current_address += step;
|
|
current_value += delta;
|
|
}
|
|
|
|
slide_count = 0;
|
|
break;
|
|
}
|
|
|
|
case CHEAT_T_IF16_EQ:
|
|
skip_next = (psx_ram_read16(psx->ram, address) != (uint16_t)value);
|
|
break;
|
|
case CHEAT_T_IF16_NE:
|
|
skip_next = (psx_ram_read16(psx->ram, address) == (uint16_t)value);
|
|
break;
|
|
case CHEAT_T_IF16_LT:
|
|
skip_next = !(psx_ram_read16(psx->ram, address) < (uint16_t)value);
|
|
break;
|
|
case CHEAT_T_IF16_GT:
|
|
skip_next = !(psx_ram_read16(psx->ram, address) > (uint16_t)value);
|
|
break;
|
|
|
|
case CHEAT_T_IF8_EQ:
|
|
skip_next = (psx_ram_read8(psx->ram, address) != (uint8_t)value);
|
|
break;
|
|
case CHEAT_T_IF8_NE:
|
|
skip_next = (psx_ram_read8(psx->ram, address) == (uint8_t)value);
|
|
break;
|
|
case CHEAT_T_IF8_LT:
|
|
skip_next = !(psx_ram_read8(psx->ram, address) < (uint8_t)value);
|
|
break;
|
|
case CHEAT_T_IF8_GT:
|
|
skip_next = !(psx_ram_read8(psx->ram, address) > (uint8_t)value);
|
|
break;
|
|
|
|
default:
|
|
/* Unimplemented type. Reported once at load time by name; SILENT here,
|
|
because this runs sixty times a second and one error-level log on a
|
|
per-frame path in this codebase once produced 89,000 lines in 105
|
|
seconds. */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void psx_cheats_shutdown(void) {
|
|
psx_cheat_entry_t* entries;
|
|
int count;
|
|
char* path;
|
|
char* unsupported;
|
|
psx_cheat_program_t* pending;
|
|
|
|
PSX_CHEATS_LOCK();
|
|
entries = g_entries;
|
|
count = g_entry_count;
|
|
path = g_source_path;
|
|
unsupported = g_unsupported;
|
|
pending = g_pending;
|
|
g_entries = NULL;
|
|
g_entry_count = 0;
|
|
g_source_path = NULL;
|
|
g_unsupported = NULL;
|
|
g_pending = NULL;
|
|
PSX_CHEATS_STORE(g_publish_pending, 0);
|
|
PSX_CHEATS_UNLOCK();
|
|
|
|
psx_cheat_free_entries(entries, count);
|
|
free(path);
|
|
free(unsupported);
|
|
free(pending);
|
|
free(g_active);
|
|
g_active = NULL;
|
|
|
|
PSX_CHEATS_STORE(g_armed_count, 0);
|
|
}
|