Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ tri_option(CHIAKI_USE_SYSTEM_CURL "Use system-provided curl instead of submodule
# CI injects real values from the CHIAKI_VERSION_* lines below at archive time (.github/workflows/deploy-ios.yml).
set(CHIAKI_VERSION_MAJOR 2)
set(CHIAKI_VERSION_MINOR 11)
set(CHIAKI_VERSION_PATCH 3)
set(CHIAKI_VERSION_PATCH 4)
set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH})

configure_file(
Expand Down
5 changes: 5 additions & 0 deletions gui/io.github.ForWard_Technologies_LLC.Pylux.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
<content_rating type="oars-1.1" />
<!-- Flathub: add <release> entries here when shipping new versions (not generated from CMake). -->
<releases>
<release date="2026-08-02" version="2.11.4">
<description>
<p>Cloud Play fixes for regions without native PlayStation cloud support: the streamable PS3 and PS4 catalog is restored, and titles such as Bloodborne now launch correctly. Improved store routing and reliability for all regions.</p>
</description>
</release>
<release date="2026-07-11" version="2.11.3">
<description>
<p>Fix Steam shortcut creation for games on Flatpak and refresh shortcut branding.</p>
Expand Down
4 changes: 4 additions & 0 deletions lib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,10 @@ if(CHIAKI_ENABLE_TESTS)
add_executable(cloudcatalog-test test_cloudcatalog/cloudcatalog-test.c)
target_link_libraries(cloudcatalog-test chiaki-lib)

add_executable(apollo-fallback-test test_cloudcatalog/apollo-fallback-test.c)
target_link_libraries(apollo-fallback-test chiaki-lib)
target_include_directories(apollo-fallback-test PRIVATE src)

add_executable(cloudsession-probe test_cloudsession/cloudsession-probe.c)
target_link_libraries(cloudsession-probe chiaki-lib)
target_include_directories(cloudsession-probe PRIVATE src)
Expand Down
23 changes: 18 additions & 5 deletions lib/include/chiaki/cloudcatalog.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,20 @@ extern "C" {
* and must be refetched.
* v3: adds "resolvedStoreLang" (server store language for the step0_5d container URL);
* bumped so existing caches refetch and clients get the field immediately rather
* than after the 24h TTL. */
#define CHIAKI_CLOUDCATALOG_SCHEMA_VERSION 3
* than after the 24h TTL.
* v4: guarantees that productId is unique in the games array. Older unified caches
* could contain the same owned PS5 edition through both psnow and pscloud routes,
* which also violated Android RecyclerView's former stable-ID assumption.
* v5: public Classics fallback uses the dedicated regional PS3 child container;
* invalidates v4 caches that could contain no PS3 titles after walking APOLLOROOT.
* v6: owned rows always expose their canonical entitlementId (and, when a store
* product id is known, storeProductId), including when the entitlement id
* equals productId.
* v7: explicit disc-upgrade rescues launch through their replacement product id.
* v8: public fallback walks every APOLLOROOT child container (genres, A-Z, PS3,
* PSP/PS1/PS2) deduped by product id, restoring the PS4 streamable catalog for
* fallback regions; invalidates v5-v7 caches that held only the PS3 child. */
#define CHIAKI_CLOUDCATALOG_SCHEMA_VERSION 8

typedef struct chiaki_cloudcatalog_config_t
{
Expand All @@ -56,10 +68,11 @@ typedef struct chiaki_cloudcatalog_result_t
* The JSON envelope (see CHIAKI_CLOUDCATALOG_SCHEMA_VERSION):
*
* {
* "schemaVersion": 3,
* "schemaVersion": 8,
* "total": <int>,
* "nativeMode": <bool>, // true when the authenticated PS Now walk succeeded
* "fallbackRegion": "US"|"GB"|..., // server-authoritative store country for container URLs
* "fallbackRegion": "US"|"HU"|..., // account country used for modern product resolution;
* // legacy PS3 ids map to the regional US/GB Classics store
* "resolvedStoreLang": "nl"|"", // server store language parsed from the native base_url;
* // clients use it for the step0_5d container URL (a non-English
* // native store 404s on the wrong language). "" in fallback mode.
Expand All @@ -68,7 +81,7 @@ typedef struct chiaki_cloudcatalog_result_t
* // locale chain settles); clients persist this verbatim
* "warning": "", // non-empty => client shows a banner verbatim (e.g. expired npsso)
* "games": [ {
* "productId": <string>, // canonical catalog id + stable dedup key
* "productId": <string>, // canonical catalog id; unique in this array
* "name": <string>,
* "imageUrl": <string>, // portrait/box art
* "landscapeImageUrl":<string>,
Expand Down
164 changes: 124 additions & 40 deletions lib/src/cloudcatalog_fetch.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Blocking network fetch flows for the unified cloud catalog. Faithful port of
// the async QNetworkAccessManager state machines in cloudcatalogbackend.cpp:
// - PS Now OAuth -> session -> stores -> APOLLOROOT root + alphabetical walk
// - public APOLLOROOT fallback pagination (region-unsupported accounts)
// - public APOLLOROOT one-level walk (region-unsupported accounts)
// - imagic 6-list fetch with locale fallback chain
// - owned entitlements OAuth(token) -> paginated internal_entitlements -> filter

Expand Down Expand Up @@ -504,24 +504,45 @@ CCNativeResult cc_fetch_psnow_native(ChiakiLog *log, const char *npsso, struct j
}

// ===========================================================================
// Public APOLLOROOT fallback pagination
// Public APOLLOROOT fallback walk
// ===========================================================================

struct json_object *cc_fetch_apollo_fallback(ChiakiLog *log, const char *account_country, bool *out_complete)
#define APOLLO_FALLBACK_MAX_CHILDREN 40
// Global request budget for one fallback fetch. Observed live usage is ~40
// pages across ~25 containers; the budget bounds a misbehaving server (200s
// with no usable total on every container) to a sane ceiling instead of
// max-children x per-container-cap.
#define APOLLO_FALLBACK_PAGE_BUDGET 200

// Paginate one container: append container_type=="product" rows to games (deduped
// across containers via the seen-id set) and record container_type=="container"
// child ids when collectors are given (root pass only — one level, like the native
// walk). *page_budget is decremented per request across the whole fallback fetch.
// Returns false when the pagination could not be confirmed complete — including
// when a container row is met where children can't be collected (nested deeper
// than one level, id missing/too long, more than max_children): those rows are
// data we did not walk, so the resulting list must not be cached as complete.
// The rows collected so far are always kept.
static bool apollo_walk_container(ChiakiLog *log, const char *store_country,
const char *container_id, struct json_object *games, struct json_object *seen,
char (*child_ids)[64], int *child_count, int max_children, int *page_budget)
{
if(out_complete)
*out_complete = true;
const char *store_country = cc_classics_store_country(account_country);
const char *container = cc_apollo_root_container_id(account_country);
char container_url[512];
snprintf(container_url, sizeof(container_url),
"https://psnow.playstation.com/store/api/pcnow/00_09_000/container/%s/en/19/%s",
store_country, container);
store_country, container_id);

struct json_object *games = json_object_new_array();
int start = 0, total = -1;
bool children_dropped = false;
for(;;)
{
if(*page_budget <= 0)
{
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback page budget exhausted at %s start=%d; list incomplete", container_id, start);
return false;
}
(*page_budget)--;

char url[700];
snprintf(url, sizeof(url), "%s?useOffers=true&gkb=1&gkb2=1&start=%d&size=100", container_url, start);
const char *headers[] = { "Accept: application/json", "User-Agent: " KAMAJI_UA };
Expand All @@ -534,68 +555,131 @@ struct json_object *cc_fetch_apollo_fallback(ChiakiLog *log, const char *account
if(cc_http_perform(log, &req, &resp) != CHIAKI_ERR_SUCCESS || resp.status_code != 200)
{
cc_http_response_fini(&resp);
// Mid-pagination failure: serve what we have this session, but report
// incomplete so the caller never caches a partial classics list.
if(out_complete)
*out_complete = false;
CHIAKI_LOGW(log, "[UNIFIED] APOLLOROOT fallback page at start=%d failed; list incomplete", start);
break;
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback %s page at start=%d failed; list incomplete", container_id, start);
return false;
}
struct json_object *obj = parse_body(&resp);
cc_http_response_fini(&resp);
if(!obj)
{
if(out_complete)
*out_complete = false;
CHIAKI_LOGW(log, "[UNIFIED] APOLLOROOT fallback page at start=%d unparseable; list incomplete", start);
break;
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback %s page at start=%d unparseable; list incomplete", container_id, start);
return false;
}
if(total < 0 && cc_json_has(obj, "total_results"))
total = cc_json_int(obj, "total_results");
int product_count = 0;
int link_count = 0;
struct json_object *links = cc_json_arr(obj, "links");
if(links)
{
size_t n = json_object_array_length(links);
for(size_t i = 0; i < n; i++)
{
struct json_object *g = json_object_array_get_idx(links, i);
if(!cc_ieq(cc_json_str(g, "container_type"), "product"))
if(!g || json_object_get_type(g) != json_type_object)
continue;
struct json_object *gc = cc_json_clone(g);
char img[1024];
cc_extract_cover_image(gc, img, sizeof(img));
if(*img)
cc_json_set_str(gc, "imageUrl", img);
json_object_array_add(games, gc);
product_count++;
link_count++;
const char *id = cc_json_str(g, "id");
const char *ctype = cc_json_str(g, "container_type");
if(cc_ieq(ctype, "product"))
{
// A product row without an id is unusable downstream and
// would dodge the dedupe; drop it.
if(!*id)
continue;
// The same product appears in several children (genre lists
// overlap the alphabetical ones); keep the first occurrence.
if(json_object_object_get_ex(seen, id, NULL))
continue;
json_object_object_add(seen, id, json_object_new_boolean(true));
struct json_object *gc = cc_json_clone(g);
char img[1024];
cc_extract_cover_image(gc, img, sizeof(img));
if(*img)
cc_json_set_str(gc, "imageUrl", img);
json_object_array_add(games, gc);
}
else if(cc_ieq(ctype, "container"))
{
if(!child_ids)
{
// A grandchild container: this walk is one level deep, so
// whatever is inside will not be collected this fetch.
if(!children_dropped)
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback %s contains nested container %s; not walked, list incomplete", container_id, *id ? id : "(no id)");
children_dropped = true;
continue;
}
if(!*id)
{
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback %s lists a container without an id; list incomplete", container_id);
children_dropped = true;
continue;
}
bool dup = false;
for(int c = 0; c < *child_count && !dup; c++)
dup = strcmp(child_ids[c], id) == 0;
if(dup)
continue;
if(*child_count >= max_children || strlen(id) >= sizeof(child_ids[0]))
{
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback child %s dropped (limit %d, id len %zu); list incomplete", id, max_children, strlen(id));
children_dropped = true;
continue;
}
snprintf(child_ids[(*child_count)++], sizeof(child_ids[0]), "%s", id);
}
}
}
json_object_put(obj);
start += 100;
if(total >= 0 && start >= total)
break; // server-confirmed end of list
if(product_count <= 0)
return !children_dropped; // server-confirmed end of list
if(link_count <= 0)
{
// Ran dry before the declared total, or the server never sent
// total_results at all (schema drift): either way we can't confirm
// completeness, so serve what we have but don't let it cache.
if(out_complete)
*out_complete = false;
CHIAKI_LOGW(log, "[UNIFIED] APOLLOROOT fallback ended at start=%d without a confirmed total; list incomplete", start - 100);
break;
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback %s ended at start=%d without a confirmed total; list incomplete", container_id, start - 100);
return false;
}
if(start >= 10000)
{
// Hard cap so a server that keeps returning products (with no usable
// Hard cap so a server that keeps returning rows (with no usable
// total) can't spin this loop forever.
if(out_complete)
*out_complete = false;
CHIAKI_LOGW(log, "[UNIFIED] APOLLOROOT fallback page cap hit at start=%d; list incomplete", start);
break;
CHIAKI_LOGW(log, "[UNIFIED] APOLLO fallback %s page cap hit at start=%d; list incomplete", container_id, start);
return false;
}
}
CHIAKI_LOGI(log, "[UNIFIED] APOLLOROOT fallback: %d titles", (int)json_object_array_length(games));
}

struct json_object *cc_fetch_apollo_fallback(ChiakiLog *log, const char *account_country, bool *out_complete)
{
const char *store_country = cc_classics_store_country(account_country);
const char *root = cc_apollo_root_container_id(account_country);

// APOLLOROOT historically listed product rows directly; today it lists one
// level of category containers (genres, A-Z, PS3, PSP/PS1/PS2, Remasters...)
// whose union is the full PS Now catalog. Collect products wherever they
// appear and walk every child once, so either server structure yields the
// complete PS3+PS4 list without hardcoding child container ids.
struct json_object *games = json_object_new_array();
struct json_object *seen = json_object_new_object();
char child_ids[APOLLO_FALLBACK_MAX_CHILDREN][64];
int child_count = 0;
int page_budget = APOLLO_FALLBACK_PAGE_BUDGET;
bool complete = apollo_walk_container(log, store_country, root, games, seen,
child_ids, &child_count, APOLLO_FALLBACK_MAX_CHILDREN, &page_budget);
for(int i = 0; i < child_count; i++)
{
if(!apollo_walk_container(log, store_country, child_ids[i], games, seen,
NULL, NULL, 0, &page_budget))
complete = false;
}
json_object_put(seen);
if(out_complete)
*out_complete = complete;
CHIAKI_LOGI(log, "[UNIFIED] APOLLO fallback walk: %d containers, %d titles%s",
child_count, (int)json_object_array_length(games), complete ? "" : " (incomplete)");
return games;
}

Expand Down
4 changes: 3 additions & 1 deletion lib/src/cloudcatalog_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ CCNativeResult cc_fetch_psnow_native(ChiakiLog *log, const char *npsso, struct j
bool *out_complete);

/**
* Public APOLLOROOT fallback pagination for @p account_country. New array or NULL.
* Public APOLLOROOT one-level walk for @p account_country: paginates the root
* and every child container once, deduped by product id (full PS3+PS4 list).
* New array or NULL.
* *out_complete (may be NULL) is set false when pagination aborted early on an
* HTTP/parse error, so callers must not cache the partial list.
*/
Expand Down
Loading