v0.16 update

This commit is contained in:
maj0r authored and GitHub committed 2026-07-20 16:39:14 -03:00
1 parent 285abce7f9
commit c3b0e23ba3
34 files changed
+4440 -3118

No files matched your search

+45 -9
View File
@@ -35,6 +35,8 @@
#include "cr_cheat_formats.h"
#include "cr_version.h"
#include "cr_cheats.h"
#include "cr_cheat_profiles.h"
#include "cr_patch_profiles.h"
#include "cr_patch_parser.h"
#include "cr_http.h"
#include "cr_remote_sources.h"
@@ -46,6 +48,8 @@
#include "cr_api_dashboard.h"
#include "cr_api_games.h"
#include "cr_api_cheats.h"
#include "cr_api_cheat_profiles.h"
#include "cr_api_patch_profiles.h"
#include "cr_api_patches.h"
#include "cr_api_sources.h"
#include "cr_api_dev.h"
@@ -60,7 +64,7 @@
#endif
#ifndef CHEATRUNNER_VERSION
#define CHEATRUNNER_VERSION "0.15"
#define CHEATRUNNER_VERSION "0.16"
#endif
#define MAX_GAMES CR_APPDB_MAX_GAMES
@@ -188,12 +192,12 @@ http_send_response(int fd, int status, const char *content_type, const uint8_t *
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %u\r\n"
"Connection: %s\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, g_http_keep_alive ? "keep-alive" : "close");
(unsigned int)body_len);
if (n > 0) {
if (socket_send_all(fd, header, (size_t)n) != 0) {
return;
@@ -211,12 +215,37 @@ http_send_response_cached(int fd, int status, const char *content_type, const ui
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %u\r\n"
"Connection: %s\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, g_http_keep_alive ? "keep-alive" : "close");
(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_gzip(int fd, int status, const char *content_type,
const uint8_t *body, size_t body_len, int cacheable) {
char header[512];
int n = snprintf(header, sizeof(header),
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Encoding: gzip\r\n"
"Content-Length: %u\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Cache-Control: %s\r\n"
"\r\n",
status, status_text_for(status), content_type ? content_type : "application/octet-stream",
(unsigned int)body_len, cacheable ? "public, max-age=3600" : "no-cache");
if (n > 0) {
if (socket_send_all(fd, header, (size_t)n) != 0) {
return;
@@ -812,6 +841,8 @@ build_games_json_array(char *buf, size_t cap, int debug_names) {
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;
int autoload_cheats = cheat_profiles_has(entries[i].title_id);
int autoload_patches = patch_profiles_has(entries[i].title_id);
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));
@@ -829,12 +860,14 @@ build_games_json_array(char *buf, size_t cap, int debug_names) {
"\"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,"
"\"autoloadCheats\":%s,\"autoloadPatches\":%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",
autoload_cheats ? "true" : "false", autoload_patches ? "true" : "false",
(unsigned long long)entries[i].play_time_seconds,
(unsigned long long)entries[i].last_access_time);
if (debug_names) {
@@ -4478,11 +4511,11 @@ http_send_png_asset(int fd) {
"HTTP/1.1 200 OK\r\n"
"Content-Type: image/png\r\n"
"Content-Length: %u\r\n"
"Connection: %s\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, g_http_keep_alive ? "keep-alive" : "close");
(unsigned int)len);
if (n > 0) {
if (socket_send_all(fd, header, (size_t)n) != 0) {
return;
@@ -4493,7 +4526,8 @@ http_send_png_asset(int fd) {
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) {
const char *client_ip, const char *body, size_t body_len,
int accepts_gzip) {
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);
@@ -4503,10 +4537,12 @@ http_route(int fd, const char *method, const char *path, const char *query,
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_dashboard_handle(fd, method, path, query, body, body_len, accepts_gzip)) 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_cheat_profiles_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_patch_profiles_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;
+2 -1
View File
@@ -4,6 +4,7 @@
#include <stddef.h>
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);
const char *client_ip, const char *body, size_t body_len,
int accepts_gzip);
#endif
+123
View File
@@ -0,0 +1,123 @@
#include <vector>
#include <string>
extern "C" {
#include <string.h>
#include "cr_api_cheat_profiles.h"
#include "cr_api_internal.h"
#include "cr_cheat_profiles.h"
#include "cr_titles.h"
#include "third_party/cJSON.h"
}
namespace {
void
handleGet(int fd, const char *query) {
char raw_tid[32] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 || !raw_tid[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing titleId\"}");
return;
}
char title_id[10] = {0};
if (!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid titleId\"}");
return;
}
char mods_json[4096];
cheat_profiles_get_json(title_id, mods_json, sizeof(mods_json));
char body[4200];
snprintf(body, sizeof(body), "{\"ok\":true,\"titleId\":\"%s\",\"mods\":%s}", title_id, mods_json);
http_send_json(fd, 200, body);
}
void
handleSave(int fd, const char *query, const char *body, size_t body_len) {
char raw_tid[32] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 || !raw_tid[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing titleId\"}");
return;
}
char title_id[10] = {0};
if (!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid titleId\"}");
return;
}
(void)body_len;
cJSON *root = cJSON_Parse(body ? body : "");
cJSON *mods_j = root;
if (root && cJSON_IsObject(root)) {
mods_j = cJSON_GetObjectItem(root, "mods");
}
if (!cJSON_IsArray(mods_j)) {
cJSON_Delete(root);
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"body must be a JSON array or {\\\"mods\\\":[...]}\"}");
return;
}
std::vector<std::string> names;
std::vector<const char *> ptrs;
cJSON *m = nullptr;
cJSON_ArrayForEach(m, mods_j) {
if (cJSON_IsString(m) && m->valuestring && m->valuestring[0]) {
names.emplace_back(m->valuestring);
}
}
cJSON_Delete(root);
ptrs.reserve(names.size());
for (const auto &n : names) {
ptrs.push_back(n.c_str());
}
cheat_profiles_save(title_id, ptrs.empty() ? nullptr : ptrs.data(), (int)ptrs.size());
char resp[64];
snprintf(resp, sizeof(resp), "{\"ok\":true,\"count\":%d}", (int)ptrs.size());
http_send_json(fd, 200, resp);
}
void
handleClear(int fd, const char *query) {
char raw_tid[32] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 || !raw_tid[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing titleId\"}");
return;
}
char title_id[10] = {0};
if (!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid titleId\"}");
return;
}
cheat_profiles_clear(title_id);
http_send_json(fd, 200, "{\"ok\":true}");
}
} // namespace
extern "C" int
cr_api_cheat_profiles_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len) {
int is_post = !strcmp(method, "POST");
if (!strcmp(path, "/api/cheats/profile")) {
handleGet(fd, query);
return 1;
}
if (!strcmp(path, "/api/cheats/profile/save")) {
if (!is_post) {
http_send_json(fd, 405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
return 1;
}
handleSave(fd, query, body, body_len);
return 1;
}
if (!strcmp(path, "/api/cheats/profile/clear")) {
if (!is_post) {
http_send_json(fd, 405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
return 1;
}
handleClear(fd, query);
return 1;
}
return 0;
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef CR_API_CHEAT_PROFILES_H
#define CR_API_CHEAT_PROFILES_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
int cr_api_cheat_profiles_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len);
#ifdef __cplusplus
}
#endif
#endif /* CR_API_CHEAT_PROFILES_H */
+6 -2
View File
@@ -32,7 +32,8 @@ handle_api_config(int fd) {
"\"allow_unsafe_mc4_apply\":%d,\"allow_unsafe_shn_apply\":%d,"
"\"cheat_log_candidates\":%d,\"cheat_mark_crash_suspect\":%d,"
"\"cheat_apply_one_at_a_time\":%d,"
"\"cheat_address_auto_detect\":%d}",
"\"cheat_address_auto_detect\":%d,"
"\"tile_autoinstall_enabled\":%d}",
g_cfg.http_port, g_cfg.auto_load_cheat_menu,
g_cfg.auto_download_missing_cheat, g_cfg.launch_kill_current, g_cfg.launch_kill_delay_ms,
g_cfg.launch_wait_timeout_ms, g_cfg.cheat_engine, g_cfg.cheat_validate_original_bytes,
@@ -53,7 +54,8 @@ handle_api_config(int fd) {
g_cfg.allow_unsafe_mc4_apply, g_cfg.allow_unsafe_shn_apply,
g_cfg.cheat_log_candidates, g_cfg.cheat_mark_crash_suspect,
g_cfg.cheat_apply_one_at_a_time,
g_cfg.cheat_address_auto_detect);
g_cfg.cheat_address_auto_detect,
g_cfg.tile_autoinstall_enabled);
pthread_mutex_unlock(&g_cfg_lock);
http_send_json(fd, 200, body);
}
@@ -199,6 +201,8 @@ handle_api_config_set(int fd, const char *query) {
} else if (!strcmp(key, "fan_max_c")) {
int v = atoi(value);
g_cfg.fan_max_c = (v >= 50 && v <= 100) ? v : 90;
} else if (!strcmp(key, "tile_autoinstall_enabled")) {
g_cfg.tile_autoinstall_enabled = atoi(value) ? 1 : 0;
} else if (!strncmp(key, "hotkey_", 7)) {
/* removed — silently ignore */
(void)value;
+25 -11
View File
@@ -1,34 +1,48 @@
#include <stdint.h>
#include <string.h>
#include "cr_api_internal.h"
#include "cr_api_dashboard.h"
#include "dashboard_html_gz.h"
#include "dashboard_css_gz.h"
#include "dashboard_js_gz.h"
extern const char g_dashboard_html[];
extern const char g_dashboard_css[];
extern const char g_dashboard_js[];
static void send_asset(int fd, const char *content_type, const char *data) {
http_send_response(fd, 200, content_type, (const uint8_t *)data, strlen(data));
}
static void send_static_asset(int fd, const char *content_type, const char *data) {
http_send_response_cached(fd, 200, content_type, (const uint8_t *)data, strlen(data));
static void
send_asset(int fd, const char *content_type, const char *data,
int accepts_gzip, const unsigned char *gz_data, unsigned long gz_len, int cacheable) {
if (accepts_gzip) {
http_send_response_gzip(fd, 200, content_type, gz_data, (size_t)gz_len, cacheable);
return;
}
if (cacheable) {
http_send_response_cached(fd, 200, content_type, (const uint8_t *)data, strlen(data));
} else {
http_send_response(fd, 200, content_type, (const uint8_t *)data, strlen(data));
}
}
int
cr_api_dashboard_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len) {
const char *query, const char *body, size_t body_len,
int accepts_gzip) {
(void)method; (void)query; (void)body; (void)body_len;
if (!strcmp(path, "/") || !strcmp(path, "/index.html") ||
!strcmp(path, "/launcher.html")) {
send_asset(fd, "text/html; charset=utf-8", g_dashboard_html);
send_asset(fd, "text/html; charset=utf-8", g_dashboard_html, accepts_gzip,
g_dashboard_html_gz, g_dashboard_html_gz_len, 0);
return 1;
}
if (!strcmp(path, "/dashboard.css")) {
send_static_asset(fd, "text/css; charset=utf-8", g_dashboard_css);
send_asset(fd, "text/css; charset=utf-8", g_dashboard_css, accepts_gzip,
g_dashboard_css_gz, g_dashboard_css_gz_len, 1);
return 1;
}
if (!strcmp(path, "/dashboard.js")) {
send_static_asset(fd, "application/javascript; charset=utf-8", g_dashboard_js);
send_asset(fd, "application/javascript; charset=utf-8", g_dashboard_js, accepts_gzip,
g_dashboard_js_gz, g_dashboard_js_gz_len, 1);
return 1;
}
if (!strcmp(path, "/CheatRunner.png") || !strcmp(path, "/favicon.png") ||
@@ -39,7 +53,7 @@ cr_api_dashboard_handle(int fd, const char *method, const char *path,
if (!strcmp(path, "/cache.appcache")) {
static const char appcache[] =
"CACHE MANIFEST\n"
"# CheatRunner v0.15\n"
"# CheatRunner v0.16\n"
"# Build: " __DATE__ " " __TIME__ "\n"
"\n"
"CACHE:\n"
+2 -1
View File
@@ -4,6 +4,7 @@
#include <stddef.h>
int cr_api_dashboard_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len);
const char *query, const char *body, size_t body_len,
int accepts_gzip);
#endif /* CR_API_DASHBOARD_H */
+2
View File
@@ -13,6 +13,8 @@ void http_send_response(int fd, int status, const char *content_type,
const uint8_t *body, size_t body_len);
void http_send_response_cached(int fd, int status, const char *content_type,
const uint8_t *body, size_t body_len);
void http_send_response_gzip(int fd, int status, const char *content_type,
const uint8_t *body, size_t body_len, int cacheable);
int query_value(const char *query, const char *key, char *out, size_t out_size);
/* ---- /api/logs ---- */
+123
View File
@@ -0,0 +1,123 @@
#include <vector>
#include <string>
extern "C" {
#include <string.h>
#include "cr_api_internal.h"
#include "cr_api_patch_profiles.h"
#include "cr_patch_profiles.h"
#include "cr_titles.h"
#include "third_party/cJSON.h"
}
namespace {
void
handleGet(int fd, const char *query) {
char raw_tid[32] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 || !raw_tid[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing titleId\"}");
return;
}
char title_id[10] = {0};
if (!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid titleId\"}");
return;
}
char ids_json[4096];
patch_profiles_get_json(title_id, ids_json, sizeof(ids_json));
char body[4200];
snprintf(body, sizeof(body), "{\"ok\":true,\"titleId\":\"%s\",\"entryIds\":%s}", title_id, ids_json);
http_send_json(fd, 200, body);
}
void
handleSave(int fd, const char *query, const char *body, size_t body_len) {
char raw_tid[32] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 || !raw_tid[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing titleId\"}");
return;
}
char title_id[10] = {0};
if (!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid titleId\"}");
return;
}
(void)body_len;
cJSON *root = cJSON_Parse(body ? body : "");
cJSON *ids_j = root;
if (root && cJSON_IsObject(root)) {
ids_j = cJSON_GetObjectItem(root, "entryIds");
}
if (!cJSON_IsArray(ids_j)) {
cJSON_Delete(root);
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"body must be a JSON array or {\\\"entryIds\\\":[...]}\"}");
return;
}
std::vector<std::string> ids;
std::vector<const char *> ptrs;
cJSON *m = nullptr;
cJSON_ArrayForEach(m, ids_j) {
if (cJSON_IsString(m) && m->valuestring && m->valuestring[0]) {
ids.emplace_back(m->valuestring);
}
}
cJSON_Delete(root);
ptrs.reserve(ids.size());
for (const auto &n : ids) {
ptrs.push_back(n.c_str());
}
patch_profiles_save(title_id, ptrs.empty() ? nullptr : ptrs.data(), (int)ptrs.size());
char resp[64];
snprintf(resp, sizeof(resp), "{\"ok\":true,\"count\":%d}", (int)ptrs.size());
http_send_json(fd, 200, resp);
}
void
handleClear(int fd, const char *query) {
char raw_tid[32] = {0};
if (query_value(query, "titleId", raw_tid, sizeof(raw_tid)) != 0 || !raw_tid[0]) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing titleId\"}");
return;
}
char title_id[10] = {0};
if (!title_id_normalize(raw_tid, title_id)) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"invalid titleId\"}");
return;
}
patch_profiles_clear(title_id);
http_send_json(fd, 200, "{\"ok\":true}");
}
} // namespace
extern "C" int
cr_api_patch_profiles_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len) {
int is_post = !strcmp(method, "POST");
if (!strcmp(path, "/api/patches/profile")) {
handleGet(fd, query);
return 1;
}
if (!strcmp(path, "/api/patches/profile/save")) {
if (!is_post) {
http_send_json(fd, 405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
return 1;
}
handleSave(fd, query, body, body_len);
return 1;
}
if (!strcmp(path, "/api/patches/profile/clear")) {
if (!is_post) {
http_send_json(fd, 405, "{\"ok\":false,\"error\":\"method_not_allowed\"}");
return 1;
}
handleClear(fd, query);
return 1;
}
return 0;
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef CR_API_PATCH_PROFILES_H
#define CR_API_PATCH_PROFILES_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
int cr_api_patch_profiles_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len);
#ifdef __cplusplus
}
#endif
#endif /* CR_API_PATCH_PROFILES_H */
+59 -22
View File
@@ -100,6 +100,65 @@ free_merged(merged_patches_t *m) {
free(m);
}
int
patch_apply_by_entry_id(const char *title_id, const char *entry_id, int force,
char *err, size_t err_size) {
merged_patches_t *m = build_merged_patches(title_id);
if (!m) { snprintf(err, err_size, "oom"); return -1; }
if (m->count == 0) {
free_merged(m);
snprintf(err, err_size, "patch_not_found");
return -1;
}
patch_entry_t *e = NULL;
for (int i = 0; i < m->count; i++) {
if (strcmp(m->entries[i].entry_id, entry_id) == 0) { e = &m->entries[i]; break; }
}
if (!e) {
free_merged(m);
snprintf(err, err_size, "entry_not_found");
return -1;
}
if (e->line_count == 0) {
free_merged(m);
snprintf(err, err_size, "no_supported_lines");
return -1;
}
if (e->has_unsupported) {
free_merged(m);
snprintf(err, err_size, "unsupported_line_type");
return -1;
}
if (!force && !e->is_mask_ver && e->app_ver[0]) {
running_game_state_t apply_st;
running_state_get(&apply_st);
char game_ver[32] = {0};
if (apply_st.running) {
const char *ver = apply_st.content_version[0] ? apply_st.content_version
: apply_st.app_version;
if (ver[0]) snprintf(game_ver, sizeof(game_ver), "%s", ver);
}
if (game_ver[0] && !cr_version_equal(e->app_ver, game_ver)) {
free_merged(m);
snprintf(err, err_size, "version_mismatch");
return -1;
}
}
patch_apply_result_t res;
int rc = patch_apply_entry_ex(title_id, e, &res);
if (rc != 0) {
snprintf(err, err_size, "%s", res.error[0] ? res.error : "apply_failed");
free_merged(m);
return -1;
}
patch_mark_applied(title_id, e->entry_id, res.pid);
free_merged(m);
return 0;
}
/* ── /api/patches?titleId=CUSA00900 ─────────────────────────────────────── */
static void
@@ -543,27 +602,5 @@ cr_api_patches_handle(int fd, const char *method, const char *path,
handle_patches_files_delete(fd, query);
return 1;
}
/* /api/patches/global — returns current enabled state */
if (strcmp(path, "/api/patches/global") == 0) {
char body[64];
snprintf(body, sizeof(body), "{\"ok\":true,\"enabled\":%s}",
patch_global_enabled() ? "true" : "false");
http_send_json(fd, 200, body);
return 1;
}
/* /api/patches/global/set?on=0|1 — enable or disable all patch dirs at once */
if (strcmp(path, "/api/patches/global/set") == 0) {
char on_s[8] = {0};
if (query_value(query, "on", on_s, sizeof(on_s)) != 0) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"missing on\"}");
return 1;
}
patch_global_set(atoi(on_s));
char body[64];
snprintf(body, sizeof(body), "{\"ok\":true,\"enabled\":%s}",
patch_global_enabled() ? "true" : "false");
http_send_json(fd, 200, body);
return 1;
}
return 0;
}
+5
View File
@@ -6,4 +6,9 @@
int cr_api_patches_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len);
/* Applies one patch entry by its stable entryId. force=1 bypasses the version
* guard. err gets a short machine-readable reason on failure. */
int patch_apply_by_entry_id(const char *title_id, const char *entry_id, int force,
char *err, size_t err_size);
#endif /* CR_API_PATCHES_H */
+296
View File
@@ -0,0 +1,296 @@
#include <mutex>
#include <string>
#include <utility>
#include <vector>
extern "C" {
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cr_cheat_profiles.h"
#include "cr_cheats.h"
#include "cr_game_monitor.h"
#include "cr_log.h"
#include "cr_notifications.h"
#include "cr_paths.h"
#include "cr_titles.h"
#include "third_party/cJSON.h"
}
namespace {
struct CheatProfile {
std::string titleId;
std::vector<std::string> mods;
};
class CheatProfileStore {
public:
static CheatProfileStore &instance() {
static CheatProfileStore inst;
return inst;
}
void load() {
std::lock_guard<std::mutex> lk(lock_);
profiles_.clear();
char *txt = nullptr;
if (read_file_text(CHEATRUNNER_CHEAT_PROFILES_PATH, &txt) != 0 || !txt) {
return;
}
cJSON *root = cJSON_Parse(txt);
free(txt);
if (!root || !cJSON_IsArray(root)) {
cJSON_Delete(root);
return;
}
int loaded = 0;
cJSON *item = nullptr;
cJSON_ArrayForEach(item, root) {
cJSON *tid_j = cJSON_GetObjectItem(item, "titleId");
cJSON *mods_j = cJSON_GetObjectItem(item, "mods");
if (!cJSON_IsString(tid_j) || !tid_j->valuestring || !cJSON_IsArray(mods_j)) {
continue;
}
char norm[10] = {0};
if (!title_id_normalize(tid_j->valuestring, norm)) {
continue;
}
CheatProfile p;
p.titleId = norm;
cJSON *m = nullptr;
cJSON_ArrayForEach(m, mods_j) {
if (cJSON_IsString(m) && m->valuestring) {
p.mods.emplace_back(m->valuestring);
}
}
if (!p.mods.empty()) {
profiles_.push_back(std::move(p));
loaded++;
}
}
cJSON_Delete(root);
if (loaded > 0) {
cr_log("info", "cheat_profiles", "loaded %d autoload profile(s) from disk", loaded);
}
}
void save(const std::string &titleId, const std::vector<std::string> &mods) {
std::lock_guard<std::mutex> lk(lock_);
auto it = findLocked(titleId);
if (mods.empty()) {
if (it != profiles_.end()) {
profiles_.erase(it);
}
} else if (it != profiles_.end()) {
it->mods = mods;
} else {
CheatProfile p;
p.titleId = titleId;
p.mods = mods;
profiles_.push_back(std::move(p));
}
saveLocked();
}
bool getMods(const std::string &titleId, std::vector<std::string> &out) {
std::lock_guard<std::mutex> lk(lock_);
auto it = findLocked(titleId);
if (it == profiles_.end()) {
return false;
}
out = it->mods;
return true;
}
void clear(const std::string &titleId) {
std::lock_guard<std::mutex> lk(lock_);
auto it = findLocked(titleId);
if (it != profiles_.end()) {
profiles_.erase(it);
saveLocked();
}
}
bool has(const std::string &titleId) {
std::lock_guard<std::mutex> lk(lock_);
return findLocked(titleId) != profiles_.end();
}
private:
std::vector<CheatProfile>::iterator findLocked(const std::string &titleId) {
for (auto it = profiles_.begin(); it != profiles_.end(); ++it) {
if (it->titleId == titleId) {
return it;
}
}
return profiles_.end();
}
void saveLocked() {
cJSON *root = cJSON_CreateArray();
if (!root) {
return;
}
for (const auto &p : profiles_) {
cJSON *item = cJSON_CreateObject();
if (!item) {
continue;
}
cJSON_AddStringToObject(item, "titleId", p.titleId.c_str());
cJSON *arr = cJSON_AddArrayToObject(item, "mods");
for (const auto &name : p.mods) {
cJSON_AddItemToArray(arr, cJSON_CreateString(name.c_str()));
}
cJSON_AddItemToArray(root, item);
}
char *txt = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!txt) {
return;
}
write_file_atomic(CHEATRUNNER_CHEAT_PROFILES_PATH, (const uint8_t *)txt, strlen(txt));
free(txt);
}
std::vector<CheatProfile> profiles_;
std::mutex lock_;
};
/* apply_cheat_json's cooldown/stability gates are shared, so retry per mod. */
void
autoloadWorker(std::string titleId, pid_t pid, std::vector<std::string> mods) {
const int kRetryMs = 1000;
const uint64_t kDeadlineMs = 45000;
uint64_t started = now_ms();
for (const auto &name : mods) {
for (;;) {
if (now_ms() - started > kDeadlineMs) {
cr_log("warn", "cheat_profiles",
"autoload gave up after %llums title=%s remaining_mod='%s'",
(unsigned long long)kDeadlineMs, titleId.c_str(), name.c_str());
return;
}
running_game_state_t st;
running_state_get(&st);
char norm[10] = {0};
if (!st.running || st.pid != pid ||
!title_id_normalize(st.title_id, norm) || titleId != norm) {
cr_log("info", "cheat_profiles", "autoload aborted (game changed/exited) title=%s",
titleId.c_str());
return;
}
int idx = cheat_find_mod_index_by_name(titleId.c_str(), name.c_str());
if (idx < 0) {
cr_log("warn", "cheat_profiles",
"autoload: mod '%s' not found in current cheat file title=%s — skipping",
name.c_str(), titleId.c_str());
break;
}
char err[256] = {0};
int rc = apply_cheat_json(titleId.c_str(), idx, 1, err, sizeof(err));
if (rc == 0) {
cr_log("info", "cheat_profiles", "autoload: enabled '%s' title=%s", name.c_str(), titleId.c_str());
break;
}
bool retryable = (rc == -2) || !strcmp(err, "app_not_stable");
if (!retryable) {
cr_log("warn", "cheat_profiles", "autoload: mod '%s' failed title=%s: %s",
name.c_str(), titleId.c_str(), err);
break;
}
usleep((useconds_t)kRetryMs * 1000);
}
}
}
} // namespace
extern "C" void
cheat_profiles_load(void) {
CheatProfileStore::instance().load();
}
extern "C" int
cheat_profiles_save(const char *title_id, const char *const *mod_names, int count) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return -1;
}
std::vector<std::string> mods;
for (int i = 0; i < count; i++) {
if (mod_names[i] && mod_names[i][0]) {
mods.emplace_back(mod_names[i]);
}
}
CheatProfileStore::instance().save(norm, mods);
return 0;
}
extern "C" int
cheat_profiles_get_json(const char *title_id, char *out, size_t out_sz) {
if (!out || out_sz == 0) {
return -1;
}
char norm[10] = {0};
std::vector<std::string> mods;
if (title_id && title_id_normalize(title_id, norm)) {
CheatProfileStore::instance().getMods(norm, mods);
}
cJSON *arr = cJSON_CreateArray();
if (!arr) {
snprintf(out, out_sz, "[]");
return -1;
}
for (const auto &name : mods) {
cJSON_AddItemToArray(arr, cJSON_CreateString(name.c_str()));
}
char *txt = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (!txt) {
snprintf(out, out_sz, "[]");
return -1;
}
snprintf(out, out_sz, "%s", txt);
free(txt);
return 0;
}
extern "C" void
cheat_profiles_clear(const char *title_id) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return;
}
CheatProfileStore::instance().clear(norm);
}
extern "C" int
cheat_profiles_has(const char *title_id) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return 0;
}
return CheatProfileStore::instance().has(norm) ? 1 : 0;
}
extern "C" void
cheat_profiles_autoload_for_title(const char *title_id, pid_t pid) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return;
}
std::vector<std::string> mods;
if (!CheatProfileStore::instance().getMods(norm, mods) || mods.empty()) {
return;
}
cr_log("info", "cheat_profiles", "autoload profile found (%zu mod(s)) title=%s — applying",
mods.size(), norm);
notify("CheatRunner: Auto-loading (%zu) cheats for %s", mods.size(), norm);
autoloadWorker(std::string(norm), pid, std::move(mods));
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef CR_CHEAT_PROFILES_H
#define CR_CHEAT_PROFILES_H
#include <stddef.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Loads saved profiles from disk; call once at boot. */
void cheat_profiles_load(void);
/* Replaces the saved autoload profile for title_id. count==0 clears it. */
int cheat_profiles_save(const char *title_id, const char *const *mod_names, int count);
/* Writes the saved profile as a JSON array of mod-name strings, e.g. ["A","B"]. */
int cheat_profiles_get_json(const char *title_id, char *out, size_t out_sz);
void cheat_profiles_clear(const char *title_id);
int cheat_profiles_has(const char *title_id);
/* No-op if no profile saved. Blocks until done — call from a background thread. */
void cheat_profiles_autoload_for_title(const char *title_id, pid_t pid);
#ifdef __cplusplus
}
#endif
#endif /* CR_CHEAT_PROFILES_H */
+53
View File
@@ -559,6 +559,59 @@ find_cheat_file_for_title(const char *title_id, char *out, size_t out_size, int
return 1;
}
/* Resolves a mod's saved name back to its index in title_id's currently-selected
* cheat file. Indices shift when a file is re-downloaded/updated, so callers that
* persist mod identity across sessions (cheat autoload profiles) must key by name,
* not index, and re-resolve at apply time. Returns -1 if not found. */
int
cheat_find_mod_index_by_name(const char *title_id, const char *mod_name) {
if (!mod_name || !mod_name[0]) return -1;
char path[256];
int kind = 0;
if (!find_cheat_file_for_title(title_id, path, sizeof(path), &kind)) return -1;
char *txt = NULL;
if (read_file_text(path, &txt) != 0 || !txt) return -1;
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 if (kind == 3) {
char *xml = mc4_decrypt_to_xml(txt, strlen(txt), NULL);
free(txt);
if (!xml) return -1;
json_txt = shn_xml_to_json(xml, strlen(xml));
free(xml);
} else {
free(txt);
}
if (!json_txt) return -1;
cJSON *root = cJSON_Parse(json_txt);
free(json_txt);
if (!root) return -1;
int found = -1;
cJSON *mods = cJSON_GetObjectItem(root, "mods");
if (cJSON_IsArray(mods)) {
int idx = 0;
cJSON *m = NULL;
cJSON_ArrayForEach(m, mods) {
cJSON *name_j = cJSON_GetObjectItem(m, "name");
if (cJSON_IsString(name_j) && name_j->valuestring && !strcmp(name_j->valuestring, mod_name)) {
found = idx;
break;
}
idx++;
}
}
cJSON_Delete(root);
return found;
}
/* Fills *ctx_out with all candidates + best selection.
* override_ver: if non-NULL and non-empty, used as preferred_ver (e.g. live game version);
* otherwise falls back to static SFO lookup. */
+1
View File
@@ -118,6 +118,7 @@ extern volatile uint64_t g_last_apply_at_ms;
extern volatile uint64_t g_post_apply_guard_until_ms;
int find_cheat_file_for_title(const char *title_id, char *out, size_t out_size, int *kind_out);
int cheat_find_mod_index_by_name(const char *title_id, const char *mod_name);
int find_cheat_candidates(const char *title_id, cheat_file_search_t *ctx_out);
/* Like find_cheat_candidates but uses override_ver (if non-NULL/non-empty) instead of static SFO lookup. */
int find_cheat_candidates_ex(const char *title_id, const char *override_ver, cheat_file_search_t *ctx_out);
+6
View File
@@ -65,6 +65,7 @@ cheatrunner_config_t g_cfg = {
.cheat_inter_mod_delay_ms = 0,
.fan_min_c = 30,
.fan_max_c = 90,
.tile_autoinstall_enabled = 1,
};
void
@@ -121,6 +122,7 @@ config_set_defaults(cheatrunner_config_t *cfg) {
.cheat_inter_mod_delay_ms = 0,
.fan_min_c = 30,
.fan_max_c = 90,
.tile_autoinstall_enabled = 1,
};
}
@@ -176,6 +178,7 @@ config_save_locked(void) {
"cheat_inter_mod_delay_ms=%d\n"
"fan_min_c=%d\n"
"fan_max_c=%d\n"
"tile_autoinstall_enabled=%d\n"
"theme=%s\n",
g_cfg.http_port, g_cfg.auto_load_cheat_menu,
g_cfg.auto_download_missing_cheat, g_cfg.launch_kill_current,
@@ -201,6 +204,7 @@ config_save_locked(void) {
g_cfg.cheat_master_code_fixup,
g_cfg.cheat_addr_cache_enabled, g_cfg.cheat_inter_mod_delay_ms,
g_cfg.fan_min_c, g_cfg.fan_max_c,
g_cfg.tile_autoinstall_enabled,
g_cfg.theme);
if (n <= 0 || (size_t)n >= sizeof(txt)) {
return -1;
@@ -348,6 +352,8 @@ config_load(void) {
} else if (!strcmp(k, "fan_max_c")) {
int cv = atoi(v);
g_cfg.fan_max_c = (cv >= 50 && cv <= 100) ? cv : 90;
} else if (!strcmp(k, "tile_autoinstall_enabled")) {
g_cfg.tile_autoinstall_enabled = atoi(v) ? 1 : 0;
}
}
fclose(fp);
+1
View File
@@ -56,6 +56,7 @@ typedef struct cheatrunner_config {
int cheat_inter_mod_delay_ms;
int fan_min_c; /* lower bound for the fan-on threshold slider (default 30) */
int fan_max_c; /* upper bound for the fan-on threshold slider (default 90) */
int tile_autoinstall_enabled;
} cheatrunner_config_t;
extern pthread_mutex_t g_cfg_lock;
+10 -4
View File
@@ -1,12 +1,12 @@
#include <arpa/inet.h>
#include <stdint.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "cr_dns.h"
@@ -119,7 +119,7 @@ cr_dns_resolve(const char *hostname, char *ip_out, size_t ip_out_sz) {
/* Try two public DNS servers — 8.8.8.8 then 1.1.1.1 */
static const char *dns_servers[] = { "8.8.8.8", "1.1.1.1", NULL };
uint16_t qid = (uint16_t)((uint32_t)time(NULL) ^ (uint32_t)(uintptr_t)hostname);
uint16_t qid = (uint16_t)arc4random();
uint8_t query[512], resp[4096];
int qlen = build_query(hostname, query, sizeof(query), qid);
if (qlen < 0) return -1;
@@ -138,8 +138,14 @@ cr_dns_resolve(const char *hostname, char *ip_out, size_t ip_out_sz) {
dns_addr.sin_port = htons(53);
dns_addr.sin_addr.s_addr = inet_addr(dns_servers[si]);
ssize_t sent = sendto(sock, query, (size_t)qlen, 0,
(struct sockaddr *)&dns_addr, sizeof(dns_addr));
/* connect() makes the kernel drop any reply not actually from this
* server/port instead of accepting a spoofed datagram from anyone. */
if (connect(sock, (struct sockaddr *)&dns_addr, sizeof(dns_addr)) != 0) {
close(sock);
continue;
}
ssize_t sent = send(sock, query, (size_t)qlen, 0);
if (sent != qlen) { close(sock); continue; }
ssize_t n = recv(sock, resp, sizeof(resp), 0);
+19 -3
View File
@@ -264,6 +264,21 @@ fan_set_request(int fd, const char *query) {
send_obj(fd, 200, r);
}
static void
fan_reset_request(int fd) {
/* Unpin only — the firmware already resets the fan override to automatic
* on the next app launch; the watcher just needs to stop re-pinning it. */
atomic_store(&g_pinned_threshold_c, 0);
fan_save_threshold(0);
cr_log("info", "fan", "threshold unpinned — returning to automatic on next app launch");
cJSON *r = cJSON_CreateObject();
if (!r) { http_send_json(fd, 500, "{\"ok\":false}"); return; }
cJSON_AddBoolToObject(r, "ok", 1);
cJSON_AddBoolToObject(r, "pinned", 0);
send_obj(fd, 200, r);
}
static void
fan_info_request(int fd) {
cJSON *r = cJSON_CreateObject();
@@ -282,8 +297,9 @@ int
cr_api_fan_handle(int fd, const char *method, const char *path,
const char *query, const char *body, size_t body_len) {
(void)method; (void)body; (void)body_len;
if (!strcmp(path, "/api/fan/temp")) { fan_temp_request(fd); return 1; }
if (!strcmp(path, "/api/fan/set")) { fan_set_request(fd, query); return 1; }
if (!strcmp(path, "/api/fan")) { fan_info_request(fd); return 1; }
if (!strcmp(path, "/api/fan/temp")) { fan_temp_request(fd); return 1; }
if (!strcmp(path, "/api/fan/set")) { fan_set_request(fd, query); return 1; }
if (!strcmp(path, "/api/fan/reset")) { fan_reset_request(fd); return 1; }
if (!strcmp(path, "/api/fan")) { fan_info_request(fd); return 1; }
return 0;
}
+34
View File
@@ -12,7 +12,9 @@
#include <ps5/kernel.h>
#include "cr_addr_cache.h"
#include "cr_cheat_profiles.h"
#include "cr_cheats.h"
#include "cr_patch_profiles.h"
#include "cr_launch.h"
#include "cr_patch_parser.h"
#include "cr_config.h"
@@ -293,6 +295,37 @@ get_current_title(char *out, size_t out_size) {
}
typedef struct {
char title_id[16];
pid_t pid;
} autoload_seq_ctx_t;
/* Patches before cheats, off the monitor thread — both calls now block. */
static void *
autoload_sequence_thread(void *arg) {
autoload_seq_ctx_t *ctx = (autoload_seq_ctx_t *)arg;
patch_profiles_autoload_for_title(ctx->title_id, ctx->pid);
cheat_profiles_autoload_for_title(ctx->title_id, ctx->pid);
free(ctx);
return NULL;
}
static void
autoload_profiles_for_title(const char *title_id, pid_t pid) {
autoload_seq_ctx_t *ctx = (autoload_seq_ctx_t *)malloc(sizeof(*ctx));
if (!ctx) {
return;
}
snprintf(ctx->title_id, sizeof(ctx->title_id), "%s", title_id ? title_id : "");
ctx->pid = pid;
pthread_t t;
if (pthread_create(&t, NULL, autoload_sequence_thread, ctx) != 0) {
free(ctx);
return;
}
pthread_detach(t);
}
void
rpc_refresh_title_and_notify(void) {
running_game_state_t cur;
@@ -425,6 +458,7 @@ rpc_refresh_title_and_notify(void) {
cr_log("info", "game", "game started %s", cur.title_id);
launch_status_recover_for_game(cur.title_id);
addr_cache_clear_for_title(cur.title_id);
autoload_profiles_for_title(cur.title_id, cur.pid);
}
}
+118 -117
View File
@@ -25,7 +25,6 @@
static volatile int g_http_listen_notified = 0;
char g_listen_ip[64] = "127.0.0.1";
int g_http_listen_fd = -1;
_Thread_local int g_http_keep_alive = 0;
#define HTTP_MAX_CONCURRENT 24
static volatile int g_active_clients = 0;
@@ -129,6 +128,25 @@ parse_content_length_header(const char *headers) {
return (size_t)v;
}
static int
request_accepts_gzip(const char *headers) {
if (!headers || !headers[0]) {
return 0;
}
const char *line = strcasestr(headers, "Accept-Encoding:");
if (!line) {
return 0;
}
const char *eol = strpbrk(line, "\r\n");
size_t line_len = eol ? (size_t)(eol - line) : strlen(line);
for (size_t i = 0; i + 4 <= line_len; i++) {
if (strncasecmp(line + i, "gzip", 4) == 0) {
return 1;
}
}
return 0;
}
static void
http_send_json(int fd, int status, const char *body) {
const char *payload = body ? body : "{\"ok\":false}";
@@ -137,12 +155,11 @@ http_send_json(int fd, int status, const char *body) {
"HTTP/1.1 %d OK\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %u\r\n"
"Connection: %s\r\n"
"Connection: close\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Cache-Control: no-cache\r\n"
"\r\n",
status, (unsigned int)strlen(payload),
g_http_keep_alive ? "keep-alive" : "close");
status, (unsigned int)strlen(payload));
if (n > 0) {
(void)socket_send_all(fd, header, (size_t)n);
(void)socket_send_all(fd, payload, strlen(payload));
@@ -168,142 +185,126 @@ http_grow_buf(char **buf, size_t *cap, size_t need) {
static void
http_handle_client(int fd, const char *client_ip) {
/* Most requests (polling, toggles) are a few KB — start small and grow only
* for the rare large upload, instead of malloc'ing MAX_REQ_SIZE every time.
* The buffer is reused across requests on a kept-alive connection. */
* for the rare large upload, instead of malloc'ing MAX_REQ_SIZE every time. */
size_t cap = 65536;
char *req = malloc(cap + 1);
if (!req) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}");
return;
}
size_t off = 0;
size_t body_len = 0;
struct timeval tv;
tv.tv_sec = 20;
tv.tv_sec = 10;
tv.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
for (;;) {
size_t off = 0;
size_t body_len = 0;
for (;;) {
if (off >= MAX_REQ_SIZE) {
break;
}
if (off >= cap && http_grow_buf(&req, &cap, off + 1) != 0) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}");
free(req);
return;
}
int n = recv(fd, req + off, cap - off, 0);
if (n <= 0) {
if (n < 0 && errno == EINTR) {
continue;
}
/* Peer closed, or idle timeout waiting for the next request on a
* kept-alive connection — either way, nothing more to do here. */
free(req);
return;
}
off += (size_t)n;
req[off] = '\0';
if (strstr(req, "\r\n\r\n") || strstr(req, "\n\n")) {
break;
}
if (off >= MAX_REQ_SIZE) {
break;
}
if (off == 0) {
free(req);
return;
}
char *headers_end = strstr(req, "\r\n\r\n");
size_t sep_len = 4;
if (!headers_end) {
headers_end = strstr(req, "\n\n");
sep_len = 2;
}
if (!headers_end) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad request\"}");
free(req);
return;
}
size_t header_bytes = (size_t)(headers_end - req);
req[header_bytes] = '\0';
size_t already_body = off > (header_bytes + sep_len) ? (off - (header_bytes + sep_len)) : 0;
size_t content_len = parse_content_length_header(req);
size_t used = header_bytes + sep_len;
if (used >= MAX_REQ_SIZE || content_len > MAX_REQ_SIZE - used - 1) {
http_send_json(fd, 413, "{\"ok\":false,\"error\":\"payload_too_large\"}");
free(req);
return;
}
/* Headers parsed — grow straight to the full known size up front, since
* pointers taken before a realloc would otherwise dangle. */
if (http_grow_buf(&req, &cap, used + content_len) != 0) {
if (off >= cap && http_grow_buf(&req, &cap, off + 1) != 0) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}");
free(req);
return;
}
while (already_body < content_len) {
if (off >= MAX_REQ_SIZE) {
http_send_json(fd, 413, "{\"ok\":false,\"error\":\"payload_too_large\"}");
int n = recv(fd, req + off, cap - off, 0);
if (n <= 0) {
if (n < 0 && errno == EINTR) {
continue;
}
if (off == 0) {
free(req);
return;
}
int n = recv(fd, req + off, cap - off, 0);
if (n <= 0) {
if (n < 0 && errno == EINTR) {
continue;
}
break;
}
off += (size_t)n;
already_body += (size_t)n;
break;
}
if (already_body < content_len) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"incomplete_body\"}");
free(req);
return;
off += (size_t)n;
req[off] = '\0';
if (strstr(req, "\r\n\r\n") || strstr(req, "\n\n")) {
break;
}
body_len = content_len;
char *body_ptr = req + header_bytes + sep_len;
body_ptr[body_len] = '\0';
char method[8] = {0};
char target[1024] = {0};
char version[16] = {0};
if (sscanf(req, "%7s %1023s %15s", method, target, version) != 3) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad request\"}");
free(req);
return;
}
char *query = strchr(target, '?');
if (query) {
*query = '\0';
query++;
} else {
query = "";
}
/* HTTP/1.1 defaults to a persistent connection unless the client asks to
* close; HTTP/1.0 is the opposite (needs an explicit keep-alive ask). */
int wants_close = strcasestr(req, "Connection: close") != NULL;
if (!strcasecmp(version, "HTTP/1.0") && !strcasestr(req, "Connection: keep-alive")) {
wants_close = 1;
}
g_http_keep_alive = !wants_close;
http_route(fd, method, target, query, client_ip ? client_ip : "unknown",
body_ptr ? body_ptr : "", body_len);
if (!g_http_keep_alive) {
free(req);
return;
}
/* Loop back and read the next request off the same connection. */
}
if (off == 0) {
free(req);
return;
}
char *headers_end = strstr(req, "\r\n\r\n");
size_t sep_len = 4;
if (!headers_end) {
headers_end = strstr(req, "\n\n");
sep_len = 2;
}
if (!headers_end) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad request\"}");
free(req);
return;
}
size_t header_bytes = (size_t)(headers_end - req);
req[header_bytes] = '\0';
size_t already_body = off > (header_bytes + sep_len) ? (off - (header_bytes + sep_len)) : 0;
size_t content_len = parse_content_length_header(req);
size_t used = header_bytes + sep_len;
if (used >= MAX_REQ_SIZE || content_len > MAX_REQ_SIZE - used - 1) {
http_send_json(fd, 413, "{\"ok\":false,\"error\":\"payload_too_large\"}");
free(req);
return;
}
/* Headers parsed — grow straight to the full known size up front, since
* pointers taken before a realloc would otherwise dangle. */
if (http_grow_buf(&req, &cap, used + content_len) != 0) {
http_send_json(fd, 500, "{\"ok\":false,\"error\":\"alloc\"}");
free(req);
return;
}
while (already_body < content_len) {
if (off >= MAX_REQ_SIZE) {
http_send_json(fd, 413, "{\"ok\":false,\"error\":\"payload_too_large\"}");
free(req);
return;
}
int n = recv(fd, req + off, cap - off, 0);
if (n <= 0) {
if (n < 0 && errno == EINTR) {
continue;
}
break;
}
off += (size_t)n;
already_body += (size_t)n;
}
if (already_body < content_len) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"incomplete_body\"}");
free(req);
return;
}
body_len = content_len;
char *body_ptr = req + header_bytes + sep_len;
body_ptr[body_len] = '\0';
char method[8] = {0};
char target[1024] = {0};
char version[16] = {0};
if (sscanf(req, "%7s %1023s %15s", method, target, version) != 3) {
http_send_json(fd, 400, "{\"ok\":false,\"error\":\"bad request\"}");
free(req);
return;
}
char *query = strchr(target, '?');
if (query) {
*query = '\0';
query++;
} else {
query = "";
}
int accepts_gzip = request_accepts_gzip(req);
http_route(fd, method, target, query, client_ip ? client_ip : "unknown",
body_ptr ? body_ptr : "", body_len, accepts_gzip);
free(req);
}
static void
@@ -369,7 +370,7 @@ http_server_thread(void *arg) {
continue;
}
bind_fails = 0;
if (listen(listen_fd, 8) != 0) {
if (listen(listen_fd, 32) != 0) {
log_msg("[HTTP] listen() failed: %d", errno);
notify("CheatRunner HTTP listen failed on %d", http_port);
close(listen_fd);
-2
View File
@@ -3,8 +3,6 @@
extern char g_listen_ip[64];
extern int g_http_listen_fd;
/* Per-connection (thread-local) keep-alive decision for the response being built. */
extern _Thread_local int g_http_keep_alive;
void *http_server_thread(void *arg);
void *http_net_watchdog_thread(void *arg);
-33
View File
@@ -536,39 +536,6 @@ patch_file_delete(const char *name, int dir_idx) {
return unlink(path) == 0 ? 0 : -1;
}
/* Returns 1 if at least one live patch directory exists (patches globally enabled). */
int
patch_global_enabled(void) {
struct stat st;
for (int i = 0; i < K_PF_NDIRS; i++) {
if (stat(k_pf_dirs[i].dir, &st) == 0 && S_ISDIR(st.st_mode))
return 1;
}
return 0;
}
/* Enable or disable all XML patch directories at once by renaming dir <-> dir.off.
* Invalidates the patch index so the next lookup rescans. */
void
patch_global_set(int on) {
struct stat st;
for (int i = 0; i < K_PF_NDIRS; i++) {
const char *live = k_pf_dirs[i].dir;
char off[512];
snprintf(off, sizeof(off), "%s.off", live);
if (on) {
if (stat(off, &st) == 0 && S_ISDIR(st.st_mode))
rename(off, live);
else if (stat(live, &st) != 0)
mkdir(live, 0755);
} else {
if (stat(live, &st) == 0 && S_ISDIR(st.st_mode))
rename(live, off);
}
}
patch_index_invalidate();
}
/* Extract all <ID>...</ID> values from the <TitleID> block and add (id, path, kind)
* entries to the index. Allows duplicate (title_id, path) pairs to be skipped. */
static void
-5
View File
@@ -126,11 +126,6 @@ void patch_clear_backups_for_pid(pid_t pid);
/* Force a rescan of the patch directories on next lookup; also clears applied state. */
void patch_index_invalidate(void);
/* Global enable/disable: rename all live patch dirs to <dir>.off (off) or back (on).
* patch_global_enabled() returns 1 if at least one live patch directory exists. */
int patch_global_enabled(void);
void patch_global_set(int on);
/* List/toggle/delete patch XML files; dir_idx: 0=xml_prospero, 1=xml, 2=external. Toggle renames .xml <-> .xml.off. */
char *patch_files_list_json(void);
int patch_file_toggle(const char *name, int on, int dir_idx);
+292
View File
@@ -0,0 +1,292 @@
#include <mutex>
#include <string>
#include <utility>
#include <vector>
extern "C" {
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cr_api_patches.h"
#include "cr_game_monitor.h"
#include "cr_log.h"
#include "cr_notifications.h"
#include "cr_patch_profiles.h"
#include "cr_paths.h"
#include "cr_titles.h"
#include "third_party/cJSON.h"
}
namespace {
struct PatchProfile {
std::string titleId;
std::vector<std::string> entryIds;
};
class PatchProfileStore {
public:
static PatchProfileStore &instance() {
static PatchProfileStore inst;
return inst;
}
void load() {
std::lock_guard<std::mutex> lk(lock_);
profiles_.clear();
char *txt = nullptr;
if (read_file_text(CHEATRUNNER_PATCH_PROFILES_PATH, &txt) != 0 || !txt) {
return;
}
cJSON *root = cJSON_Parse(txt);
free(txt);
if (!root || !cJSON_IsArray(root)) {
cJSON_Delete(root);
return;
}
int loaded = 0;
cJSON *item = nullptr;
cJSON_ArrayForEach(item, root) {
cJSON *tid_j = cJSON_GetObjectItem(item, "titleId");
cJSON *eids_j = cJSON_GetObjectItem(item, "entryIds");
if (!cJSON_IsString(tid_j) || !tid_j->valuestring || !cJSON_IsArray(eids_j)) {
continue;
}
char norm[10] = {0};
if (!title_id_normalize(tid_j->valuestring, norm)) {
continue;
}
PatchProfile p;
p.titleId = norm;
cJSON *m = nullptr;
cJSON_ArrayForEach(m, eids_j) {
if (cJSON_IsString(m) && m->valuestring) {
p.entryIds.emplace_back(m->valuestring);
}
}
if (!p.entryIds.empty()) {
profiles_.push_back(std::move(p));
loaded++;
}
}
cJSON_Delete(root);
if (loaded > 0) {
cr_log("info", "patch_profiles", "loaded %d autoload profile(s) from disk", loaded);
}
}
void save(const std::string &titleId, const std::vector<std::string> &entryIds) {
std::lock_guard<std::mutex> lk(lock_);
auto it = findLocked(titleId);
if (entryIds.empty()) {
if (it != profiles_.end()) {
profiles_.erase(it);
}
} else if (it != profiles_.end()) {
it->entryIds = entryIds;
} else {
PatchProfile p;
p.titleId = titleId;
p.entryIds = entryIds;
profiles_.push_back(std::move(p));
}
saveLocked();
}
bool getEntryIds(const std::string &titleId, std::vector<std::string> &out) {
std::lock_guard<std::mutex> lk(lock_);
auto it = findLocked(titleId);
if (it == profiles_.end()) {
return false;
}
out = it->entryIds;
return true;
}
void clear(const std::string &titleId) {
std::lock_guard<std::mutex> lk(lock_);
auto it = findLocked(titleId);
if (it != profiles_.end()) {
profiles_.erase(it);
saveLocked();
}
}
bool has(const std::string &titleId) {
std::lock_guard<std::mutex> lk(lock_);
return findLocked(titleId) != profiles_.end();
}
private:
std::vector<PatchProfile>::iterator findLocked(const std::string &titleId) {
for (auto it = profiles_.begin(); it != profiles_.end(); ++it) {
if (it->titleId == titleId) {
return it;
}
}
return profiles_.end();
}
void saveLocked() {
cJSON *root = cJSON_CreateArray();
if (!root) {
return;
}
for (const auto &p : profiles_) {
cJSON *item = cJSON_CreateObject();
if (!item) {
continue;
}
cJSON_AddStringToObject(item, "titleId", p.titleId.c_str());
cJSON *arr = cJSON_AddArrayToObject(item, "entryIds");
for (const auto &eid : p.entryIds) {
cJSON_AddItemToArray(arr, cJSON_CreateString(eid.c_str()));
}
cJSON_AddItemToArray(root, item);
}
char *txt = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!txt) {
return;
}
write_file_atomic(CHEATRUNNER_PATCH_PROFILES_PATH, (const uint8_t *)txt, strlen(txt));
free(txt);
}
std::vector<PatchProfile> profiles_;
std::mutex lock_;
};
/* Keyed by entryId (stable across reloads), not array index. No force-retry on version mismatch. */
void
autoloadWorker(std::string titleId, pid_t pid, std::vector<std::string> entryIds) {
const int kRetryMs = 1500;
const uint64_t kDeadlineMs = 45000;
uint64_t started = now_ms();
for (const auto &eid : entryIds) {
for (;;) {
if (now_ms() - started > kDeadlineMs) {
cr_log("warn", "patch_profiles",
"autoload gave up after %llums title=%s remaining_entry=%s",
(unsigned long long)kDeadlineMs, titleId.c_str(), eid.c_str());
return;
}
running_game_state_t st;
running_state_get(&st);
char norm[10] = {0};
if (!st.running || st.pid != pid ||
!title_id_normalize(st.title_id, norm) || titleId != norm) {
cr_log("info", "patch_profiles", "autoload aborted (game changed/exited) title=%s",
titleId.c_str());
return;
}
char err[64] = {0};
int rc = patch_apply_by_entry_id(titleId.c_str(), eid.c_str(), 0, err, sizeof(err));
if (rc == 0) {
cr_log("info", "patch_profiles", "autoload: applied entry=%s title=%s",
eid.c_str(), titleId.c_str());
usleep(500 * 1000); /* pace consecutive applies to match cheat_apply_cooldown_ms's default (cr_config.c) — patch_apply has no cooldown of its own */
break;
}
bool permanent = !strcmp(err, "entry_not_found") || !strcmp(err, "no_supported_lines") ||
!strcmp(err, "unsupported_line_type") || !strcmp(err, "version_mismatch") ||
!strcmp(err, "patch_not_found");
if (permanent) {
cr_log("warn", "patch_profiles", "autoload: entry=%s failed title=%s: %s",
eid.c_str(), titleId.c_str(), err);
break;
}
usleep((useconds_t)kRetryMs * 1000);
}
}
}
} // namespace
extern "C" void
patch_profiles_load(void) {
PatchProfileStore::instance().load();
}
extern "C" int
patch_profiles_save(const char *title_id, const char *const *entry_ids, int count) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return -1;
}
std::vector<std::string> ids;
for (int i = 0; i < count; i++) {
if (entry_ids[i] && entry_ids[i][0]) {
ids.emplace_back(entry_ids[i]);
}
}
PatchProfileStore::instance().save(norm, ids);
return 0;
}
extern "C" int
patch_profiles_get_json(const char *title_id, char *out, size_t out_sz) {
if (!out || out_sz == 0) {
return -1;
}
char norm[10] = {0};
std::vector<std::string> ids;
if (title_id && title_id_normalize(title_id, norm)) {
PatchProfileStore::instance().getEntryIds(norm, ids);
}
cJSON *arr = cJSON_CreateArray();
if (!arr) {
snprintf(out, out_sz, "[]");
return -1;
}
for (const auto &eid : ids) {
cJSON_AddItemToArray(arr, cJSON_CreateString(eid.c_str()));
}
char *txt = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (!txt) {
snprintf(out, out_sz, "[]");
return -1;
}
snprintf(out, out_sz, "%s", txt);
free(txt);
return 0;
}
extern "C" void
patch_profiles_clear(const char *title_id) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return;
}
PatchProfileStore::instance().clear(norm);
}
extern "C" int
patch_profiles_has(const char *title_id) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return 0;
}
return PatchProfileStore::instance().has(norm) ? 1 : 0;
}
extern "C" void
patch_profiles_autoload_for_title(const char *title_id, pid_t pid) {
char norm[10] = {0};
if (!title_id || !title_id_normalize(title_id, norm)) {
return;
}
std::vector<std::string> ids;
if (!PatchProfileStore::instance().getEntryIds(norm, ids) || ids.empty()) {
return;
}
cr_log("info", "patch_profiles", "autoload profile found (%zu entr%s) title=%s — applying",
ids.size(), ids.size() == 1 ? "y" : "ies", norm);
notify("CheatRunner: Auto-loading (%zu) patches for %s", ids.size(), norm);
autoloadWorker(std::string(norm), pid, std::move(ids));
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef CR_PATCH_PROFILES_H
#define CR_PATCH_PROFILES_H
#include <stddef.h>
#include <sys/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Loads saved profiles from disk; call once at boot. */
void patch_profiles_load(void);
/* Replaces the saved autoload profile for title_id. count==0 clears it. */
int patch_profiles_save(const char *title_id, const char *const *entry_ids, int count);
/* Writes the saved profile as a JSON array of entryId strings, e.g. ["A1B2C3D4"]. */
int patch_profiles_get_json(const char *title_id, char *out, size_t out_sz);
void patch_profiles_clear(const char *title_id);
int patch_profiles_has(const char *title_id);
/* No-op if no profile saved. Blocks until done — call from a background thread. */
void patch_profiles_autoload_for_title(const char *title_id, pid_t pid);
#ifdef __cplusplus
}
#endif
#endif /* CR_PATCH_PROFILES_H */
+2
View File
@@ -32,6 +32,8 @@
#define CHEATRUNNER_TITLE_PREFS_PATH "/data/cheatrunner/title_prefs.json"
#define CHEATRUNNER_FAVORITES_PATH "/data/cheatrunner/favorites.json"
#define CHEATRUNNER_FAN_PATH "/data/cheatrunner/fan.json"
#define CHEATRUNNER_CHEAT_PROFILES_PATH "/data/cheatrunner/cheat_profiles.json"
#define CHEATRUNNER_PATCH_PROFILES_PATH "/data/cheatrunner/patch_profiles.json"
#define CHEATRUNNER_CRASH_LOG_PATH "/data/cheatrunner/crash.log"
/* General utilities */
+9
View File
@@ -6,6 +6,7 @@
#include <sys/syscall.h>
#include <unistd.h>
#include "cr_config.h"
#include "cr_log.h"
#include "cr_notifications.h"
#include "cr_paths.h"
@@ -119,6 +120,14 @@ tile_autoinstall_thread(void *arg) {
cr_log("info", "tile_pkg", "no embedded PKG at build time — skipping");
return NULL;
#else
pthread_mutex_lock(&g_cfg_lock);
int autoinstall_enabled = g_cfg.tile_autoinstall_enabled;
pthread_mutex_unlock(&g_cfg_lock);
if (!autoinstall_enabled) {
cr_log("info", "tile_pkg", "auto-install disabled in config — skipping");
return NULL;
}
/* Already installed — avoid triggering AppPrepareOverwriteByPackage every
* boot (that crashes SceShellCore's SceAppInstallerJobQueue on some FWs). */
struct stat st;
+77
View File
@@ -858,6 +858,83 @@
" font-family: var(--cr-font-mono);\n"
" }\n"
"\n"
" .cr-repo-controls {\n"
" display: flex;\n"
" flex-direction: column;\n"
" gap: 10px;\n"
" margin-top: 12px;\n"
" padding: 12px;\n"
" border: 1px solid var(--cr-line);\n"
" border-radius: var(--cr-radius-sm);\n"
" background: var(--cr-panel-2);\n"
" }\n"
" .cr-repo-source-row {\n"
" display: flex;\n"
" align-items: center;\n"
" gap: 10px;\n"
" flex-wrap: wrap;\n"
" }\n"
" .cr-repo-source-row label {\n"
" flex: 0 0 auto;\n"
" font-size: 13px;\n"
" color: var(--cr-white-2);\n"
" }\n"
" .cr-repo-select {\n"
" flex: 1 1 200px;\n"
" min-width: 160px;\n"
" width: auto;\n"
" }\n"
" .cr-repo-overwrite {\n"
" display: flex;\n"
" align-items: center;\n"
" gap: 6px;\n"
" margin-left: auto;\n"
" font-size: 13px;\n"
" color: var(--cr-muted);\n"
" white-space: nowrap;\n"
" }\n"
" .cr-repo-actions {\n"
" display: flex;\n"
" gap: 8px;\n"
" flex-wrap: wrap;\n"
" padding-top: 10px;\n"
" border-top: 1px solid var(--cr-line);\n"
" }\n"
" .cr-repo-progress {\n"
" margin-top: 12px;\n"
" }\n"
" .cr-repo-progress-track {\n"
" background: var(--cr-panel-3);\n"
" border-radius: 4px;\n"
" height: 8px;\n"
" overflow: hidden;\n"
" }\n"
" .cr-repo-progress-fill {\n"
" height: 100%;\n"
" width: 0%;\n"
" background: var(--cr-brand);\n"
" transition: width .3s;\n"
" }\n"
" .cr-repo-status-line {\n"
" margin-top: 6px;\n"
" font-size: 13px;\n"
" color: var(--cr-white-2);\n"
" }\n"
" .cr-repo-current-file {\n"
" margin-top: 2px;\n"
" font-size: 12px;\n"
" color: var(--cr-muted);\n"
" word-break: break-all;\n"
" }\n"
" .cr-repo-result {\n"
" margin-top: 10px;\n"
" font-size: 13px;\n"
" }\n"
" .cr-repo-missing {\n"
" margin-top: 4px;\n"
" font-size: 12px;\n"
" }\n"
"\n"
" .cr-cheat-row {\n"
" display: grid;\n"
" grid-template-columns: 1fr auto;\n"
+33 -23
View File
@@ -26,7 +26,7 @@
" </div>\n"
" </div>\n"
" <div class=\"topbar-right\">\n"
" <span class=\"cr-pill\" id=\"headVersion\">v0.15</span>\n"
" <span class=\"cr-pill\" id=\"headVersion\">v0.16</span>\n"
" <span class=\"cr-pill\" id=\"headRunning\">No game</span>\n"
" <span class=\"cr-pill\" id=\"headEngine\">Engine ?</span>\n"
" <button class=\"cr-gear-button\" id=\"settingsBtn\" title=\"Settings\" aria-label=\"Open settings\"><svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65c-.03-.24-.24-.42-.49-.42h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z\"/></svg><span class=\"cr-gear-label\">Settings</span></button>\n"
@@ -143,6 +143,7 @@
" <div class=\"row\" style=\"margin-top:8px;\">\n"
" <button class=\"btn compact\" id=\"fanUnitBtn\" title=\"Toggle temperature unit\">&deg;C</button>\n"
" <button class=\"btn primary compact\" id=\"fanApplyBtn\" disabled>Apply</button>\n"
" <button class=\"btn secondary compact\" id=\"fanResetBtn\" title=\"Clear the pinned threshold and return to the console's automatic fan curve\">Reset to Auto</button>\n"
" <span class=\"cr-fan-hint\" id=\"fanHint\"></span>\n"
" </div>\n"
" </div>\n"
@@ -209,28 +210,34 @@
" </div>\n"
" <div id=\"repoPanel\" class=\"cr-modal-panel\" style=\"display:none;\">\n"
" <div class=\"cheat-info\">Download cheat files from a community repository into /data/cheatrunner/cheats/.</div>\n"
" <div class=\"row\" style=\"margin-top:12px;\">\n"
" <label style=\"align-self:center;margin-right:6px;\">Source:</label>\n"
" <select id=\"repoSource\" class=\"input\" style=\"flex:0 0 auto;width:auto;\">\n"
" <option value=\"hencollection\">HEN Cheats Collection</option>\n"
" <option value=\"ps5cheats\">PS5 Cheats</option>\n"
" <option value=\"goldhen\">GoldHEN Cheats</option>\n"
" <option value=\"henppsa\">HEN PPSA Cheats</option>\n"
" </select>\n"
" <label style=\"align-self:center;margin:0 6px;\">\n"
" <input type=\"checkbox\" id=\"repoOverwrite\"> Overwrite existing\n"
" </label>\n"
" <button class=\"btn\" id=\"repoDownloadBtn\">Download Selected Source</button>\n"
" <button class=\"btn secondary\" id=\"repoDownloadAllBtn\">Download All Sources</button>\n"
" </div>\n"
" <div id=\"repoProgress\" style=\"display:none;margin-top:12px;\">\n"
" <div style=\"background:var(--bg3,#333);border-radius:4px;height:8px;overflow:hidden;\">\n"
" <div id=\"repoProgressBar\" style=\"height:100%;background:var(--accent,#4a9eff);width:0%;transition:width .3s;\"></div>\n"
" <div class=\"cr-repo-controls\">\n"
" <div class=\"cr-repo-source-row\">\n"
" <label for=\"repoSource\">Source:</label>\n"
" <select id=\"repoSource\" class=\"input cr-repo-select\">\n"
" <option value=\"hencollection\">HEN Cheats Collection</option>\n"
" <option value=\"ps5cheats\">PS5 Cheats</option>\n"
" <option value=\"goldhen\">GoldHEN Cheats</option>\n"
" <option value=\"henppsa\">HEN PPSA Cheats</option>\n"
" </select>\n"
" <label class=\"cr-repo-overwrite\">\n"
" <input type=\"checkbox\" id=\"repoOverwrite\"> Overwrite existing\n"
" </label>\n"
" </div>\n"
" <div class=\"cr-repo-actions\">\n"
" <button class=\"btn\" id=\"repoDownloadBtn\">Download Selected Source</button>\n"
" <button class=\"btn secondary\" id=\"repoDownloadAllBtn\">Download All Sources</button>\n"
" </div>\n"
" <div id=\"repoStatusLine\" style=\"margin-top:6px;font-size:13px;opacity:.85;\"></div>\n"
" <div id=\"repoCurrentFile\" style=\"margin-top:2px;font-size:12px;opacity:.6;word-break:break-all;\"></div>\n"
" </div>\n"
" <div id=\"repoResult\" style=\"margin-top:10px;font-size:13px;\"></div>\n"
" <div id=\"repoProgress\" class=\"cr-repo-progress\" style=\"display:none;\">\n"
" <div class=\"cr-repo-progress-track\">\n"
" <div id=\"repoProgressBar\" class=\"cr-repo-progress-fill\"></div>\n"
" </div>\n"
" <div id=\"repoStatusLine\" class=\"cr-repo-status-line\"></div>\n"
" <div id=\"repoCurrentFile\" class=\"cr-repo-current-file\"></div>\n"
" </div>\n"
" <div id=\"repoResult\" class=\"cr-repo-result\"></div>\n"
" <div id=\"repoMissingList\" class=\"cr-repo-missing\"></div>\n"
" <div id=\"repoLastSync\" class=\"cr-panel-note\"></div>\n"
" <div class=\"cr-panel-note\">Downloads happen in the background. Files are saved per-format to cheats/json, cheats/shn, cheats/mc4. The page remains responsive during download.</div>\n"
" </div>\n"
" <div id=\"patchPanel\" class=\"cr-modal-panel\" style=\"display:none;\">\n"
@@ -241,8 +248,11 @@
" <button class=\"btn\" id=\"reloadStateBtn\">Refresh State</button>\n"
" <button class=\"btn danger compact\" id=\"clearCrashFlagsBtn\" style=\"display:none\">Clear Crash Flags</button>\n"
" <button class=\"btn danger compact\" id=\"disableAllCheatsBtn\" style=\"display:none\">Disable All Cheats</button>\n"
" <button class=\"btn secondary\" id=\"copyCheatDebugBtn\">Copy Cheat Debug</button>\n"
" <button class=\"btn secondary\" id=\"copyDiagBundleBtn\">Copy Diagnostic Bundle</button>\n"
" <button class=\"btn secondary compact\" id=\"saveAutoloadBtn\" style=\"display:none\" title=\"Save the currently ON cheats as this game's autoload profile\">Save Autoload Profile</button>\n"
" <button class=\"btn danger compact\" id=\"clearAutoloadBtn\" style=\"display:none\" title=\"Stop auto-applying cheats when this game launches\">Clear Autoload Profile</button>\n"
" <button class=\"btn secondary compact\" id=\"savePatchAutoloadBtn\" style=\"display:none\" title=\"Save the currently applied patches as this game's autoload profile\">Save Autoload Profile</button>\n"
" <button class=\"btn danger compact\" id=\"clearPatchAutoloadBtn\" style=\"display:none\" title=\"Stop auto-applying patches when this game launches\">Clear Autoload Profile</button>\n"
" <button class=\"btn secondary\" id=\"copyCheatModalLogsBtn\" title=\"Copy all logs\" aria-label=\"Copy all logs\">Copy Logs</button>\n"
" <button class=\"btn secondary\" id=\"modalDoneBtn\">Done</button>\n"
" </div>\n"
" </div>\n"
+2990 -2883
View File
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -55,7 +55,14 @@ jb_escalate_pid(pid_t pid) {
if (kernel_set_ucred_caps(pid, caps) != 0) {
rc = -1;
}
if (kernel_set_ucred_attrs(pid, 0x80) != 0) {
uint8_t attrs[32];
if (kernel_get_ucred_attrs(pid, attrs) == 0) {
attrs[3] |= 0x80; /* ptrace */
if (kernel_set_ucred_attrs(pid, attrs) != 0) {
rc = -1;
}
} else {
rc = -1;
}
+5 -1
View File
@@ -20,6 +20,8 @@
#include "cr_paths.h"
#include "cr_shutdown.h"
#include "cr_cheats.h"
#include "cr_cheat_profiles.h"
#include "cr_patch_profiles.h"
#include "cr_addr_cache.h"
#include "cr_title_prefs.h"
#include "cr_favorites.h"
@@ -27,7 +29,7 @@
#include "cr_tile_pkg.h"
#ifndef CHEATRUNNER_VERSION
#define CHEATRUNNER_VERSION "0.15"
#define CHEATRUNNER_VERSION "0.16"
#endif
/* Async-signal-safe crash handler: writes a one-line entry to the crash log
@@ -133,6 +135,8 @@ main(void) {
addr_cache_load();
title_prefs_load();
favorites_load();
cheat_profiles_load();
patch_profiles_load();
fan_init();
cr_tile_autoinstall_init();
rpc_refresh_title_and_notify();