diff --git a/.gitignore b/.gitignore index ba7d02c..1747ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ *.o *.err /tmp/ +config/config.ps4.json +plugin/build/ diff --git a/deploy/SETUP.md b/deploy/SETUP.md index 7c0431c..0ecc1bb 100644 --- a/deploy/SETUP.md +++ b/deploy/SETUP.md @@ -1,45 +1,56 @@ # orbisRPC — Deploy + setup guide -Target: GoldHEN 2.2 jailbroken PS4 (FW 9.00), daemon route. +Target: GoldHEN 2.3+ jailbroken PS4, **GoldHEN plugin route** (the native, +auto-starting mechanism — same one the GTA mod menu uses). -## 1. Copy the payload to the console +**Why a plugin and not a payload:** `/data/GoldHEN/payloads/` is NOT an +auto-load folder (PPPwn stage2 only loads `goldhen.bin` from there; payload +auto-loading from a folder is an unimplemented GoldHEN feature request #296). +The only auto-start mechanism that works on-boot with zero PC involvement is +the GoldHEN **plugin loader**, driven by `/data/GoldHEN/plugins.ini`. -Via FTP (the console must be awake and online). GoldHEN's `/data/GoldHEN/payloads/` -auto-loader takes a **raw ELF** payload named `.bin`, so deploy `orbisrpc.elf` -(the real ELF — `build/orbisrpc.bin` is the fself tool's eboot output, not this): +## 1. Copy the plugin to the console + +Via FTP (console must be awake and online), build it first: ``` -put build/orbisrpc.elf -> /data/GoldHEN/payloads/orbisrpc.bin +OO_PS4_TOOLCHAIN=~/PS4Toolchain/OpenOrbis/PS4Toolchain \ + GOLDHEN_SDK=/tmp/ghsdk LLD=/Users/mac/lldbuild/build/bin/ld.lld \ + make -C plugin +# -> plugin/build/orbisrpc_plugin.prx ``` -GoldHEN auto-loads payloads from `/data/GoldHEN/payloads/` at boot and keeps -them running in the background while games run in the foreground. +Then deploy: -Also make the config dir + seed the config: +``` +put plugin/build/orbisrpc_plugin.prx -> /data/GoldHEN/plugins/orbisrpc_plugin.prx +``` + +## 2. plugins.ini + +`/data/GoldHEN/plugins.ini` must list the plugin. The `[default]` section loads +it for ANY game title, so presence auto-starts every time a game launches: + +``` +[default] +/data/GoldHEN/plugins/orbisrpc_plugin.prx=true +``` + +Existing per-title sections (e.g. the GTA menu `[CUSA00411]`) are untouched. +A ready-made file lives at `plugin/plugins.ini.ps4`. + +## 3. Seed the config ``` mkdir /data/orbisRPC put config/config.json -> /data/orbisRPC/config.json ``` -## 2. First boot check +The plugin reads `/data/orbisRPC/config.json` (cfg.h: `CFG_PATH`), logs to +`/data/orbisRPC/log.txt` (`LOG_PATH`), and caches per-game state in +`/data/orbisRPC/.lastgame/`. -Reboot (or run the payload from the GoldHEN loader) and check: - -``` -get /data/orbisRPC/log.txt -``` - -Expected lines: - -``` -[..] config not found at /data/orbisRPC/config.json; defaults applied (only first run) -[..] FATAL: set client_id + auth_code (or refresh) in /data/orbisRPC/config.json -``` - -The FATAL exit is expected until you supply Discord credentials (step 3). - -## 3. One-time Discord app setup +## 4. One-time Discord app setup 1. Open https://discord.com/developers/applications -> New Application -> name it (e.g. `orbisRPC`) -> Create. @@ -63,30 +74,27 @@ The FATAL exit is expected until you supply Discord credentials (step 3). refused is fine — just copy the `code=XXXX` part). 7. Paste that code into `config.json` as `auth_code`. -## 4. Reboot + validate +## 5. Launch a game + validate -Reboot the console. `log.txt` should show: +Launch any game (e.g. GTA V). The plugin loads into the game process, reads the +title id via the GoldHEN SDK, and starts the daemon. Check: + +``` +get /data/orbisRPC/log.txt +``` + +Expected lines: ``` [..] token refreshed, expires_in=604799s [..] gateway connected, hb=41250ms -``` - -Play a game. After the poll interval (`poll_interval_s`, default 12 s): - -``` -[..] detect: active name= tid=CUSAxxxxx [..] presence: ``` -Your Discord profile now shows **Playing ** with a timer. Exit to the -home screen and presence clears: +Your Discord profile now shows **Playing ** with a timer. Exit to the home +screen and presence clears (the plugin unloads with the game process). -``` -[..] presence: (cleared) -``` - -## 5. Token lifecycle +## 6. Token lifecycle The daemon exchanges the `auth_code` once for an access + refresh token (`token_expires_at`, `refresh_token` in config). It refreshes automatically @@ -98,12 +106,13 @@ Config lives on the PS4 at `/data/orbisRPC/config.json` — no PC needed at runt | Symptom | Cause / fix | |---|---| -| `FATAL: set client_id` | config missing / `SET_ME` placeholder. Fill in step 3. | -| `oauth failed rc=401` | wrong client_id/secret, or the auth_code expired (10 min). Re-do step 3.6. | +| no `log.txt` at all after launching a game | plugin didn't load: check `plugins.ini` has the `[default]` section + correct path, and GoldHEN has `[PluginLoader] PluginLoader_Enabled=1` in its config.ini. | +| `FATAL: set client_id` | config missing / `SET_ME` placeholder. Fill in step 4. | +| `oauth failed rc=401` | wrong client_id/secret, or the auth_code expired (10 min). Re-do step 4.6. | | `no 101:` in log | Discord blocked the TLS handshake — check clock / network. | | `gateway dropped` every poll | WS keepalive mismatch; the reconnect loop is automatic. | -| presence shows but clears instantly | foreground detection returned active=0 (home screen). | | game name shows `CUSAxxxxx` | app.xml/app.db lookup didn't find a human name; titleId fallback. | +| plugin loads into a system app | `plugin.c` filters non-game titleids (NPXS...) and skips them. | ## Build on Mac (if rebuilding) @@ -111,5 +120,20 @@ Config lives on the PS4 at `/data/orbisRPC/config.json` — no PC needed at runt OO_PS4_TOOLCHAIN=~/PS4Toolchain/OpenOrbis/PS4Toolchain \ LLD=/Users/mac/lldbuild/build/bin/ld.lld \ ./scripts/build.sh -# -> build/orbisrpc.fself +# -> build/orbisrpc.elf (payload ELF, BinLoader route — not auto-loaded) + +OO_PS4_TOOLCHAIN=~/PS4Toolchain/OpenOrbis/PS4Toolchain \ + GOLDHEN_SDK=/tmp/ghsdk LLD=/Users/mac/lldbuild/build/bin/ld.lld \ + make -C plugin +# -> plugin/build/orbisrpc_plugin.prx (the deployed artifact) ``` + +## Repo layout + +``` +orbisrpc/daemon.c shared daemon loop (payload + plugin use it) +orbisrpc/detect.c foreground detection + titleid->name resolution +plugin/plugin.c GoldHEN plugin entry (plugin_load / plugin_unload) +plugin/Makefile builds orbisrpc_plugin.prx against GoldHEN SDK +plugin/plugins.ini.ps4 ready-to-deploy plugins.ini with [default] section +``` \ No newline at end of file diff --git a/orbisrpc/daemon.c b/orbisrpc/daemon.c new file mode 100644 index 0000000..fea35d4 --- /dev/null +++ b/orbisrpc/daemon.c @@ -0,0 +1,136 @@ +/* daemon.c - orbisRPC core loop, usable from the ELF payload (main.c) or from a + * GoldHEN plugin. When `fixed_game_name` is non-NULL the daemon posts presence + * for that game unconditionally (the plugin runs inside the game process and + * already knows the title); otherwise it detects the foreground game. + * + * A `stop` flag is polled so a plugin_unload() can shut the loop down cleanly. + */ +#include "cfg.h" +#include "log.h" +#include "http.h" +#include "ws.h" +#include "discord.h" +#include "detect.h" +#include +#include +#include +#include +#include +#include +#include + +extern cfg_t g_cfg; + +static volatile int s_stop = 0; + +/* Tells a running daemon_run() to shut down (called from plugin_unload). */ +void daemon_request_stop(void){ s_stop = 1; } +void daemon_clear_stop(void){ s_stop = 0; } + +static int ensure_token(void){ + int64_t now=time(NULL); + if(g_cfg.access_token[0] && now < g_cfg.token_expires_at - 120) return 0; /* fresh */ + char at[256]="", rt[256]=""; int64_t exp=0; + int rc = http_oauth_token(g_cfg.client_id, g_cfg.client_secret, + g_cfg.auth_code, + g_cfg.refresh_token[0]?g_cfg.refresh_token:NULL, + at, sizeof at, g_cfg.refresh_token[0]?rt:NULL, g_cfg.refresh_token[0]?sizeof g_cfg.refresh_token:0, + &exp); + if(rc==0 && at[0]){ + strncpy(g_cfg.access_token, at, sizeof g_cfg.access_token-1); + if(rt[0]) strncpy(g_cfg.refresh_token, rt, sizeof g_cfg.refresh_token-1); + g_cfg.token_expires_at = exp; + memset(g_cfg.auth_code,0,sizeof g_cfg.auth_code); /* one-time use */ + cfg_save(CFG_PATH, &g_cfg); + log_msg("token refreshed, expires_in=%lds", (long)(exp-now)); + return 0; + } + log_msg("oauth failed rc=%d", rc); + return rc; +} + +static void set_game_presence(discord_t *d, const char *name){ + if(!name||!*name){ discord_clear_presence(d); return; } + discord_set_presence(d, + g_cfg.presence_state[0]?g_cfg.presence_state:"On PS4", /* state */ + name, /* details = game name */ + 1, 1, /* party */ + "orbisrpc:playing", /* large_image key (asset) */ + name); /* large_text */ + log_msg("presence: %s", name); +} + +/* fixed_game_name != NULL -> post presence for that game only, no detection. + * NULL -> poll the foreground app like the original payload daemon. */ +int daemon_run(const char *fixed_game_name){ + s_stop = 0; + mkdir(DATA_DIR, 0777); /* plugins/payloads may create their own dir */ + log_init(LOG_PATH); + cfg_load(CFG_PATH, &g_cfg); + if(!g_cfg.enabled){ log_msg("disabled in config; exiting"); log_close(); return 0; } + if(!g_cfg.client_id[0] || strcmp(g_cfg.client_id,"SET_ME")==0){ + log_msg("FATAL: set client_id + auth_code (or refresh) in %s", CFG_PATH); + log_close(); + return 1; + } + discord_t dc; + /* keep the daemon alive: transient OAuth/gateway failures retry instead + * of exiting (a GoldHEN plugin only runs while the game process lives) */ + for(;;){ /* outer: token refresh + fresh connect */ + if(s_stop) break; + if(ensure_token()!=0){ + log_msg("no valid token; retry in %ds", g_cfg.poll_interval_s); + sleep((unsigned)g_cfg.poll_interval_s); + continue; + } + if(discord_connect(&dc, g_cfg.access_token, 0)!=0){ + log_msg("gateway connect failed; retry in %ds", g_cfg.poll_interval_s); + sleep((unsigned)g_cfg.poll_interval_s); + continue; + } + char last_name[128]=""; + int64_t last_change = 0; + while(1){ /* inner: live session */ + if(s_stop) break; + char name[128]=""; char path[128]=""; + if(fixed_game_name){ + strncpy(name, fixed_game_name, sizeof name-1); + }else{ + int active = detect_foreground_active(); + if(active && detect_current_game(name,sizeof name,path,sizeof path)==0 && name[0]){ + /* ok */ + }else{ + name[0]=0; + } + } + if(name[0]){ + int64_t now=time(NULL); + if(strcmp(name,last_name)!=0 || now-last_change > 300){ + set_game_presence(&dc, name); + strncpy(last_name, name, sizeof last_name-1); + last_change = now; + } + } else { + if(last_name[0] || fixed_game_name==NULL){ + set_game_presence(&dc, NULL); /* clear when nothing foreground */ + last_name[0]=0; last_change=time(NULL); + } + } + if(discord_tick(&dc) != 0){ + log_msg("gateway dropped; reconnecting..."); + if(discord_reconnect(&dc)!=0){ + ws_close(&dc.ws); + break; /* outer loop: full retry (token + connect) */ + } + last_name[0]=0; /* force re-push of the current game */ + } + int iv = g_cfg.poll_interval_s; + for(int i=0; i Discord presence. Runs as GoldHEN payload. */ -#include "cfg.h" -#include "log.h" -#include "http.h" -#include "ws.h" -#include "discord.h" -#include "detect.h" -#include "jsonlite.h" -#include "b64.h" -#include -#include -#include -#include -#include -#include -#include - -extern cfg_t g_cfg; - -static int ensure_token(void){ - int64_t now=time(NULL); - if(g_cfg.access_token[0] && now < g_cfg.token_expires_at - 120) return 0; /* fresh */ - char at[256]="", rt[256]=""; int64_t exp=0; - int rc = http_oauth_token(g_cfg.client_id, g_cfg.client_secret, - g_cfg.auth_code, - g_cfg.refresh_token[0]?g_cfg.refresh_token:NULL, - at, sizeof at, g_cfg.refresh_token[0]?rt:NULL, g_cfg.refresh_token[0]?sizeof g_cfg.refresh_token:0, - &exp); - if(rc==0 && at[0]){ - strncpy(g_cfg.access_token, at, sizeof g_cfg.access_token-1); - if(rt[0]) strncpy(g_cfg.refresh_token, rt, sizeof g_cfg.refresh_token-1); - g_cfg.token_expires_at = exp; - memset(g_cfg.auth_code,0,sizeof g_cfg.auth_code); /* one-time use */ - cfg_save(CFG_PATH, &g_cfg); - log_msg("token refreshed, expires_in=%lds", (long)(exp-now)); - return 0; - } - log_msg("oauth failed rc=%d", rc); - return rc; -} - -static void set_game_presence(discord_t *d, const char *name){ - if(!name||!*name){ discord_clear_presence(d); return; } - discord_set_presence(d, - g_cfg.presence_state[0]?g_cfg.presence_state:"On PS4", /* state */ - name, /* details = game name */ - 1, 1, /* party */ - "orbisrpc:playing", /* large_image key (asset) */ - name); /* large_text */ - log_msg("presence: %s", name); -} +/* main.c - orbisRPC payload entry: detect game -> Discord presence. + * Runs as a GoldHEN payload (BinLoader push) or standalone ELF. */ +#include "daemon.h" +#include int main(int argc, char **argv){ (void)argc;(void)argv; - mkdir(DATA_DIR, 0777); /* payloads may create their own dir */ - log_init(LOG_PATH); - cfg_load(CFG_PATH, &g_cfg); - if(!g_cfg.enabled){ log_msg("disabled in config; exiting"); return 0; } - if(!g_cfg.client_id[0] || strcmp(g_cfg.client_id,"SET_ME")==0){ - log_msg("FATAL: set client_id + auth_code (or refresh) in %s", CFG_PATH); - return 1; - } - discord_t dc; - /* keep the daemon alive: transient OAuth/gateway failures retry instead - * of exiting (a GoldHEN payload only runs once per boot) */ - for(;;){ /* outer: token refresh + fresh connect */ - if(ensure_token()!=0){ - log_msg("no valid token; retry in %ds", g_cfg.poll_interval_s); - sleep((unsigned)g_cfg.poll_interval_s); - continue; - } - if(discord_connect(&dc, g_cfg.access_token, 0)!=0){ - log_msg("gateway connect failed; retry in %ds", g_cfg.poll_interval_s); - sleep((unsigned)g_cfg.poll_interval_s); - continue; - } - char last_name[128]=""; - int64_t last_change = 0; - while(1){ /* inner: live session */ - char name[128]=""; char path[128]=""; - int active = detect_foreground_active(); - if(active && detect_current_game(name,sizeof name,path,sizeof path)==0 && name[0]){ - int64_t now=time(NULL); - if(strcmp(name,last_name)!=0 || now-last_change > 300){ - set_game_presence(&dc, name); - strncpy(last_name, name, sizeof last_name-1); - last_change = now; - } - } else { - if(last_name[0] || !active){ - set_game_presence(&dc, NULL); /* clear when nothing foreground */ - last_name[0]=0; last_change=time(NULL); - } - } - if(discord_tick(&dc) != 0){ - log_msg("gateway dropped; reconnecting..."); - if(discord_reconnect(&dc)!=0){ - ws_close(&dc.ws); - break; /* outer loop: full retry (token + connect) */ - } - last_name[0]=0; /* force re-push of the current game */ - } - usleep((useconds_t)(g_cfg.poll_interval_s * 1000000)); - } - } - discord_clear_presence(&dc); - ws_close(&dc.ws); - log_close(); - return 0; + return daemon_run(NULL); } \ No newline at end of file diff --git a/plugin/Makefile b/plugin/Makefile new file mode 100644 index 0000000..f57f7ed --- /dev/null +++ b/plugin/Makefile @@ -0,0 +1,76 @@ +# orbisRPC GoldHEN plugin build (plugin/). +# Builds plugin.prx — a GoldHEN plugin that runs the orbisRPC daemon inside the +# game process, so presence auto-starts at every boot with no PC involvement. + +DEBUG_FLAGS = -D__FINAL__=1 +TYPE = final + +OUTPUT_PRX := orbisrpc_plugin +BUILD_DIR := build +TARGET := $(BUILD_DIR)/$(OUTPUT_PRX).prx +TARGET_ELF := $(BUILD_DIR)/$(OUTPUT_PRX).elf + +# Root vars +TOOLCHAIN := $(OO_PS4_TOOLCHAIN) +GH_SDK := $(GOLDHEN_SDK) +ROOT := .. +SRCDIR := . +ORBISRPC := $(ROOT)/orbisrpc +INTDIR := $(BUILD_DIR) + +# Our daemon sources (payload main.c is NOT part of the plugin; plugin.c is the entry) +DAEMON_SRC := $(ORBISRPC)/log.c $(ORBISRPC)/cfg.c $(ORBISRPC)/jsonlite.c $(ORBISRPC)/b64.c \ + $(ORBISRPC)/http.c $(ORBISRPC)/ws.c $(ORBISRPC)/discord.c $(ORBISRPC)/detect.c \ + $(ORBISRPC)/daemon.c +PLUGIN_SRC := $(SRCDIR)/plugin.c +CFILES := $(PLUGIN_SRC) $(DAEMON_SRC) +OBJS := $(patsubst %.c,$(INTDIR)/%.o,$(notdir $(CFILES))) + +LIBS := -lSceLibcInternal -lSceNet -lSceNetCtl -lSceLibreSSL -lSceSsl -lSceHttp \ + -lSceSysmodule -lSceUserService -lSceAppInstUtil -lSceAppContent \ + -lGoldHEN_Hook -lkernel + +EXTRAFLAGS := $(DEBUG_FLAGS) -std=gnu11 -Wall -Wno-unused \ + -Wno-int-conversion -Wno-incompatible-pointer-types \ + -Wno-address-of-packed-member + +CFLAGS := --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -c $(EXTRAFLAGS) \ + -isysroot $(TOOLCHAIN) -isystem $(TOOLCHAIN)/include \ + -I$(GH_SDK)/include -I$(ORBISRPC) -I$(SRCDIR) +LDFLAGS := -m elf_x86_64 -pie --script $(TOOLCHAIN)/link.x -e _init --eh-frame-hdr \ + -L$(TOOLCHAIN)/lib -L$(GH_SDK) $(LIBS) + +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) + CC := /usr/local/opt/llvm/bin/clang + LD := $(LLD) + CDIR := macos + FSUFFIX := -macos +else + CC := clang + LD := ld.lld + CDIR := linux + FSUFFIX := +endif + +_unused := $(shell mkdir -p $(INTDIR)) + +$(INTDIR)/%.o: $(ORBISRPC)/%.c + $(CC) $(CFLAGS) -o $@ $< + +$(INTDIR)/plugin.o: $(SRCDIR)/plugin.c + $(CC) $(CFLAGS) -o $@ $< + +$(TARGET): $(OBJS) + $(LD) $(GH_SDK)/build/crtprx.o $(OBJS) -o $(TARGET_ELF) $(LDFLAGS) + $(TOOLCHAIN)/bin/$(CDIR)/create-fself$(FSUFFIX) -in=$(TARGET_ELF) -out=$(TARGET_ELF).oelf \ + --lib=$(TARGET) --paid 0x3800000000000011 + +.PHONY: clean +.DEFAULT_GOAL := all + +all: $(TARGET) + @echo "plugin -> $(TARGET)" + +clean: + rm -rf $(BUILD_DIR) \ No newline at end of file diff --git a/plugin/plugin.c b/plugin/plugin.c new file mode 100644 index 0000000..f24a0f4 --- /dev/null +++ b/plugin/plugin.c @@ -0,0 +1,92 @@ +/* plugin.c - orbisRPC as a GoldHEN plugin (.prx loaded into the game process). + * + * GoldHEN loads plugins per game title via /data/GoldHEN/plugins.ini. A + * plugin runs inside the game's process, so: + * - we KNOW which game is running: sys_sdk_proc_info() -> procInfo.titleid + * - the game has full network access (this is the whole point) + * - it auto-starts at every boot/jailbreak with zero PC involvement + * + * plugin_load() resolves the titleid to a display name and starts the shared + * daemon thread (daemon.c) which handles token refresh, the Discord gateway + * and presence. plugin_unload() stops it cleanly. + */ +#include +#include +#include +#include +#include +#include "Common.h" +#include "daemon.h" +#include "detect.h" + +/* Only post presence for real game titles (CUSA/PPSA/etc). System apps + * (NPXS..., shell) launching would otherwise post bogus "playing" state. */ +static int is_game_titleid(const char *t){ + static const char *pfx[] = { "CUSA", "PPSA", "PCSE", "PCJS", "EPSA", "PCSB", NULL }; + for(int i=0; pfx[i]; i++) if(strncmp(t, pfx[i], 4)==0) return 1; + return 0; +} + +#define attr_public __attribute__((visibility("default"))) +#define attr_module_hidden __attribute__((weak)) __attribute__((visibility("hidden"))) + +attr_public const char *g_pluginName = "orbisrpc"; +attr_public const char *g_pluginDesc = "Discord Rich Presence for PS4"; +attr_public const char *g_pluginAuth = "orbisRPC"; +attr_public uint32_t g_pluginVersion = 0x00000100; /* 1.00 */ + +struct proc_info procInfo; + +static void *daemon_thread(void *args){ + /* args = display name of the game we are loaded into */ + char name[128]; + strncpy(name, (const char *)args, sizeof name - 1); + name[sizeof name - 1] = 0; + daemon_clear_stop(); + daemon_run(name); + return NULL; +} + +int32_t attr_public plugin_load(int32_t argc, const char *argv[]){ + (void)argc; (void)argv; + klog("[orbisrpc] <%s\\Ver.0x%08x> %s\n", g_pluginName, g_pluginVersion, __func__); + klog("[orbisrpc] Plugin Author(s): %s\n", g_pluginAuth); + + if(sys_sdk_proc_info(&procInfo) != 0){ + klog("[orbisrpc] sys_sdk_proc_info failed; not starting\n"); + return 0; + } + klog("[orbisrpc] loaded into process: pid=%d name=%s titleid=%s\n", + procInfo.pid, procInfo.name, procInfo.titleid); + + if(!is_game_titleid(procInfo.titleid)){ + klog("[orbisrpc] not a game title (system app?); not starting daemon\n"); + return 0; + } + + /* Resolve a display name for the title we are loaded into. */ + static char game_name[128]; + detect_name_for_title(procInfo.titleid, game_name, sizeof game_name); + klog("[orbisrpc] game: %s\n", game_name); + + OrbisPthread thread; + scePthreadCreate(&thread, NULL, daemon_thread, game_name, "orbisrpc_daemon"); + return 0; +} + +int32_t attr_public plugin_unload(int32_t argc, const char *argv[]){ + (void)argc; (void)argv; + klog("[orbisrpc] <%s\\Ver.0x%08x> %s\n", g_pluginName, g_pluginVersion, __func__); + daemon_request_stop(); + return 0; +} + +int32_t attr_module_hidden module_start(int64_t argc, const void *args){ + (void)argc; (void)args; + return 0; +} + +int32_t attr_module_hidden module_stop(int64_t argc, const void *args){ + (void)argc; (void)args; + return 0; +} \ No newline at end of file diff --git a/plugin/plugins.ini.ps4 b/plugin/plugins.ini.ps4 new file mode 100644 index 0000000..a638c59 --- /dev/null +++ b/plugin/plugins.ini.ps4 @@ -0,0 +1,25 @@ +; GoldHEN plugins.ini — GTA V mod menu + orbisRPC Discord presence +; +; [default] loads plugins for ANY game title, so orbisRPC auto-starts +; whenever a game launches (no PC needed). +; +[default] +/data/GoldHEN/plugins/orbisrpc_plugin.prx=true + +; Lotus SPRX (primary mod menu — requires GTA V 1.48 update) +; Press R1 + Square in-game to open +; +[CUSA00411] +/data/GoldHEN/plugins/gtaPayload_148.sprx=true + +[CUSA00419] +/data/GoldHEN/plugins/gtaPayload_148.sprx=true + +; Scorpion Menu v1.2B (alternate mod menu — backup option) +; Uncomment these lines if you prefer Scorpion over Lotus: +; +;[CUSA00411] +;/data/GoldHEN/plugins/ScorpionMenu-v1.2b-148.prx=true +; +;[CUSA00419] +;/data/GoldHEN/plugins/ScorpionMenu-v1.2b-148.prx=true \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh index 25a2b3d..17510e0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -19,7 +19,7 @@ cd "$ROOT" export OO_PS4_TOOLCHAIN="$SDK" OUT="$ROOT/build"; mkdir -p "$OUT" echo "=== compiling ===" -for f in log cfg jsonlite b64 http ws detect discord main; do +for f in log cfg jsonlite b64 http ws detect discord daemon main; do "$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || { echo "compile $f FAILED"; exit 1; } done echo "=== linking ($LD) ==="