mirror of
https://github.com/SirHumza/orbisRPC.git
synced 2026-10-06 09:00:37 +02:00
WIP: mp: art resolution via external-assets + icon pack
This commit is contained in:
1 parent
49672ec2c8
commit
7dbb021290
8 files changed
+300
-11
No files matched your search
@@ -0,0 +1,33 @@
|
||||
name: ci
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
host:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: host unit tests
|
||||
run: make -C tests clean && make -C tests test
|
||||
sdk-payload:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install llvm
|
||||
run: sudo apt-get update -qq && sudo apt-get install -qy llvm lld
|
||||
- name: fetch ps4-payload-sdk
|
||||
run: |
|
||||
curl -sL -o /tmp/sdk.zip https://github.com/ps4-payload-dev/sdk/releases/latest/download/ps4-payload-sdk.zip
|
||||
mkdir -p ~/ps4-payload-sdk && unzip -q -o /tmp/sdk.zip -d ~/ps4-payload-sdk
|
||||
- name: build SDK daemon payload
|
||||
run: |
|
||||
export PS4_PAYLOAD_SDK="$HOME/ps4-payload-sdk/ps4-payload-sdk"
|
||||
export PATH="/usr/lib/llvm-14/bin:$PATH"
|
||||
./scripts/build_sdk.sh
|
||||
- name: gate daemon-world linkage only
|
||||
run: |
|
||||
NEEDED=$(llvm-readelf -d build-sdk/orbisrpc_sdk.elf | grep NEEDED | sed 's/.*\[//;s/\]//')
|
||||
echo "$NEEDED"
|
||||
echo "$NEEDED" | grep -q libkernel_web.sprx || { echo "missing libkernel_web"; exit 1; }
|
||||
echo "$NEEDED" | grep -q libSceLibcInternal.sprx || { echo "missing libSceLibcInternal"; exit 1; }
|
||||
echo "$NEEDED" | grep -q libSceNet.sprx || { echo "missing libSceNet"; exit 1; }
|
||||
if echo "$NEEDED" | grep -q 'libkernel\.so'; then echo "FORBIDDEN: app-world libkernel.so"; exit 1; fi
|
||||
echo "linkage OK"
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/* art.c - external-asset resolution (host-testable parsing + PS4 HTTPS).
|
||||
* Pure parsing lives in art_parse_mp() (unit-tested); the POST itself
|
||||
* mirrors updater.c's bounded HTTPS client. */
|
||||
#include "art.h"
|
||||
#include "jsonlite.h"
|
||||
#include "log.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
/* Extract external_asset_path for url from an external-assets response.
|
||||
* Returns 1 + out ("mp:"+path), else 0. Pure function, host-tested. */
|
||||
int art_parse_mp(const char *body, size_t len, const char *url,
|
||||
char *out, size_t cap){
|
||||
if(!body || !url || !out || cap < 8) return 0;
|
||||
jl_val_t *r = jl_parse(body, len);
|
||||
if(!r || r->type != JL_ARRAY){ if(r) jl_free(r); return 0; }
|
||||
int rc = 0;
|
||||
for(size_t i = 0; ; i++){
|
||||
const jl_val_t *e = jl_arr_at(r, i);
|
||||
if(!e) break;
|
||||
if(e->type != JL_OBJECT) continue;
|
||||
const jl_val_t *u = jl_obj_get(e, "url");
|
||||
const jl_val_t *p = jl_obj_get(e, "external_asset_path");
|
||||
if(!u || u->type != JL_STRING || !u->str) continue;
|
||||
if(!p || p->type != JL_STRING || !p->str) continue;
|
||||
if(strcmp(u->str, url) != 0) continue;
|
||||
if(strlen(p->str) > cap - 5) continue;
|
||||
snprintf(out, cap, "mp:%s", p->str);
|
||||
rc = 1;
|
||||
break;
|
||||
}
|
||||
jl_free(r);
|
||||
return rc;
|
||||
}
|
||||
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
#include "tls.h"
|
||||
#include "clock.h"
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#elif defined(__PS4__)
|
||||
#include "tls.h"
|
||||
#include "clock.h"
|
||||
#include <orbis/Net.h>
|
||||
#include <orbis/Sysmodule.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#define ART_HOST "discord.com"
|
||||
#define ART_DEADLINE_S 12
|
||||
#define ART_RESP_MAX (16u*1024u)
|
||||
|
||||
static char s_last_url[512] = "";
|
||||
static char s_last_mp[512] = "";
|
||||
|
||||
static int art_post(const char *app_id, const char *token, const char *url,
|
||||
char *out_body, size_t body_cap, size_t *out_len){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
struct addrinfo hints, *res = NULL;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
if(getaddrinfo(ART_HOST, "443", &hints, &res) != 0 || !res) return -1;
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if(fd < 0){ freeaddrinfo(res); return -1; }
|
||||
struct timeval tv = { ART_DEADLINE_S, 0 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
if(connect(fd, res->ai_addr, res->ai_addrlen) < 0){
|
||||
close(fd); freeaddrinfo(res); return -1;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
{
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if(fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
}
|
||||
#else
|
||||
/* Host test build: no transport here (parse path still testable). */
|
||||
(void)app_id; (void)token; (void)url;
|
||||
(void)out_body; (void)body_cap; (void)out_len;
|
||||
return -1;
|
||||
#endif
|
||||
#if defined(ORBISRPC_SDK_PAYLOAD) || defined(__PS4__)
|
||||
tls_ctx_t *t = tls_start(fd, ART_HOST);
|
||||
if(!t){
|
||||
#ifdef ORBISRPC_SDK_PAYLOAD
|
||||
close(fd);
|
||||
#else
|
||||
sceNetSocketClose(fd);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
char payload[768];
|
||||
int pl = snprintf(payload, sizeof payload, "{\"urls\":[\"%s\"]}", url);
|
||||
if(pl <= 0 || pl >= (int)sizeof payload){ tls_free(t); return -1; }
|
||||
char req[1024];
|
||||
int rl = snprintf(req, sizeof req,
|
||||
"POST /api/v10/applications/%s/external-assets HTTP/1.1\r\n"
|
||||
"Host: %s\r\nAuthorization: %s\r\nContent-Type: application/json\r\n"
|
||||
"Content-Length: %d\r\nConnection: close\r\n\r\n",
|
||||
app_id, ART_HOST, token, pl);
|
||||
if(rl <= 0 || rl >= (int)sizeof req){ tls_free(t); return -1; }
|
||||
if(tls_write(t, req, (size_t)rl) < 0){ tls_free(t); return -1; }
|
||||
if(tls_write(t, payload, (size_t)pl) < 0){ tls_free(t); return -1; }
|
||||
size_t bl = 0;
|
||||
int64_t dl = orbis_mono_s() + ART_DEADLINE_S + 10;
|
||||
int status = 0;
|
||||
for(;;){
|
||||
char tmp[1024];
|
||||
int r = tls_read(t, tmp, sizeof tmp - 1);
|
||||
if(r < 0) break;
|
||||
if(r == 0){
|
||||
if(orbis_mono_s() > dl) break;
|
||||
usleep(20000);
|
||||
continue;
|
||||
}
|
||||
if(bl == 0 && bl + (size_t)r < body_cap){
|
||||
/* first chunk: parse status line */
|
||||
tmp[r] = 0;
|
||||
if(!strncmp(tmp, "HTTP/1.", 7)) status = atoi(tmp + 9);
|
||||
}
|
||||
if(bl + (size_t)r >= body_cap) break;
|
||||
memcpy(out_body + bl, tmp, (size_t)r);
|
||||
bl += (size_t)r;
|
||||
if(orbis_mono_s() > dl) break;
|
||||
}
|
||||
tls_free(t);
|
||||
if(bl == 0 || status != 200) return -1;
|
||||
out_body[bl] = 0;
|
||||
/* strip headers */
|
||||
char *e = strstr(out_body, "\r\n\r\n");
|
||||
if(!e) return -1;
|
||||
size_t hlen = (size_t)(e - out_body) + 4;
|
||||
if(out_len) *out_len = bl - hlen;
|
||||
memmove(out_body, e + 4, bl - hlen + 1);
|
||||
return 0;
|
||||
#else
|
||||
/* Host test build: no transport; parse path still testable. */
|
||||
(void)app_id; (void)token; (void)url;
|
||||
(void)out_body; (void)body_cap; (void)out_len;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
int art_resolve_mp(const char *app_id, const char *token, const char *url,
|
||||
char *out_mp, size_t cap){
|
||||
if(!app_id || !app_id[0] || !token || !token[0] || !url || !url[0] ||
|
||||
!out_mp || cap < 8)
|
||||
return 0;
|
||||
if(!strcmp(url, s_last_url) && s_last_mp[0]){
|
||||
strncpy(out_mp, s_last_mp, cap - 1);
|
||||
out_mp[cap - 1] = 0;
|
||||
return 1;
|
||||
}
|
||||
static char resp[ART_RESP_MAX];
|
||||
size_t rlen = 0;
|
||||
if(art_post(app_id, token, url, resp, sizeof resp, &rlen) != 0){
|
||||
log_msg("art: external-assets POST failed");
|
||||
return 0;
|
||||
}
|
||||
if(!art_parse_mp(resp, rlen, url, out_mp, cap)){
|
||||
log_msg("art: no mp path for url");
|
||||
return 0;
|
||||
}
|
||||
strncpy(s_last_url, url, sizeof s_last_url - 1);
|
||||
strncpy(s_last_mp, out_mp, sizeof s_last_mp - 1);
|
||||
log_msg("art: resolved mp (%zuB)", strlen(out_mp));
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/* art.h - Discord external-asset (mp:) resolution for artwork.
|
||||
*
|
||||
* Verified on hardware 2026-09-23: raw https URLs and dangling asset keys
|
||||
* in large_image either drop the whole activity or render "?". The working
|
||||
* path used by every functional tool: POST the image URL to
|
||||
* /applications/{app}/external-assets, use the returned mp: path.
|
||||
* Results cached per boot (one call per title). */
|
||||
#ifndef ORBISRPC_ART_H
|
||||
#define ORBISRPC_ART_H
|
||||
#include <stddef.h>
|
||||
/* Resolve url -> mp: path. Returns 1 and fills out_mp ("mp:..." NUL) on
|
||||
* success, 0 when unusable (caller sends no art). Not thread-safe. */
|
||||
int art_resolve_mp(const char *app_id, const char *token, const char *url,
|
||||
char *out_mp, size_t cap);
|
||||
/* Pure helper (host-tested): extract mp path for url from response. */
|
||||
int art_parse_mp(const char *body, size_t len, const char *url,
|
||||
char *out, size_t cap);
|
||||
#endif
|
||||
@@ -174,12 +174,21 @@ int daemon_run(const char *fixed_game_name){
|
||||
static int cand_hits = 0, miss_hits = 0;
|
||||
int64_t last_poll = 0;
|
||||
int64_t last_alive = 0;
|
||||
static int home_posted = 0;
|
||||
/* re-post after every (re)connect so Discord never sticks on stale */
|
||||
int need_post = active && last[0];
|
||||
while(!s_stop){ /* inner: live session, serviced every second */
|
||||
int64_t now = orbis_mono_s();
|
||||
if(now - last_tsync >= 3600){ last_tsync = now; time_sync(); }
|
||||
if(now != last_poll){
|
||||
/* Sleep/wake detection without a suspend API: if the loop
|
||||
* itself froze (Rest Mode), mono jumps and heartbeats died.
|
||||
* Log it and force a re-post so presence resumes cleanly. */
|
||||
if(last_poll != 0 && now - last_poll > 120){
|
||||
log_msg("wake: loop was frozen %llds (likely Rest Mode); resuming",
|
||||
(long long)(now - last_poll));
|
||||
need_post = active && last[0];
|
||||
}
|
||||
last_poll = now;
|
||||
if(now - last_alive >= 60){
|
||||
last_alive = now;
|
||||
@@ -247,7 +256,17 @@ int daemon_run(const char *fixed_game_name){
|
||||
prev_started = started;
|
||||
prev_end = now;
|
||||
last[0]=0; sess_tid[0]=0; active=0; started=0;
|
||||
home_posted = 0;
|
||||
}
|
||||
} else if(!home_posted){
|
||||
/* Home screen support: no game running. Post a timerless
|
||||
* 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;
|
||||
discord_set_presence_ex(&dc, state, "PlayStation 4", NULL,
|
||||
g_cfg.application_id, NULL, NULL, 0);
|
||||
log_msg("presence: home");
|
||||
home_posted = 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-8
@@ -12,6 +12,7 @@
|
||||
#include "clock.h"
|
||||
#include "b64.h"
|
||||
#include "log.h"
|
||||
#include "art.h"
|
||||
#include "jsonlite.h"
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
@@ -249,20 +250,39 @@ int discord_set_presence_ex(discord_t *d, const char *state, const char *name,
|
||||
if(!art_warned){ art_warned = 1;
|
||||
log_msg("art: no application_id or art_base_url; presence sends without artwork"); }
|
||||
}
|
||||
/* Artwork: asset KEYS into our own application only (verified on
|
||||
* hardware 2026-09-23). External-URL large_image makes Discord drop
|
||||
* the ENTIRE activity, so URLs are never sent — upload each title's
|
||||
* icon in the developer portal (key = lowercase title id) instead. */
|
||||
(void)art_url; (void)art_base_url;
|
||||
/* Artwork (verified on hardware 2026-09-23): raw https URLs and
|
||||
* 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]){
|
||||
char key[16];
|
||||
if(asset_key(title_id, key, sizeof key) >= 4){
|
||||
char mp[512] = "";
|
||||
const char *src_url = (art_url&&art_url[0]) ? art_url : NULL;
|
||||
char pack_url[320] = "";
|
||||
if(!src_url && art_base_url&&art_base_url[0]&&title_id){
|
||||
char key[16];
|
||||
if(asset_key(title_id, key, sizeof key) >= 4){
|
||||
int n=snprintf(pack_url,sizeof pack_url,"%s%s.png",art_base_url,key);
|
||||
if(n>0 && (size_t)n<sizeof pack_url) src_url = pack_url;
|
||||
}
|
||||
}
|
||||
if(src_url && d->token[0] &&
|
||||
art_resolve_mp(application_id, d->token, src_url, mp, sizeof mp)){
|
||||
jl_val_t *as=jl_new_object();
|
||||
if(as){
|
||||
jl_obj_set(as,"large_image",jl_new_string(key));
|
||||
jl_obj_set(as,"large_image",jl_new_string(mp));
|
||||
jl_obj_set(as,"large_text",jl_new_string(name?name:""));
|
||||
jl_obj_set(act,"assets",as);
|
||||
}
|
||||
} else {
|
||||
char key[16];
|
||||
if(asset_key(title_id, key, sizeof key) >= 4){
|
||||
jl_val_t *as=jl_new_object();
|
||||
if(as){
|
||||
jl_obj_set(as,"large_image",jl_new_string(key));
|
||||
jl_obj_set(as,"large_text",jl_new_string(name?name:""));
|
||||
jl_obj_set(act,"assets",as);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
jl_val_t *dd=jl_new_object();
|
||||
|
||||
@@ -15,7 +15,7 @@ OUT="build-sdk"
|
||||
mkdir -p "$OUT"
|
||||
CFLAGS="-O2 -Wall -DORBISRPC_SDK_PAYLOAD -Iorbisrpc -Ithird_party/mbedtls/include"
|
||||
echo "=== daemon sources (SDK) ==="
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_util tls ws detect discord daemon compat clock lock timesync main; do
|
||||
for f in log cfg jsonlite b64 sfo tmdb_crypto tmdb updater updater_util tls ws detect discord daemon compat clock lock timesync art main; do
|
||||
# clock has no .c (header-only helper lives in compat.c); skip if missing
|
||||
[ -f "orbisrpc/$f.c" ] || continue
|
||||
"$CC" $CFLAGS -c -o "$OUT/$f.o" "orbisrpc/$f.c" || { echo "FAIL: $f"; exit 1; }
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ all: $(TARGET)
|
||||
test: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
$(TARGET): test_utils.c ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c
|
||||
$(CC) $(CFLAGS) -o $@ test_utils.c ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c
|
||||
$(TARGET): test_utils.c ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c
|
||||
$(CC) $(CFLAGS) -o $@ test_utils.c ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../orbisrpc/tmdb_crypto.h"
|
||||
#include "../orbisrpc/updater.h"
|
||||
#include "../orbisrpc/nametable.h"
|
||||
#include "../orbisrpc/art.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -182,6 +183,23 @@ static void test_nametable(void) {
|
||||
assert(nametable_lookup("CUSA00740", out, 4) == 0);
|
||||
assert(out[3] == 0);
|
||||
}
|
||||
static void test_art_parse(void) {
|
||||
/* Real external-assets response shape (verified against live API). */
|
||||
const char *body = "[{\"url\":\"https://example.com/a.png\","
|
||||
"\"external_asset_path\":\"external/ABC/https/example.com/a.png\"},"
|
||||
"{\"url\":\"https://example.com/b.png\","
|
||||
"\"external_asset_path\":\"external/DEF/https/example.com/b.png\"}]";
|
||||
char out[128] = {0};
|
||||
assert(art_parse_mp(body, strlen(body),
|
||||
"https://example.com/b.png", out, sizeof out) == 1);
|
||||
assert(strcmp(out, "mp:external/DEF/https/example.com/b.png") == 0);
|
||||
assert(art_parse_mp(body, strlen(body),
|
||||
"https://example.com/zzz.png", out, sizeof out) == 0);
|
||||
assert(art_parse_mp("[]", 2, "x", out, sizeof out) == 0);
|
||||
assert(art_parse_mp(NULL, 0, "x", out, sizeof out) == 0);
|
||||
assert(art_resolve_mp("1", "t", "https://example.com/a.png", out, sizeof out) == 0);
|
||||
}
|
||||
|
||||
static void test_base64(void) {
|
||||
char out[32];
|
||||
assert(b64_encode((const unsigned char *)"", 0, out) == 0);
|
||||
@@ -204,6 +222,7 @@ int main(void) {
|
||||
test_sfo();
|
||||
test_nametable();
|
||||
test_base64();
|
||||
test_art_parse();
|
||||
puts("utility tests passed");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user