Gateway precision, SFO OOB fix, build lists, artwork runbook

- discord: HELLO requires text frame; READY/HELLO timeout diagnostics;
  invalid-session fast retry path (-3); clamp warning; malformed close
- daemon: invalid session resets backoff instead of doubling it
- sfo: bounded key scan (no OOB on malformed files) + regression test
- build.sh + plugin Makefile compile sfo.c (link verified)
- deploy/ARTWORK.md: shared-app asset pipeline, no-URL rule
This commit is contained in:
SirHumza committed 2026-09-19 10:18:39 +02:00
1 parent cdb431cfa6
commit 28f6275a05
8 files changed
+59 -7

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
# Cover art pipeline
Discord serves Rich Presence images only from assets uploaded to a
Discord application. There is no URL mode and no automatic official art
for custom presences. One shared app covers every install.
## One-time maintainer setup
1. Create an application at https://discord.com/developers/applications.
2. Rich Presence, Art Assets, upload one PNG per game. Asset name must be
the lowercase title ID, e.g. `cusa00740` for Terraria. 512x512 PNG.
3. Copy the Application ID into the repo default (`config/config.json`
`application_id`) so every install inherits it.
## Where icons live on the PS4
`/user/appmeta/<TITLEID>/icon0.png` (around 50KB). Pull over FTP:
```
get /user/appmeta/CUSA00740/icon0.png -> cusa00740.png
```
Upload as asset `cusa00740`.
## Runtime behavior
The daemon sends `assets: { large_image: "<lower titleId>",
large_text: "<game name>" }` only when `application_id` is set. Without
it, presence posts with no artwork and logs one notice. Missing assets
for a title also degrade to no image. Nothing crashes on absent art.
+1
View File
@@ -174,6 +174,7 @@ int daemon_run(const char *fixed_game_name){
log_close();
return 2;
}
if(tr == -3){ log_msg("invalid session; fresh identify"); backoff = base_poll; ws_close(&dc.ws); break; }
if(tr != 0){ log_msg("gateway dropped; reconnecting"); break; }
for(int i = 0; i < 10 && !s_stop; i++) usleep(100000);
+13 -4
View File
@@ -146,7 +146,8 @@ int discord_connect(discord_t *d, const char *token){
/* HELLO (text frame carrying {"op":10,...}) */
char buf[2048]; int op=0,fin=0;
int nr=rx_frame(d,buf,sizeof buf,&op,&fin,now+15);
if(nr<=0 || (op!=1 && op!=0)){
if(nr==-4){ log_msg("no HELLO (timeout)"); ws_close(&d->ws); d->connected=0; return -1; }
if(nr<=0 || op!=1){
log_msg("no HELLO (nr=%d op=%d)",nr,op);
ws_close(&d->ws); d->connected=0;
return -1;
@@ -161,6 +162,11 @@ int discord_connect(discord_t *d, const char *token){
jl_free(h);
}
if(!d->hb_interval_ms) d->hb_interval_ms=45000;
if(d->hb_interval_ms < 5000){
log_msg("discord: hb_interval %lldms suspiciously small; clamping to 5000",
(long long)d->hb_interval_ms);
d->hb_interval_ms = 5000;
}
if(send_identify(d, token) < 0){
log_msg("discord: identify send failed");
ws_close(&d->ws); d->connected=0;
@@ -172,6 +178,7 @@ int discord_connect(discord_t *d, const char *token){
int64_t dl=time(NULL)+20;
for(;;){
nr=rx_frame(d,buf,sizeof buf,&op,&fin,dl);
if(nr==-4){ log_msg("no READY after identify (timeout)"); break; }
if(nr<=0){ log_msg("no READY after identify (nr=%d)",nr); break; }
if(op==8){
if(!fin){ log_msg("fragmented close; reconnecting"); ws_close(&d->ws); d->connected=0; return -1; }
@@ -182,7 +189,7 @@ int discord_connect(discord_t *d, const char *token){
return code==4004 ? -2 : -1;
}
if(op==9){ ws_pong(&d->ws); continue; }
if(op!=1 && op!=0) continue;
if(op!=1) continue;
int go = gw_op(buf, (size_t)nr);
if(go==11){ d->last_ack=time(NULL); continue; }
if(go==0 && is_ready(buf, (size_t)nr)){
@@ -323,10 +330,12 @@ int discord_tick(discord_t *d){
log_msg("gateway: reconnect requested");
d->connected=0;
return -1;
case 9: /* INVALID_SESSION */
case 9: /* INVALID_SESSION: session dead, fresh IDENTIFY needed.
* Distinct code so the caller retries promptly instead of
* doubling into a long backoff. */
log_msg("gateway: invalid session");
d->connected=0;
return -1;
return -3;
case 11:
d->last_ack=now;
break;
+1 -1
View File
@@ -24,5 +24,5 @@ int discord_set_presence_ex(discord_t *d, const char *state, const char *name,
const char *title_id, const char *application_id,
int64_t started_epoch); /* op 3 */
int discord_clear_presence(discord_t *d); /* clear activity, stay online */
int discord_tick(discord_t *d); /* 0 ok; -1 drop/reconnect; -2 auth-fatal */
int discord_tick(discord_t *d); /* 0 ok; -1 drop/reconnect; -2 auth-fatal; -3 invalid session (retry promptly) */
#endif
+6 -1
View File
@@ -36,10 +36,15 @@ int sfo_title(const unsigned char *buf, size_t n, char *out, size_t cap){
for(uint32_t i = 0; i < h->count; i++){
size_t ko = (size_t)h->key_off + e[i].key_off;
if(ko >= n) continue;
/* keys must be null-terminated INSIDE the buffer: bound the scan */
size_t kmax = n - ko;
size_t klen = 0;
while(klen < kmax && buf[ko + klen]) klen++;
if(klen >= kmax) continue; /* unterminated key: malformed */
const char *key = (const char *)(buf + ko);
/* keys of interest: TITLE first, then language variants TITLE_XX */
int is_title = (!strcmp(key, "TITLE") ||
(!strncmp(key, "TITLE_", 6) && strlen(key) == 8));
(klen == 8 && !memcmp(key, "TITLE_", 6)));
if(!is_title) continue;
if(e[i].fmt != 0x0004 && e[i].fmt != 0x0204) continue;
size_t dlen = e[i].len;
+1
View File
@@ -20,6 +20,7 @@ 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)/sfo.c \
$(ORBISRPC)/tls.c $(ORBISRPC)/ws.c $(ORBISRPC)/discord.c $(ORBISRPC)/detect.c \
$(ORBISRPC)/daemon.c
PLUGIN_SRC := $(SRCDIR)/plugin.c $(SRCDIR)/compat.c
+1 -1
View File
@@ -47,7 +47,7 @@ LDFLAGS="-m elf_x86_64 -pie --eh-frame-hdr -L$SDK/lib $LIBS $SDK/lib/crt1.o --sc
export OO_PS4_TOOLCHAIN="$SDK"
OUT="$ROOT/build"; mkdir -p "$OUT"
echo "=== compiling (CC=$CC LD=$LD SDK=$SDK) ==="
for f in log cfg jsonlite b64 tls ws detect discord daemon compat main; do
for f in log cfg jsonlite b64 sfo tls ws detect discord daemon compat main; do
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || fail "compile $f"
done
echo "=== mbedtls (skip net_sockets/timing: POSIX-only) ==="
+6
View File
@@ -77,6 +77,12 @@ static void test_sfo(void) {
/* tiny output buffer still safe */
assert(sfo_title(sfo, 64, out, 4) == 0);
assert(out[3] == 0);
/* unterminated key region: must fail, not read OOB */
memset(sfo, 0x41, sizeof sfo);
sfo[0]=0x00; sfo[1]='P'; sfo[2]='S'; sfo[3]='F';
sfo[8]=36; sfo[12]=48; sfo[16]=1;
sfo[20]=0; sfo[22]=0x04; sfo[24]=9; sfo[28]=16; sfo[32]=0;
assert(sfo_title(sfo, 64, out, sizeof out) != 0);
}
static void test_nametable(void) {
char out[64];