mirror of
https://github.com/notmaj0r/CheatRunner.git
synced 2026-10-18 23:07:10 -05:00
v0.16 update
This commit is contained in:
1 parent
285abce7f9
commit
c3b0e23ba3
34 files changed
+4440
-3118
No files matched your search
+45
-9
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 ---- */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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\">°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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user