diff --git a/orbisrpc/cfg.c b/orbisrpc/cfg.c index 251c6f7..c7c3a0a 100644 --- a/orbisrpc/cfg.c +++ b/orbisrpc/cfg.c @@ -18,6 +18,8 @@ void cfg_defaults(cfg_t *c) { c->poll_interval_s = 12; strncpy(c->token, "SET_ME", sizeof(c->token)-1); strncpy(c->presence_state, "On PS4", sizeof(c->presence_state)-1); + c->home_art[0] = 0; + c->n_titles = 0; /* Default art backend: our own Sony-CDN icon pack, resolved through * Discord's external-assets proxy (mp:) at post time. Works from the * start with zero setup; missing titles degrade to no art. */ @@ -68,8 +70,34 @@ int cfg_load(const char *path, cfg_t *c) { STR("token", token); STR("application_id", application_id); STR("art_base_url", art_base_url); + STR("home_art", home_art); STR("presence_state", presence_state); #undef STR + /* User-local title overrides: {"CUSA11995": "Marvel's Spider-Man"}. + * Defensive: wrong types, overlong keys/names, and overflow past + * CFG_MAX_TITLES are ignored, never fatal. */ + c->n_titles = 0; + o = jl_obj_get(root, "titles"); + if(o && o->type == JL_OBJECT){ + for(jl_val_t *p = o->child; p && c->n_titles < CFG_MAX_TITLES; p = p->next){ + const jl_val_t *v = p->child ? p->child : p; + if(!p->str || !v || v->type != JL_STRING || !v->str) continue; + size_t kl = strlen(p->str), vl = strlen(v->str); + if(kl == 0 || kl >= (size_t)CFG_TITLEID_LEN) continue; + if(vl < 2 || vl >= (size_t)CFG_TITLENAME_LEN) continue; + int ok = 1; + for(size_t i = 0; i < kl; i++){ + char ch = p->str[i]; + if(!((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_')){ ok = 0; break; } + } + if(!ok) continue; + strncpy(c->title_ids[c->n_titles], p->str, CFG_TITLEID_LEN - 1); + c->title_ids[c->n_titles][CFG_TITLEID_LEN - 1] = 0; + strncpy(c->title_names[c->n_titles], v->str, CFG_TITLENAME_LEN - 1); + c->title_names[c->n_titles][CFG_TITLENAME_LEN - 1] = 0; + c->n_titles++; + } + } o = jl_obj_get(root, "enabled"); if (o && o->type == JL_BOOL) c->enabled = (int)o->num; o = jl_obj_get(root, "auto_update"); if (o && o->type == JL_BOOL) c->auto_update = (int)o->num; o = jl_obj_get(root, "debug"); if (o && o->type == JL_BOOL) c->debug = (int)o->num; @@ -83,6 +111,18 @@ int cfg_load(const char *path, cfg_t *c) { return 0; } +int cfg_title(const cfg_t *c, const char *titleId, char *out, size_t cap){ + if(!c || !titleId || !titleId[0] || !out || cap == 0) return -1; + for(int i = 0; i < c->n_titles; i++){ + if(!strcmp(c->title_ids[i], titleId)){ + strncpy(out, c->title_names[i], cap - 1); + out[cap - 1] = 0; + return out[0] ? 0 : -1; + } + } + return -1; +} + int cfg_save(const char *path, const cfg_t *c) { if(!path || !c) return -1; jl_val_t *r = jl_new_object(); @@ -96,6 +136,15 @@ int cfg_save(const char *path, const cfg_t *c) { jl_obj_set(r, "debug", jl_new_bool(c->debug)); jl_obj_set(r, "poll_interval_s", jl_new_number((double)c->poll_interval_s)); jl_obj_set(r, "presence_state", jl_new_string(c->presence_state)); + jl_obj_set(r, "home_art", jl_new_string(c->home_art)); + if(c->n_titles > 0){ + jl_val_t *t = jl_new_object(); + if(t){ + for(int i = 0; i < c->n_titles; i++) + jl_obj_set(t, c->title_ids[i], jl_new_string(c->title_names[i])); + jl_obj_set(r, "titles", t); + } + } char *s = jl_stringify(r); if(!s){ log_msg("cfg_save: serialization failed"); jl_free(r); return -1; } /* write tmp + fsync + rename so a power loss can't corrupt the config */ @@ -116,4 +165,5 @@ int cfg_save(const char *path, const cfg_t *c) { } } else { log_msg("cfg_save: cannot write %s", tmp); } free(s); jl_free(r); + return ok ? 0 : -1; } diff --git a/orbisrpc/cfg.h b/orbisrpc/cfg.h index c34cf72..94eb441 100644 --- a/orbisrpc/cfg.h +++ b/orbisrpc/cfg.h @@ -2,12 +2,20 @@ #ifndef CFG_H #define CFG_H #include +#include #define CFG_PATH "/data/orbisRPC/config.json" #define LOG_PATH "/data/orbisRPC/log.txt" #define DATA_DIR "/data/orbisRPC" /* Config schema version. Stamp on load so re-saves converge; unknown * future versions load defensively (known fields only). */ #define CFG_SCHEMA_VERSION 1 +/* User-local title overrides: {"CUSA11995": "Marvel's Spider-Man"}. + * Lives in the user's own config.json (never shipped) for games no + * on-box source can name (no pronunciation.xml, TMDB blocked, ...). + * Fixed-size tables: no malloc, PS4-safe. */ +#define CFG_MAX_TITLES 64 +#define CFG_TITLEID_LEN 16 +#define CFG_TITLENAME_LEN 128 typedef struct { int schema_version; char token[512]; /* Discord user session token */ @@ -15,6 +23,12 @@ typedef struct { char art_base_url[256]; /* optional: icon pack base URL, e.g. * https://raw.githubusercontent.com/.../icons/ * large_image becomes .png */ + char home_art[256]; /* optional: idle/home tile art. http(s) URL + * (mp:-proxied) or uploaded Discord asset key. + * empty = try home.png, else none */ + int n_titles; + char title_ids[CFG_MAX_TITLES][CFG_TITLEID_LEN]; + char title_names[CFG_MAX_TITLES][CFG_TITLENAME_LEN]; int enabled; int auto_update; /* check GitHub releases once per boot, stage newer */ int debug; /* verbose debug logging (per-poll detail, no secrets) */ @@ -27,4 +41,6 @@ int cfg_load(const char *path, cfg_t *c); * surface, not assume). */ int cfg_save(const char *path, const cfg_t *c); void cfg_defaults(cfg_t *c); +/* User override lookup: 0 + name copied when titleId has an entry. */ +int cfg_title(const cfg_t *c, const char *titleId, char *out, size_t cap); #endif diff --git a/orbisrpc/daemon.c b/orbisrpc/daemon.c index 153fb00..2e42db2 100644 --- a/orbisrpc/daemon.c +++ b/orbisrpc/daemon.c @@ -500,8 +500,9 @@ int daemon_run(const char *fixed_game_name){ * home presence once (instead of bare online), clear it * the moment a game commits. */ const char *state = g_cfg.presence_state[0] ? g_cfg.presence_state : NULL; - if(discord_set_presence_ex(&dc, state, "PlayStation 4", NULL, - g_cfg.application_id, NULL, NULL, 0) == 0){ + if(discord_set_presence_ex(&dc, state, "PlayStation 4", "home", + g_cfg.application_id, g_cfg.art_base_url, + g_cfg.home_art[0] ? g_cfg.home_art : NULL, 0) == 0){ log_msg("presence: home"); pres_set(&pres, PS_HOME); n_posts++; diff --git a/orbisrpc/detect.c b/orbisrpc/detect.c index d7d6c5a..67ae056 100644 --- a/orbisrpc/detect.c +++ b/orbisrpc/detect.c @@ -19,6 +19,7 @@ * foreground) is reliable via ShellCoreUtil. */ #include "detect.h" +#include "cfg.h" #include "log.h" #include "sfo.h" #include "tmdb.h" @@ -532,6 +533,7 @@ int detect_current_game(char *out_name, size_t cap, char *out_path, size_t p_cap } /* cheap, game-process-safe sources first; Sony TMDB (network) * resolves anything local sources miss, on any console. */ + if(!named && cfg_title(&g_cfg, titleId, out_name, cap)==0){ named=1; log_msg("name: %s via config", out_name); } if(!named && pronunc_title(titleId, out_name, cap)==0){ named=1; log_msg("name: %s via appmeta", out_name); } if(!named){ if(sfo_file_title(titleId, out_name, cap)==0){ named=1; log_msg("name: %s via sfo", out_name); } } if(!named){ if(appxml_title(titleId, out_name, cap)==0){ named=1; log_msg("name: %s via appxml", out_name); } } @@ -566,6 +568,7 @@ int detect_name_for_title(const char *titleId, char *out_name, size_t cap){ } /* Game-process-safe only: small reads plus one bounded network * lookup; no multi-megabyte scans anywhere in this codebase. */ + if(cfg_title(&g_cfg, titleId, out_name, cap)==0){ log_msg("name: %s via config", out_name); resolve_remember(titleId, out_name, "", 1); return 0; } if(pronunc_title(titleId, out_name, cap)==0){ log_msg("name: %s via appmeta", out_name); resolve_remember(titleId, out_name, "", 1); return 0; } else log_msg("name: appmeta miss for %s", titleId); if(sfo_file_title(titleId, out_name, cap)==0){ log_msg("name: %s via sfo", out_name); resolve_remember(titleId, out_name, "", 1); return 0; } diff --git a/orbisrpc/discord.c b/orbisrpc/discord.c index eb1caf9..e40fe80 100644 --- a/orbisrpc/discord.c +++ b/orbisrpc/discord.c @@ -281,7 +281,35 @@ int discord_set_presence_ex(discord_t *d, const char *state, const char *name, * dangling keys drop the whole activity. Two working forms, tried in * order: (1) mp: proxy resolved via external-assets for the URL we * have (Sony CDN or art_base_url pack); (2) uploaded asset key. */ - if(title_id&&title_id[0]&&application_id&&application_id[0]){ + if(title_id&&!strcmp(title_id,"home")&&application_id&&application_id[0]){ + /* Idle tile: PlayStation logo. home_art (arrives as art_url): + * http(s) URL -> mp: proxy; bare value -> uploaded asset key + * used as-is; empty -> home.png when the pack + * carries it. A URL that fails mp: sends NO assets (never a + * dangling key: those drop the whole activity). */ + const char *hsrc = (art_url&&art_url[0]) ? art_url : NULL; + char home_pack[320] = ""; + if(!hsrc && art_base_url&&art_base_url[0]){ + int n=snprintf(home_pack,sizeof home_pack,"%shome.png",art_base_url); + if(n>0 && (size_t)ntoken[0] && art_resolve_mp(application_id, d->token, hsrc, hmp, sizeof hmp)) + himg = hmp; + } else if(hsrc){ + himg = hsrc; /* operator-supplied uploaded key: trusted */ + } + if(himg){ + jl_val_t *as=jl_new_object(); + if(as){ + jl_obj_set(as,"large_image",jl_new_string(himg)); + jl_obj_set(as,"large_text",jl_new_string(name?name:"")); + jl_obj_set(act,"assets",as); + } + } + } else if(title_id&&title_id[0]&&application_id&&application_id[0]){ char mp[512] = ""; const char *src_url = (art_url&&art_url[0]) ? art_url : NULL; char pack_url[320] = ""; diff --git a/tests/Makefile b/tests/Makefile index ed2abcb..b7bca1f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -8,7 +8,7 @@ MBEDTLS_DIR := ../third_party/mbedtls/library # Same exclusion set as scripts/build.sh (POSIX-only modules). MBEDTLS_SRCS := $(filter-out $(MBEDTLS_DIR)/net_sockets.c $(MBEDTLS_DIR)/timing.c $(MBEDTLS_DIR)/entropy_poll.c,$(wildcard $(MBEDTLS_DIR)/*.c)) -ORBIS_SRCS := ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c ../orbisrpc/health.c ../orbisrpc/manifest.c ../orbisrpc/compat.c +ORBIS_SRCS := ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c ../orbisrpc/health.c ../orbisrpc/manifest.c ../orbisrpc/compat.c ../orbisrpc/cfg.c ORBIS_OBJS := $(patsubst ../orbisrpc/%.c,$(OBJDIR)/orbis_%.o,$(ORBIS_SRCS)) MBEDTLS_OBJS := $(patsubst $(MBEDTLS_DIR)/%.c,$(OBJDIR)/mbed_%.o,$(MBEDTLS_SRCS)) diff --git a/tests/test_utils.c b/tests/test_utils.c index cfd5a88..4f344ce 100644 --- a/tests/test_utils.c +++ b/tests/test_utils.c @@ -6,6 +6,7 @@ #include "../orbisrpc/art.h" #include "../orbisrpc/health.h" #include "../orbisrpc/manifest.h" +#include "../orbisrpc/cfg.h" #include #include #include @@ -385,6 +386,45 @@ static void test_base64(void) { assert(strcmp(out, "Zm9v") == 0); } +static void test_cfg_titles(void) { + char dir[64], path[96], path2[96]; + assert(make_tmpdir(dir, sizeof dir) == 0); + snprintf(path, sizeof path, "%s/cfg.json", dir); + snprintf(path2, sizeof path2, "%s/cfg2.json", dir); + FILE *f = fopen(path, "wb"); + assert(f); + fputs("{\"token\":\"t\",\"titles\":{\"CUSA11995\":\"Marvel's Spider-Man\"," + "\"BAD KEY!\": \"junk\", \"CUSA00001\": \"x\", \"TOOLONGTITLEID12345\": \"y\"," + "\"CUSA00002\": 42}}", f); + fclose(f); + cfg_t c; + assert(cfg_load(path, &c) == 0); + assert(c.n_titles == 1); /* only the well-formed entry survives */ + char name[128]; + assert(cfg_title(&c, "CUSA11995", name, sizeof name) == 0); + assert(!strcmp(name, "Marvel's Spider-Man")); + assert(cfg_title(&c, "CUSA99999", name, sizeof name) != 0); + assert(cfg_title(&c, "", name, sizeof name) != 0); + assert(cfg_title(NULL, "CUSA11995", name, sizeof name) != 0); + /* round-trip: save preserves overrides + home_art */ + strncpy(c.home_art, "pslogo", sizeof c.home_art - 1); + assert(cfg_save(path2, &c) == 0); + cfg_t c2; + assert(cfg_load(path2, &c2) == 0); + assert(c2.n_titles == 1); + assert(cfg_title(&c2, "CUSA11995", name, sizeof name) == 0); + assert(!strcmp(name, "Marvel's Spider-Man")); + assert(!strcmp(c2.home_art, "pslogo")); + /* missing titles object: zero overrides, lookup misses */ + f = fopen(path, "wb"); + assert(f); + fputs("{\"token\":\"t\"}", f); + fclose(f); + assert(cfg_load(path, &c) == 0); + assert(c.n_titles == 0); + assert(c.home_art[0] == 0); +} + int main(void) { test_json(); test_gateway_op_spoof(); @@ -399,6 +439,7 @@ int main(void) { test_health_stage_activate(); test_manifest(); test_manifest_sig(); + test_cfg_titles(); puts("utility tests passed"); return 0; }