Files
CheatRunner/src/cr_api.c
T
2026-06-18 17:19:03 -03:00

4463 lines
175 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <ps5/kernel.h>
#include "ps5sdk_compat.h"
#include "jb.h"
#include "priv_bootstrap.h"
#include "pt.h"
#include "third_party/cJSON.h"
#include "cr_log.h"
#include "cr_paths.h"
#include "cr_config.h"
#include "cr_notifications.h"
#include "cr_activity.h"
#include "cr_titles.h"
#include "cr_appdb.h"
#include "cr_game_monitor.h"
#include "cr_launch.h"
#include "cr_memory.h"
#include "cr_json.h"
#include "cr_cheat_formats.h"
#include "cr_version.h"
#include "cr_cheats.h"
#include "cr_patch_parser.h"
#include "cr_http.h"
#include "cr_remote_sources.h"
#include "cr_source_jobs.h"
#include "cr_repo_mirror.h"
#include "cr_api.h"
#include "cr_api_internal.h"
#include "cr_conflict.h"
#include "cr_api_dashboard.h"
#include "cr_api_games.h"
#include "cr_api_cheats.h"
#include "cr_api_patches.h"
#include "cr_api_sources.h"
#include "cr_api_dev.h"
#include "cr_api_logs.h"
#include "cr_fan.h"
#include "cr_profile.h"
#include "cr_shutdown.h"
#include "cr_store_lookup.h"
#include "http_client.h"
#if CHEATRUNNER_HAVE_BROWSER_OPEN
#include "cr_browser.h"
#endif
#ifndef CHEATRUNNER_VERSION
#define CHEATRUNNER_VERSION "0.14"
#endif
#define MAX_GAMES CR_APPDB_MAX_GAMES
const char g_dashboard_html[] =
#include "dashboard_html.inc"
;
const char g_dashboard_css[] =
#include "dashboard_css.inc"
;
const char g_dashboard_js[] =
#include "dashboard_js.inc"
;
#include "cheatrunner_png.h"
static int read_title_lookup_cache_name(const char *title_id, char *out, size_t out_size);
static int write_title_lookup_cache_name(const char *title_id, const char *name);
static int
url_decode(char *dst, size_t dst_size, const char *src, size_t src_len) {
size_t o = 0;
for (size_t i = 0; i < src_len; i++) {
if (o + 1 >= dst_size) {
return -1;
}
if (src[i] == '+') {
dst[o++] = ' ';
continue;
}
if (src[i] == '%' && i + 2 < src_len) {
int hi = isdigit((unsigned char)src[i + 1]) ? src[i + 1] - '0' : (tolower((unsigned char)src[i + 1]) - 'a' + 10);
int lo = isdigit((unsigned char)src[i + 2]) ? src[i + 2] - '0' : (tolower((unsigned char)src[i + 2]) - 'a' + 10);
if (hi < 0 || hi > 15 || lo < 0 || lo > 15) {
return -1;
}
dst[o++] = (char)((hi << 4) | lo);
i += 2;
continue;
}
dst[o++] = src[i];
}
dst[o] = '\0';
return 0;
}
int
query_value(const char *query, const char *key, char *out, size_t out_size) {
if (!query || !key || !out || out_size == 0) {
return -1;
}
size_t key_len = strlen(key);
const char *p = query;
while (*p) {
const char *amp = strchr(p, '&');
size_t part_len = amp ? (size_t)(amp - p) : strlen(p);
const char *eq = memchr(p, '=', part_len);
if (eq) {
size_t k_len = (size_t)(eq - p);
if (k_len == key_len && strncmp(p, key, key_len) == 0) {
const char *val = eq + 1;
size_t val_len = part_len - (k_len + 1);
return url_decode(out, out_size, val, val_len);
}
}
if (!amp) {
break;
}
p = amp + 1;
}
return -1;
}
static const char *
status_text_for(int status) {
switch (status) {
case 200:
return "OK";
case 400:
return "Bad Request";
case 404:
return "Not Found";
case 405:
return "Method Not Allowed";
case 409:
return "Conflict";
case 413:
return "Payload Too Large";
case 429:
return "Too Many Requests";
case 500:
return "Internal Server Error";
case 502:
return "Bad Gateway";
case 503:
return "Service Unavailable";
default:
return "OK";
}
}
static int
socket_send_all(int fd, const void *buf, size_t len) {
const uint8_t *p = (const uint8_t *)buf;
size_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, p + sent, len - sent, 0);
if (n > 0) {
sent += (size_t)n;
continue;
}
if (n < 0 && errno == EINTR) {
continue;
}
return -1;
}
return 0;
}
void
http_send_response(int fd, int status, const char *content_type, const uint8_t *body, size_t body_len) {
char header[512];
int n = snprintf(header, sizeof(header),
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Cache-Control: no-cache\r\n"
"\r\n",
status, status_text_for(status), content_type ? content_type : "application/octet-stream",
(unsigned int)body_len);
if (n > 0) {
if (socket_send_all(fd, header, (size_t)n) != 0) {
return;
}
if (body_len > 0) {
(void)socket_send_all(fd, body, body_len);
}
}
}
void
http_send_response_cached(int fd, int status, const char *content_type, const uint8_t *body, size_t body_len) {
char header[512];
int n = snprintf(header, sizeof(header),
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Cache-Control: public, max-age=3600\r\n"
"\r\n",
status, status_text_for(status), content_type ? content_type : "application/octet-stream",
(unsigned int)body_len);
if (n > 0) {
if (socket_send_all(fd, header, (size_t)n) != 0) {
return;
}
if (body_len > 0) {
(void)socket_send_all(fd, body, body_len);
}
}
}
void
http_send_json(int fd, int status, const char *body) {
http_send_response(fd, status, "application/json", (const uint8_t *)body, strlen(body));
}
void
http_send_oom(int fd) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"oom\"}");
}
static int
read_title_lookup_cache_name(const char *title_id, char *out, size_t out_size) {
if (!title_id || !out || out_size == 0) {
return 0;
}
out[0] = '\0';
char path[512];
snprintf(path, sizeof(path), "%s/%s.txt", CHEATRUNNER_CACHE_TITLE_NAMES_DIR, title_id);
char *txt = NULL;
if (read_file_text(path, &txt) != 0 || !txt) {
free(txt);
return 0;
}
str_trim(txt);
if (!txt[0] || cr_title_name_is_unresolved(title_id, txt)) {
free(txt);
return 0;
}
snprintf(out, out_size, "%s", txt);
free(txt);
return 1;
}
static int
write_title_lookup_cache_name(const char *title_id, const char *name) {
if (!title_id || !name || !name[0] || cr_title_name_is_unresolved(title_id, name)) {
return -1;
}
ensure_dir_recursive(CHEATRUNNER_CACHE_TITLE_NAMES_DIR);
char path[512];
snprintf(path, sizeof(path), "%s/%s.txt", CHEATRUNNER_CACHE_TITLE_NAMES_DIR, title_id);
return write_file_atomic(path, (const uint8_t *)name, strlen(name));
}
void
handle_api_status(int fd) {
char title[32];
char body[512];
int http_port = CHEATRUNNER_HTTP_PORT;
pthread_mutex_lock(&g_cfg_lock);
http_port = g_cfg.http_port;
pthread_mutex_unlock(&g_cfg_lock);
get_current_title(title, sizeof(title));
snprintf(body, sizeof(body),
"{\"ok\":true,\"titleId\":\"%s\",\"httpPort\":%d,"
"\"browserUrl\":\"http://%s:%d/\"}",
title, http_port, g_listen_ip, http_port);
http_send_json(fd, 200, body);
}
void
handle_api_running(int fd) {
running_game_state_t st;
if (read_running_state(&st) != 0 || !st.running) {
http_send_json(fd, 200, "{\"ok\":true,\"running\":false}");
return;
}
char name[512];
char cv[128];
char av[128];
char body[1400];
char *name_esc = json_escape(st.title_name);
snprintf(name, sizeof(name), "%s", name_esc ? name_esc : st.title_name);
free(name_esc);
char *cv_esc = json_escape(st.content_version);
char *av_esc = json_escape(st.app_version);
snprintf(cv, sizeof(cv), "%s", cv_esc ? cv_esc : st.content_version);
snprintf(av, sizeof(av), "%s", av_esc ? av_esc : st.app_version);
free(cv_esc);
free(av_esc);
snprintf(body, sizeof(body),
"{\"ok\":true,\"running\":true,\"titleId\":\"%s\",\"titleName\":\"%s\",\"pid\":%d,"
"\"appId\":\"0x%X\",\"imageBase\":\"0x%lx\",\"platform\":\"%s\","
"\"contentVersion\":\"%s\",\"appVersion\":\"%s\",\"startedAt\":%llu}",
st.title_id, name, (int)st.pid, (unsigned int)st.app_id, (long)st.image_base, st.platform, cv, av,
(unsigned long long)st.started_at);
http_send_json(fd, 200, body);
}
void
handle_api_debug_process(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
running_game_state_t st;
if (read_running_state(&st) != 0 || !st.running || strcmp(st.title_id, title_id) != 0) {
http_send_json(fd, 200, "{\"ok\":true,\"titleId\":\"\",\"running\":false}");
return;
}
char body[1024];
snprintf(body, sizeof(body),
"{\"ok\":true,\"titleId\":\"%s\",\"running\":true,\"pid\":%d,\"appId\":\"0x%X\","
"\"imageBase\":\"0x%lx\",\"ebootPath\":\"%s\",\"contentVersion\":\"%s\",\"appVersion\":\"%s\"}",
st.title_id, (int)st.pid, (unsigned int)st.app_id, (long)st.image_base, st.eboot_path, st.content_version,
st.app_version);
http_send_json(fd, 200, body);
}
static volatile long long g_startup_ms = 0;
static pthread_mutex_t g_games_cache_lock = PTHREAD_MUTEX_INITIALIZER;
static char *g_games_cache_json = NULL;
static size_t g_games_cache_len = 0;
static int g_games_scan_busy = 0;
static long long g_games_cache_ts_ms = 0;
static long long g_games_last_refresh_req_ms = 0;
/* Mismatch warn throttle: suppress repeated warns for the same (title, mod) within 60s */
#define MISMATCH_WARN_SLOTS 64
#define MISMATCH_WARN_INTERVAL_MS 60000ULL
static struct {
char title_id[16];
int mod_index;
uint64_t last_ms;
} g_mismatch_warn_slots[MISMATCH_WARN_SLOTS];
static pthread_mutex_t g_mismatch_warn_lock = PTHREAD_MUTEX_INITIALIZER;
/* cheat_select log dedup: only log when selection state changes */
static struct {
char title_id[10];
char cheat_path[256];
char detected_ver[64];
int version_match;
} g_cheat_select_last = {0};
static pthread_mutex_t g_cheat_select_lock = PTHREAD_MUTEX_INITIALIZER;
static const char *
mismatch_warn_level(const char *title_id, int mod_index) {
uint64_t now = now_ms();
int oldest = 0;
const char *level;
pthread_mutex_lock(&g_mismatch_warn_lock);
uint64_t oldest_ms = g_mismatch_warn_slots[0].last_ms;
for (int i = 0; i < MISMATCH_WARN_SLOTS; i++) {
if (g_mismatch_warn_slots[i].mod_index == mod_index &&
strncmp(g_mismatch_warn_slots[i].title_id, title_id, 15) == 0) {
if (now - g_mismatch_warn_slots[i].last_ms < MISMATCH_WARN_INTERVAL_MS) {
level = "debug";
} else {
g_mismatch_warn_slots[i].last_ms = now;
level = "warn";
}
pthread_mutex_unlock(&g_mismatch_warn_lock);
return level;
}
if (g_mismatch_warn_slots[i].last_ms < oldest_ms) {
oldest_ms = g_mismatch_warn_slots[i].last_ms;
oldest = i;
}
}
/* New entry: evict oldest slot */
strncpy(g_mismatch_warn_slots[oldest].title_id, title_id, 15);
g_mismatch_warn_slots[oldest].title_id[15] = '\0';
g_mismatch_warn_slots[oldest].mod_index = mod_index;
g_mismatch_warn_slots[oldest].last_ms = now;
pthread_mutex_unlock(&g_mismatch_warn_lock);
return "warn";
}
void
handle_api_health(int fd) {
struct timespec _now;
clock_gettime(CLOCK_MONOTONIC, &_now);
long long now_ms = (long long)_now.tv_sec * 1000LL + (long long)(_now.tv_nsec / 1000000L);
long long uptime = g_startup_ms ? (now_ms - g_startup_ms) : 0;
pthread_mutex_lock(&g_games_cache_lock);
int games_busy = g_games_scan_busy;
pthread_mutex_unlock(&g_games_cache_lock);
int source_busy = cr_source_jobs_is_busy();
pthread_mutex_lock(&g_repo_mirror.lock);
int mirror_busy = (g_repo_mirror.state == REPO_MIRROR_RUNNING);
pthread_mutex_unlock(&g_repo_mirror.lock);
char ls_health_phase[32];
char ls_health_title[16];
int ls_health_busy;
uint64_t ls_health_started_ms;
pthread_mutex_lock(&g_launch_status_lock);
ls_health_busy = g_launch_status.busy;
ls_health_started_ms = g_launch_status.started_ms;
snprintf(ls_health_phase, sizeof(ls_health_phase), "%s", g_launch_status.phase);
snprintf(ls_health_title, sizeof(ls_health_title), "%s", g_launch_status.title_id);
pthread_mutex_unlock(&g_launch_status_lock);
long long launch_age_ms = 0;
if (ls_health_busy && ls_health_started_ms > 0 && now_ms >= (long long)ls_health_started_ms)
launch_age_ms = now_ms - (long long)ls_health_started_ms;
int launch_stale = ls_health_busy && launch_age_ms > 60000;
int http_active = cr_http_active_clients();
int http_max = cr_http_max_concurrent();
long long http_tmr = cr_http_too_many_requests_count();
char buf[640];
snprintf(buf, sizeof(buf),
"{\"ok\":true,\"version\":\"%s\",\"uptimeMs\":%lld,"
"\"busy\":{\"gamesScan\":%s,\"sourceJob\":%s,\"repoMirror\":%s,\"launch\":%s},"
"\"launch\":{\"busy\":%s,\"phase\":\"%s\",\"titleId\":\"%s\",\"ageMs\":%lld,\"stale\":%s},"
"\"http\":{\"activeClients\":%d,\"maxConcurrent\":%d,\"tooManyRequestsCount\":%lld}}",
CHEATRUNNER_VERSION, uptime,
games_busy ? "true" : "false",
source_busy ? "true" : "false",
mirror_busy ? "true" : "false",
ls_health_busy ? "true" : "false",
ls_health_busy ? "true" : "false",
ls_health_phase, ls_health_title, launch_age_ms,
launch_stale ? "true" : "false",
http_active, http_max, http_tmr);
http_send_json(fd, 200, buf);
}
void
handle_api_logs(int fd, const char *query) {
int limit = 200;
int since = -1;
char buf[32];
buf[0] = '\0';
if (query_value(query, "all", buf, sizeof(buf)) == 0 && atoi(buf) != 0) {
limit = MAX_LOG_ENTRIES;
} else if (query_value(query, "limit", buf, sizeof(buf)) == 0) {
int v = atoi(buf);
if (v > 0 && v <= MAX_LOG_ENTRIES) { limit = v; }
}
if (query_value(query, "since", buf, sizeof(buf)) == 0) {
since = atoi(buf);
}
cJSON *root = cJSON_CreateObject();
cJSON *arr = cJSON_AddArrayToObject(root, "logs");
cJSON_AddBoolToObject(root, "ok", 1);
int last_seq = -1;
int truncated = 0;
pthread_mutex_lock(&g_log_lock);
int eligible = 0;
for (int i = 0; i < g_log_count; i++) {
int idx = (g_log_start + i) % MAX_LOG_ENTRIES;
if (since < 0 || g_logs[idx].seq > since) { eligible++; }
}
int skip = (eligible > limit) ? (eligible - limit) : 0;
if (skip > 0) { truncated = 1; }
int skipped = 0;
for (int i = 0; i < g_log_count; i++) {
int idx = (g_log_start + i) % MAX_LOG_ENTRIES;
if (since >= 0 && g_logs[idx].seq <= since) { continue; }
if (skipped < skip) { skipped++; continue; }
cJSON *e = cJSON_CreateObject();
cJSON_AddNumberToObject(e, "seq", g_logs[idx].seq);
cJSON_AddNumberToObject(e, "ts", (double)g_logs[idx].ts);
cJSON_AddStringToObject(e, "level", g_logs[idx].level);
cJSON_AddStringToObject(e, "tag", g_logs[idx].tag);
cJSON_AddStringToObject(e, "message", g_logs[idx].message);
cJSON_AddItemToArray(arr, e);
last_seq = g_logs[idx].seq;
}
pthread_mutex_unlock(&g_log_lock);
cJSON_AddNumberToObject(root, "lastSeq", last_seq);
cJSON_AddBoolToObject(root, "truncated", truncated);
char *txt = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!txt) {
http_send_oom(fd);
return;
}
http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt));
free(txt);
}
void
handle_api_logs_clear(int fd) {
pthread_mutex_lock(&g_log_lock);
g_log_start = 0;
g_log_count = 0;
pthread_mutex_unlock(&g_log_lock);
http_send_json(fd, 200, "{\"ok\":true}");
}
void
handle_api_notifications(int fd) {
cJSON *root = cJSON_CreateObject();
cJSON *arr = cJSON_AddArrayToObject(root, "notifications");
cJSON_AddBoolToObject(root, "ok", 1);
pthread_mutex_lock(&g_notifications_lock);
for (int i = 0; i < g_notifications_count; i++) {
cJSON *e = cJSON_CreateObject();
cJSON_AddNumberToObject(e, "id", g_notifications[i].id);
cJSON_AddNumberToObject(e, "ts", (double)g_notifications[i].ts);
cJSON_AddBoolToObject(e, "read", g_notifications[i].read ? 1 : 0);
cJSON_AddStringToObject(e, "kind", g_notifications[i].kind);
cJSON_AddStringToObject(e, "message", g_notifications[i].message);
cJSON_AddItemToArray(arr, e);
}
pthread_mutex_unlock(&g_notifications_lock);
char *txt = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!txt) {
http_send_oom(fd);
return;
}
http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt));
free(txt);
}
void
handle_api_notifications_read(int fd, const char *query) {
char id_s[32] = {0};
int mark_all = query_value(query, "all", id_s, sizeof(id_s)) == 0 && atoi(id_s) == 1;
int wanted_id = -1;
if (!mark_all && query_value(query, "id", id_s, sizeof(id_s)) == 0) {
wanted_id = atoi(id_s);
}
pthread_mutex_lock(&g_notifications_lock);
for (int i = 0; i < g_notifications_count; i++) {
if (mark_all || (wanted_id > 0 && g_notifications[i].id == wanted_id)) {
g_notifications[i].read = 1;
}
}
pthread_mutex_unlock(&g_notifications_lock);
notifications_save();
http_send_json(fd, 200, "{\"ok\":true}");
}
void
handle_api_notifications_clear(int fd) {
pthread_mutex_lock(&g_notifications_lock);
g_notifications_count = 0;
pthread_mutex_unlock(&g_notifications_lock);
notifications_save();
http_send_json(fd, 200, "{\"ok\":true}");
}
void
handle_api_activity(int fd) {
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", 1);
pthread_mutex_lock(&g_activity_lock);
cJSON_AddNumberToObject(root, "launchCount", g_activity_launch_count);
cJSON_AddNumberToObject(root, "lastLaunch", (double)g_activity_last_launch);
cJSON_AddStringToObject(root, "lastPlayedTitleId", g_activity_last_played_title_id);
cJSON_AddStringToObject(root, "lastCheatUsed", g_activity_last_cheat_used);
cJSON *titles = cJSON_AddArrayToObject(root, "titles");
for (int i = 0; i < g_activity_titles_count; i++) {
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "titleId", g_activity_titles[i].title_id);
cJSON_AddNumberToObject(e, "launchCount", g_activity_titles[i].launch_count);
cJSON_AddNumberToObject(e, "lastLaunch", (double)g_activity_titles[i].last_launch);
cJSON_AddNumberToObject(e, "totalSeconds", g_activity_titles[i].total_seconds);
cJSON_AddStringToObject(e, "lastCheat", g_activity_titles[i].last_cheat);
cJSON_AddItemToArray(titles, e);
}
pthread_mutex_unlock(&g_activity_lock);
char *txt = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!txt) {
http_send_oom(fd);
return;
}
http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt));
free(txt);
}
void
handle_api_activity_title(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 ||
!title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
cJSON *root = cJSON_CreateObject();
cJSON_AddBoolToObject(root, "ok", 1);
cJSON_AddStringToObject(root, "titleId", title_id);
pthread_mutex_lock(&g_activity_lock);
int idx = activity_find_title_index_locked(title_id);
if (idx >= 0) {
cJSON_AddNumberToObject(root, "launchCount", g_activity_titles[idx].launch_count);
cJSON_AddNumberToObject(root, "lastLaunch", (double)g_activity_titles[idx].last_launch);
cJSON_AddNumberToObject(root, "totalSeconds", g_activity_titles[idx].total_seconds);
cJSON_AddStringToObject(root, "lastCheat", g_activity_titles[idx].last_cheat);
} else {
cJSON_AddNumberToObject(root, "launchCount", 0);
cJSON_AddNumberToObject(root, "lastLaunch", 0);
cJSON_AddNumberToObject(root, "totalSeconds", 0);
cJSON_AddStringToObject(root, "lastCheat", "");
}
pthread_mutex_unlock(&g_activity_lock);
char *txt = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!txt) {
http_send_oom(fd);
return;
}
http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt));
free(txt);
}
void
handle_api_activity_reset(int fd) {
pthread_mutex_lock(&g_activity_lock);
g_activity_launch_count = 0;
g_activity_last_launch = 0;
g_activity_titles_count = 0;
g_activity_last_played_title_id[0] = '\0';
g_activity_last_cheat_used[0] = '\0';
pthread_mutex_unlock(&g_activity_lock);
activity_save();
http_send_json(fd, 200, "{\"ok\":true}");
}
void
handle_api_state(int fd) {
running_game_state_t rs;
running_state_get(&rs);
int local_count = 0;
int has_cheat = 0;
int cheat_kind = 0;
const char *cheat_fmt = "";
char cheat_path[256];
if (rs.running && find_cheat_file_for_title(rs.title_id, cheat_path, sizeof(cheat_path), &cheat_kind)) {
has_cheat = 1;
cheat_fmt = cheat_kind == 1 ? "json" : (cheat_kind == 2 ? "shn" : "mc4");
}
DIR *d = opendir(CHEATRUNNER_CHEATS_DIR);
if (d) {
struct dirent *ent = NULL;
while ((ent = readdir(d)) != NULL) {
if (ent->d_name[0] == '.') {
continue;
}
if (recognised_cheat_extension(ent->d_name)) {
local_count++;
}
}
closedir(d);
}
int http_port = CHEATRUNNER_HTTP_PORT;
int cheat_engine = 1;
int auto_load = 1;
int auto_download = 0;
int allow_force_enable = 0;
int cheat_state_delay_ms = 8000;
int dev_reload_enabled = 0;
int dev_shutdown_delay_ms = 700;
pthread_mutex_lock(&g_cfg_lock);
http_port = g_cfg.http_port;
cheat_engine = g_cfg.cheat_engine;
auto_load = g_cfg.auto_load_cheat_menu;
auto_download = g_cfg.auto_download_missing_cheat;
allow_force_enable = g_cfg.allow_force_enable;
cheat_state_delay_ms = g_cfg.cheat_state_after_launch_delay_ms;
dev_reload_enabled = g_cfg.dev_reload_enabled;
dev_shutdown_delay_ms = g_cfg.dev_shutdown_delay_ms;
pthread_mutex_unlock(&g_cfg_lock);
char last_local[128];
pthread_mutex_lock(&g_activity_lock);
snprintf(last_local, sizeof(last_local), "%s", g_activity_last_cheat_used);
pthread_mutex_unlock(&g_activity_lock);
char *name_esc = json_escape(rs.title_name);
char *last_esc = json_escape(last_local);
/* Pre-compute cheatFormat JSON value once — avoids the old double-build. */
char cheat_format_field[16];
if (has_cheat) snprintf(cheat_format_field, sizeof(cheat_format_field), "\"%s\"", cheat_fmt);
else strcpy(cheat_format_field, "null");
int can_patch = cr_priv_can_patch_game_memory();
char body[4096];
if (rs.running) {
snprintf(
body, sizeof(body),
"{\"ok\":true,\"version\":\"%s\",\"browserUrl\":\"http://%s:%d/\",\"httpPort\":%d,"
"\"cheatSource\":\"local\",\"running\":{\"running\":true,\"titleId\":\"%s\",\"titleName\":\"%s\","
"\"platform\":\"%s\",\"appId\":\"0x%X\",\"pid\":%d,\"imageBase\":\"0x%lx\",\"contentVersion\":\"%s\","
"\"appVersion\":\"%s\",\"startedAt\":%llu,\"hasCheat\":%s,\"cheatFormat\":%s},"
"\"cheats\":{\"engine\":%s,\"activeCount\":0,\"lastApplied\":\"%s\",\"localCount\":%d,\"canPatchGameMemory\":%s},"
"\"config\":{\"autoLoadCheatMenu\":%s,"
"\"autoDownloadMissingCheat\":%s,\"allowForceEnable\":%s,\"cheatStateAfterLaunchDelayMs\":%d},"
"\"dev\":{\"reloadEnabled\":%s,\"shutdownDelayMs\":%d}}",
CHEATRUNNER_VERSION, g_listen_ip, http_port, http_port, rs.title_id, name_esc ? name_esc : rs.title_name,
rs.platform, rs.app_id, (int)rs.pid, (long)rs.image_base, rs.content_version, rs.app_version,
(unsigned long long)rs.started_at, has_cheat ? "true" : "false",
cheat_format_field, cheat_engine ? "true" : "false", last_esc ? last_esc : "", local_count,
can_patch ? "true" : "false",
auto_load ? "true" : "false", auto_download ? "true" : "false", allow_force_enable ? "true" : "false",
cheat_state_delay_ms, dev_reload_enabled ? "true" : "false", dev_shutdown_delay_ms);
} else {
snprintf(
body, sizeof(body),
"{\"ok\":true,\"version\":\"%s\",\"browserUrl\":\"http://%s:%d/\",\"httpPort\":%d,"
"\"cheatSource\":\"local\",\"running\":{\"running\":false},"
"\"cheats\":{\"engine\":%s,\"activeCount\":0,\"lastApplied\":\"%s\",\"localCount\":%d,\"canPatchGameMemory\":%s},"
"\"config\":{\"autoLoadCheatMenu\":%s,"
"\"autoDownloadMissingCheat\":%s,\"allowForceEnable\":%s,\"cheatStateAfterLaunchDelayMs\":%d},"
"\"dev\":{\"reloadEnabled\":%s,\"shutdownDelayMs\":%d}}",
CHEATRUNNER_VERSION, g_listen_ip, http_port, http_port, cheat_engine ? "true" : "false",
last_esc ? last_esc : "", local_count, can_patch ? "true" : "false",
auto_load ? "true" : "false", auto_download ? "true" : "false",
allow_force_enable ? "true" : "false", cheat_state_delay_ms, dev_reload_enabled ? "true" : "false",
dev_shutdown_delay_ms);
}
/* Splice in appdb diagnostics before final closing brace */
{
char amode[32] = {0}, areason[64] = {0};
size_t alast = 0;
appdb_diag_get(amode, sizeof(amode), areason, sizeof(areason), &alast);
size_t blen = strlen(body);
if (blen > 0 && body[blen - 1] == '}') {
char *aesc = json_escape(areason);
int compiled = CHEATRUNNER_HAVE_SQLITE_APPDB;
int is_sqlite = (strcmp(amode, "sqlite") == 0);
char appdb_frag[384];
snprintf(appdb_frag, sizeof(appdb_frag),
",\"appdb\":{\"compiled\":%s,\"mode\":\"%s\",\"fallback\":%s,\"lastCount\":%u,\"reason\":\"%s\"}}",
compiled ? "true" : "false",
amode[0] ? amode : "unknown",
is_sqlite ? "false" : "true",
(unsigned)alast,
aesc ? aesc : "");
free(aesc);
if (blen - 1 + strlen(appdb_frag) < sizeof(body)) {
snprintf(body + blen - 1, sizeof(body) - blen + 1, "%s", appdb_frag);
}
}
}
free(last_esc);
free(name_esc);
http_send_json(fd, 200, body);
}
/* Returns length of the games JSON array written into buf (does NOT include NUL). */
static size_t
build_games_json_array(char *buf, size_t cap, int debug_names) {
game_entry_t *entries = calloc(MAX_GAMES, sizeof(*entries));
if (!entries) {
if (cap >= 3) { buf[0] = '['; buf[1] = ']'; buf[2] = '\0'; }
return 2;
}
size_t count = 0;
appdb_collect_games(entries, &count);
char running_title[32];
get_current_title(running_title, sizeof(running_title));
/* Move the active game to the top of the list */
if (running_title[0]) {
for (size_t i = 1; i < count; i++) {
if (strcmp(running_title, entries[i].title_id) == 0) {
game_entry_t tmp = entries[i];
memmove(&entries[1], &entries[0], i * sizeof(game_entry_t));
entries[0] = tmp;
break;
}
}
}
size_t off = 0;
off += (size_t)snprintf(buf + off, cap - off, "[");
size_t emitted = 0;
for (size_t i = 0; i < count; i++) {
if (!is_game_title_id(entries[i].title_id)) continue;
char ver[32] = "";
char content_id[96] = "";
char icon_path[512];
char pic0_path[512];
char cheat_path[256];
int cheat_kind = 0;
int has_icon = resolve_icon_path(entries[i].title_id, icon_path, sizeof(icon_path)) == 0;
int has_pic0 = resolve_pic0_path(entries[i].title_id, pic0_path, sizeof(pic0_path)) == 0;
int has_cheat = find_cheat_file_for_title(entries[i].title_id, cheat_path, sizeof(cheat_path), &cheat_kind);
char patch_xml_path[256] = "";
int has_patch = (patch_find_xml_for_title(entries[i].title_id, patch_xml_path, sizeof(patch_xml_path)) == 0);
int is_app = cr_title_is_known_media_app(entries[i].title_id, entries[i].title_name);
const char *platform = is_app ? "APP" : platform_for_title_id(entries[i].title_id);
int running = strcmp(running_title, entries[i].title_id) == 0;
const char *cheat_fmt = has_cheat ? (cheat_kind == 1 ? "json" : (cheat_kind == 2 ? "shn" : "mc4")) : "";
if (read_param_value_by_title_id(entries[i].title_id, "contentVersion", ver, sizeof(ver)) != 0)
read_param_value_by_title_id(entries[i].title_id, "appVersion", ver, sizeof(ver));
if (entries[i].content_id[0])
snprintf(content_id, sizeof(content_id), "%s", entries[i].content_id);
else
read_param_value_by_title_id(entries[i].title_id, "contentId", content_id, sizeof(content_id));
char *name_esc = json_escape(entries[i].title_name);
char *ver_esc = json_escape(ver[0] ? ver : "unknown");
char *cid_esc = json_escape(content_id[0] ? content_id : "");
if (!name_esc) { free(ver_esc); free(cid_esc); continue; }
off += (size_t)snprintf(
buf + off, cap - off,
"%s{\"titleId\":\"%s\",\"titleName\":\"%s\",\"platform\":\"%s\",\"kind\":\"%s\",\"isApp\":%s,"
"\"version\":\"%s\","
"\"contentId\":\"%s\",\"icon\":%s,\"iconUrl\":\"/appdb/icon?id=%s\",\"pic0\":%s,"
"\"pic0Url\":\"/appdb/pic0?id=%s\",\"hasCheat\":%s,\"cheatFormat\":\"%s\",\"hasPatch\":%s,\"running\":%s,"
"\"playTime\":%llu,\"lastAccessTime\":%llu",
emitted == 0 ? "" : ",", entries[i].title_id, name_esc, platform,
is_app ? "app" : "game", is_app ? "true" : "false",
ver_esc ? ver_esc : "unknown", cid_esc ? cid_esc : "",
has_icon ? "true" : "false", entries[i].title_id, has_pic0 ? "true" : "false", entries[i].title_id,
has_cheat ? "true" : "false", cheat_fmt, has_patch ? "true" : "false", running ? "true" : "false",
(unsigned long long)entries[i].play_time_seconds,
(unsigned long long)entries[i].last_access_time);
if (debug_names) {
char *src_esc = json_escape(entries[i].name_source[0] ? entries[i].name_source : "fallback_title_id");
off += (size_t)snprintf(buf + off, cap - off, ",\"nameSource\":\"%s\",\"nameResolved\":%s",
src_esc ? src_esc : "fallback_title_id",
entries[i].name_resolved ? "true" : "false");
free(src_esc);
}
off += (size_t)snprintf(buf + off, cap - off, "}");
emitted++;
free(name_esc); free(ver_esc); free(cid_esc);
if (off + 256 >= cap) break;
}
off += (size_t)snprintf(buf + off, cap - off, "]");
free(entries);
return off;
}
static void *
games_scan_thread(void *arg) {
(void)arg;
size_t cap = 262144;
char *arr = malloc(cap);
if (!arr) {
pthread_mutex_lock(&g_games_cache_lock);
g_games_scan_busy = 0;
pthread_mutex_unlock(&g_games_cache_lock);
return NULL;
}
size_t arr_len = build_games_json_array(arr, cap, 0);
pthread_mutex_lock(&g_games_cache_lock);
free(g_games_cache_json);
g_games_cache_json = arr;
g_games_cache_len = arr_len;
g_games_cache_ts_ms = (long long)now_ms();
g_games_scan_busy = 0;
pthread_mutex_unlock(&g_games_cache_lock);
return NULL;
}
void
handle_api_games(int fd, const char *query) {
int debug_names = 0;
char dbg_q[8] = {0};
if (query_value(query, "debug", dbg_q, sizeof(dbg_q)) == 0 && atoi(dbg_q) != 0)
debug_names = 1;
/* debug mode: bypass cache, scan synchronously */
if (debug_names) {
size_t cap = 262144;
char *arr = malloc(cap);
if (!arr) { http_send_oom(fd); return; }
size_t arr_len = build_games_json_array(arr, cap, 1);
size_t resp_cap = arr_len + 32;
char *resp = malloc(resp_cap);
if (!resp) { free(arr); http_send_oom(fd); return; }
size_t resp_len = (size_t)snprintf(resp, resp_cap, "{\"ok\":true,\"games\":%.*s}", (int)arr_len, arr);
http_send_response(fd, 200, "application/json", (const uint8_t *)resp, resp_len);
free(resp); free(arr);
return;
}
char refresh_q[8] = {0};
int force_refresh = (query_value(query, "refresh", refresh_q, sizeof(refresh_q)) == 0 && atoi(refresh_q) != 0);
const int refresh_min_interval_ms = 5000;
int ttl_ms = 30000;
pthread_mutex_lock(&g_cfg_lock);
ttl_ms = g_cfg.games_cache_ttl_ms;
pthread_mutex_unlock(&g_cfg_lock);
if (ttl_ms < 1000 || ttl_ms > 300000) {
ttl_ms = 30000;
}
long long now = (long long)now_ms();
int need_start = 0;
int throttled = 0;
long long cache_age_ms = -1;
pthread_mutex_lock(&g_games_cache_lock);
int has_cache = (g_games_cache_json != NULL);
if (has_cache && g_games_cache_ts_ms > 0 && now >= g_games_cache_ts_ms) {
cache_age_ms = now - g_games_cache_ts_ms;
}
int cache_fresh = (has_cache && cache_age_ms >= 0 && cache_age_ms < ttl_ms);
if (!g_games_scan_busy) {
if (!has_cache) {
g_games_scan_busy = 1;
g_games_last_refresh_req_ms = now;
need_start = 1;
} else if (force_refresh) {
if (g_games_last_refresh_req_ms > 0 && (now - g_games_last_refresh_req_ms) < refresh_min_interval_ms) {
throttled = 1;
} else {
g_games_scan_busy = 1;
g_games_last_refresh_req_ms = now;
need_start = 1;
}
} else if (!cache_fresh) {
g_games_scan_busy = 1;
g_games_last_refresh_req_ms = now;
need_start = 1;
}
}
int busy = g_games_scan_busy;
pthread_mutex_unlock(&g_games_cache_lock);
if (need_start) {
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, games_scan_thread, NULL) != 0) {
pthread_mutex_lock(&g_games_cache_lock);
g_games_scan_busy = 0;
busy = 0;
if (!g_games_cache_json) {
cache_age_ms = -1;
}
pthread_mutex_unlock(&g_games_cache_lock);
}
pthread_attr_destroy(&attr);
}
pthread_mutex_lock(&g_games_cache_lock);
char *arr_copy = NULL;
size_t arr_len = 0;
if (g_games_cache_json) {
arr_len = g_games_cache_len;
arr_copy = malloc(arr_len + 1);
if (arr_copy) { memcpy(arr_copy, g_games_cache_json, arr_len); arr_copy[arr_len] = '\0'; }
if (g_games_cache_ts_ms > 0 && now >= g_games_cache_ts_ms) {
cache_age_ms = now - g_games_cache_ts_ms;
}
}
pthread_mutex_unlock(&g_games_cache_lock);
if (!arr_copy) {
const char *msg = busy
? "{\"ok\":true,\"games\":[],\"refreshing\":true,\"cacheAgeMs\":-1,\"throttled\":false}"
: "{\"ok\":true,\"games\":[],\"refreshing\":false,\"cacheAgeMs\":-1,\"throttled\":false}";
http_send_json(fd, 200, msg);
return;
}
if (cache_age_ms < 0) {
cache_age_ms = 0;
}
size_t resp_cap = arr_len + 128;
char *resp = malloc(resp_cap);
if (!resp) { free(arr_copy); http_send_oom(fd); return; }
size_t resp_len = (size_t)snprintf(resp, resp_cap,
"{\"ok\":true,\"games\":%.*s,\"refreshing\":%s,\"cacheAgeMs\":%lld,\"throttled\":%s}",
(int)arr_len, arr_copy, busy ? "true" : "false",
cache_age_ms, throttled ? "true" : "false");
http_send_response(fd, 200, "application/json", (const uint8_t *)resp, resp_len);
free(resp);
free(arr_copy);
}
void
handle_appdb_lookup(int fd, const char *query) {
char title_id[32] = {0};
char upper_id[16] = {0};
char name[256] = {0};
char source[64] = "local";
char body[1024];
int cache_enabled = 1;
if (query_value(query, "id", title_id, sizeof(title_id)) != 0) {
query_value(query, "titleId", title_id, sizeof(title_id));
}
if (!title_id[0] || !title_id_normalize(title_id, upper_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad id\"}");
return;
}
if (!is_valid_title_id(upper_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"unsupported title id\"}");
return;
}
pthread_mutex_lock(&g_cfg_lock);
cache_enabled = g_cfg.title_lookup_cache_enabled;
pthread_mutex_unlock(&g_cfg_lock);
if (cache_enabled && read_title_lookup_cache_name(upper_id, name, sizeof(name))) {
snprintf(source, sizeof(source), "%s", "cache_lookup");
cr_log("debug", "title.lookup", "cache hit title=%s name=\"%s\"", upper_id, name);
}
if (cr_title_name_is_unresolved(upper_id, name)) {
/* Heap-allocate — 1024 × 404 bytes ≈ 404 KB would overflow the thread stack. */
game_entry_t *entries = calloc(MAX_GAMES, sizeof(*entries));
if (entries) {
size_t count = 0;
appdb_collect_games(entries, &count);
for (size_t i = 0; i < count; i++) {
if (!strcmp(entries[i].title_id, upper_id) &&
!cr_title_name_is_unresolved(upper_id, entries[i].title_name)) {
snprintf(name, sizeof(name), "%s", entries[i].title_name);
snprintf(source, sizeof(source), "%s", entries[i].name_source[0] ? entries[i].name_source : "local");
break;
}
}
free(entries);
}
}
if (cr_title_name_is_unresolved(upper_id, name) &&
read_param_value_by_title_id(upper_id, "titleName", name, sizeof(name)) == 0 &&
!cr_title_name_is_unresolved(upper_id, name)) {
snprintf(source, sizeof(source), "%s", "param_json");
}
/* PlayStation Store lookup as last resort before falling back to the title ID string. */
if (cr_title_name_is_unresolved(upper_id, name)) {
int store_enabled = 0;
int store_timeout_ms = 3000;
pthread_mutex_lock(&g_cfg_lock);
store_enabled = g_cfg.title_lookup_enabled;
store_timeout_ms = g_cfg.title_lookup_timeout_ms > 0 ? g_cfg.title_lookup_timeout_ms : 3000;
pthread_mutex_unlock(&g_cfg_lock);
if (store_enabled) {
char cid[96] = {0};
read_param_value_by_title_id(upper_id, "contentId", cid, sizeof(cid));
if (cid[0] && cr_store_lookup(cid, name, sizeof(name), store_timeout_ms)) {
snprintf(source, sizeof(source), "ps_store");
cr_log("info", "title.lookup", "store hit title=%s name=\"%s\"", upper_id, name);
}
}
}
if (cr_title_name_is_unresolved(upper_id, name)) {
snprintf(name, sizeof(name), "%s", upper_id);
snprintf(source, sizeof(source), "%s", "fallback_title_id");
} else if (cache_enabled && strcmp(source, "cache_lookup") != 0) {
write_title_lookup_cache_name(upper_id, name);
}
char *name_esc = json_escape(name);
snprintf(body, sizeof(body), "{\"ok\":true,\"id\":\"%s\",\"titleId\":\"%s\",\"name\":\"%s\",\"source\":\"%s\"}",
upper_id, upper_id, name_esc ? name_esc : "", source);
free(name_esc);
http_send_json(fd, 200, body);
}
void
handle_api_titles_lookup(int fd, const char *query) {
handle_appdb_lookup(fd, query);
}
void
handle_appdb_icon(int fd, const char *query) {
char title_id[32] = {0};
char norm_id[10] = {0};
char path[512];
uint8_t *buf = NULL;
size_t len = 0;
if (query_value(query, "id", title_id, sizeof(title_id)) != 0 || !title_id_normalize(title_id, norm_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad id\"}");
return;
}
if (cache_media_ensure(norm_id, 0, path, sizeof(path)) != 0 || read_file_bytes(path, &buf, &len) != 0) {
http_send_json(fd, 404, "{\"ok\":false,\"error\":\"no icon\"}");
return;
}
http_send_response(fd, 200, "image/png", buf, len);
free(buf);
}
void
handle_appdb_pic0(int fd, const char *query) {
char title_id[32] = {0};
char norm_id[10] = {0};
char path[512];
uint8_t *buf = NULL;
size_t len = 0;
if (query_value(query, "id", title_id, sizeof(title_id)) != 0 || !title_id_normalize(title_id, norm_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad id\"}");
return;
}
if (cache_media_ensure(norm_id, 1, path, sizeof(path)) != 0 || read_file_bytes(path, &buf, &len) != 0) {
http_send_json(fd, 404, "{\"ok\":false,\"error\":\"no pic0\"}");
return;
}
http_send_response(fd, 200, "image/png", buf, len);
free(buf);
}
void
handle_api_cheats_index(int fd) {
ensure_data_dirs();
cJSON *root = cJSON_CreateObject();
cJSON *files = cJSON_AddArrayToObject(root, "files");
if (!root || !files) {
cJSON_Delete(root);
http_send_oom(fd);
return;
}
cJSON_AddBoolToObject(root, "ok", 1);
DIR *d = opendir(CHEATRUNNER_CHEATS_DIR);
if (d) {
struct dirent *ent = NULL;
char seen[512][16];
int seen_n = 0;
while ((ent = readdir(d)) != NULL) {
const char *name = ent->d_name;
if (name[0] == '.') {
continue;
}
int kind = recognised_cheat_extension(name);
if (!kind || !is_safe_filename(name)) {
continue;
}
char id[16] = {0};
if (!extract_title_id_prefix(name, id, sizeof(id))) {
continue;
}
int dup = 0;
for (int i = 0; i < seen_n; i++) {
if (!strcmp(seen[i], id)) {
dup = 1;
break;
}
}
if (dup) {
continue;
}
if (seen_n < (int)(sizeof(seen) / sizeof(seen[0]))) {
snprintf(seen[seen_n], sizeof(seen[seen_n]), "%s", id);
seen_n++;
}
char best_path[256];
int best_kind = 0;
char display[256];
display[0] = '\0';
if (find_cheat_file_for_title(id, best_path, sizeof(best_path), &best_kind)) {
read_cheat_display_name(best_path, best_kind, display, sizeof(display));
} else {
best_kind = kind;
}
const char *fmt = best_kind == 1 ? "json" : (best_kind == 2 ? "shn" : "mc4");
cJSON *e = cJSON_CreateObject();
cJSON_AddStringToObject(e, "titleId", id);
cJSON_AddStringToObject(e, "name", display[0] ? display : id);
cJSON_AddStringToObject(e, "format", fmt);
cJSON_AddItemToArray(files, e);
}
closedir(d);
}
pid_t pid = -1;
intptr_t base = 0;
char running[16] = {0};
if (get_running_game(&pid, running, sizeof(running), &base) == 0) {
cJSON *running_obj = cJSON_AddObjectToObject(root, "running");
if (running_obj) {
char base_hex[32];
snprintf(base_hex, sizeof(base_hex), "0x%lx", (long)base);
cJSON_AddStringToObject(running_obj, "titleId", running);
cJSON_AddNumberToObject(running_obj, "pid", pid);
cJSON_AddStringToObject(running_obj, "imageBase", base_hex);
}
} else {
cJSON_AddNullToObject(root, "running");
}
char *body = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!body) {
http_send_oom(fd);
return;
}
http_send_response(fd, 200, "application/json", (const uint8_t *)body, strlen(body));
free(body);
}
void
handle_api_cheats_get(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 ||
!title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
/* Optional ?format=json|shn|mc4 — lets the UI pick a specific file. */
char fmt_s[8] = {0};
int req_kind = 0;
query_value(query, "format", fmt_s, sizeof(fmt_s));
if (!strcasecmp(fmt_s, "json")) req_kind = 1;
else if (!strcasecmp(fmt_s, "shn")) req_kind = 2;
else if (!strcasecmp(fmt_s, "mc4")) req_kind = 3;
/* Collect all candidates to build the formats list and pick a file. */
cheat_file_search_t ctx;
memset(&ctx, 0, sizeof(ctx));
find_cheat_candidates(title_id, &ctx);
if (!ctx.best_path[0]) {
http_send_json(fd, 404, "{\"ok\":false,\"error\":\"no cheat file\"}");
return;
}
/* Derive set of available format kinds from the candidates list. */
int avail_kinds[4] = {0}; /* indexed by kind 1..3 */
for (int i = 0; i < ctx.candidate_count; i++) {
int k = ctx.candidates[i].kind;
if (k >= 1 && k <= 3) avail_kinds[k] = 1;
}
/* Select file: requested format if available, otherwise the best candidate. */
char path[256] = {0};
int kind = 0;
if (req_kind && avail_kinds[req_kind]) {
for (int i = 0; i < ctx.candidate_count; i++) {
if (ctx.candidates[i].kind == req_kind) {
snprintf(path, sizeof(path), "%s", ctx.candidates[i].path);
kind = req_kind;
break;
}
}
}
if (!path[0]) {
snprintf(path, sizeof(path), "%s", ctx.best_path);
kind = ctx.best_kind;
}
char *txt = NULL;
if (read_file_text(path, &txt) != 0 || !txt) {
http_send_json(fd, 404, "{\"ok\":false,\"error\":\"could not read cheat file\"}");
return;
}
/* Convert to JSON string. */
static const char * const kname[] = {NULL, "json", "shn", "mc4"};
char *json = NULL;
if (kind == 1) {
json = txt; txt = NULL;
} else if (kind == 2) {
json = shn_xml_to_json(txt, strlen(txt));
free(txt); txt = NULL;
if (!json) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"SHN parse failed\"}"); return; }
} else {
char *xml = mc4_decrypt_to_xml(txt, strlen(txt), NULL);
free(txt); txt = NULL;
if (!xml) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"MC4 decrypt failed\"}"); return; }
json = shn_xml_to_json(xml, strlen(xml));
free(xml);
if (!json) { http_send_json(fd, 500, "{\"ok\":false,\"error\":\"MC4 XML parse failed\"}"); return; }
}
/* Build formats JSON array: e.g. ["json","shn"] */
char fmts_json[64];
int fp = 0;
fmts_json[fp++] = '[';
int fmts_first = 1;
for (int k = 1; k <= 3; k++) {
if (!avail_kinds[k]) continue;
if (!fmts_first) fmts_json[fp++] = ',';
fmts_first = 0;
fmts_json[fp++] = '"';
size_t kl = strlen(kname[k]);
memcpy(fmts_json + fp, kname[k], kl); fp += (int)kl;
fmts_json[fp++] = '"';
}
fmts_json[fp++] = ']'; fmts_json[fp] = 0;
/* Inject ,\"formats\":[...],"activeFormat":"..." before the final '}'. */
char inject[128];
int inj = snprintf(inject, sizeof(inject),
",\"formats\":%s,\"activeFormat\":\"%s\"}",
fmts_json, (kind >= 1 && kind <= 3) ? kname[kind] : "json");
char *end = strrchr(json, '}');
if (!end || inj <= 0) {
http_send_response(fd, 200, "application/json", (const uint8_t *)json, strlen(json));
free(json);
return;
}
*end = '\0';
size_t base_len = (size_t)(end - json);
char *out = malloc(base_len + (size_t)inj + 1);
if (!out) {
free(json);
http_send_oom(fd);
return;
}
memcpy(out, json, base_len);
memcpy(out + base_len, inject, (size_t)inj + 1);
free(json);
http_send_response(fd, 200, "application/json", (const uint8_t *)out, base_len + (size_t)inj);
free(out);
}
/* Single-slot decoded-JSON cache keyed by (title_id, path, mtime, kind); avoids re-decrypting MC4/SHN on every state poll. */
static struct {
pthread_mutex_t lock;
char title_id[10];
char path[256];
time_t mtime;
int kind;
char *json_txt;
} g_cheat_doc_cache = { .lock = PTHREAD_MUTEX_INITIALIZER };
/* Conflict map cache: rebuilt when cheat path or mtime changes. */
static struct {
pthread_mutex_t lock;
char path[256];
time_t mtime;
cheat_conflict_map_t map;
int ready;
} g_conflict_cache = { .lock = PTHREAD_MUTEX_INITIALIZER };
/* Rebuild g_conflict_cache if the cheat file changed.
* Must be called after the doc cache has been loaded (we borrow the cached JSON). */
static void
conflict_cache_ensure(const char *path, time_t mtime) {
/* Check+build under one lock so two concurrent callers can't both double-build (and leak entries_p). */
pthread_mutex_lock(&g_conflict_cache.lock);
if (g_conflict_cache.ready &&
g_conflict_cache.mtime == mtime &&
strcmp(g_conflict_cache.path, path) == 0) {
pthread_mutex_unlock(&g_conflict_cache.lock);
return;
}
pthread_mutex_unlock(&g_conflict_cache.lock);
/* Borrow the JSON text from the doc cache */
pthread_mutex_lock(&g_cheat_doc_cache.lock);
char *json_copy = g_cheat_doc_cache.json_txt ? strdup(g_cheat_doc_cache.json_txt) : NULL;
pthread_mutex_unlock(&g_cheat_doc_cache.lock);
if (!json_copy) return;
cheat_conflict_map_t tmp;
int built = (conflict_map_build(json_copy, &tmp) == 0);
free(json_copy);
if (built) {
pthread_mutex_lock(&g_conflict_cache.lock);
/* Re-check inside the lock: another thread may have already rebuilt */
if (!g_conflict_cache.ready ||
g_conflict_cache.mtime != mtime ||
strcmp(g_conflict_cache.path, path) != 0) {
g_conflict_cache.map = tmp;
g_conflict_cache.mtime = mtime;
g_conflict_cache.ready = 1;
snprintf(g_conflict_cache.path, sizeof(g_conflict_cache.path), "%s", path);
cr_log("info", "conflicts",
"conflict map built path=%s mods=%d pairs=%d",
path, tmp.mod_count, tmp.pair_count);
}
pthread_mutex_unlock(&g_conflict_cache.lock);
}
}
static cJSON *
load_cheat_json_root_for_title_ex(const char *title_id, char *err, size_t err_size, char *path_out, size_t path_out_size,
int *kind_out) {
char path[256];
int kind = 0;
if (!find_cheat_file_for_title(title_id, path, sizeof(path), &kind)) {
snprintf(err, err_size, "no cheat file");
return NULL;
}
/* Stat the file to get mtime for cache validation */
struct stat st_buf;
time_t file_mtime = 0;
if (stat(path, &st_buf) == 0) {
file_mtime = st_buf.st_mtime;
}
/* Check cache: if title_id + path + mtime + kind all match, parse from cached text */
pthread_mutex_lock(&g_cheat_doc_cache.lock);
if (g_cheat_doc_cache.json_txt &&
g_cheat_doc_cache.kind == kind &&
g_cheat_doc_cache.mtime == file_mtime && file_mtime != 0 &&
strcmp(g_cheat_doc_cache.path, path) == 0 &&
strcmp(g_cheat_doc_cache.title_id, title_id) == 0) {
char *cached = strdup(g_cheat_doc_cache.json_txt);
pthread_mutex_unlock(&g_cheat_doc_cache.lock);
if (!cached) {
snprintf(err, err_size, "cache alloc failed");
return NULL;
}
cJSON *root = cJSON_Parse(cached);
free(cached);
if (!root) {
snprintf(err, err_size, "json parse failed (cache)");
return NULL;
}
if (path_out && path_out_size > 0) snprintf(path_out, path_out_size, "%s", path);
if (kind_out) *kind_out = kind;
return root;
}
pthread_mutex_unlock(&g_cheat_doc_cache.lock);
/* Cache miss: load from disk */
char *txt = NULL;
if (read_file_text(path, &txt) != 0 || !txt) {
snprintf(err, err_size, "could not read cheat file");
return NULL;
}
char *json_txt = NULL;
if (kind == 1) {
json_txt = txt;
} else if (kind == 2) {
json_txt = shn_xml_to_json(txt, strlen(txt));
free(txt);
} else {
char *xml = mc4_decrypt_to_xml(txt, strlen(txt), NULL);
free(txt);
if (!xml) {
snprintf(err, err_size, "MC4 decrypt failed");
return NULL;
}
json_txt = shn_xml_to_json(xml, strlen(xml));
free(xml);
}
if (!json_txt) {
snprintf(err, err_size, "parse failed");
return NULL;
}
/* Update cache */
char *cache_copy = strdup(json_txt);
if (cache_copy) {
pthread_mutex_lock(&g_cheat_doc_cache.lock);
free(g_cheat_doc_cache.json_txt);
g_cheat_doc_cache.json_txt = cache_copy;
g_cheat_doc_cache.kind = kind;
g_cheat_doc_cache.mtime = file_mtime;
snprintf(g_cheat_doc_cache.path, sizeof(g_cheat_doc_cache.path), "%s", path);
snprintf(g_cheat_doc_cache.title_id, sizeof(g_cheat_doc_cache.title_id), "%s", title_id);
pthread_mutex_unlock(&g_cheat_doc_cache.lock);
}
cJSON *root = cJSON_Parse(json_txt);
free(json_txt);
if (!root) {
snprintf(err, err_size, "json parse failed");
return NULL;
}
if (path_out && path_out_size > 0) {
snprintf(path_out, path_out_size, "%s", path);
}
if (kind_out) {
*kind_out = kind;
}
return root;
}
static cJSON *
load_cheat_json_root_for_title(const char *title_id, char *err, size_t err_size) {
return load_cheat_json_root_for_title_ex(title_id, err, err_size, NULL, 0, NULL);
}
typedef enum cheat_state_kind {
CHEAT_STATE_OFF = 0,
CHEAT_STATE_ON,
CHEAT_STATE_MIXED,
CHEAT_STATE_MISMATCH,
CHEAT_STATE_GAME_NOT_RUNNING,
CHEAT_STATE_PROCESS_NOT_FOUND,
CHEAT_STATE_BASE_NOT_READY,
CHEAT_STATE_READ_FAILED,
CHEAT_STATE_INVALID_CHEAT,
CHEAT_STATE_GAME_LOADING,
CHEAT_STATE_BASELINE_UNKNOWN,
CHEAT_STATE_ADDRESS_UNRESOLVED,
CHEAT_STATE_FORMAT_NEEDS_REVIEW,
CHEAT_STATE_OFF_UNVERIFIED,
CHEAT_STATE_ON_UNVERIFIED,
CHEAT_STATE_CRASH_SUSPECT,
CHEAT_STATE_CONFLICT,
CHEAT_STATE_UNKNOWN
} cheat_state_kind_t;
static const char *
cheat_state_key(cheat_state_kind_t s) {
switch (s) {
case CHEAT_STATE_OFF:
return "off";
case CHEAT_STATE_ON:
return "on";
case CHEAT_STATE_MIXED:
return "mixed";
case CHEAT_STATE_MISMATCH:
return "mismatch";
case CHEAT_STATE_GAME_NOT_RUNNING:
return "game_not_ready";
case CHEAT_STATE_PROCESS_NOT_FOUND:
return "process_not_found";
case CHEAT_STATE_BASE_NOT_READY:
return "base_not_ready";
case CHEAT_STATE_READ_FAILED:
return "read_failed";
case CHEAT_STATE_INVALID_CHEAT:
return "invalid_cheat";
case CHEAT_STATE_GAME_LOADING:
return "game_loading";
case CHEAT_STATE_BASELINE_UNKNOWN:
return "baseline_unknown";
case CHEAT_STATE_ADDRESS_UNRESOLVED:
return "address_unresolved";
case CHEAT_STATE_FORMAT_NEEDS_REVIEW:
return "format_needs_review";
case CHEAT_STATE_OFF_UNVERIFIED:
return "off_unverified";
case CHEAT_STATE_ON_UNVERIFIED:
return "on_unverified";
case CHEAT_STATE_CRASH_SUSPECT:
return "crash_suspect";
case CHEAT_STATE_CONFLICT:
return "conflict";
default:
return "unknown";
}
}
static const char *
cheat_state_label(cheat_state_kind_t s) {
switch (s) {
case CHEAT_STATE_OFF:
return "OFF";
case CHEAT_STATE_ON:
return "ON";
case CHEAT_STATE_MIXED:
return "PARTIAL";
case CHEAT_STATE_MISMATCH:
return "VERSION MISMATCH";
case CHEAT_STATE_GAME_NOT_RUNNING:
return "GAME NOT READY";
case CHEAT_STATE_PROCESS_NOT_FOUND:
return "PROCESS NOT FOUND";
case CHEAT_STATE_BASE_NOT_READY:
return "BASE NOT READY";
case CHEAT_STATE_READ_FAILED:
return "READ FAILED";
case CHEAT_STATE_INVALID_CHEAT:
return "INVALID CHEAT";
case CHEAT_STATE_GAME_LOADING:
return "WAITING FOR GAME";
case CHEAT_STATE_BASELINE_UNKNOWN:
return "BASELINE UNKNOWN";
case CHEAT_STATE_ADDRESS_UNRESOLVED:
return "ADDRESS UNRESOLVED";
case CHEAT_STATE_FORMAT_NEEDS_REVIEW:
return "FORMAT NEEDS REVIEW";
case CHEAT_STATE_OFF_UNVERIFIED:
return "OFF";
case CHEAT_STATE_ON_UNVERIFIED:
return "ON";
case CHEAT_STATE_CRASH_SUSPECT:
return "CRASH SUSPECT";
case CHEAT_STATE_CONFLICT:
return "CONFLICT";
default:
return "UNKNOWN";
}
}
void
handle_api_cheats_state(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
char err[128] = {0};
char cheat_path[256] = {0};
int cheat_kind = 0;
int debug_mode = 0;
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
{
char dbg[8] = {0};
if (query_value(query, "debug", dbg, sizeof(dbg)) == 0 && atoi(dbg) != 0) {
debug_mode = 1;
}
}
cJSON *root = load_cheat_json_root_for_title_ex(title_id, err, sizeof(err), cheat_path, sizeof(cheat_path), &cheat_kind);
if (!root) {
char body[256];
snprintf(body, sizeof(body), "{\"ok\":false,\"error\":\"%s\"}", err);
http_send_json(fd, 404, body);
return;
}
cr_log("debug", "cheats", "state request title=%s debug=%d", title_id, debug_mode);
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (!cJSON_IsArray(mods)) {
cJSON_Delete(root);
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid mods\"}");
return;
}
running_game_state_t st;
int running_ok = (read_running_state(&st) == 0 && st.running);
int title_match = running_ok && strcmp(st.title_id, title_id) == 0;
pid_t pid = running_ok ? st.pid : -1;
intptr_t base = running_ok ? st.image_base : 0;
int attach_ok = 0;
int is_ps2_st = 0;
int global_state_delay_ms = 0;
int auto_detect = 1;
char mc4_fb_s[16] = "legacy";
char shn_fb_s[16] = "legacy";
pthread_mutex_lock(&g_cfg_lock);
global_state_delay_ms = g_cfg.cheat_state_after_launch_delay_ms;
auto_detect = g_cfg.cheat_address_auto_detect;
snprintf(mc4_fb_s, sizeof(mc4_fb_s), "%s", g_cfg.cheat_mc4_unverified_fallback);
snprintf(shn_fb_s, sizeof(shn_fb_s), "%s", g_cfg.cheat_shn_unverified_fallback);
pthread_mutex_unlock(&g_cfg_lock);
int in_loading_window = 0;
{
uint64_t last_verified = __atomic_load_n(&g_last_launch_verified_at_ms, __ATOMIC_ACQUIRE);
if (title_match && global_state_delay_ms > 0 && last_verified > 0) {
uint64_t age = now_ms() - last_verified;
if (age < (uint64_t)global_state_delay_ms) {
in_loading_window = 1;
}
}
}
int can_probe = title_match && pid > 0 && base != 0 && !in_loading_window;
if (running_ok) {
cr_log("debug", "cheats", "running title=%s pid=%d base=0x%lx appId=0x%X", st.title_id, (int)pid, (long)base,
(unsigned int)st.app_id);
}
/* Fast liveness check: ESRCH = process is gone; EPERM = alive but unprivileged signal. */
if (can_probe && kill(pid, 0) != 0 && errno == ESRCH) {
can_probe = 0;
}
/* Skip ptrace during an apply — attaching again here races the apply thread's attach and hangs. */
if (can_probe && g_cheat_applying) {
can_probe = 0;
}
/* Probing reads are mdbg-backed and need no ptrace attach. */
if (can_probe) {
attach_ok = 1;
is_ps2_st = process_is_ps2_emu(pid);
}
const char *fmt = cheat_kind == 1 ? "json" : (cheat_kind == 2 ? "shn" : "mc4");
cr_log("debug", "cheats", "loaded file=%s format=%s mods=%d", cheat_path, fmt, cJSON_GetArraySize(mods));
/* Determine version metadata for cheat selection diagnostics */
char detected_ver[64] = {0};
char cheat_ver[32] = {0};
char detected_ver_norm[32] = {0};
char cheat_ver_norm[32] = {0};
int version_match = 0;
if (running_ok) {
if (st.content_version[0])
snprintf(detected_ver, sizeof(detected_ver), "%s", st.content_version);
else if (st.app_version[0])
snprintf(detected_ver, sizeof(detected_ver), "%s", st.app_version);
}
{
/* Extract version token from cheat filename using shared helper */
const char *cfname = strrchr(cheat_path, '/');
cfname = cfname ? cfname + 1 : cheat_path;
cr_version_from_filename(cfname, cheat_ver, sizeof(cheat_ver));
cr_version_normalize(cheat_ver, cheat_ver_norm, sizeof(cheat_ver_norm));
cr_version_normalize(detected_ver, detected_ver_norm, sizeof(detected_ver_norm));
version_match = (cheat_ver_norm[0] && detected_ver_norm[0] &&
strcmp(cheat_ver_norm, detected_ver_norm) == 0) ? 1 : 0;
}
{
const char *dv = detected_ver[0] ? detected_ver : "unknown";
pthread_mutex_lock(&g_cheat_select_lock);
if (strcmp(g_cheat_select_last.title_id, title_id) != 0 ||
strcmp(g_cheat_select_last.cheat_path, cheat_path) != 0 ||
strcmp(g_cheat_select_last.detected_ver, dv) != 0 ||
g_cheat_select_last.version_match != version_match) {
cr_log("info", "cheat_select",
"[game] title=%s detectedVer=%s cheatVer=%s versionMatch=%d cheatPath=%s",
title_id, dv, cheat_ver[0] ? cheat_ver : "unknown", version_match, cheat_path);
snprintf(g_cheat_select_last.title_id, sizeof(g_cheat_select_last.title_id), "%s", title_id);
snprintf(g_cheat_select_last.cheat_path, sizeof(g_cheat_select_last.cheat_path), "%s", cheat_path);
snprintf(g_cheat_select_last.detected_ver,sizeof(g_cheat_select_last.detected_ver),"%s", dv);
g_cheat_select_last.version_match = version_match;
}
pthread_mutex_unlock(&g_cheat_select_lock);
}
/* Rebuild conflict map if cheat file changed (uses doc cache for JSON) */
{
struct stat cmap_st;
time_t cmap_mtime = (stat(cheat_path, &cmap_st) == 0) ? cmap_st.st_mtime : 0;
conflict_cache_ensure(cheat_path, cmap_mtime);
}
/* Gather all candidates for response (uses live game version for classification) */
cheat_file_search_t cand_ctx;
memset(&cand_ctx, 0, sizeof(cand_ctx));
find_cheat_candidates_ex(title_id, detected_ver[0] ? detected_ver : NULL, &cand_ctx);
cJSON *out = cJSON_CreateObject();
cJSON_AddBoolToObject(out, "ok", 1);
cJSON_AddStringToObject(out, "titleId", title_id);
cJSON_AddStringToObject(out, "cheatPath", cheat_path);
cJSON_AddStringToObject(out, "cheatFormat", fmt);
cJSON_AddStringToObject(out, "detectedVersion", detected_ver[0] ? detected_ver : "unknown");
cJSON_AddStringToObject(out, "selectedCheatVersion", cheat_ver[0] ? cheat_ver : "unknown");
cJSON_AddBoolToObject(out, "versionMatch", version_match);
{
static const char * const sel_mode_strs[] = {"none","exact","generic","wrong_version","manual"};
int sk = cand_ctx.selection_kind;
const char *sel_mode = (sk >= 0 && sk <= 4) ? sel_mode_strs[sk] : "none";
cJSON_AddStringToObject(out, "selectionMode", sel_mode);
}
/* candidates array */
{
cJSON *cands_arr = cJSON_AddArrayToObject(out, "candidates");
if (cands_arr) {
static const char * const match_strs[] = {"exact","wrong_version","generic","unknown"};
for (int ci = 0; ci < cand_ctx.candidate_count; ci++) {
cJSON *ce = cJSON_CreateObject();
if (!ce) break;
const char *cp = cand_ctx.candidates[ci].path;
const char *cfn = strrchr(cp, '/');
cfn = cfn ? cfn + 1 : cp;
int ck = cand_ctx.candidates[ci].kind;
const char *cfmt = ck == 1 ? "json" : (ck == 2 ? "shn" : "mc4");
const char *cv = cand_ctx.candidates[ci].version;
char cvn[32] = {0};
if (cv[0]) cr_version_normalize(cv, cvn, sizeof(cvn));
int cm = cand_ctx.candidates[ci].match;
const char *cmatch = (cm >= 0 && cm <= 3) ? match_strs[cm] : "unknown";
int is_selected = (cand_ctx.best_path[0] && strcmp(cp, cand_ctx.best_path) == 0);
cJSON_AddStringToObject(ce, "path", cp);
cJSON_AddStringToObject(ce, "filename", cfn);
cJSON_AddStringToObject(ce, "format", cfmt);
cJSON_AddStringToObject(ce, "version", cv[0] ? cv : "");
cJSON_AddStringToObject(ce, "versionNormalized", cvn[0] ? cvn : "");
cJSON_AddStringToObject(ce, "match", cmatch);
cJSON_AddBoolToObject(ce, "selected", is_selected);
cJSON_AddNumberToObject(ce, "score", cand_ctx.candidates[ci].score);
cJSON_AddItemToArray(cands_arr, ce);
}
}
}
cJSON *game = cJSON_AddObjectToObject(out, "game");
if (game) {
cJSON_AddBoolToObject(game, "running", running_ok ? 1 : 0);
cJSON_AddStringToObject(game, "runningTitleId", running_ok ? st.title_id : "");
cJSON_AddNumberToObject(game, "pid", running_ok ? st.pid : -1);
char base_hex[32];
snprintf(base_hex, sizeof(base_hex), "0x%lx", (long)(running_ok ? st.image_base : 0));
cJSON_AddStringToObject(game, "base", base_hex);
cJSON_AddStringToObject(game, "contentVersion", running_ok ? st.content_version : "");
cJSON_AddStringToObject(game, "appVersion", running_ok ? st.app_version : "");
}
/* Detect if any mod entry uses legacy_unverified mode (MC4/SHN without expected bytes) */
{
int has_legacy_unv = 0;
if (cheat_kind != 1) {
cJSON *m2 = NULL;
cJSON_ArrayForEach(m2, mods) {
cJSON *mem2 = cJSON_GetObjectItem(m2, "memory");
if (!cJSON_IsArray(mem2)) mem2 = cJSON_GetObjectItem(m2, "patches");
if (!cJSON_IsArray(mem2)) continue;
cJSON *e2 = NULL;
cJSON_ArrayForEach(e2, mem2) {
cJSON *exp2 = cJSON_GetObjectItem(e2, "expected");
if (!cJSON_IsString(exp2) || !exp2->valuestring || !exp2->valuestring[0]) {
has_legacy_unv = 1; break;
}
}
if (has_legacy_unv) break;
}
}
cJSON_AddBoolToObject(out, "hasLegacyUnverified", has_legacy_unv);
}
cJSON *arr = cJSON_AddArrayToObject(out, "mods");
cJSON *mismatches = cJSON_AddArrayToObject(out, "mismatches");
int summary_on = 0, summary_off = 0, summary_mismatch = 0, summary_not_ready = 0, summary_baseline_unknown = 0;
int idx = 0;
cJSON *mod = NULL;
cJSON_ArrayForEach(mod, mods) {
cJSON *e = cJSON_CreateObject();
int mod_index = idx++;
cJSON_AddNumberToObject(e, "index", mod_index);
cJSON *n = cJSON_GetObjectItem(mod, "name");
const char *mod_name = cJSON_IsString(n) ? n->valuestring : "mod";
cJSON_AddStringToObject(e, "name", mod_name);
cJSON *mem = cJSON_GetObjectItem(mod, "memory");
if (!cJSON_IsArray(mem)) {
mem = cJSON_GetObjectItem(mod, "patches");
}
cheat_state_kind_t state = CHEAT_STATE_UNKNOWN;
const char *reason = "";
int off_val_matches = 0; /* entries where mem matches ValueOff (soft OFF indicator) */
if (!cJSON_IsArray(mem) || cJSON_GetArraySize(mem) == 0) {
state = CHEAT_STATE_INVALID_CHEAT;
reason = "mod has no memory entries";
} else if (!running_ok) {
state = CHEAT_STATE_GAME_NOT_RUNNING;
reason = "no game is currently running";
} else if (!title_match) {
state = CHEAT_STATE_PROCESS_NOT_FOUND;
reason = "running game does not match cheat title";
} else if (in_loading_window) {
state = CHEAT_STATE_GAME_LOADING;
reason = "game just started";
} else if (pid <= 0) {
state = CHEAT_STATE_PROCESS_NOT_FOUND;
reason = "process not found";
} else if (base == 0) {
state = CHEAT_STATE_BASE_NOT_READY;
reason = "base address not ready";
} else if (!attach_ok) {
state = CHEAT_STATE_PROCESS_NOT_FOUND;
reason = "pt_attach failed";
} else {
/* Per-mod base: respect module_name, matching apply_cheat_json behaviour. */
intptr_t mod_base_st = base;
{
cJSON *mn_j_st = cJSON_GetObjectItem(mod, "module_name");
const char *mn_st = (cJSON_IsString(mn_j_st) && mn_j_st->valuestring) ? mn_j_st->valuestring : "";
if (mn_st[0] && resolve_module_base(pid, mn_st, base, &mod_base_st) != 0) {
state = CHEAT_STATE_PROCESS_NOT_FOUND;
reason = "module_not_loaded";
}
}
int total = 0;
int on_matches = 0;
int off_matches = 0;
int mismatch = 0;
int baseline_unknown = 0;
int mismatch_logged = 0;
int ff_hook = 0;
cJSON *m = NULL;
cJSON_ArrayForEach(m, mem) {
if (state != CHEAT_STATE_UNKNOWN) break;
cJSON *off_j = cJSON_GetObjectItem(m, "offset");
cJSON *on_j = cJSON_GetObjectItem(m, "on");
cJSON *off_j2 = cJSON_GetObjectItem(m, "off");
cJSON *expected_j = cJSON_GetObjectItem(m, "expected");
cJSON *abs_j = cJSON_GetObjectItem(m, "absolute");
if (!cJSON_IsString(off_j) || !cJSON_IsString(on_j) || !cJSON_IsString(off_j2)) {
state = CHEAT_STATE_INVALID_CHEAT;
reason = "memory entry missing offset/on/off";
break;
}
uint8_t on_b[128], off_b[128], exp_b[128], cur[128];
size_t on_len = 0, off_len = 0, exp_len = 0;
if (parse_hex_bytes_checked(on_j->valuestring, on_b, sizeof(on_b), &on_len) != 0 ||
parse_hex_bytes_checked(off_j2->valuestring, off_b, sizeof(off_b), &off_len) != 0 || on_len != off_len) {
state = CHEAT_STATE_INVALID_CHEAT;
reason = "invalid on/off bytes";
break;
}
uint64_t off_u = 0;
if (parse_offset_hex_checked(off_j->valuestring, &off_u) != 0) {
state = CHEAT_STATE_INVALID_CHEAT;
reason = "invalid offset";
break;
}
if (cJSON_IsString(expected_j) && expected_j->valuestring && expected_j->valuestring[0]) {
if (parse_hex_bytes_checked(expected_j->valuestring, exp_b, sizeof(exp_b), &exp_len) != 0 || exp_len != on_len) {
state = CHEAT_STATE_INVALID_CHEAT;
reason = "invalid expected bytes";
break;
}
}
int af_s = 0, inj_s = 0, adet_s = auto_detect;
get_cheat_addr_flags(cheat_kind, cJSON_IsTrue(abs_j) ? 1 : 0, auto_detect, &af_s, &inj_s, &adet_s);
/* PS2 emulation: all addresses absolute (matches apply_cheat_json). */
if (is_ps2_st) { af_s = 1; inj_s = 0; adet_s = 0; }
/* JSON off_bytes/explicit expected are reliable; MC4/SHN ValueOff is not. */
int exp_rel_s = (exp_len > 0) || (cheat_kind == 1 && off_len > 0);
const uint8_t *expect_cmp_s = exp_rel_s ? (exp_len > 0 ? exp_b : off_b) : NULL;
/* Skip the offbytes probe when there's no reliable expected: a blind read can hit PS5 GPU/MMIO and hang under ptrace. */
const char *fb_str_s = (cheat_kind == 2) ? shn_fb_s : mc4_fb_s;
/* Default passive reads to RELATIVE: a raw MC4/SHN offset can hit PS5 GPU/MMIO and freeze the game just from polling state. */
cr_addr_fallback_policy_t fb_pol_s = CR_ADDR_FALLBACK_RELATIVE;
if (!exp_rel_s) {
if (strcmp(fb_str_s, "absolute") == 0) fb_pol_s = CR_ADDR_FALLBACK_ABSOLUTE;
else if (strcmp(fb_str_s, "block") == 0) fb_pol_s = CR_ADDR_FALLBACK_BLOCK;
/* "relative", "legacy", or unrecognized → RELATIVE (safe default) */
}
int adet_state = exp_rel_s ? adet_s : 0;
intptr_t addr = cheat_resolve_write_addr_ex(pid, mod_base_st, off_u,
af_s, inj_s, adet_state,
on_b, on_len, expect_cmp_s, exp_rel_s,
fb_pol_s, NULL, 1);
/* Try absolute candidate for ADDRESS_UNRESOLVED detection */
intptr_t abs_cand2 = (intptr_t)off_u;
intptr_t rel_cand2 = mod_base_st + (intptr_t)off_u;
cr_log("debug", "cheats.state",
"read_state mod=%d off=0x%llx addr=0x%lx abs=0x%lx rel=0x%lx len=%zu",
idx, (unsigned long long)off_u, (long)addr,
(long)abs_cand2, (long)rel_cand2, on_len);
if (read_process_memory(pid, addr, cur, on_len) != 0) {
/* Try the other candidate before declaring unresolved */
intptr_t alt = (addr == abs_cand2) ? rel_cand2 : abs_cand2;
cr_log("debug", "cheats.state",
"read_state_retry mod=%d addr=0x%lx (primary failed)", idx, (long)alt);
if (on_len <= sizeof(cur) && read_process_memory(pid, alt, cur, on_len) == 0) {
addr = alt;
} else {
cr_log("warn", "cheats.state",
"read_state_unresolved mod=%d off=0x%llx abs=0x%lx rel=0x%lx",
idx, (unsigned long long)off_u, (long)abs_cand2, (long)rel_cand2);
state = CHEAT_STATE_ADDRESS_UNRESOLVED;
reason = "read failed on both address candidates";
break;
}
}
total++;
int is_on = memcmp(cur, on_b, on_len) == 0;
/* For MC4/SHN without explicit expected bytes, off_b is NOT a reliable original */
int off_reliable = (cheat_kind == 1) || (exp_len > 0);
int is_off = off_reliable && (memcmp(cur, (exp_len > 0 ? exp_b : off_b), on_len) == 0);
/* Another mod claimed this address (cave/JMP redirect) — treat as OFF, not mismatch, for mutually-exclusive cheats. */
int cross_mod_inactive = 0;
if (!is_on && !is_off && off_reliable) {
if (on_len >= 16) {
cross_mod_inactive = 1; /* cave — another mod's shellcode lives here */
} else if (on_len >= 5 && on_b[0] == 0xE9 && cur[0] == 0xE9) {
cross_mod_inactive = 1; /* hook redirect — a different JMP is already installed */
}
}
if (is_on) {
on_matches++;
}
if (is_off || cross_mod_inactive) {
off_matches++;
}
if (!is_on && !is_off && !cross_mod_inactive && off_reliable) {
mismatch++;
if (on_len > 0 && on_len < 16) {
int all_ff = 1;
for (size_t _bz = 0; _bz < on_len; _bz++) {
if (cur[_bz] != 0xFF) { all_ff = 0; break; }
}
if (all_ff) ff_hook++;
}
}
if (!is_on && !off_reliable) {
/* ValueOff match means disabled/never-applied; counted separately so state can be OFF_UNVERIFIED, not BASELINE_UNKNOWN. */
if (off_len > 0 && memcmp(cur, off_b, on_len) == 0) {
off_val_matches++;
} else {
baseline_unknown++;
}
}
if (!is_on && !is_off && !cross_mod_inactive && off_reliable) {
if (!mismatch_logged) {
char cur_hex[512], on_hex[128], exp_hex[128], addr_hex[32], off_hex_addr[32];
bytes_to_hex(cur, on_len > 32 ? 32 : on_len, cur_hex, sizeof(cur_hex));
bytes_to_hex(on_b, on_len > 16 ? 16 : on_len, on_hex, sizeof(on_hex));
bytes_to_hex(exp_len > 0 ? exp_b : off_b, on_len > 16 ? 16 : on_len, exp_hex, sizeof(exp_hex));
snprintf(addr_hex, sizeof(addr_hex), "0x%lx", (long)addr);
snprintf(off_hex_addr, sizeof(off_hex_addr), "0x%llX", (unsigned long long)off_u);
/* Detect if current bytes are all printable ASCII — strong sign the
* offset landed in string/data rather than executable code. */
int cur_is_ascii = (on_len > 0);
for (size_t _ai = 0; _ai < on_len && cur_is_ascii; _ai++) {
if (cur[_ai] < 0x20 || cur[_ai] > 0x7E) cur_is_ascii = 0;
}
cr_log(mismatch_warn_level(title_id, mod_index), "cheats",
"mismatch title=%s contentVersion=%s file=%s format=%s mod=\"%s\" modIndex=%d writeIndex=%d pid=%d "
"base=0x%lx offset=%s addr=%s expected=%s...(len=%zu) current=%s...(len=%zu) on=%s...(len=%zu)%s",
title_id, st.content_version[0] ? st.content_version : "unknown", cheat_path, fmt, mod_name, mod_index,
total - 1, (int)pid, (long)mod_base_st, off_hex_addr, addr_hex, exp_hex, on_len, cur_hex, on_len, on_hex, on_len,
cur_is_ascii ? " [hint: current bytes are printable ASCII — offset likely points to game string data, not code]" : "");
mismatch_logged = 1;
}
if (debug_mode && mismatches && cJSON_GetArraySize(mismatches) < 32) {
/* Cap hex field length at 32 bytes (64 hex chars) to bound JSON output size */
size_t hex_cap = on_len > 32 ? 32 : on_len;
char cur_hex[128], on_hex[128], off_hex[128], exp_hex[128], addr_hex[32], off_hex_addr[32];
char abs_addr_hex[32], rel_addr_hex[32];
char abs_cur_hex[128], rel_cur_hex[128];
uint8_t abs_buf[32], rel_buf[32];
intptr_t abs_cand = (intptr_t)off_u;
intptr_t rel_cand = mod_base_st + (intptr_t)off_u;
int abs_read = (hex_cap <= sizeof(abs_buf) && read_process_memory(pid, abs_cand, abs_buf, hex_cap) == 0);
int rel_read = (hex_cap <= sizeof(rel_buf) && read_process_memory(pid, rel_cand, rel_buf, hex_cap) == 0);
bytes_to_hex(cur, hex_cap, cur_hex, sizeof(cur_hex));
bytes_to_hex(on_b, hex_cap, on_hex, sizeof(on_hex));
bytes_to_hex(off_b, hex_cap, off_hex, sizeof(off_hex));
bytes_to_hex(exp_len > 0 ? exp_b : off_b, hex_cap, exp_hex, sizeof(exp_hex));
snprintf(addr_hex, sizeof(addr_hex), "0x%lx", (long)addr);
snprintf(off_hex_addr, sizeof(off_hex_addr), "0x%llX", (unsigned long long)off_u);
snprintf(abs_addr_hex, sizeof(abs_addr_hex), "0x%lx", (long)abs_cand);
snprintf(rel_addr_hex, sizeof(rel_addr_hex), "0x%lx", (long)rel_cand);
if (abs_read) { bytes_to_hex(abs_buf, hex_cap, abs_cur_hex, sizeof(abs_cur_hex)); } else { snprintf(abs_cur_hex, sizeof(abs_cur_hex), "(read failed)"); }
if (rel_read) { bytes_to_hex(rel_buf, hex_cap, rel_cur_hex, sizeof(rel_cur_hex)); } else { snprintf(rel_cur_hex, sizeof(rel_cur_hex), "(read failed)"); }
cJSON *mm = cJSON_CreateObject();
cJSON_AddStringToObject(mm, "mod", mod_name);
cJSON_AddNumberToObject(mm, "modIndex", mod_index);
cJSON_AddNumberToObject(mm, "writeIndex", total - 1);
cJSON_AddStringToObject(mm, "offset", off_hex_addr);
cJSON_AddStringToObject(mm, "address", addr_hex);
cJSON_AddStringToObject(mm, "expected", exp_hex);
cJSON_AddStringToObject(mm, "off", off_hex);
cJSON_AddStringToObject(mm, "current", cur_hex);
cJSON_AddStringToObject(mm, "on", on_hex);
/* both address candidates for diagnosis */
cJSON *cands = cJSON_AddArrayToObject(mm, "candidates");
if (cands) {
cJSON *ca = cJSON_CreateObject();
cJSON_AddStringToObject(ca, "mode", "absolute");
cJSON_AddStringToObject(ca, "address", abs_addr_hex);
cJSON_AddStringToObject(ca, "current", abs_cur_hex);
cJSON_AddBoolToObject(ca, "readOk", abs_read);
cJSON_AddItemToArray(cands, ca);
cJSON *cr2 = cJSON_CreateObject();
cJSON_AddStringToObject(cr2, "mode", "relative");
cJSON_AddStringToObject(cr2, "address", rel_addr_hex);
cJSON_AddStringToObject(cr2, "current", rel_cur_hex);
cJSON_AddBoolToObject(cr2, "readOk", rel_read);
cJSON_AddItemToArray(cands, cr2);
}
cJSON_AddItemToArray(mismatches, mm);
}
}
}
if (state == CHEAT_STATE_UNKNOWN) {
if (on_matches == total && total > 0) {
state = CHEAT_STATE_ON;
} else if (mismatch > 0 && baseline_unknown == 0 && on_matches == 0 &&
off_matches == 0 && off_val_matches == 0) {
/* All patches have reliable baseline and none match — true version mismatch */
state = CHEAT_STATE_MISMATCH;
reason = ff_hook > 0
? "hook address reads 0xFF — base+offset lands in unmapped memory; try address mode 'absolute'"
: "current bytes match neither ON nor expected/off";
} else if (off_matches == total && total > 0 && baseline_unknown == 0 && off_val_matches == 0) {
state = CHEAT_STATE_OFF;
} else if ((off_matches + off_val_matches) == total && total > 0 &&
baseline_unknown == 0 && mismatch == 0 && off_val_matches > 0) {
/* All entries' ValueOff bytes match — cheat is in its disabled state. */
state = CHEAT_STATE_OFF_UNVERIFIED;
reason = "MC4/SHN: ValueOff bytes match current memory; cheat appears disabled.";
} else if (baseline_unknown > 0 && mismatch == 0 && on_matches == 0) {
/* No ON matches, no reliable mismatches — not applied either way, so report BASELINE_UNKNOWN not MIXED. */
state = CHEAT_STATE_BASELINE_UNKNOWN;
reason = "MC4/SHN: no reliable original bytes; state cannot be confirmed";
} else {
state = CHEAT_STATE_MIXED;
reason = ff_hook > 0
? "hook address reads 0xFF — base+offset may be in unmapped memory"
: "some writes differ";
}
}
}
/* Promote BASELINE_UNKNOWN → OFF_UNVERIFIED when mod was explicitly disabled this session */
if (state == CHEAT_STATE_BASELINE_UNKNOWN && can_probe &&
mod_disabled_check(title_id, pid, mod_index)) {
state = CHEAT_STATE_OFF_UNVERIFIED;
reason = "Disabled using runtime backup; MC4 baseline unknown.";
}
/* Promote BASELINE_UNKNOWN → ON_UNVERIFIED when mod was explicitly enabled this session */
if (state == CHEAT_STATE_BASELINE_UNKNOWN && can_probe &&
mod_enabled_check(title_id, pid, mod_index)) {
state = CHEAT_STATE_ON_UNVERIFIED;
reason = "Enabled this session; MC4/SHN baseline unknown, cannot confirm bytes.";
}
/* Mark crash_suspect if game stopped shortly after enabling */
{
pthread_mutex_lock(&g_crash_guard_lock);
for (int _ci = 0; _ci < g_crash_suspects_n; _ci++) {
if (strcmp(g_crash_suspects[_ci].title_id, title_id) == 0 &&
g_crash_suspects[_ci].mod_index == mod_index) {
state = CHEAT_STATE_CRASH_SUSPECT;
reason = "Game stopped shortly after enabling. Use 'Clear Crash Flags' to re-enable.";
break;
}
}
pthread_mutex_unlock(&g_crash_guard_lock);
}
/* Conflict detection: find mods that share address ranges with this one.
* If any conflicting mod is currently active → promote non-ON states to CONFLICT. */
int conflict_mods[16];
int conflict_count = 0;
char conflict_reason_buf[128];
{
pthread_mutex_lock(&g_conflict_cache.lock);
if (g_conflict_cache.ready) {
conflict_count = conflict_map_get_for_mod(&g_conflict_cache.map, mod_index,
conflict_mods, 16);
}
pthread_mutex_unlock(&g_conflict_cache.lock);
}
int active_conflict_mod = -1;
if (conflict_count > 0 && can_probe &&
state != CHEAT_STATE_ON && state != CHEAT_STATE_ON_UNVERIFIED &&
state != CHEAT_STATE_CRASH_SUSPECT) {
for (int _ci = 0; _ci < conflict_count; _ci++) {
if (mod_enabled_check(title_id, pid, conflict_mods[_ci])) {
active_conflict_mod = conflict_mods[_ci];
break;
}
}
if (active_conflict_mod >= 0 &&
(state == CHEAT_STATE_OFF || state == CHEAT_STATE_OFF_UNVERIFIED ||
state == CHEAT_STATE_MIXED || state == CHEAT_STATE_BASELINE_UNKNOWN ||
state == CHEAT_STATE_MISMATCH)) {
pthread_mutex_lock(&g_conflict_cache.lock);
const char *cn = conflict_map_mod_name(&g_conflict_cache.map, active_conflict_mod);
if (cn && cn[0])
snprintf(conflict_reason_buf, sizeof(conflict_reason_buf),
"Conflicts with active mod: %s", cn);
else
snprintf(conflict_reason_buf, sizeof(conflict_reason_buf),
"Conflicts with active mod #%d", active_conflict_mod);
pthread_mutex_unlock(&g_conflict_cache.lock);
state = CHEAT_STATE_CONFLICT;
reason = conflict_reason_buf;
}
}
/* Emit conflictsWith array for every mod that has known conflicts */
if (conflict_count > 0) {
cJSON *cw_arr = cJSON_AddArrayToObject(e, "conflictsWith");
if (cw_arr) {
pthread_mutex_lock(&g_conflict_cache.lock);
for (int _ci = 0; _ci < conflict_count; _ci++) {
cJSON *cw = cJSON_CreateObject();
cJSON_AddNumberToObject(cw, "modIndex", conflict_mods[_ci]);
const char *cn = conflict_map_mod_name(&g_conflict_cache.map, conflict_mods[_ci]);
if (cn && cn[0]) cJSON_AddStringToObject(cw, "name", cn);
cJSON_AddItemToArray(cw_arr, cw);
}
pthread_mutex_unlock(&g_conflict_cache.lock);
}
}
cJSON_AddStringToObject(e, "state", cheat_state_key(state));
cJSON_AddStringToObject(e, "label", cheat_state_label(state));
if (reason && reason[0]) {
cJSON_AddStringToObject(e, "reason", reason);
}
/* visualState + capability flags for frontend */
{
const char *visual_state;
int can_enable_flag, can_disable_flag;
if (state == CHEAT_STATE_ON || state == CHEAT_STATE_ON_UNVERIFIED) {
visual_state = "on"; can_enable_flag = 0; can_disable_flag = 1;
} else if (state == CHEAT_STATE_OFF || state == CHEAT_STATE_OFF_UNVERIFIED ||
state == CHEAT_STATE_BASELINE_UNKNOWN) {
visual_state = "off"; can_enable_flag = 1; can_disable_flag = 0;
} else if (state == CHEAT_STATE_CONFLICT) {
/* Conflicting mods can still be enabled — engine auto-disables the blocker */
visual_state = "conflict"; can_enable_flag = 1; can_disable_flag = 0;
} else if (state == CHEAT_STATE_MIXED) {
visual_state = "mixed"; can_enable_flag = 0; can_disable_flag = 1;
} else if (state == CHEAT_STATE_CRASH_SUSPECT) {
visual_state = "error"; can_enable_flag = 0; can_disable_flag = 0;
} else {
visual_state = "disabled"; can_enable_flag = 0; can_disable_flag = 0;
}
int can_toggle = can_enable_flag || can_disable_flag;
const char *next_action = can_enable_flag ? "enable" : (can_disable_flag ? "disable" : "none");
int next_on = can_enable_flag ? 1 : 0;
/* block_reason: use the existing reason string when toggle is blocked */
const char *block_reason = can_toggle ? "" : (reason && reason[0] ? reason : "");
cJSON_AddStringToObject(e, "visualState", visual_state);
cJSON_AddBoolToObject(e, "canEnable", can_enable_flag);
cJSON_AddBoolToObject(e, "canDisable", can_disable_flag);
cJSON_AddBoolToObject(e, "canToggle", can_toggle);
cJSON_AddStringToObject(e, "nextAction", next_action);
cJSON_AddBoolToObject(e, "nextOn", next_on);
if (block_reason[0]) cJSON_AddStringToObject(e, "blockReason", block_reason);
}
if (state == CHEAT_STATE_ON || state == CHEAT_STATE_ON_UNVERIFIED) {
summary_on++;
} else if (state == CHEAT_STATE_OFF || state == CHEAT_STATE_OFF_UNVERIFIED) {
summary_off++;
} else if (state == CHEAT_STATE_MISMATCH) {
summary_mismatch++;
} else if (state == CHEAT_STATE_BASELINE_UNKNOWN) {
summary_baseline_unknown++;
} else {
summary_not_ready++;
}
cJSON_AddItemToArray(arr, e);
}
cJSON_Delete(root);
cJSON *summary = cJSON_AddObjectToObject(out, "summary");
if (summary) {
cJSON_AddNumberToObject(summary, "on", summary_on);
cJSON_AddNumberToObject(summary, "off", summary_off);
cJSON_AddNumberToObject(summary, "mismatch", summary_mismatch);
cJSON_AddNumberToObject(summary, "baselineUnknown", summary_baseline_unknown);
cJSON_AddNumberToObject(summary, "notReady", summary_not_ready);
}
cr_log("debug", "cheats", "state summary title=%s on=%d off=%d mismatch=%d baselineUnknown=%d notReady=%d file=%s format=%s pid=%d base=0x%lx",
title_id, summary_on, summary_off, summary_mismatch, summary_baseline_unknown, summary_not_ready, cheat_path, fmt, (int)pid, (long)base);
char *txt = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!txt) {
http_send_oom(fd);
return;
}
http_send_response(fd, 200, "application/json", (const uint8_t *)txt, strlen(txt));
free(txt);
}
void
handle_api_cheats_clear_crash_flags(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
pthread_mutex_lock(&g_crash_guard_lock);
int cleared = 0;
for (int i = g_crash_suspects_n - 1; i >= 0; i--) {
if (strcmp(g_crash_suspects[i].title_id, title_id) == 0) {
g_crash_suspects[i] = g_crash_suspects[--g_crash_suspects_n];
cleared++;
}
}
pthread_mutex_unlock(&g_crash_guard_lock);
if (cleared > 0) {
crash_suspects_save();
}
char body[64];
snprintf(body, sizeof(body), "{\"ok\":true,\"cleared\":%d}", cleared);
cr_log("info", "cheats", "cleared crash flags title=%s n=%d", title_id, cleared);
http_send_json(fd, 200, body);
}
/* POST /api/cheats/select: path must be in the current candidates list; wrong_version requires force=1. */
void
handle_api_cheats_select(int fd, const char *query) {
char raw_tid[32] = {0}, title_id[10] = {0};
char path_raw[512] = {0};
char force_s[4] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 ||
!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
if (query_value(query, "path", path_raw, sizeof(path_raw)) != 0 || !path_raw[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing path\"}");
return;
}
int force = (query_value(query, "force", force_s, sizeof(force_s)) == 0 && atoi(force_s) != 0);
/* Get current candidates to validate the path */
running_game_state_t sel_st;
char live_ver[64] = {0};
if (read_running_state(&sel_st) == 0 && sel_st.running &&
strcmp(sel_st.title_id, title_id) == 0) {
if (sel_st.content_version[0])
snprintf(live_ver, sizeof(live_ver), "%s", sel_st.content_version);
else if (sel_st.app_version[0])
snprintf(live_ver, sizeof(live_ver), "%s", sel_st.app_version);
}
cheat_file_search_t sel_ctx;
memset(&sel_ctx, 0, sizeof(sel_ctx));
find_cheat_candidates_ex(title_id, live_ver[0] ? live_ver : NULL, &sel_ctx);
/* Check path is in candidates */
int found_idx = -1;
for (int i = 0; i < sel_ctx.candidate_count; i++) {
if (strcmp(sel_ctx.candidates[i].path, path_raw) == 0) {
found_idx = i;
break;
}
}
if (found_idx < 0) {
cr_log("warn", "cheat_select", "manual_select rejected: path not in candidates title=%s path=%s", title_id, path_raw);
http_send_json(fd, 403, "{\"ok\":false,\"error\":\"path not in candidate list for this title\"}");
return;
}
/* Require force for wrong_version */
if (sel_ctx.candidates[found_idx].match == CAND_MATCH_WRONG_VERSION && !force) {
http_send_json(fd, 409,
"{\"ok\":false,\"error\":\"version_mismatch\",\"message\":\"This file has a different version than the running game. Add force=1 to override.\"}");
return;
}
/* Block file switch when mods from the current (different) file are still active.
* Switching files while hooks point to caves that the new file will overwrite causes SIGSEGV. */
if (sel_st.running && strcmp(sel_st.title_id, title_id) == 0 && !force) {
char cur_path[256] = {0};
int cur_kind = 0;
find_cheat_file_for_title(title_id, cur_path, sizeof(cur_path), &cur_kind);
if (cur_path[0] && strcmp(cur_path, path_raw) != 0 &&
cheat_any_enabled_for_title(title_id, sel_st.pid)) {
http_send_json(fd, 409,
"{\"ok\":false,\"error\":\"active_mods_conflict\","
"\"message\":\"Active mods from the current cheat file are still enabled. "
"Disable all mods before switching cheat files to avoid a game crash. "
"Add force=1 to override.\"}");
return;
}
}
if (cheat_manual_select(title_id, path_raw) != 0) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"store failed\"}");
return;
}
const char *fname = strrchr(path_raw, '/');
fname = fname ? fname + 1 : path_raw;
cr_log("info", "cheat_select", "manual_select title=%s path=%s match=%d force=%d",
title_id, path_raw, sel_ctx.candidates[found_idx].match, force);
char body[512];
snprintf(body, sizeof(body), "{\"ok\":true,\"titleId\":\"%s\",\"path\":\"%s\",\"filename\":\"%s\"}",
title_id, path_raw, fname);
http_send_json(fd, 200, body);
}
/* POST /api/cheats/select/auto?titleId=CUSA00900
* Reset to auto-selection (clear manual override). */
void
handle_api_cheats_select_auto(int fd, const char *query) {
char raw_tid[32] = {0}, title_id[10] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 ||
!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
cheat_manual_select_clear(title_id);
cr_log("info", "cheat_select", "manual_select_clear title=%s", title_id);
char body[64];
snprintf(body, sizeof(body), "{\"ok\":true,\"titleId\":\"%s\"}", title_id);
http_send_json(fd, 200, body);
}
void
handle_api_cheats_debug(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
char q[96];
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
snprintf(q, sizeof(q), "titleId=%s&debug=1", title_id);
handle_api_cheats_state(fd, q);
}
static cheat_state_kind_t
check_mod_enable_state(const char *title_id, int mod_index, char *reason, size_t reason_size) {
char err[128] = {0};
int cheat_kind_cme = 0;
cJSON *root = load_cheat_json_root_for_title_ex(title_id, err, sizeof(err), NULL, 0, &cheat_kind_cme);
if (!root) {
running_game_state_t gs_fb;
if (read_running_state(&gs_fb) != 0 || !gs_fb.running) {
snprintf(reason, reason_size, "no game is currently running");
return CHEAT_STATE_GAME_NOT_RUNNING;
}
snprintf(reason, reason_size, "%s", err[0] ? err : "invalid cheat");
return CHEAT_STATE_INVALID_CHEAT;
}
int auto_detect_cme = 1;
char mc4_fb_cme[16] = "legacy";
char shn_fb_cme[16] = "legacy";
pthread_mutex_lock(&g_cfg_lock);
auto_detect_cme = g_cfg.cheat_address_auto_detect;
snprintf(mc4_fb_cme, sizeof(mc4_fb_cme), "%s", g_cfg.cheat_mc4_unverified_fallback);
snprintf(shn_fb_cme, sizeof(shn_fb_cme), "%s", g_cfg.cheat_shn_unverified_fallback);
pthread_mutex_unlock(&g_cfg_lock);
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (!cJSON_IsArray(mods) || mod_index < 0 || mod_index >= cJSON_GetArraySize(mods)) {
cJSON_Delete(root);
snprintf(reason, reason_size, "mod index out of range");
return CHEAT_STATE_INVALID_CHEAT;
}
running_game_state_t st;
if (read_running_state(&st) != 0 || !st.running) {
cJSON_Delete(root);
snprintf(reason, reason_size, "no game is currently running");
return CHEAT_STATE_GAME_NOT_RUNNING;
}
if (strcmp(st.title_id, title_id) != 0) {
cJSON_Delete(root);
snprintf(reason, reason_size, "running game does not match cheat title");
return CHEAT_STATE_PROCESS_NOT_FOUND;
}
if (st.pid <= 0) {
cJSON_Delete(root);
snprintf(reason, reason_size, "process not found");
return CHEAT_STATE_PROCESS_NOT_FOUND;
}
if (st.image_base == 0) {
cJSON_Delete(root);
snprintf(reason, reason_size, "base address not ready");
return CHEAT_STATE_BASE_NOT_READY;
}
int delay_ms = 8000;
pthread_mutex_lock(&g_cfg_lock);
delay_ms = g_cfg.cheat_state_after_launch_delay_ms;
pthread_mutex_unlock(&g_cfg_lock);
{
uint64_t _lv = __atomic_load_n(&g_last_launch_verified_at_ms, __ATOMIC_ACQUIRE);
if (delay_ms > 0 && _lv > 0) {
uint64_t age = now_ms() - _lv;
if (age < (uint64_t)delay_ms) {
cJSON_Delete(root);
snprintf(reason, reason_size, "game still loading");
return CHEAT_STATE_GAME_LOADING;
}
}
}
/* Fast path: MC4/SHN mods without expected bytes always yield BASELINE_UNKNOWN.
* Skip ptrace to eliminate the double-attach race (check then apply both attach). */
if (cheat_kind_cme != 1) {
cJSON *mod_fp = cJSON_GetArrayItem(mods, mod_index);
cJSON *mem_fp = cJSON_GetObjectItem(mod_fp, "memory");
if (!cJSON_IsArray(mem_fp)) mem_fp = cJSON_GetObjectItem(mod_fp, "patches");
int has_expected_fp = 0;
if (cJSON_IsArray(mem_fp)) {
cJSON *m_fp = NULL;
cJSON_ArrayForEach(m_fp, mem_fp) {
cJSON *ej = cJSON_GetObjectItem(m_fp, "expected");
if (cJSON_IsString(ej) && ej->valuestring && ej->valuestring[0]) {
has_expected_fp = 1; break;
}
}
}
if (!has_expected_fp) {
cJSON_Delete(root);
cheat_state_kind_t fast_st = CHEAT_STATE_BASELINE_UNKNOWN;
snprintf(reason, reason_size, "MC4/SHN: no reliable original bytes");
pthread_mutex_lock(&g_crash_guard_lock);
for (int _ci = 0; _ci < g_crash_suspects_n; _ci++) {
if (strcmp(g_crash_suspects[_ci].title_id, title_id) == 0 &&
g_crash_suspects[_ci].mod_index == mod_index) {
fast_st = CHEAT_STATE_CRASH_SUSPECT;
snprintf(reason, reason_size, "Game stopped shortly after enabling this cheat.");
break;
}
}
pthread_mutex_unlock(&g_crash_guard_lock);
return fast_st;
}
}
/* read_process_memory is mdbg-backed and needs no ptrace attach. */
if (kill(st.pid, 0) != 0 && errno == ESRCH) {
cJSON_Delete(root);
snprintf(reason, reason_size, "game has exited");
return CHEAT_STATE_PROCESS_NOT_FOUND;
}
int is_ps2_cme = process_is_ps2_emu(st.pid);
cheat_state_kind_t state = CHEAT_STATE_UNKNOWN;
cJSON *mod = cJSON_GetArrayItem(mods, mod_index);
cJSON *mem = cJSON_GetObjectItem(mod, "memory");
if (!cJSON_IsArray(mem)) {
mem = cJSON_GetObjectItem(mod, "patches");
}
if (!cJSON_IsArray(mem) || cJSON_GetArraySize(mem) == 0) {
state = CHEAT_STATE_INVALID_CHEAT;
snprintf(reason, reason_size, "mod has no memory entries");
} else {
/* Per-mod base: respect module_name, matching apply_cheat_json behaviour. */
intptr_t mod_base_cme = st.image_base;
{
cJSON *mn_j_cme = cJSON_GetObjectItem(mod, "module_name");
const char *mn_cme = (cJSON_IsString(mn_j_cme) && mn_j_cme->valuestring) ? mn_j_cme->valuestring : "";
if (mn_cme[0] && resolve_module_base(st.pid, mn_cme, st.image_base, &mod_base_cme) != 0) {
state = CHEAT_STATE_PROCESS_NOT_FOUND;
snprintf(reason, reason_size, "module_not_loaded");
}
}
cJSON *m = NULL;
int on_matches = 0;
int off_matches = 0;
int total = 0;
int mismatches = 0;
int baseline_unknown_cme = 0;
cJSON_ArrayForEach(m, mem) {
if (state != CHEAT_STATE_UNKNOWN) break;
cJSON *off_j = cJSON_GetObjectItem(m, "offset");
cJSON *on_j = cJSON_GetObjectItem(m, "on");
cJSON *off_j2 = cJSON_GetObjectItem(m, "off");
cJSON *expected_j = cJSON_GetObjectItem(m, "expected");
cJSON *abs_j = cJSON_GetObjectItem(m, "absolute");
if (!cJSON_IsString(off_j) || !cJSON_IsString(on_j) || !cJSON_IsString(off_j2)) {
state = CHEAT_STATE_INVALID_CHEAT;
snprintf(reason, reason_size, "memory entry missing offset/on/off");
break;
}
uint8_t on_b[128], off_b[128], exp_b[128], cur[128];
size_t on_len = 0, off_len = 0, exp_len = 0;
if (parse_hex_bytes_checked(on_j->valuestring, on_b, sizeof(on_b), &on_len) != 0 ||
parse_hex_bytes_checked(off_j2->valuestring, off_b, sizeof(off_b), &off_len) != 0 || on_len != off_len) {
state = CHEAT_STATE_INVALID_CHEAT;
snprintf(reason, reason_size, "invalid on/off bytes");
break;
}
uint64_t off_u = 0;
if (parse_offset_hex_checked(off_j->valuestring, &off_u) != 0) {
state = CHEAT_STATE_INVALID_CHEAT;
snprintf(reason, reason_size, "invalid offset");
break;
}
if (cJSON_IsString(expected_j) && expected_j->valuestring && expected_j->valuestring[0]) {
if (parse_hex_bytes_checked(expected_j->valuestring, exp_b, sizeof(exp_b), &exp_len) != 0 || exp_len != on_len) {
state = CHEAT_STATE_INVALID_CHEAT;
snprintf(reason, reason_size, "invalid expected bytes");
break;
}
}
int af_cme = 0, inj_cme = 0, adet_cme = auto_detect_cme;
get_cheat_addr_flags(cheat_kind_cme, cJSON_IsTrue(abs_j) ? 1 : 0, auto_detect_cme, &af_cme, &inj_cme, &adet_cme);
/* PS2 emulation: all addresses absolute (matches apply_cheat_json). */
if (is_ps2_cme) { af_cme = 1; inj_cme = 0; adet_cme = 0; }
int exp_rel_cme = (exp_len > 0) || (cheat_kind_cme == 1 && off_len > 0);
const uint8_t *expect_cme = exp_rel_cme ? (exp_len > 0 ? exp_b : off_b) : NULL;
const char *fb_str_cme = (cheat_kind_cme == 2) ? shn_fb_cme : mc4_fb_cme;
cr_addr_fallback_policy_t fb_pol_cme = CR_ADDR_FALLBACK_LEGACY;
if (!exp_rel_cme) {
if (strcmp(fb_str_cme, "absolute") == 0) fb_pol_cme = CR_ADDR_FALLBACK_ABSOLUTE;
else if (strcmp(fb_str_cme, "relative") == 0) fb_pol_cme = CR_ADDR_FALLBACK_RELATIVE;
else if (strcmp(fb_str_cme, "block") == 0) fb_pol_cme = CR_ADDR_FALLBACK_BLOCK;
}
intptr_t addr = cheat_resolve_write_addr_ex(st.pid, mod_base_cme, off_u,
af_cme, inj_cme, adet_cme,
on_b, on_len, expect_cme, exp_rel_cme,
fb_pol_cme, NULL, 1);
if (read_process_memory(st.pid, addr, cur, on_len) != 0) {
state = CHEAT_STATE_READ_FAILED;
snprintf(reason, reason_size, "read failed");
break;
}
total++;
int is_on = memcmp(cur, on_b, on_len) == 0;
int off_reliable_cme = (cheat_kind_cme == 1) || (exp_len > 0);
int is_off = off_reliable_cme && (memcmp(cur, (exp_len > 0 ? exp_b : off_b), on_len) == 0);
if (is_on) { on_matches++; }
if (is_off) { off_matches++; }
if (!is_on && !is_off && off_reliable_cme) { mismatches++; }
if (!is_on && !off_reliable_cme) { baseline_unknown_cme++; }
}
if (state == CHEAT_STATE_UNKNOWN) {
if (on_matches == total && total > 0) {
state = CHEAT_STATE_ON;
} else if (mismatches > 0 && baseline_unknown_cme == 0 && on_matches == 0 && off_matches == 0) {
state = CHEAT_STATE_MISMATCH;
snprintf(reason, reason_size, "current bytes match neither ON nor expected/off");
} else if (baseline_unknown_cme > 0 && mismatches == 0 && on_matches == 0) {
state = CHEAT_STATE_BASELINE_UNKNOWN;
snprintf(reason, reason_size, "MC4/SHN: no reliable original bytes");
} else if (off_matches == total && total > 0 && baseline_unknown_cme == 0) {
state = CHEAT_STATE_OFF;
} else {
state = CHEAT_STATE_MIXED;
}
}
}
cJSON_Delete(root);
/* Block crash_suspect mods from being re-enabled */
if (state != CHEAT_STATE_ON) {
pthread_mutex_lock(&g_crash_guard_lock);
for (int _ci = 0; _ci < g_crash_suspects_n; _ci++) {
if (strcmp(g_crash_suspects[_ci].title_id, title_id) == 0 &&
g_crash_suspects[_ci].mod_index == mod_index) {
state = CHEAT_STATE_CRASH_SUSPECT;
if (reason && reason_size > 0)
snprintf(reason, reason_size, "Game stopped shortly after enabling this cheat.");
break;
}
}
pthread_mutex_unlock(&g_crash_guard_lock);
}
return state;
}
static void
classify_cheat_error_code(const char *msg, char *out, size_t out_size) {
const char *code = "apply_failed";
if (msg && *msg) {
if (strstr(msg, "bytes mismatch")) {
code = "bytes_mismatch";
} else if (strstr(msg, "verify_failed")) {
code = "verify_failed";
} else if (strstr(msg, "pt_attach failed")) {
code = "attach_failed";
} else if (strstr(msg, "no game is currently running")) {
code = "process_not_found";
} else if (strstr(msg, "cheat JSON parse failed") || strstr(msg, "cheat format parse failed") ||
strstr(msg, "no cheat file")) {
code = "invalid_cheat";
} else if (strstr(msg, "memory write failed")) {
code = "write_failed";
} else if (strstr(msg, "cheat engine disabled")) {
code = "engine_disabled";
} else if (strstr(msg, "app_not_stable")) {
code = "app_not_stable";
}
}
snprintf(out, out_size, "%s", code);
}
/* GET /api/cheats/apply-dryrun: shows what a real apply would do (addresses, bytes, protection) with no writes. */
void
handle_api_cheats_apply_dryrun(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
char mod_s[16] = {0};
char err[128] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 ||
!title_id_normalize(raw_title_id, title_id) ||
query_value(query, "mod", mod_s, sizeof(mod_s)) != 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"titleId and mod required\"}");
return;
}
int mod_index = atoi(mod_s);
if (mod_index < 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"mod must be >= 0\"}");
return;
}
char cheat_path[256] = {0};
int cheat_kind = 0;
cJSON *root = load_cheat_json_root_for_title_ex(title_id, err, sizeof(err), cheat_path, sizeof(cheat_path), &cheat_kind);
if (!root) {
char body[256];
snprintf(body, sizeof(body), "{\"ok\":false,\"error\":\"%s\"}", err);
http_send_json(fd, 404, body);
return;
}
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (!cJSON_IsArray(mods) || mod_index >= cJSON_GetArraySize(mods)) {
cJSON_Delete(root);
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"mod index out of range\"}");
return;
}
running_game_state_t st;
int running_ok = (read_running_state(&st) == 0 && st.running && strcmp(st.title_id, title_id) == 0);
pid_t pid = running_ok ? st.pid : -1;
intptr_t base = running_ok ? st.image_base : 0;
int auto_detect = 1;
pthread_mutex_lock(&g_cfg_lock);
auto_detect = g_cfg.cheat_address_auto_detect;
pthread_mutex_unlock(&g_cfg_lock);
/* Probing reads are mdbg-backed and need no ptrace attach. */
int attached = 0;
if (running_ok && pid > 0) {
attached = (kill(pid, 0) == 0 || errno != ESRCH);
}
int is_ps2_dr = attached ? process_is_ps2_emu(pid) : 0;
cJSON *out = cJSON_CreateObject();
cJSON_AddBoolToObject(out, "ok", 1);
cJSON_AddStringToObject(out, "titleId", title_id);
cJSON_AddStringToObject(out, "cheatFile", cheat_path);
cJSON_AddStringToObject(out, "format", cheat_kind == 1 ? "json" : (cheat_kind == 2 ? "shn" : "mc4"));
{
char ph[32]; snprintf(ph, sizeof(ph), "0x%lx", (long)base);
cJSON_AddNumberToObject(out, "pid", (double)(running_ok ? pid : -1));
cJSON_AddStringToObject(out, "base", ph);
cJSON_AddBoolToObject(out, "attached", attached);
}
cJSON_AddNumberToObject(out, "mod", mod_index);
cJSON *mod = cJSON_GetArrayItem(mods, mod_index);
cJSON *mod_name_j = cJSON_GetObjectItem(mod, "name");
cJSON_AddStringToObject(out, "modName",
cJSON_IsString(mod_name_j) && mod_name_j->valuestring ? mod_name_j->valuestring : "");
/* Per-mod base: respect module_name, matching apply_cheat_json behaviour. */
intptr_t mod_base_dr = base;
if (attached && pid > 0) {
cJSON *mn_j_dr = cJSON_GetObjectItem(mod, "module_name");
const char *mn_dr = (cJSON_IsString(mn_j_dr) && mn_j_dr->valuestring) ? mn_j_dr->valuestring : "";
if (mn_dr[0]) resolve_module_base(pid, mn_dr, base, &mod_base_dr);
}
cJSON *memory = cJSON_GetObjectItem(mod, "memory");
if (!cJSON_IsArray(memory)) memory = cJSON_GetObjectItem(mod, "patches");
cJSON *writes = cJSON_AddArrayToObject(out, "writes");
if (cJSON_IsArray(memory)) {
int wi = 0;
cJSON *m = NULL;
cJSON_ArrayForEach(m, memory) {
cJSON *off_j = cJSON_GetObjectItem(m, "offset");
cJSON *on_j = cJSON_GetObjectItem(m, "on");
cJSON *off_j2 = cJSON_GetObjectItem(m, "off");
cJSON *exp_j = cJSON_GetObjectItem(m, "expected");
cJSON *abs_j = cJSON_GetObjectItem(m, "absolute");
cJSON *we = cJSON_CreateObject();
cJSON_AddNumberToObject(we, "index", wi);
if (!cJSON_IsString(off_j) || !cJSON_IsString(on_j) || !cJSON_IsString(off_j2)) {
cJSON_AddStringToObject(we, "error", "missing offset/on/off");
cJSON_AddItemToArray(writes, we);
wi++;
continue;
}
uint8_t on_b[128], off_b[128], exp_b[128], cur_b[128];
size_t on_len = 0, off_len = 0, exp_len = 0;
uint64_t off_u = 0;
if (parse_hex_bytes_checked(on_j->valuestring, on_b, sizeof(on_b), &on_len) != 0 ||
parse_hex_bytes_checked(off_j2->valuestring, off_b, sizeof(off_b), &off_len) != 0 ||
parse_offset_hex_checked(off_j->valuestring, &off_u) != 0 ||
on_len == 0 || on_len != off_len) {
cJSON_AddStringToObject(we, "error", "hex parse failed or on/off size mismatch");
cJSON_AddItemToArray(writes, we);
wi++;
continue;
}
if (cJSON_IsString(exp_j) && exp_j->valuestring && exp_j->valuestring[0]) {
parse_hex_bytes_checked(exp_j->valuestring, exp_b, sizeof(exp_b), &exp_len);
}
cJSON_AddStringToObject(we, "rawOffset", off_j->valuestring);
cJSON_AddStringToObject(we, "onBytes", on_j->valuestring);
cJSON_AddStringToObject(we, "offBytes", off_j2->valuestring);
cJSON_AddNumberToObject(we, "len", (double)on_len);
int af = 0, inj = 0, adet = auto_detect;
get_cheat_addr_flags(cheat_kind, cJSON_IsTrue(abs_j) ? 1 : 0, auto_detect, &af, &inj, &adet);
/* PS2 emulation: all addresses absolute (matches apply_cheat_json). */
if (is_ps2_dr) { af = 1; inj = 0; adet = 0; }
int exp_rel_dr = (exp_len > 0) || (cheat_kind == 1 && off_len > 0);
const uint8_t *expect_cmp = exp_rel_dr ? (exp_len > 0 ? exp_b : off_b) : NULL;
intptr_t addr = (pid > 0 && attached) ?
cheat_resolve_write_addr_ex(pid, mod_base_dr, off_u, af, inj, adet,
on_b, on_len, expect_cmp, exp_rel_dr,
CR_ADDR_FALLBACK_LEGACY, NULL, 1) :
(af ? (intptr_t)off_u : mod_base_dr + (intptr_t)off_u);
char addrh[32]; snprintf(addrh, sizeof(addrh), "0x%lx", (long)addr);
cJSON_AddStringToObject(we, "addr", addrh);
intptr_t page = (intptr_t)ROUND_PG_DOWN((uintptr_t)addr);
size_t span = (size_t)(ROUND_PG_UP((uintptr_t)addr + on_len) - (uintptr_t)page);
char pageh[32]; snprintf(pageh, sizeof(pageh), "0x%lx", (long)page);
cJSON_AddStringToObject(we, "page", pageh);
snprintf(pageh, sizeof(pageh), "0x%zx", span);
cJSON_AddStringToObject(we, "span", pageh);
if (attached && pid > 0) {
int prot = kernel_get_vmem_protection(pid, page, span);
cJSON_AddNumberToObject(we, "vmProt", (double)prot);
int rdok = (read_process_memory(pid, addr, cur_b, on_len) == 0);
cJSON_AddBoolToObject(we, "readOk", rdok);
if (rdok) {
char cur_hex[49] = {0};
fmt_hex16(cur_b, on_len, cur_hex, sizeof(cur_hex));
cJSON_AddStringToObject(we, "currentBytes", cur_hex);
int is_on = (memcmp(cur_b, on_b, on_len) == 0);
int is_off = (exp_len > 0) ? (memcmp(cur_b, exp_b, on_len) == 0) :
(memcmp(cur_b, off_b, on_len) == 0);
cJSON_AddBoolToObject(we, "currentIsOn", is_on);
cJSON_AddBoolToObject(we, "currentIsOff", is_off);
}
} else {
cJSON_AddBoolToObject(we, "readOk", 0);
cJSON_AddStringToObject(we, "currentBytes", "");
}
cJSON_AddItemToArray(writes, we);
wi++;
}
}
cJSON_Delete(root);
char *body = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (body) {
http_send_json(fd, 200, body);
free(body);
} else {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"json encode failed\"}");
}
}
void
handle_api_cheats_toggle(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
char idx_s[16] = {0};
char on_s[16] = {0};
char body[512];
char err[256] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id) ||
query_value(query, "index", idx_s, sizeof(idx_s)) != 0 || query_value(query, "on", on_s, sizeof(on_s)) != 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing args\"}");
return;
}
int idx = atoi(idx_s);
int on = strcmp(on_s, "0") != 0;
int force = 0;
char force_s[8] = {0};
if (query_value(query, "force", force_s, sizeof(force_s)) == 0 && atoi(force_s) != 0) {
force = 1;
}
if (idx < 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad index\"}");
return;
}
int allow_force = 0;
int allow_unsafe_mc4 = 0;
int allow_legacy_mc4 = 0;
int allow_legacy_shn = 0;
pthread_mutex_lock(&g_cfg_lock);
allow_force = g_cfg.allow_force_enable;
allow_unsafe_mc4 = g_cfg.allow_unsafe_mc4_apply;
allow_legacy_mc4 = g_cfg.allow_legacy_mc4_without_expected;
allow_legacy_shn = g_cfg.allow_legacy_shn_without_expected;
pthread_mutex_unlock(&g_cfg_lock);
if (on && !(force && allow_force)) {
char reason[160] = {0};
cheat_state_kind_t st = check_mod_enable_state(title_id, idx, reason, sizeof(reason));
int block = 0;
/* Allow BASELINE_UNKNOWN if unsafe is on, or if legacy fallback is permitted
* (apply_cheat_json will enforce the actual per-format policy). */
if (st == CHEAT_STATE_BASELINE_UNKNOWN &&
!allow_unsafe_mc4 && !allow_legacy_mc4 && !allow_legacy_shn) {
block = 1;
} else if (st == CHEAT_STATE_CRASH_SUSPECT) {
block = 1;
} else if (st == CHEAT_STATE_ADDRESS_UNRESOLVED || st == CHEAT_STATE_FORMAT_NEEDS_REVIEW ||
st == CHEAT_STATE_MISMATCH || st == CHEAT_STATE_GAME_NOT_RUNNING ||
st == CHEAT_STATE_PROCESS_NOT_FOUND || st == CHEAT_STATE_BASE_NOT_READY ||
st == CHEAT_STATE_READ_FAILED || st == CHEAT_STATE_INVALID_CHEAT ||
st == CHEAT_STATE_GAME_LOADING) {
block = 1;
}
if (block) {
char *esc = json_escape(reason[0] ? reason : "refusing to patch");
snprintf(body, sizeof(body), "{\"ok\":false,\"error\":\"%s\",\"message\":\"%s\"}",
st == CHEAT_STATE_MISMATCH ? "version_mismatch" : cheat_state_key(st), esc ? esc : "refusing to patch");
free(esc);
cr_log("warn", "cheats", "refusing enable mod=%d title=%s reason=%s", idx, title_id, reason);
http_send_json(fd, 400, body);
return;
}
}
int rc = apply_cheat_json(title_id, idx, on, err, sizeof(err));
if (rc == -2) {
http_send_json(fd, 429,
"{\"ok\":false,\"error\":\"apply_in_progress\","
"\"message\":\"Another cheat is being applied or monitored. Wait for the safety window to finish.\"}");
return;
}
if (rc != 0) {
char *esc = json_escape(err[0] ? err : "apply failed");
char code[64];
running_game_state_t st;
running_state_get(&st);
classify_cheat_error_code(err, code, sizeof(code));
if (!strcmp(code, "app_not_stable")) {
char *msg_esc = json_escape("Game is not stable enough for patching yet. Wait a few seconds and try again.");
snprintf(body, sizeof(body),
"{\"ok\":false,\"error\":\"app_not_stable\",\"message\":\"%s\"}",
msg_esc ? msg_esc : "Game not stable");
free(msg_esc);
free(esc);
http_send_json(fd, 400, body);
return;
}
snprintf(body, sizeof(body), "{\"ok\":false,\"error\":\"%s\"}", esc ? esc : "apply failed");
free(esc);
notification_add("cheat_error", "Cheat apply failed: %s idx=%d (%s)", title_id, idx, err);
cr_log("error", "cheats", "apply failed %s idx=%d: %s", title_id, idx, err);
http_send_json(fd, 400, body);
return;
}
snprintf(body, sizeof(body), "{\"ok\":true,\"enabled\":%s}", on ? "true" : "false");
http_send_json(fd, 200, body);
}
static int
cheats_delete_from_dir(const char *dir, const char *title_id) {
int removed = 0;
DIR *d = opendir(dir);
if (!d) return 0;
struct dirent *ent = NULL;
while ((ent = readdir(d)) != NULL) {
const char *name = ent->d_name;
if (name[0] == '.' || !is_safe_filename(name) || !recognised_cheat_extension(name)) continue;
char id[10];
if (!extract_title_id_prefix(name, id, sizeof(id))) continue;
if (strcmp(id, title_id) != 0) continue;
char path[768];
snprintf(path, sizeof(path), "%s/%s", dir, name);
if (unlink(path) == 0 || errno == ENOENT) removed++;
}
closedir(d);
return removed;
}
void
handle_api_cheats_delete(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
int removed = 0;
/* Delete from top-level and all format subdirectories */
removed += cheats_delete_from_dir(CHEATRUNNER_CHEATS_DIR, title_id);
removed += cheats_delete_from_dir(CHEATRUNNER_CHEATS_JSON_DIR, title_id);
removed += cheats_delete_from_dir(CHEATRUNNER_CHEATS_SHN_DIR, title_id);
removed += cheats_delete_from_dir(CHEATRUNNER_CHEATS_MC4_DIR, title_id);
char body[128];
snprintf(body, sizeof(body), "{\"ok\":true,\"removed\":%d}", removed);
http_send_json(fd, 200, body);
}
void
handle_api_cheats_engine(int fd) {
char body[64];
pthread_mutex_lock(&g_cfg_lock);
snprintf(body, sizeof(body), "{\"ok\":true,\"enabled\":%s}", g_cfg.cheat_engine ? "true" : "false");
pthread_mutex_unlock(&g_cfg_lock);
http_send_json(fd, 200, body);
}
void
handle_api_cheats_engine_toggle(int fd, const char *query) {
char on_s[16] = {0};
int on = 0;
if (query_value(query, "on", on_s, sizeof(on_s)) != 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing on\"}");
return;
}
on = atoi(on_s) ? 1 : 0;
pthread_mutex_lock(&g_cfg_lock);
g_cfg.cheat_engine = on;
int rc = config_save_locked();
pthread_mutex_unlock(&g_cfg_lock);
if (rc != 0) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"save failed\"}");
return;
}
notification_add("cheat_engine", "Cheat engine %s", on ? "enabled" : "disabled");
cr_log("info", "cheats", "cheat engine %s", on ? "enabled" : "disabled");
http_send_json(fd, 200, on ? "{\"ok\":true,\"enabled\":true}" : "{\"ok\":true,\"enabled\":false}");
}
void
handle_api_cheats_raw(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
char path[256];
int kind = 0;
char *txt = NULL;
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
if (!find_cheat_file_for_title(title_id, path, sizeof(path), &kind)) {
http_send_json(fd, 404, "{\"ok\":false,\"error\":\"no cheat file\"}");
return;
}
if (read_file_text(path, &txt) != 0 || !txt) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"read failed\"}");
return;
}
http_send_response(fd, 200, "text/plain; charset=utf-8", (const uint8_t *)txt, strlen(txt));
free(txt);
}
void
handle_api_cheats_validate(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
char err[128] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
cJSON *root = load_cheat_json_root_for_title(title_id, err, sizeof(err));
if (!root) {
char body[256];
snprintf(body, sizeof(body), "{\"ok\":false,\"error\":\"%s\"}", err);
http_send_json(fd, 400, body);
return;
}
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (!cJSON_IsArray(mods)) {
cJSON_Delete(root);
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"mods missing\"}");
return;
}
int invalid = 0;
cJSON *m = NULL;
cJSON_ArrayForEach(m, mods) {
cJSON *mem = cJSON_GetObjectItem(m, "memory");
if (!cJSON_IsArray(mem)) {
mem = cJSON_GetObjectItem(m, "patches");
}
if (!cJSON_IsArray(mem) || cJSON_GetArraySize(mem) == 0) {
invalid++;
continue;
}
cJSON *w = NULL;
cJSON_ArrayForEach(w, mem) {
cJSON *off_j = cJSON_GetObjectItem(w, "offset");
cJSON *on_j = cJSON_GetObjectItem(w, "on");
cJSON *off_j2 = cJSON_GetObjectItem(w, "off");
if (!cJSON_IsString(off_j) || !cJSON_IsString(on_j) || !cJSON_IsString(off_j2)) {
invalid++;
continue;
}
uint8_t b1[128], b2[128];
size_t l1 = 0, l2 = 0;
uint64_t off_u = 0;
if (parse_offset_hex_checked(off_j->valuestring, &off_u) != 0 ||
parse_hex_bytes_checked(on_j->valuestring, b1, sizeof(b1), &l1) != 0 ||
parse_hex_bytes_checked(off_j2->valuestring, b2, sizeof(b2), &l2) != 0 || l1 != l2) {
invalid++;
}
}
}
cJSON_Delete(root);
char body[256];
snprintf(body, sizeof(body), "{\"ok\":%s,\"titleId\":\"%s\",\"invalidCount\":%d}", invalid ? "false" : "true", title_id, invalid);
http_send_json(fd, invalid ? 400 : 200, body);
}
void
handle_api_cheats_upload(int fd, const char *query, const char *body, size_t body_len) {
char filename[128] = {0};
if (query_value(query, "filename", filename, sizeof(filename)) != 0 || !filename[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing filename\"}");
return;
}
if (!is_safe_filename(filename)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"unsafe filename\"}");
return;
}
const char *ext = strrchr(filename, '.');
if (!ext) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"no extension\"}");
return;
}
const char *dir = NULL;
size_t max_size = 0;
if (strcasecmp(ext, ".json") == 0) {
dir = CHEATRUNNER_CHEATS_JSON_DIR;
max_size = 2 * 1024 * 1024;
} else if (strcasecmp(ext, ".shn") == 0) {
dir = CHEATRUNNER_CHEATS_SHN_DIR;
max_size = 1024 * 1024;
} else if (strcasecmp(ext, ".mc4") == 0) {
dir = CHEATRUNNER_CHEATS_MC4_DIR;
max_size = 1024 * 1024;
} else {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"unsupported extension\"}");
return;
}
if (body_len == 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"empty body\"}");
return;
}
if (body_len > max_size) {
char resp[192];
snprintf(resp, sizeof(resp),
"{\"ok\":false,\"error\":\"payload_too_large\",\"message\":\"Upload exceeds maximum allowed size for this format.\",\"maxBytes\":%u}",
(unsigned int)max_size);
http_send_json(fd, 413, resp);
return;
}
char path[512];
snprintf(path, sizeof(path), "%s/%s", dir, filename);
if (write_file_atomic(path, (const uint8_t *)body, body_len) != 0) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"write failed\"}");
return;
}
char resp[256];
snprintf(resp, sizeof(resp), "{\"ok\":true,\"filename\":\"%s\"}", filename);
http_send_json(fd, 200, resp);
}
/* GET /api/cheats/address-debug: shows which of absolute/relative candidate addresses matches on/off/expected; needs game running. */
void
handle_api_cheats_address_debug(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
char cheat_path[256] = {0};
char err[128] = {0};
int cheat_kind = 0;
cJSON *root = load_cheat_json_root_for_title_ex(title_id, err, sizeof(err), cheat_path, sizeof(cheat_path), &cheat_kind);
if (!root) {
char body[256];
snprintf(body, sizeof(body), "{\"ok\":false,\"error\":\"%s\"}", err);
http_send_json(fd, 404, body);
return;
}
running_game_state_t st;
int running_ok = (read_running_state(&st) == 0 && st.running);
int title_match = running_ok && strcmp(st.title_id, title_id) == 0;
pid_t pid = running_ok ? st.pid : -1;
intptr_t base = running_ok ? st.image_base : 0;
int auto_detect = 1;
char mc4_fb_adbg[16] = "legacy";
char shn_fb_adbg[16] = "legacy";
pthread_mutex_lock(&g_cfg_lock);
auto_detect = g_cfg.cheat_address_auto_detect;
snprintf(mc4_fb_adbg, sizeof(mc4_fb_adbg), "%s", g_cfg.cheat_mc4_unverified_fallback);
snprintf(shn_fb_adbg, sizeof(shn_fb_adbg), "%s", g_cfg.cheat_shn_unverified_fallback);
pthread_mutex_unlock(&g_cfg_lock);
/* Probing reads are mdbg-backed and need no ptrace attach. */
int attach_ok = 0;
if (title_match && pid > 0 && base != 0) {
attach_ok = (kill(pid, 0) == 0 || errno != ESRCH);
}
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (!cJSON_IsArray(mods)) { mods = cJSON_GetObjectItem(root, "patches"); }
cJSON *out = cJSON_CreateObject();
cJSON_AddBoolToObject(out, "ok", 1);
cJSON_AddStringToObject(out, "titleId", title_id);
cJSON_AddStringToObject(out, "cheatPath", cheat_path);
const char *fmt_s = cheat_kind == 1 ? "json" : (cheat_kind == 2 ? "shn" : "mc4");
cJSON_AddStringToObject(out, "cheatFormat", fmt_s);
{
cJSON *g = cJSON_AddObjectToObject(out, "game");
if (g) {
cJSON_AddBoolToObject(g, "running", running_ok);
cJSON_AddBoolToObject(g, "titleMatch", title_match);
cJSON_AddBoolToObject(g, "attachOk", attach_ok);
char bh[32]; snprintf(bh, sizeof(bh), "0x%lx", (long)base);
cJSON_AddStringToObject(g, "base", bh);
cJSON_AddNumberToObject(g, "pid", (double)(running_ok ? pid : -1));
}
}
cJSON_AddBoolToObject(out, "autoDetect", auto_detect);
cJSON *mod_arr = cJSON_AddArrayToObject(out, "mods");
if (cJSON_IsArray(mods)) {
cJSON *mod = NULL;
cJSON_ArrayForEach(mod, mods) {
cJSON *mod_obj = cJSON_CreateObject();
cJSON *n = cJSON_GetObjectItem(mod, "name");
cJSON_AddStringToObject(mod_obj, "name", cJSON_IsString(n) ? n->valuestring : "");
cJSON *mem = cJSON_GetObjectItem(mod, "memory");
if (!cJSON_IsArray(mem)) mem = cJSON_GetObjectItem(mod, "patches");
cJSON *patch_arr = cJSON_AddArrayToObject(mod_obj, "patches");
if (cJSON_IsArray(mem)) {
int wi = 0;
cJSON *m = NULL;
cJSON_ArrayForEach(m, mem) {
cJSON *off_j = cJSON_GetObjectItem(m, "offset");
cJSON *on_j = cJSON_GetObjectItem(m, "on");
cJSON *off_j2= cJSON_GetObjectItem(m, "off");
cJSON *exp_j = cJSON_GetObjectItem(m, "expected");
cJSON *abs_j = cJSON_GetObjectItem(m, "absolute");
cJSON *pe = cJSON_CreateObject();
cJSON_AddNumberToObject(pe, "index", wi++);
if (!cJSON_IsString(off_j) || !cJSON_IsString(on_j) || !cJSON_IsString(off_j2)) {
cJSON_AddStringToObject(pe, "error", "missing offset/on/off");
cJSON_AddItemToArray(patch_arr, pe);
continue;
}
uint8_t on_b[128], off_b[128], exp_b[128];
size_t on_len = 0, off_len = 0, exp_len = 0;
uint64_t off_u = 0;
if (parse_hex_bytes_checked(on_j->valuestring, on_b, sizeof(on_b), &on_len) != 0 ||
parse_hex_bytes_checked(off_j2->valuestring, off_b, sizeof(off_b), &off_len) != 0 ||
on_len != off_len || on_len == 0) {
cJSON_AddStringToObject(pe, "error", "invalid on/off bytes");
cJSON_AddItemToArray(patch_arr, pe);
continue;
}
if (parse_offset_hex_checked(off_j->valuestring, &off_u) != 0) {
cJSON_AddStringToObject(pe, "error", "invalid offset");
cJSON_AddItemToArray(patch_arr, pe);
continue;
}
if (cJSON_IsString(exp_j) && exp_j->valuestring && exp_j->valuestring[0]) {
parse_hex_bytes_checked(exp_j->valuestring, exp_b, sizeof(exp_b), &exp_len);
}
char off_hex[32]; snprintf(off_hex, sizeof(off_hex), "0x%llX", (unsigned long long)off_u);
cJSON_AddStringToObject(pe, "offset", off_hex);
cJSON_AddBoolToObject(pe, "absoluteFlag", cJSON_IsTrue(abs_j));
intptr_t abs_cand = (intptr_t)off_u;
intptr_t rel_cand = base + (intptr_t)off_u;
int exp_rel_adbg = (exp_len > 0) || (cheat_kind == 1 && off_len > 0);
const uint8_t *expect_cmp = exp_rel_adbg ? (exp_len > 0 ? exp_b : off_b) : NULL;
const char *fb_str_adbg = (cheat_kind == 2) ? shn_fb_adbg : mc4_fb_adbg;
cr_addr_fallback_policy_t fb_pol_adbg = CR_ADDR_FALLBACK_LEGACY;
if (!exp_rel_adbg) {
if (strcmp(fb_str_adbg, "absolute") == 0) fb_pol_adbg = CR_ADDR_FALLBACK_ABSOLUTE;
else if (strcmp(fb_str_adbg, "relative") == 0) fb_pol_adbg = CR_ADDR_FALLBACK_RELATIVE;
else if (strcmp(fb_str_adbg, "block") == 0) fb_pol_adbg = CR_ADDR_FALLBACK_BLOCK;
}
cr_addr_resolve_status_t resolve_st_adbg = CR_ADDR_RESOLVE_OK_VERIFIED;
int af_adbg = 0, inj_adbg = 0, adet_adbg = auto_detect;
get_cheat_addr_flags(cheat_kind, cJSON_IsTrue(abs_j) ? 1 : 0, auto_detect,
&af_adbg, &inj_adbg, &adet_adbg);
intptr_t resolved = cheat_resolve_write_addr_ex(pid, base, off_u,
af_adbg, inj_adbg, adet_adbg,
on_b, on_len, expect_cmp, exp_rel_adbg,
fb_pol_adbg, &resolve_st_adbg, 1);
char res_hex[32]; snprintf(res_hex, sizeof(res_hex), "0x%lx", (long)resolved);
cJSON_AddStringToObject(pe, "resolvedAddress", res_hex);
cJSON_AddStringToObject(pe, "resolvedMode",
resolved == abs_cand ? "absolute" : "relative");
cJSON_AddNumberToObject(pe, "resolvedStatus", (double)resolve_st_adbg);
cJSON_AddBoolToObject(pe, "expectedReliable", exp_rel_adbg);
cJSON_AddStringToObject(pe, "fallbackPolicy", fb_str_adbg);
cJSON *cands_j = cJSON_AddArrayToObject(pe, "candidates");
/* absolute candidate */
{
cJSON *ca = cJSON_CreateObject();
char ah[32]; snprintf(ah, sizeof(ah), "0x%lx", (long)abs_cand);
cJSON_AddStringToObject(ca, "mode", "absolute");
cJSON_AddStringToObject(ca, "address", ah);
if (attach_ok && on_len <= 128) {
uint8_t buf[128];
int rd = read_process_memory(pid, abs_cand, buf, on_len);
cJSON_AddBoolToObject(ca, "readOk", rd == 0);
if (rd == 0) {
char hx[512]; bytes_to_hex(buf, on_len, hx, sizeof(hx));
cJSON_AddStringToObject(ca, "current", hx);
cJSON_AddBoolToObject(ca, "matchOn", memcmp(buf, on_b, on_len) == 0);
cJSON_AddBoolToObject(ca, "matchOff",
expect_cmp ? memcmp(buf, expect_cmp, on_len) == 0 : 0);
}
} else {
cJSON_AddBoolToObject(ca, "readOk", 0);
}
cJSON_AddItemToArray(cands_j, ca);
}
/* relative candidate */
{
cJSON *cr2 = cJSON_CreateObject();
char rh[32]; snprintf(rh, sizeof(rh), "0x%lx", (long)rel_cand);
cJSON_AddStringToObject(cr2, "mode", "relative");
cJSON_AddStringToObject(cr2, "address", rh);
if (attach_ok && on_len <= 128) {
uint8_t buf[128];
int rd = read_process_memory(pid, rel_cand, buf, on_len);
cJSON_AddBoolToObject(cr2, "readOk", rd == 0);
if (rd == 0) {
char hx[512]; bytes_to_hex(buf, on_len, hx, sizeof(hx));
cJSON_AddStringToObject(cr2, "current", hx);
cJSON_AddBoolToObject(cr2, "matchOn", memcmp(buf, on_b, on_len) == 0);
cJSON_AddBoolToObject(cr2, "matchOff",
expect_cmp ? memcmp(buf, expect_cmp, on_len) == 0 : 0);
}
} else {
cJSON_AddBoolToObject(cr2, "readOk", 0);
}
cJSON_AddItemToArray(cands_j, cr2);
}
char on_hex_s[512], off_hex_s[512];
bytes_to_hex(on_b, on_len, on_hex_s, sizeof(on_hex_s));
bytes_to_hex(off_b, on_len, off_hex_s, sizeof(off_hex_s));
cJSON_AddStringToObject(pe, "on", on_hex_s);
cJSON_AddStringToObject(pe, "valueOff", off_hex_s);
if (exp_len > 0) {
char exp_hex_s[512]; bytes_to_hex(exp_b, on_len, exp_hex_s, sizeof(exp_hex_s));
cJSON_AddStringToObject(pe, "expected", exp_hex_s);
} else {
cJSON_AddNullToObject(pe, "expected");
}
cJSON_AddNumberToObject(pe, "byteLen", (double)on_len);
cJSON_AddItemToArray(patch_arr, pe);
}
}
cJSON_AddItemToArray(mod_arr, mod_obj);
}
}
cJSON_Delete(root);
char *s = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!s) { http_send_oom(fd); return; }
http_send_json(fd, 200, s);
free(s);
}
void
handle_api_cheats_sources(int fd) {
source_config_model_t model;
source_model_load(&model);
int sources_enabled = 1;
cfg_get_cheat_remote_opts(&sources_enabled, NULL, NULL, NULL);
cJSON *out = cJSON_CreateObject();
cJSON_AddBoolToObject(out, "ok", 1);
cJSON *arr = cJSON_AddArrayToObject(out, "sources");
for (int i = 0; i < model.cheat_count; i++) {
cJSON *s = cJSON_CreateObject();
cJSON_AddStringToObject(s, "id", model.cheat_sources[i].id);
cJSON_AddStringToObject(s, "name", model.cheat_sources[i].name);
cJSON_AddStringToObject(s, "type", model.cheat_sources[i].type);
cJSON_AddBoolToObject(s, "enabled", sources_enabled && model.cheat_sources[i].enabled);
cJSON_AddItemToArray(arr, s);
}
char *txt = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!txt) {
http_send_oom(fd);
return;
}
http_send_json(fd, 200, txt);
free(txt);
}
/* ---- /api/sources/jobs/start ---- */
void
handle_api_sources_jobs_start(int fd, const char *body_json) {
cJSON *req = NULL;
if (parse_json_from_body(body_json, &req) != 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid_json\"}");
return;
}
int job_id = 0;
int rc = cr_source_job_start(req, &job_id);
cJSON_Delete(req);
if (rc == -2) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"unknown_type\"}");
return;
}
if (rc == -3) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid_titleId\"}");
return;
}
if (rc == -4) {
http_send_json(fd, 503, "{\"ok\":false,\"error\":\"job_queue_full\",\"message\":\"Too many concurrent remote operations. Try again.\"}");
return;
}
if (rc != 0) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"job_start_failed\"}");
return;
}
char resp[64];
snprintf(resp, sizeof(resp), "{\"ok\":true,\"jobId\":%d}", job_id);
http_send_json(fd, 202, resp);
}
/* ---- /api/sources/jobs/status ---- */
void
handle_api_sources_jobs_status(int fd, const char *query) {
char id_str[32] = {0};
if (query_value(query, "id", id_str, sizeof(id_str)) != 0 || !id_str[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing_id\"}");
return;
}
int job_id = atoi(id_str);
if (job_id <= 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid_id\"}");
return;
}
cJSON *out = cr_source_job_status_json(job_id);
if (!out) {
http_send_oom(fd);
return;
}
int status = cJSON_GetObjectItem(out, "notFound") ? 404 : 200;
char *txt = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!txt) {
http_send_oom(fd);
return;
}
http_send_json(fd, status, txt);
free(txt);
}
/* ---- Legacy stubs (replaced by /api/sources/jobs/start) ---- */
void
handle_api_cheats_remote_find(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
char version[64] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 ||
!title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid_titleId\"}");
return;
}
query_value(query, "version", version, sizeof(version));
(void)title_id; (void)version;
http_send_json(fd, 409, "{\"ok\":false,\"error\":\"use_jobs\",\"message\":\"Use POST /api/sources/jobs/start instead.\"}");
}
void
handle_api_cheats_remote_download(int fd, const char *body_json) {
(void)body_json;
http_send_json(fd, 409, "{\"ok\":false,\"error\":\"use_jobs\",\"message\":\"Use POST /api/sources/jobs/start instead.\"}");
}
void
handle_api_cheats_find(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 ||
!title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid titleId\"}");
return;
}
cheat_file_search_t ctx;
int found_local = find_cheat_candidates(title_id, &ctx);
char cands_buf[2048] = "[";
int cpos = 1;
for (int ci = 0; ci < ctx.candidate_count && ci < MAX_CHEAT_CANDIDATES; ci++) {
const char *cf = ctx.candidates[ci].kind == 1 ? "json" : (ctx.candidates[ci].kind == 2 ? "shn" : "mc4");
const char *cb = strrchr(ctx.candidates[ci].path, '/');
cb = cb ? cb + 1 : ctx.candidates[ci].path;
char entry[512];
int en = snprintf(entry, sizeof(entry), "%s{\"format\":\"%s\",\"filename\":\"%s\",\"path\":\"%s\",\"score\":%d}",
ci > 0 ? "," : "", cf, cb, ctx.candidates[ci].path, ctx.candidates[ci].score);
if (en > 0 && cpos + en + 2 < (int)sizeof(cands_buf)) {
memcpy(cands_buf + cpos, entry, en);
cpos += en;
}
}
cands_buf[cpos++] = ']';
cands_buf[cpos] = '\0';
if (found_local) {
const char *sel_fmt = ctx.best_kind == 1 ? "json" : (ctx.best_kind == 2 ? "shn" : "mc4");
const char *sel_base = strrchr(ctx.best_path, '/');
sel_base = sel_base ? sel_base + 1 : ctx.best_path;
char body[4096];
snprintf(body, sizeof(body),
"{\"ok\":true,\"local\":true,\"source\":\"local\",\"titleId\":\"%s\","
"\"format\":\"%s\",\"filename\":\"%s\",\"path\":\"%s\","
"\"selectedReason\":\"highest score local file\","
"\"candidateCount\":%d,\"candidates\":%s}",
title_id, sel_fmt, sel_base, ctx.best_path,
ctx.candidate_count, cands_buf);
http_send_json(fd, 200, body);
return;
}
char body[512];
snprintf(body, sizeof(body),
"{\"ok\":false,\"error\":\"no_local_cheat_found\","
"\"message\":\"No local cheat file found in /data/cheatrunner/cheats.\","
"\"candidateCount\":%d,\"candidates\":%s}",
ctx.candidate_count, cands_buf);
http_send_json(fd, 404, body);
}
void
handle_api_cheats_download(int fd, const char *query) {
(void)query;
http_send_json(fd, 410,
"{\"ok\":false,\"error\":\"remote_download_removed\","
"\"message\":\"Remote cheat download was removed. Use local cheat files in /data/cheatrunner/cheats.\"}");
}
/* Scan a single subdirectory of CHEATRUNNER_CHEATS_DIR and append JSON file objects. */
static void
cheats_list_scan_subdir(const char *fmt_name, int kind, cJSON *arr) {
char dir[512];
snprintf(dir, sizeof(dir), "%s/%s", CHEATRUNNER_CHEATS_DIR, fmt_name);
DIR *d = opendir(dir);
if (!d) return;
struct dirent *ent = NULL;
while ((ent = readdir(d)) != NULL) {
const char *name = ent->d_name;
if (name[0] == '.') continue;
if (!is_safe_filename(name) || !recognised_cheat_extension(name)) continue;
char full[768];
snprintf(full, sizeof(full), "%s/%s", dir, name);
struct stat st;
if (stat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue;
cJSON *e = cJSON_CreateObject();
if (!e) continue;
char rel[512];
snprintf(rel, sizeof(rel), "%s/%s", fmt_name, name);
char title_id[10] = {0};
extract_title_id_prefix(name, title_id, sizeof(title_id));
cJSON_AddStringToObject(e, "path", rel);
cJSON_AddStringToObject(e, "format", fmt_name);
cJSON_AddNumberToObject(e, "kind", kind);
cJSON_AddStringToObject(e, "titleId", title_id);
cJSON_AddItemToArray(arr, e);
}
closedir(d);
}
void
handle_api_cheats_list(int fd) {
cJSON *out = cJSON_CreateObject();
if (!out) { http_send_oom(fd); return; }
cJSON_AddBoolToObject(out, "ok", 1);
cJSON *arr = cJSON_AddArrayToObject(out, "files");
/* Scan recognised format subdirectories */
cheats_list_scan_subdir("json", 1, arr);
cheats_list_scan_subdir("shn", 2, arr);
cheats_list_scan_subdir("mc4", 3, arr);
/* Also scan the top-level cheats dir for files placed directly there */
DIR *d = opendir(CHEATRUNNER_CHEATS_DIR);
if (d) {
struct dirent *ent = NULL;
while ((ent = readdir(d)) != NULL) {
const char *name = ent->d_name;
if (name[0] == '.') continue;
if (!is_safe_filename(name) || !recognised_cheat_extension(name)) continue;
char full[768];
snprintf(full, sizeof(full), "%s/%s", CHEATRUNNER_CHEATS_DIR, name);
struct stat st;
if (stat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue;
cJSON *e = cJSON_CreateObject();
if (!e) continue;
char title_id[10] = {0};
extract_title_id_prefix(name, title_id, sizeof(title_id));
/* Determine format from extension */
const char *fmt = "json";
int kind = 1;
size_t nl = strlen(name);
if (nl > 4 && strcasecmp(name + nl - 4, ".mc4") == 0) { fmt = "mc4"; kind = 3; }
else if (nl > 4 && strcasecmp(name + nl - 4, ".shn") == 0) { fmt = "shn"; kind = 2; }
cJSON_AddStringToObject(e, "path", name);
cJSON_AddStringToObject(e, "format", fmt);
cJSON_AddNumberToObject(e, "kind", kind);
cJSON_AddStringToObject(e, "titleId", title_id);
cJSON_AddItemToArray(arr, e);
}
closedir(d);
}
char *txt = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!txt) { http_send_oom(fd); return; }
http_send_json(fd, 200, txt);
free(txt);
}
/* POST /api/cheats/repo/download?source=<name|all>[&overwrite=1] */
void
handle_api_cheats_repo_download(int fd, const char *query) {
char source[32] = {0};
char ovr_s[4] = {0};
query_value(query, "source", source, sizeof(source));
query_value(query, "overwrite", ovr_s, sizeof(ovr_s));
int overwrite = (ovr_s[0] == '1') ? 1 : 0;
if (!source[0] || (strcmp(source, "hencollection") != 0 &&
strcmp(source, "ps5cheats") != 0 &&
strcmp(source, "goldhen") != 0 &&
strcmp(source, "henppsa") != 0 &&
strcmp(source, "all") != 0)) {
http_send_json(fd, 400,
"{\"ok\":false,\"error\":\"invalid_source\","
"\"message\":\"source must be hencollection, ps5cheats, goldhen, henppsa, or all\"}");
return;
}
if (repo_mirror_start(source, overwrite) != 0) {
char buf[256];
repo_mirror_status_json(buf, sizeof(buf));
http_send_json(fd, 409, buf);
return;
}
char buf[512];
snprintf(buf, sizeof(buf),
"{\"ok\":true,\"state\":\"running\",\"source\":\"%s\",\"overwrite\":%d}", source, overwrite);
http_send_json(fd, 200, buf);
}
/* GET /api/cheats/repo/download/status */
void
handle_api_cheats_repo_download_status(int fd) {
char buf[2048];
repo_mirror_status_json(buf, sizeof(buf));
http_send_json(fd, 200, buf);
}
/* Legacy /api/cheats/download-all — kept for backward compat, starts a mirror of all sources */
void
handle_api_cheats_download_all(int fd) {
if (repo_mirror_start("all", 0) == 0) {
http_send_json(fd, 200, "{\"ok\":true,\"state\":\"running\",\"source\":\"all\",\"overwrite\":0}");
} else {
/* Already running or other conflict — return current status */
handle_api_cheats_repo_download_status(fd);
}
}
void
handle_api_cheats_download_all_status(int fd) {
handle_api_cheats_repo_download_status(fd);
}
/* ── /api/dev/privileges ────────────────────────────────────────── */
void
handle_api_dev_privileges(int fd) {
const cr_priv_status_t *ps = cr_priv_get();
cJSON *out = cJSON_CreateObject();
if (!out) { http_send_oom(fd); return; }
cJSON_AddBoolToObject(out, "ok", ps->can_patch_game ? 1 : 0);
cJSON_AddNumberToObject(out, "uid", ps->uid);
cJSON_AddNumberToObject(out, "gid", ps->gid);
cJSON_AddNumberToObject(out, "euid", ps->euid);
cJSON_AddNumberToObject(out, "egid", ps->egid);
cJSON_AddBoolToObject(out, "uidOk", ps->uid_ok);
cJSON_AddBoolToObject(out, "sandboxEscaped", ps->sandbox_ok);
cJSON_AddBoolToObject(out, "rootDirOk", ps->rootvnode_ok);
cJSON_AddBoolToObject(out, "sceAuthOk", ps->authid_ok);
cJSON_AddBoolToObject(out, "sceCapsOk", ps->caps_ok);
cJSON_AddBoolToObject(out, "sceAttrsOk", ps->attrs_ok);
cJSON_AddBoolToObject(out, "kernelRwOk", ps->kernel_rw_ok);
cJSON_AddBoolToObject(out, "kernelMprotectOk", ps->kernel_mprotect_ok);
cJSON_AddBoolToObject(out, "gameMemoryPatchOk", ps->can_patch_game);
cJSON_AddBoolToObject(out, "canPatchGameMemory", ps->can_patch_game);
char authid_hex[24];
snprintf(authid_hex, sizeof(authid_hex), "0x%llx", (unsigned long long)ps->authid);
cJSON_AddStringToObject(out, "authId", authid_hex);
cJSON *warn_arr = cJSON_AddArrayToObject(out, "warnings");
if (warn_arr) {
for (int i = 0; i < ps->n_warnings; i++) {
cJSON *ws = cJSON_CreateString(ps->warnings[i]);
if (ws) cJSON_AddItemToArray(warn_arr, ws);
}
}
char *s = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!s) { http_send_oom(fd); return; }
http_send_json(fd, 200, s);
free(s);
}
/* ── /api/dev/diag ──────────────────────────────────────────────── */
void
handle_api_dev_diag(int fd) {
cJSON *out = cJSON_CreateObject();
if (!out) { http_send_oom(fd); return; }
cJSON_AddBoolToObject(out, "ok", 1);
char *s = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!s) { http_send_oom(fd); return; }
http_send_json(fd, 200, s);
free(s);
}
void
handle_api_dev_open_browser(int fd, const char *method, const char *query) {
if (strcmp(method, "POST") != 0) {
http_send_json(fd, 405, "{\"ok\":false,\"error\":\"method_not_allowed\",\"message\":\"Use POST.\"}");
return;
}
int dev_enabled = 0;
pthread_mutex_lock(&g_cfg_lock);
dev_enabled = g_cfg.dev_reload_enabled;
pthread_mutex_unlock(&g_cfg_lock);
if (!dev_enabled) {
http_send_json(fd, 403, "{\"ok\":false,\"error\":\"dev_reload_disabled\"}");
return;
}
char raw_title_id[32] = {0};
char title_id[10] = {0};
query_value(query, "titleId", raw_title_id, sizeof(raw_title_id));
if (raw_title_id[0] && !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid_titleId\"}");
return;
}
if (!title_id[0]) {
running_game_state_t st;
memset(&st, 0, sizeof(st));
if (read_running_state(&st) == 0 && st.running && st.title_id[0]) {
snprintf(title_id, sizeof(title_id), "%s", st.title_id);
}
}
char url[192];
if (title_id[0]) {
snprintf(url, sizeof(url), "http://127.0.0.1:9999/#trainer=%s", title_id);
} else {
snprintf(url, sizeof(url), "http://127.0.0.1:9999");
}
#if CHEATRUNNER_HAVE_BROWSER_OPEN
int rc = cr_browser_open_url(url);
char body[512];
snprintf(body, sizeof(body), "{\"ok\":true,\"url\":\"%s\",\"titleId\":\"%s\",\"rc\":%d}",
url, title_id, rc);
http_send_json(fd, 200, body);
#else
(void)url;
(void)title_id;
http_send_json(fd, 501, "{\"ok\":false,\"error\":\"browser_open_not_compiled\",\"message\":\"Browser test endpoint is unavailable in this build.\"}");
#endif
}
/* ── /api/dev/memtest ───────────────────────────────────────────── */
void
handle_api_dev_memtest(int fd) {
cJSON *out = cJSON_CreateObject();
if (!out) { http_send_oom(fd); return; }
cJSON_AddBoolToObject(out, "ok", 1);
/* Level 1: self-process buffer test */
static const uint8_t PATTERN[8] = {0xDE,0xAD,0xBE,0xEF,0xCA,0xFE,0xBA,0xBE};
uint8_t *buf = (uint8_t *)mmap(NULL, 0x4000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int self_ok = 0;
char self_err[64] = "mmap failed";
if (buf != MAP_FAILED) {
memset(buf, 0, 8);
memcpy(buf, PATTERN, sizeof(PATTERN));
if (memcmp(buf, PATTERN, sizeof(PATTERN)) == 0) {
memset(buf, 0, 8);
self_ok = 1;
self_err[0] = '\0';
} else {
snprintf(self_err, sizeof(self_err), "readback mismatch");
}
munmap(buf, 0x4000);
}
cJSON *self = cJSON_AddObjectToObject(out, "selfTest");
if (self) {
cJSON_AddBoolToObject(self, "ok", self_ok);
if (!self_ok) cJSON_AddStringToObject(self, "error", self_err);
}
/* Level 2: running game process dry test (read + mprotect probe, no writes) */
running_game_state_t rgs;
running_state_get(&rgs);
cJSON *game = cJSON_AddObjectToObject(out, "gameProcessTest");
if (game) {
if (!rgs.running || rgs.pid <= 0) {
cJSON_AddBoolToObject(game, "skipped", 1);
cJSON_AddStringToObject(game, "reason", "no game running");
} else {
cJSON_AddBoolToObject(game, "skipped", 0);
cJSON_AddNumberToObject(game, "pid", (double)rgs.pid);
char base_hex[24];
snprintf(base_hex, sizeof(base_hex), "0x%lx", (long)rgs.image_base);
cJSON_AddStringToObject(game, "imageBase", base_hex);
/* Probe read at image base — requires ptrace attach first */
uint8_t probe[8] = {0};
int pt_rc = -1;
int mprotect_rc = -1;
int attach_rc = -1;
if (rgs.image_base) {
intptr_t pg = (intptr_t)ROUND_PG_DOWN((uintptr_t)rgs.image_base);
size_t sp = (size_t)0x4000;
int orig_prot = kernel_get_vmem_protection(rgs.pid, pg, sp);
/* Read-only probe: never set W+X here, PS5's hypervisor enforces W^X and RWX can panic the kernel. */
mprotect_rc = kernel_mprotect(rgs.pid, pg, sp, PROT_READ | PROT_EXEC);
if (mprotect_rc == 0) {
attach_rc = pt_attach_timed(rgs.pid, 2000);
if (attach_rc == 0) {
pt_rc = pt_copyout(rgs.pid, rgs.image_base, probe, sizeof(probe));
pt_detach(rgs.pid, 0);
}
int rp = orig_prot >= 0 ? orig_prot : (PROT_READ | PROT_EXEC);
kernel_mprotect(rgs.pid, pg, sp, rp);
}
}
char probe_hex[28] = {0};
fmt_hex16(probe, sizeof(probe), probe_hex, sizeof(probe_hex));
cJSON_AddBoolToObject(game, "mprotectOk", mprotect_rc == 0);
cJSON_AddBoolToObject(game, "ptAttachOk", attach_rc == 0);
cJSON_AddBoolToObject(game, "ptReadOk", pt_rc >= 0);
cJSON_AddStringToObject(game, "probeBytes", probe_hex);
cJSON_AddNumberToObject(game, "mprotectRc", mprotect_rc);
cJSON_AddNumberToObject(game, "ptAttachRc", attach_rc);
cJSON_AddNumberToObject(game, "ptReadRc", pt_rc);
}
}
char *s = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!s) { http_send_oom(fd); return; }
http_send_json(fd, 200, s);
free(s);
}
void
handle_api_dev_shutdown(int fd, const char *method, const char *query, const char *client_ip) {
(void)query;
int enabled = 0;
int delay_ms = 700;
char body[512];
if (strcmp(method, "POST") != 0) {
http_send_json(fd, 405, "{\"ok\":false,\"error\":\"method_not_allowed\",\"message\":\"Use POST for shutdown.\"}");
return;
}
pthread_mutex_lock(&g_cfg_lock);
enabled = g_cfg.dev_reload_enabled;
delay_ms = g_cfg.dev_shutdown_delay_ms;
pthread_mutex_unlock(&g_cfg_lock);
if (!enabled) {
cr_log("warn", "dev", "shutdown rejected: dev reload disabled");
http_send_json(fd, 403,
"{\"ok\":false,\"error\":\"dev_reload_disabled\",\"message\":\"Dev reload mode is disabled.\"}");
return;
}
cr_log("info", "dev", "shutdown request received from %s", client_ip ? client_ip : "unknown");
cr_log("info", "dev", "shutdown accepted; exiting in %dms", delay_ms);
snprintf(body, sizeof(body),
"{\"ok\":true,\"message\":\"CheatRunner is shutting down. Send the new ELF from your PC.\",\"delayMs\":%d}",
delay_ms);
http_send_json(fd, 200, body);
cheatrunner_request_shutdown(delay_ms);
}
/* ── /api/user/context ──────────────────────────────────────────── */
void
handle_api_user_context(int fd) {
char uid_cfg[32] = "auto";
pthread_mutex_lock(&g_cfg_lock);
snprintf(uid_cfg, sizeof(uid_cfg), "%s", g_cfg.launch_user_id);
pthread_mutex_unlock(&g_cfg_lock);
/* Determine configured mode */
const char *mode = "auto";
char mode_user[24] = "";
if (strcasecmp(uid_cfg, "none") == 0) {
mode = "none";
} else if (strcasecmp(uid_cfg, "auto") != 0 && uid_cfg[0] != '\0') {
mode = "config";
snprintf(mode_user, sizeof(mode_user), "%s", uid_cfg);
}
/* Foreground user */
uint32_t fguser = 0xFFFFFFFFu;
int fg_valid = (sceUserServiceGetForegroundUser(&fguser) == 0 &&
fguser != 0xFFFFFFFFu && (int32_t)fguser != -1);
char fg_str[24];
if (fg_valid) snprintf(fg_str, sizeof(fg_str), "0x%08X", fguser);
else snprintf(fg_str, sizeof(fg_str), "none");
/* Login user list */
uint32_t uid_list[16] = {0};
size_t uid_actual = 0;
int list_ok = 0;
char users_json[320] = "[]";
#if CHEATRUNNER_HAVE_SCE_USER_LIST
list_ok = (sceUserServiceGetLoginUserIdList(uid_list, 16, &uid_actual) == 0);
if (list_ok && uid_actual > 0) {
char tmp[320];
int pos = 0;
tmp[pos++] = '[';
for (size_t i = 0; i < uid_actual && i < 16 && pos < (int)sizeof(tmp) - 20; i++) {
if (i > 0) tmp[pos++] = ',';
pos += snprintf(tmp + pos, sizeof(tmp) - pos, "\"0x%08X\"", uid_list[i]);
}
if (pos < (int)sizeof(tmp) - 2) { tmp[pos++] = ']'; tmp[pos] = '\0'; }
snprintf(users_json, sizeof(users_json), "%s", tmp);
}
#endif
/* Resolve which user would be used for launch */
const char *src = "none";
char resolved_str[24] = "none";
int resolved_ok = 0;
if (strcmp(mode, "none") == 0) {
src = "none";
} else if (strcmp(mode, "config") == 0) {
src = "config";
snprintf(resolved_str, sizeof(resolved_str), "%s", mode_user);
resolved_ok = 1;
} else { /* auto */
if (fg_valid) {
src = "foreground";
snprintf(resolved_str, sizeof(resolved_str), "0x%08X", fguser);
resolved_ok = 1;
}
#if CHEATRUNNER_HAVE_SCE_USER_LIST
else if (list_ok && uid_actual == 1 &&
uid_list[0] != 0xFFFFFFFFu && (int32_t)uid_list[0] != -1) {
src = "login_list";
snprintf(resolved_str, sizeof(resolved_str), "0x%08X", uid_list[0]);
resolved_ok = 1;
}
#endif
}
const char *msg = resolved_ok ? "user resolved"
: (strcmp(mode, "none") == 0 ? "user context disabled" : "no valid user found");
char body[896];
snprintf(body, sizeof(body),
"{\"ok\":true,"
"\"configured\":{\"mode\":\"%s\",\"userId\":\"%s\"},"
"\"foreground\":{\"ok\":%s,\"userId\":\"%s\"},"
"\"loginUserList\":{\"available\":%s,\"count\":%zu,\"users\":%s},"
"\"resolved\":{\"ok\":%s,\"source\":\"%s\",\"userId\":\"%s\"},"
"\"message\":\"%s\"}",
mode, mode_user,
fg_valid ? "true" : "false", fg_str,
list_ok ? "true" : "false", uid_actual, users_json,
resolved_ok ? "true" : "false", src, resolved_str,
msg);
http_send_json(fd, 200, body);
}
/* ── /api/diag/title ────────────────────────────────────────────── */
void
handle_api_diag_title(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
cJSON *out = cJSON_CreateObject();
if (!out) {
http_send_oom(fd);
return;
}
cJSON_AddBoolToObject(out, "ok", 1);
cJSON_AddStringToObject(out, "titleId", title_id);
running_game_state_t rgs;
running_state_get(&rgs);
int title_match = rgs.running && strcmp(rgs.title_id, title_id) == 0;
{
cJSON *r = cJSON_AddObjectToObject(out, "running");
if (r) {
cJSON_AddBoolToObject(r, "running", rgs.running);
cJSON_AddBoolToObject(r, "titleMatch", title_match);
cJSON_AddStringToObject(r, "runningTitleId", rgs.title_id);
cJSON_AddNumberToObject(r, "pid", (double)(rgs.running ? (int)rgs.pid : -1));
cJSON_AddNumberToObject(r, "appId", (double)rgs.app_id);
char base_hex[32]; snprintf(base_hex, sizeof(base_hex), "0x%lx", (long)rgs.image_base);
cJSON_AddStringToObject(r, "imageBase", base_hex);
}
}
{
char ls_phase[64] = {0}, ls_title[12] = {0}, ls_message[160] = {0};
char ls_method[64] = {0}, ls_hex[16] = {0};
int ls_busy = 0, ls_rc = 0, ls_verified = 0;
pthread_mutex_lock(&g_launch_status_lock);
snprintf(ls_phase, sizeof(ls_phase), "%s", g_launch_status.phase);
snprintf(ls_title, sizeof(ls_title), "%s", g_launch_status.title_id);
snprintf(ls_message, sizeof(ls_message), "%s", g_launch_status.message);
snprintf(ls_method, sizeof(ls_method), "%s", g_launch_status.method);
snprintf(ls_hex, sizeof(ls_hex), "%s", g_launch_status.hex);
ls_busy = g_launch_status.busy;
ls_rc = g_launch_status.rc;
ls_verified = g_launch_status.verified;
pthread_mutex_unlock(&g_launch_status_lock);
cJSON *ls = cJSON_AddObjectToObject(out, "launchStatus");
if (ls) {
cJSON_AddBoolToObject(ls, "busy", ls_busy);
cJSON_AddStringToObject(ls, "phase", ls_phase);
cJSON_AddStringToObject(ls, "titleId", ls_title);
cJSON_AddStringToObject(ls, "message", ls_message);
cJSON_AddStringToObject(ls, "method", ls_method);
cJSON_AddStringToObject(ls, "rcHex", ls_hex);
cJSON_AddNumberToObject(ls, "rc", ls_rc);
cJSON_AddBoolToObject(ls, "verified", ls_verified);
}
}
{
char cheat_path[256] = {0};
int cheat_kind = 0;
char cheat_err[128] = {0};
int found = 0;
int mod_count = 0;
cJSON *root = load_cheat_json_root_for_title_ex(title_id, cheat_err, sizeof(cheat_err), cheat_path, sizeof(cheat_path), &cheat_kind);
if (root) {
found = 1;
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (!cJSON_IsArray(mods)) mods = cJSON_GetObjectItem(root, "patches");
if (cJSON_IsArray(mods)) mod_count = cJSON_GetArraySize(mods);
cJSON_Delete(root);
}
const char *fmt_str = cheat_kind == 1 ? "json" : (cheat_kind == 2 ? "shn" : (cheat_kind == 3 ? "mc4" : "none"));
cJSON *cf = cJSON_AddObjectToObject(out, "cheat");
if (cf) {
cJSON_AddBoolToObject(cf, "found", found);
cJSON_AddStringToObject(cf, "format", fmt_str);
cJSON_AddStringToObject(cf, "path", found ? cheat_path : "");
cJSON_AddNumberToObject(cf, "modCount", mod_count);
if (!found && cheat_err[0]) cJSON_AddStringToObject(cf, "error", cheat_err);
}
}
{
cheat_file_search_t cfs;
find_cheat_candidates(title_id, &cfs);
cJSON *scan = cJSON_AddObjectToObject(out, "scan");
if (scan) {
cJSON_AddNumberToObject(scan, "candidateCount", cfs.candidate_count);
cJSON_AddStringToObject(scan, "preferredVer", cfs.preferred_ver);
cJSON *cands = cJSON_AddArrayToObject(scan, "candidates");
if (cands) {
for (int i = 0; i < cfs.candidate_count; i++) {
cJSON *ce = cJSON_CreateObject();
const char *p = cfs.candidates[i].path;
const char *base = strrchr(p, '/');
cJSON_AddStringToObject(ce, "file", base ? base + 1 : p);
cJSON_AddStringToObject(ce, "path", p);
cJSON_AddNumberToObject(ce, "score", cfs.candidates[i].score);
const char *fmt = cfs.candidates[i].kind == 1 ? "json" : (cfs.candidates[i].kind == 2 ? "shn" : "mc4");
cJSON_AddStringToObject(ce, "format", fmt);
cJSON_AddItemToArray(cands, ce);
}
}
}
}
{
char uid_cfg[32] = {0}, mc4mode[16] = {0}, shnmode[16] = {0};
int unsafe_mc4 = 0;
pthread_mutex_lock(&g_cfg_lock);
snprintf(uid_cfg, sizeof(uid_cfg), "%s", g_cfg.launch_user_id);
snprintf(mc4mode, sizeof(mc4mode), "%s", g_cfg.cheat_mc4_address_mode);
snprintf(shnmode, sizeof(shnmode), "%s", g_cfg.cheat_shn_address_mode);
unsafe_mc4 = g_cfg.allow_unsafe_mc4_apply;
pthread_mutex_unlock(&g_cfg_lock);
cJSON *cfg = cJSON_AddObjectToObject(out, "config");
if (cfg) {
cJSON_AddStringToObject(cfg, "launchUserId", uid_cfg[0] ? uid_cfg : "auto");
cJSON_AddStringToObject(cfg, "cheatMc4AddressMode", mc4mode[0] ? mc4mode : "auto");
cJSON_AddStringToObject(cfg, "cheatShnAddressMode", shnmode[0] ? shnmode : "auto");
cJSON_AddBoolToObject(cfg, "allowUnsafeMc4Apply", unsafe_mc4);
}
}
{
const cr_priv_status_t *ps = cr_priv_get();
cJSON *priv = cJSON_AddObjectToObject(out, "privileges");
if (priv) {
cJSON_AddNumberToObject(priv, "uid", ps->uid);
cJSON_AddNumberToObject(priv, "gid", ps->gid);
cJSON_AddBoolToObject(priv, "uidOk", ps->uid_ok);
cJSON_AddBoolToObject(priv, "sandboxEscaped", ps->sandbox_ok);
cJSON_AddBoolToObject(priv, "sceAuthOk", ps->authid_ok);
cJSON_AddBoolToObject(priv, "sceCapsOk", ps->caps_ok);
cJSON_AddBoolToObject(priv, "kernelRwOk", ps->kernel_rw_ok);
cJSON_AddBoolToObject(priv, "kernelMprotectOk", ps->kernel_mprotect_ok);
cJSON_AddBoolToObject(priv, "canPatchGameMemory", ps->can_patch_game);
cJSON *wa = cJSON_AddArrayToObject(priv, "warnings");
if (wa) {
for (int i = 0; i < ps->n_warnings; i++) {
cJSON *ws = cJSON_CreateString(ps->warnings[i]);
if (ws) cJSON_AddItemToArray(wa, ws);
}
}
}
}
{
last_game_exit_t lge;
pthread_mutex_lock(&g_last_game_exit_lock);
lge = g_last_game_exit;
pthread_mutex_unlock(&g_last_game_exit_lock);
if (lge.title_id[0]) {
cJSON *lge_j = cJSON_AddObjectToObject(out, "lastGameExit");
if (lge_j) {
cJSON_AddStringToObject(lge_j, "titleId", lge.title_id);
char appid_hex[16]; snprintf(appid_hex, sizeof(appid_hex), "0x%x", lge.app_id);
cJSON_AddStringToObject(lge_j, "appId", appid_hex);
cJSON_AddNumberToObject(lge_j, "pid", (double)lge.pid);
cJSON_AddStringToObject(lge_j, "reason", lge.reason);
cJSON_AddNumberToObject(lge_j, "tsMs", (double)lge.ts_ms);
cJSON_AddNumberToObject(lge_j, "elapsedAfterLastCheatMs", (double)lge.elapsed_after_last_cheat_ms);
cJSON_AddNumberToObject(lge_j, "lastModIndex", (double)lge.last_mod_index);
cJSON_AddStringToObject(lge_j, "lastModName", lge.last_mod_name);
cJSON_AddBoolToObject(lge_j, "suspected", lge.suspected);
}
}
}
{
last_apply_rec_t lar;
pthread_mutex_lock(&g_last_apply_lock);
lar = g_last_apply_rec;
pthread_mutex_unlock(&g_last_apply_lock);
if (lar.ts_ms > 0) {
cJSON *lar_j = cJSON_AddObjectToObject(out, "lastApply");
if (lar_j) {
cJSON_AddStringToObject(lar_j, "titleId", lar.title_id);
cJSON_AddNumberToObject(lar_j, "modIndex", (double)lar.mod_index);
cJSON_AddStringToObject(lar_j, "modName", lar.mod_name);
cJSON_AddBoolToObject(lar_j, "effectiveOn", lar.effective_on);
cJSON_AddBoolToObject(lar_j, "ok", lar.ok);
cJSON_AddBoolToObject(lar_j, "hookCodecave", lar.hook_codecave);
cJSON_AddNumberToObject(lar_j, "caveCount", (double)lar.cave_count);
cJSON_AddNumberToObject(lar_j, "hookCount", (double)lar.hook_count);
cJSON_AddNumberToObject(lar_j, "tsMs", (double)lar.ts_ms);
cJSON *writes_j = cJSON_AddArrayToObject(lar_j, "writes");
if (writes_j) {
for (int _i = 0; _i < lar.write_count; _i++) {
cJSON *w = cJSON_CreateObject();
if (w) {
char addr_hex[20]; snprintf(addr_hex, sizeof(addr_hex), "0x%lx", (long)lar.writes[_i].addr);
cJSON_AddStringToObject(w, "addr", addr_hex);
cJSON_AddNumberToObject(w, "len", (double)lar.writes[_i].len);
cJSON_AddBoolToObject(w, "isCave", lar.writes[_i].is_cave);
cJSON_AddItemToArray(writes_j, w);
}
}
}
}
}
}
cJSON_AddBoolToObject(out, "scannerPausedDuringApply", g_cheat_applying ? 1 : 0);
char *s = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!s) { http_send_oom(fd); return; }
http_send_json(fd, 200, s);
free(s);
}
/* ── /api/cheats/scan ───────────────────────────────────────────── */
void
handle_api_cheats_scan(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
cheat_file_search_t cfs;
memset(&cfs, 0, sizeof(cfs));
find_cheat_candidates(title_id, &cfs);
cJSON *out = cJSON_CreateObject();
if (!out) {
http_send_oom(fd);
return;
}
cJSON_AddBoolToObject(out, "ok", 1);
cJSON_AddStringToObject(out, "titleId", title_id);
cJSON_AddStringToObject(out, "dir", CHEATRUNNER_CHEATS_DIR);
cJSON_AddNumberToObject(out, "candidateCount", cfs.candidate_count);
cJSON_AddStringToObject(out, "preferredVersion", cfs.preferred_ver);
cJSON_AddStringToObject(out, "selected", cfs.best_path[0] ? cfs.best_path : "");
const char *sfmt = cfs.best_kind == 1 ? "json" : (cfs.best_kind == 2 ? "shn" : (cfs.best_kind == 3 ? "mc4" : "none"));
cJSON_AddStringToObject(out, "selectedFormat", cfs.best_path[0] ? sfmt : "none");
cJSON *cands = cJSON_AddArrayToObject(out, "candidates");
if (cands) {
for (int i = 0; i < cfs.candidate_count; i++) {
cJSON *ce = cJSON_CreateObject();
const char *p = cfs.candidates[i].path;
const char *base = strrchr(p, '/');
cJSON_AddStringToObject(ce, "file", base ? base + 1 : p);
cJSON_AddStringToObject(ce, "path", p);
cJSON_AddNumberToObject(ce, "score", cfs.candidates[i].score);
const char *fmt = cfs.candidates[i].kind == 1 ? "json" : (cfs.candidates[i].kind == 2 ? "shn" : "mc4");
cJSON_AddStringToObject(ce, "format", fmt);
cJSON_AddBoolToObject(ce, "selected", strcmp(p, cfs.best_path) == 0);
cJSON_AddItemToArray(cands, ce);
}
}
char *s = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!s) { http_send_oom(fd); return; }
http_send_json(fd, 200, s);
free(s);
}
/* ── /api/cheats/mc4-debug ──────────────────────────────────────── */
void
handle_api_cheats_mc4_debug(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 || !title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
char cheat_path[256] = {0};
char err[128] = {0};
int cheat_kind = 0;
cJSON *root = load_cheat_json_root_for_title_ex(title_id, err, sizeof(err), cheat_path, sizeof(cheat_path), &cheat_kind);
if (!root) {
char body[256]; snprintf(body, sizeof(body), "{\"ok\":false,\"error\":\"%s\"}", err);
http_send_json(fd, 404, body);
return;
}
if (cheat_kind == 1) {
cJSON_Delete(root);
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"cheat is JSON format; mc4-debug is for SHN/MC4 only\"}");
return;
}
cJSON *out = cJSON_CreateObject();
if (!out) {
cJSON_Delete(root);
http_send_oom(fd);
return;
}
cJSON_AddBoolToObject(out, "ok", 1);
cJSON_AddStringToObject(out, "titleId", title_id);
cJSON_AddStringToObject(out, "cheatPath", cheat_path);
cJSON_AddStringToObject(out, "format", cheat_kind == 2 ? "shn" : "mc4");
if (cheat_kind == 3) {
char *txt = NULL;
if (read_file_text(cheat_path, &txt) == 0 && txt) {
size_t xml_sz = 0;
char *xml = mc4_decrypt_to_xml(txt, strlen(txt), &xml_sz);
free(txt);
if (xml) {
char excerpt[2048];
snprintf(excerpt, sizeof(excerpt), "%.*s", (int)(xml_sz < 1999u ? xml_sz : 1999u), xml);
cJSON_AddStringToObject(out, "xmlExcerpt", excerpt);
free(xml);
} else {
cJSON_AddStringToObject(out, "xmlExcerpt", "(decrypt failed)");
}
}
}
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (!cJSON_IsArray(mods)) mods = cJSON_GetObjectItem(root, "patches");
cJSON *mod_arr = cJSON_AddArrayToObject(out, "mods");
if (cJSON_IsArray(mods) && mod_arr) {
cJSON *mod = NULL;
cJSON_ArrayForEach(mod, mods) {
cJSON *mo = cJSON_CreateObject();
if (!mo) break;
cJSON *n = cJSON_GetObjectItem(mod, "name");
cJSON_AddStringToObject(mo, "name", cJSON_IsString(n) ? n->valuestring : "");
cJSON *mem = cJSON_GetObjectItem(mod, "memory");
if (!cJSON_IsArray(mem)) mem = cJSON_GetObjectItem(mod, "patches");
cJSON *patch_arr = cJSON_AddArrayToObject(mo, "patches");
if (cJSON_IsArray(mem) && patch_arr) {
cJSON *m = NULL;
int pi = 0;
cJSON_ArrayForEach(m, mem) {
cJSON *pe = cJSON_CreateObject();
if (!pe) break;
cJSON_AddNumberToObject(pe, "index", pi++);
cJSON *on_j = cJSON_GetObjectItem(m, "on");
cJSON *off_j = cJSON_GetObjectItem(m, "off");
cJSON *exp_j = cJSON_GetObjectItem(m, "expected");
cJSON *abs_j = cJSON_GetObjectItem(m, "absolute");
cJSON_AddStringToObject(pe, "valueOn", cJSON_IsString(on_j) ? on_j->valuestring : "");
cJSON_AddStringToObject(pe, "valueOff", cJSON_IsString(off_j) ? off_j->valuestring : "");
cJSON_AddStringToObject(pe, "expected", cJSON_IsString(exp_j) ? exp_j->valuestring : "");
cJSON_AddBoolToObject(pe, "absoluteFlag", cJSON_IsTrue(abs_j));
size_t elen = 0;
if (cJSON_IsString(exp_j) && exp_j->valuestring && exp_j->valuestring[0]) {
uint8_t tmp[128];
parse_hex_bytes_checked(exp_j->valuestring, tmp, sizeof(tmp), &elen);
}
int off_reliable = (elen > 0);
cJSON_AddBoolToObject(pe, "offReliable", off_reliable);
cJSON_AddStringToObject(pe, "offNote",
off_reliable ? "expected field present — off comparison is reliable" :
"no expected field — ValueOff may not match original bytes");
cJSON_AddItemToArray(patch_arr, pe);
}
}
cJSON_AddItemToArray(mod_arr, mo);
}
}
cJSON_Delete(root);
char *s = cJSON_PrintUnformatted(out);
cJSON_Delete(out);
if (!s) { http_send_oom(fd); return; }
http_send_json(fd, 200, s);
free(s);
}
void
handle_api_cheats_disable_all(int fd, const char *query) {
char raw_title_id[32] = {0};
char title_id[10] = {0};
if (query_value(query, "titleId", raw_title_id, sizeof(raw_title_id)) != 0 ||
!title_id_normalize(raw_title_id, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad titleId\"}");
return;
}
running_game_state_t rs;
running_state_get(&rs);
if (!rs.running || strcmp(rs.title_id, title_id) != 0) {
http_send_json(fd, 200, "{\"ok\":true,\"disabled\":0,\"reason\":\"game_not_running\"}");
return;
}
char err[128] = {0};
char path_out[256] = {0};
int kind_out = 0;
cJSON *root = load_cheat_json_root_for_title_ex(title_id, err, sizeof(err),
path_out, sizeof(path_out), &kind_out);
if (!root) {
http_send_json(fd, 404, "{\"ok\":false,\"error\":\"no cheat file\"}");
return;
}
cJSON *mods = cJSON_GetObjectItem(root, "mods");
int n = cJSON_IsArray(mods) ? cJSON_GetArraySize(mods) : 0;
int disabled = 0;
int need_delay = 0;
for (int i = 0; i < n; i++) {
if (mod_enabled_check(title_id, rs.pid, i)) {
if (need_delay) usleep(1000000);
char apply_err[256] = {0};
/* Reset cooldown guard so rapid sequential disables aren't blocked by it.
* Setting to 0 makes elapsed = now - 0 = very large, bypassing the pre-check. */
g_last_apply_at_ms = 0;
if (apply_cheat_json(title_id, i, 0, apply_err, sizeof(apply_err)) == 0)
disabled++;
need_delay = 1;
}
}
cJSON_Delete(root);
char body_buf[128];
snprintf(body_buf, sizeof(body_buf), "{\"ok\":true,\"disabled\":%d}", disabled);
http_send_json(fd, 200, body_buf);
}
void
http_send_png_asset(int fd) {
char header[256];
size_t len = sizeof(g_cheatrunner_png);
int n = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: image/png\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Cache-Control: public, max-age=86400\r\n"
"\r\n",
(unsigned int)len);
if (n > 0) {
if (socket_send_all(fd, header, (size_t)n) != 0) {
return;
}
(void)socket_send_all(fd, g_cheatrunner_png, len);
}
}
void
http_route(int fd, const char *method, const char *path, const char *query,
const char *client_ip, const char *body, size_t body_len) {
if (!g_startup_ms) {
struct timespec _ts; clock_gettime(CLOCK_MONOTONIC, &_ts);
g_startup_ms = (long long)_ts.tv_sec * 1000LL + (long long)(_ts.tv_nsec / 1000000L);
}
if (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) {
http_send_json(fd, 405, "{\"ok\":false,\"error\":\"GET/POST only\"}");
return;
}
if (cr_api_dashboard_handle(fd, method, path, query, body, body_len)) return;
if (cr_api_games_handle(fd, method, path, query, body, body_len)) return;
if (cr_api_cheats_handle(fd, method, path, query, body, body_len)) return;
if (cr_api_patches_handle(fd, method, path, query, body, body_len)) return;
if (cr_api_sources_handle(fd, method, path, query, body, body_len)) return;
if (cr_api_logs_handle(fd, method, path, query, body, body_len)) return;
if (cr_api_fan_handle(fd, method, path, query, body, body_len)) return;
if (cr_api_profile_handle(fd, method, path, query, body, body_len)) return;
/* shutdown needs client_ip - handle directly */
if (!strcmp(path, "/api/dev/shutdown")) {
handle_api_dev_shutdown(fd, method, query, client_ip);
return;
}
if (cr_api_dev_handle(fd, method, path, query, body, body_len)) return;
http_send_json(fd, 404, "{\"ok\":false,\"error\":\"not found\"}");
}