Skip to content

Release/beta#25

Open
forward-technologies wants to merge 99 commits into
masterfrom
release/beta
Open

Release/beta#25
forward-technologies wants to merge 99 commits into
masterfrom
release/beta

Conversation

@forward-technologies

@forward-technologies forward-technologies commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Brings the PS Plus Cloud Play catalog and streaming experience to full parity across Qt, Android, and iOS, with all catalog fetch/merge logic centralized in libchiaki (zero client-side catalog logic). Also adds an in-stream performance stats overlay, an artificial RTT safety offset for cloud sessions, Cloud Play UI polish, a cross-platform game-language picker, and an opt-in Android test-APK CI path.

What's included

Cloud catalog (centralized in libchiaki)

  • Unified fetch + merge of Apollo/PS Now and Imagic/PS5 Cloud catalogs in one shared backend; clients consume the result directly.
  • Deterministic owned-game dedupe, trial/cross-buy handling, and cross-gen (disc-upgrade) resolution.
  • Owned PS5 product-id override on merged catalog cards; stream the owned full game for disc-upgrade titles.
  • PS3 Classics cloud streaming (region-generic) and PS Plus Cloud Play region catalog with PS4/PS5 streaming + ownership.
  • Server-authoritative store language (resolvedStoreLang): the product→entitlement lookup uses the store language parsed from the Kamaji base_url, not the catalog-settled locale — fixing /container/{CC}/{lang}/ 404s when a non-English native store (e.g. NL) is handed the wrong language. schemaVersion bumped to 3 so existing caches refetch and pick it up.
  • Expired-session warning and device-based browse parity across all three platforms.
  • The "not offered natively in your region" banner now appears only for a genuine region result — suppressed on an auth failure (the login/expired prompt is the real reason) and during catalog load (no stale-state flash).

Streaming

  • On-screen stats overlay (bitrate, packet loss, dropped-frames/sec, negotiated resolution, FPS, live RTT) sourced from libchiaki metrics with EMA smoothing - toggled from the in-stream overlay, off by default with no per-frame cost.
  • RTT safety offset for cloud sessions only (-20 ms, clamped to a 1 ms floor) to curb false "ping too high" rejections; Remote Play is unaffected.

UI polish

  • Cloud Play card refinements (neon-outlined platform badge, softer bottom gradient) matched across iOS and Android.
  • Cross-platform game-language picker with datacenter auto-matching and a concise inline note + details popup.
  • Android landscape one-tap game launch fix; portrait tablet stream-start crash fix.

CI / tooling

  • deploy-android.yml gains an opt-in APK-only checkbox (build_apk) that produces an installable, sideloadable APK artifact and skips the Google Play publish - handy for letting testers try a build before release.
  • Version bump to 2.10.22 (single source of truth in CMakeLists.txt; propagates to Android versionName/versionCode, iOS, macOS/Linux).
  • Added cloudcatalog_merge unit tests.

Credits

This builds on foundational work by nyakaspeter (the PS Plus Cloud Play / cloud streaming groundwork) and Chazq2023 (the cloud catalog and ownership work), and incorporates ideas surfaced by Leeiiiiiii's Android fork - notably the in-stream performance overlay concept and the PSNOW entitlement/SKU resolution investigations. Thanks to all three for the groundwork and inspiration.

Test plan

  • Qt, Android, and iOS show identical owned + streamable game counts.
  • Stats overlay toggles on/off and shows live, sensible values during a cloud session.
  • Logs confirm cloud-session ping reduced by 20 ms; Remote Play ping unchanged.
  • Game-language picker selects the matching datacenter and streams in that region.
  • Non-English native account (e.g. NL): step0_5d builds /container/{CC}/{lang}/ with the store's real language; owned + non-owned games resolve and stream.
  • No/expired NPSSO shows only the login banner (not the region banner), and the region banner does not flash during catalog load.
  • Android landscape: a single tap launches a cloud game.
  • Running the Android workflow with the APK-only box checked produces an installable artifact and performs no Play upload.
  • Sideloaded APK installs and streams cleanly.

nyakaspeter and others added 30 commits June 3, 2026 14:23
…ary + ownership (Qt/iOS/Android)

Reworks PlayStation Plus Cloud Play end to end so it works in English-only
regions (e.g. Hungary), streams owned PS4 and PS5 titles correctly, shows
cross-gen editions, and classifies ownership (full game vs trial vs add-on).
Applied across the Qt (C++/QML), iOS (Swift) and Android (Kotlin) clients.

Catalog / region
- Store-locale fallback chain (lang-COUNTRY -> en-COUNTRY -> en-US) so the
  imagic catalog loads in every region; the validated locale persists.
- Accept PS4 (not just PS5) cloud titles in the merge; capture
  streamingSupported=false subscription titles into the library-stream
  supplement from every subscription list (these stream via the legacy
  Kamaji/kratos path even though they are absent from public cloud browse).
- Scope the views: Game Catalog = PS Plus subscription lists (plusCatalog tag);
  Library "all" = full streamable universe + owned; Library "owned" = owned.
- Catalog falls back to the imagic catalog when the legacy PS Now /user/stores
  browse 404s (it does in many regions).
- Dedupe per game per platform so cross-gen PS4/PS5 editions both appear.
- Broaden the owned-games filter; match owned entitlements by conceptId in
  addition to product id / stable key.

Owned-title streaming (entitlement resolution)
- PS5 streams the owned PRODUCT id, not the entitlement id: a cross-gen upgrade
  (PS4 purchase + free PS5 copy) carries a stale original-SKU entitlement id that
  Gaikai's cloud catalog has no game for (-> noGameForEntitlementId); product_id
  is the current streamable SKU. (Fixed Alan Wake Remastered, Death Stranding DC.)
- When several SKUs collapse to one edition (base game + bonus/upgrade/avatars),
  keep the canonical full-game entitlement -- the one whose entitlement id EQUALS
  its product_id. Package/feature flags don't disambiguate (Death Stranding DC's
  "Bonus Content" is also PSGD + feature_type 3), so the id==product_id signal is
  what selects the real game over a DLC product Gaikai can't stream.
- PS4 streams the catalog's streamable variant (e.g. God of War's "...N" SKU whose
  Kamaji container holds the PS-Now license_type=4 SKU), not the owned download SKU;
  derive the streaming platform from the owned product (cross-gen catalog entries
  list the other generation). PS4 (CUSA) -> Kamaji/psnow; PS5 (PPSA) -> direct
  Gaikai (cronos). Datacenter ping no longer hard-fails on a measurement error (Qt).

Ownership classification (feature_type)
- feature_type 3/5 = full game owned, 1 = trial / free-to-play, 0 = add-on/DLC.
- Drop feature_type==0 extras from the owned set (DLC/themes/avatars are never a
  base game). Keep trials and free-to-play; a trial is kept as its own card so the
  full version still shows separately as "Add Game" (a trial does not collapse into
  the full-game catalog entry).

Cross-gen owned-library split
- Key owned-edition identity on conceptId + PLATFORM (matching the catalog tab) in
  both the owned cross-reference dedupe and the library merge, so a title owned on
  PS4 and PS5 (e.g. Days Gone + Days Gone Remastered) shows two separate,
  independently-streamable cards.

Platform labels
- Derive PS4/PS5 from the title id (CUSA/PPSA) instead of the hard-coded
  platform="ps5" -- Android at display time (CloudGameAdapter), iOS in the
  parser/deserializer (self-correcting the cache); Qt already did.

Catalog ownership UX
- Cross-reference the catalog against owned entitlements (mark-only): owned ->
  "Stream", non-owned modern cloud titles -> "Add Game"; OWNED / NOT OWNED badge.

Build
- Remove the committed machine-specific org.gradle.java.home (an absolute Windows
  path that broke every non-Windows / CI Gradle build); document selecting the
  JDK 21 daemon per-machine via JAVA_HOME / ~/.gradle / the IDE Gradle JDK setting.

Verified
- Qt/macOS (Hungary / PS Plus Premium): catalog loads region-wide; owned PS4 (God
  of War) and PS5 (Alan Wake Remastered, Death Stranding DC) stream from Library
  and Catalog; cross-gen Days Gone shows + streams both editions; a trial (Cyberpunk)
  shows its own Stream card plus an "Add Game" card for the full version; adding the
  PS5 Remaster lets Spider-Man stream; labels and OWNED/NOT OWNED badges correct.
- iOS: swiftc -parse clean. Android: compiles (compileDebugKotlin). Mobile not
  device-re-tested for the latest streaming/ownership pass.

Upstream reconciliation (PR #15)
- Sits on top of the merged "PS5 cloud ownership matching" PR (#15) and incorporates its
  useful additions: bundle-sibling expansion (a bundle entitlement, e.g. RE7 Gold, expands
  to its component games via componentIdsByProductId) and stable-key matching on the
  entitlement id. These are grafted onto our cross-reference as additive fallbacks (they only
  fire when our direct cascade finds no match), keeping our dedupe (conceptId+platform +
  canonical-entitlement rank), feature_type filtering and field convention where the two
  approaches differed.

Known limitations
- Some PS Plus titles are download-only (no cloud-streaming SKU, e.g. Far Cry 5,
  original Spider-Man PS4): indistinguishable in the catalog from streamable PS4
  titles, so they appear but fail at sessions/start with noGameForEntitlementId.
- PS5 catalog-only titles must be added to the library externally (PS App) first.
- PS3 titles absent from the modern imagic API; PS5 HEVC video-decode freeze is a
  separate pipeline issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PS Plus Premium streams ~250-330 PS3 Classics that never appear in the
imagic/gameslist catalog the rest of cloud play uses. Source them from the
public pcnow ("Apollo") container API and stream them via the existing
Gaikai konan path.

- Catalog: new fetchPs3Catalog walks the public Apollo PS3 container (no auth),
  paginated; surfaced in the Game Catalog and Library "all" views (not "owned").
  PS3 cards always show "Stream Game".
- Region-generic: pcnow has two Classics id families -- Americas/SCEA
  (store MSF192018, UP/NPUA/BLUS ids, child APOLLOPS3GAMES) and PAL/SCEE
  (store MSF192014, EP/NPEA/NPEB/BLES ids, child APOLLOPS3). The account
  region group selects the store; everything outside the Americas -> PAL.
- Streaming: for legacy (non-CUSA/PPSA) ids, resolve product->entitlement in
  the region-group store, and skip the regional checkout/acquire on a 404
  (Premium auto-authorizes at Gaikai; the checkout is unavailable in regions
  without a pcnow storefront, e.g. Hungary).
- PS4 (CUSA) / PS5 (PPSA) paths unchanged.

Ported across macOS (Qt), iOS (Swift), Android (Kotlin). macOS + Android
verified streaming on a real PS Plus Premium account; iOS compile-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Starting a cloud stream locks the activity to landscape via
requestedOrientation. On large tablets, portrait<->landscape also changes
screenLayout/smallestScreenSize, which MainActivity didn't declare in
configChanges -- so Android recreated the activity, detached
CloudPlayFragment, and the in-flight startCloudStreaming coroutine then
crashed on requireActivity() ("Fragment not attached to an activity").

Declare screenLayout|smallestScreenSize so MainActivity handles the rotation
itself instead of being recreated, keeping the fragment attached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PS Plus disc-upgrade entitlements (feature_type 5, e.g. Horizon Forbidden
West EP9000-PPSA01521) are the SKU the imagic browse catalog binds the
concept to, but Gaikai refuses to cloud-stream them
("disc-upgrade-unsupported"). The owned streamable edition (e.g. the
Complete Edition PPSA17903) is a different title id that is absent from the
catalog and -- like every commerce-API entitlement -- carries no conceptId,
so the owned cross-reference never matches it and only the unstreamable
disc-upgrade SKU survives the dedupe.

Add a disc-upgrade rescue to the owned cross-reference on all platforms
(Qt/iOS/Android): when a concept's surviving owned SKU is a disc upgrade,
adopt the product id of a same-name full-game (feature_type 3) owned SKU so
the card streams the edition Gaikai accepts. Since the only in-data bridge
is the title name, it is guarded to stay safe: same platform only (a PS5
disc upgrade can never resolve to a PS4 CUSA SKU), prefer the canonical base
game (product_id == entitlement id), and bail on genuine ambiguity rather
than guess.

Verified on macOS: Horizon Forbidden West now streams PPSA17903 instead of
the rejected PPSA01521.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cards

The "all" library view merges owned entitlements into the browse catalog. For
PS5 (PPSA) the override of the catalog card's product id was guarded
(if (!existing.product_id)), so it applied only when the catalog card had no id.
When the browse row carries a product id -- e.g. Horizon Forbidden West's
concept is bound to the disc-upgrade SKU PPSA01521 -- the guard kept that
unstreamable id even though the cross-reference had rescued the owned full game
(PPSA17903), so Gaikai rejected it with "disc-upgrade-unsupported".

Override unconditionally for PS5, matching the iOS and Android merges (which
always copy the owned storeProductId). The owned PS5 product IS the streamable
entitlement, so it must win over the catalog's fixed per-concept SKU. PS4 (CUSA)
is unaffected (the whole block is PS5-only).

Fixes Horizon Forbidden West failing to stream on the Steam Deck / Linux build
while macOS and Android worked -- the guard only happened to pass on those when
the catalog cache had a null product id (data-dependent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ps5CloudPlatformToken() takes a GAME OBJECT (it reads game.productId / game.id),
but the "all"-view merge passed it the product-id STRING. A string has no
.productId/.id/.device, so it always returned "", the `=== "ps5"` test was never
true, and the block that copies the owned product id onto the matched catalog
card never executed -- for any game.

That left the catalog card's own (often unstreamable) SKU in place. For Horizon
Forbidden West the catalog binds the concept to the disc-upgrade SKU PPSA01521,
so the "all" filter streamed that and Gaikai rejected it
("disc-upgrade-unsupported"), while the "owned" filter worked (it uses the
cross-reference output directly, which already carries the rescued PPSA17903).

Pass the game object so the platform check resolves to "ps5" and the owned
product id wins. Pre-existing bug -- the earlier guard/un-guard edits were both
inside this dead block, which is why neither changed anything. iOS/Android were
unaffected (their merges copy storeProductId with no platform-token check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ports nyakaspeter's PR #16 (fix/cloud-play-catalog-and-ps4-streaming) on top
of the post-#17 master (Android streaming/input/UI refactor ad331dd).

Conflicts resolved:
- CloudGameAdapter.kt: keep our recycled-card focus-stroke reset, adopt the
  PR's PPSA/CUSA title-id platform-badge derivation.
- gradle.properties: take the PR's removal of the machine-specific
  org.gradle.java.home Windows path.

Co-authored-by: Cursor <cursoragent@cursor.com>
…o re-login

The PS Plus cloud catalog cross-references the user's owned entitlements to
mark games "Stream" (owned) vs "Add Game" (not owned). When that resolution
failed (expired npsso / OAuth login_required, or a network error) the failure
was silently swallowed: every game was marked "Not Owned" AND the result was
cached for 24h, so all owned games showed "Add Game" until the cache expired,
with no indication to re-login.

- crossReferenceOwnership now propagates the failure instead of swallowing it.
- fetchPs5CloudCatalog / fetchPlusCatalog / fetchPsnowCatalog skip caching the
  ownership-merged result on failure (the raw v3 catalog + PS3 classics still
  cache), so the next open retries once the session is valid again.
- Surface a clear warning: session-expired vs network, distinguished by the
  OAuth/entitlements error. Reset the warning per fetch.

Also fix a stray indentation glitch in CloudPlayFragment.onGameClicked from the
ported PR.

Verified on-device (Pixel 6, US PS Plus, currently-expired token): the all-
"Not Owned" pscloud_catalog.json is no longer written on ownership failure.

Co-authored-by: Cursor <cursoragent@cursor.com>
…fixes and Cloud Play UI redesign.

Fix cross-buy PS5 streaming by deriving serviceType from entitlement platform_id and using platform-disciplined merge so PS4 licenses cannot corrupt PS5 cards. Add unified catalog assembly with streamability gate and Apollo region fallback on mobile, align Qt/Android/iOS Cloud Play headers and filters, and add iOS CachedAsyncImage for reliable cover art loading.

Co-authored-by: Cursor <cursoragent@cursor.com>
…s across Qt, iOS, Android

Port the Qt cloud-catalog merge fixes to iOS and Android for parity:
- Deterministic owned-entitlement tiebreak (stream rank -> GS package ->
  sku_id -> product_id -> id) so the catalog is stable regardless of the
  PSN entitlements response order.
- Suppress redundant trial (feature_type 1) cards when the same product is
  also fully owned (F2P cross-buy wrappers, e.g. Trackmania).
- Process pscloud (PS5) owned claims before psnow (PS3/PS4) so a cross-buy
  PPSA wrapper is dropped cleanly instead of orphaning the browse row.
- Drop psnow entitlements that land on a PS5-class card (cross-buy wrapper)
  rather than appending a bogus duplicate / ghost card.

Adds sku_id to the parsed entitlement on iOS/Android for the deterministic
tiebreak. Qt also restores serviceType stamping and the QML pscloud routing
guard for PS1-classic store wrappers (Worms World Party).

Co-authored-by: Cursor <cursoragent@cursor.com>
…parity across Qt, iOS, Android

Expired-NPSSO handling (Qt, iOS, Android):
- On native PS Now auth failure, surface the "log in again" warning and STOP:
  do not fall back to the public APOLLOROOT walk (that path is only for
  region-unsupported accounts) and do not cache the degraded catalog.
- iOS/Android no longer let the PS5 (imagic) fetch clobber the session warning;
  Qt's warning string is aligned to match mobile.

Catalog parity (Qt, iOS, Android now emit identical card sets):
- Decide PS5-platform browse membership from the authoritative imagic `device`
  array (or PPSA id), not the CUSA/PPSA productId token, so cross-gen titles
  (PS4 SKU with PS5 device support) are no longer dropped on mobile.
- Skip imagic browse rows already present in the Apollo (PS Now) catalog so a
  title in both lists is not emitted twice (Crow Country / Grandia / HUMANITY).
- categoryFor now resolves the catalog category from serviceType the same way
  Qt does (psnow and pscloud both short-circuit), independent of the routing-only
  streamServiceType isOwned gate, so non-owned pscloud PS4 rows are purchaseable.

Verified: Qt, iOS, and Android unified caches converge to identical totals and
per-card category/serviceType/ownership (4930 / 97 owned / 780 streamable /
4053 purchaseable), zero duplicates, identical productId sets.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ame-language picker

Move the entire cloud catalog fetch/merge/cache/cross-reference pipeline into
libchiaki (new cloudcatalog_* sources + curl_http) so Qt, iOS, and Android consume
one display-and-stream-ready contract with zero client-side catalog logic. Region
detection and Gaikai bare-language conversion also live in the lib and are exposed
to all three UIs (Q_INVOKABLE, Obj-C bridge, JNI).

Add a "Cloud Settings" game-language picker (Auto + supported locales, each shown
with its locale code) above Game Library/Catalog on iOS and Android. The manual
language override is stored separately from the auto-detected catalog/region locale
so it is never clobbered by settledLocale/Kamaji writes; streaming prefers the
override and falls back to the catalog locale. "Auto" clears the override. Datacenter
auto-matching removed; a short region/datacenter caveat is surfaced compactly
(popup on iOS, dialog header on Android, caption on Qt).

Remove now-dead per-platform catalog/ownership code superseded by the lib.

Co-authored-by: Cursor <cursoragent@cursor.com>
…card polish

libchiaki:
- Apply a cloud-only RTT safety offset (-20ms, clamped to 1ms min) at the single
  senkusha measurement point so the latency gate, /datacenters/select, /allocate,
  and the settings display all see the adjusted value. Scoped via service_type so
  Remote Play is untouched (new CHIAKI_CLOUD_RTT_* constants in common.h).
- Add a contributor-guidance block to cloudcatalog_unified.c documenting the
  shared-merge ground rules (all logic in libchiaki, imagic=owned PS5 /
  Apollo=PS3-PS4, graceful Apollo region fallback, no title-ID regex matching).

Cross-platform (Qt, Android, iOS):
- Separate the manual stream-language setting from the auto catalog locale.
- Preserve previously-measured datacenter ping RTTs instead of clobbering the
  picker with a no-RTT list before pinging.

Android:
- Shorten the inline cloud-language note and show the full caveat in a popup
  only when a specific language is chosen.

iOS:
- Redesign the Cloud Play card platform badge as a neon-outlined corner tag
  (own bottom-right layer) so it no longer competes with the title for space.

Tests:
- Add cloudcatalog_merge unit test + a desktop fetch harness.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add an opt-in on-screen streaming stats overlay (bitrate, packet loss,
dropped frames/sec, FPS, live RTT, resolution) across Qt, Android, and
iOS. All values are computed in libchiaki and read via a single
getter (Android JNI sessionGetMetrics, iOS ChiakiSessionBridge metrics
helper), so clients only render — no per-frame instrumentation. FPS/RTT
use an EMA in libchiaki to keep the readout stable. The overlay is a
single top-centered row toggled from the in-stream menu, with a light
translucent background matching across platforms.

Android Cloud Play polish: per-platform neon badge (ps5 blue / ps4
indigo / ps3 purple) with glow to match iOS; lighter, correctly
oriented bottom gradient behind the title; and a landscape card fix so
a single tap launches a game (removed stray focusableInTouchMode that
forced a focus-then-activate two-tap; TV still works via the
programmatic enableFocusableInTouchModeForTv path).

Co-authored-by: Cursor <cursoragent@cursor.com>
Add a "build_apk" checkbox to the manual deploy-android workflow that
produces an installable APK artifact and skips the Google Play publish,
so testers can sideload a build before it ships. Signed release APK when
signing secrets are present, debug APK otherwise.

Bump CHIAKI_VERSION to 2.10.22 (single source of truth in CMakeLists.txt;
propagates to Android versionName/versionCode, iOS, and macOS/Linux).

Co-authored-by: Cursor <cursoragent@cursor.com>
…ed-frame count

- deploy-android.yml: pin the APK-only build to arm64-v8a so the sideload
  artifact is always an installable arm64 split (was picking a stray ABI via
  sort|tail). Add explicit !inputs.build_apk guards to the Play-publish steps
  so an APK-only run can never upload to Google Play.
- videoreceiver: increment cumulative_frames_lost at each loss site so the
  stats overlay's running total stays accurate even on loss paths that return
  before the next successful flush. Overlay-counter accuracy only; no change to
  decode/FEC/flush behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
The stats overlay being at most one frame behind is immaterial, and the
change touched streaming-adjacent code for no user-visible benefit. Keep
only the safe CI fixes (APK ABI pin + Play-publish guards) from the prior commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the shared cloud catalog cache whenever the active account or
catalog locale changes so one account never sees another's owned games:

- Qt/Android/iOS: invalidate the libchiaki-owned catalog cache on NPSSO
  login/logout/re-entry and on cloud-language change.
- Qt: add Settings::NpssoTokenChanged and CloudCatalogBackend::cacheInvalidated;
  the cloud view re-fetches on invalidation so the visible grid never lingers
  on the previous account's games.
- Qt: fix profile-switch ordering/staleness. CloudCatalogBackend now gets
  setSettings() and is rebound to the new profile before invalidateCache()
  runs, so the reload reads the new account's NPSSO instead of the old
  (deleted) Settings (also removes a latent use-after-free on the next fetch).

Co-authored-by: Cursor <cursoragent@cursor.com>
Make log viewing crystal-clear and never-hang across platforms:
- macOS (scripts/build-macos.sh) and iOS (ios/build.sh): every launch path
  captures to one fixed log file and returns immediately; `logs` does a bounded
  one-shot dump. Detach background watchdog subshells from the terminal so a
  piped `... | tail`/`grep` no longer blocks on the auto-stop window. iOS drops
  the obsolete PYLUX_DEV_NO_STREAM toggle and dead foreground-stream helper.
- Promote the local Android build script out of tmp/ into android/build-local.sh
  (mirrors deploy-android.yml; --logs-dump for one-shot, non-hanging logcat).

Co-authored-by: Cursor <cursoragent@cursor.com>
…e table

Co-authored-by: Cursor <cursoragent@cursor.com>
…ses resolvedStoreCountry

Co-authored-by: Cursor <cursoragent@cursor.com>
Phase 2 hardcoded "en" when resolvedStoreCountry was set, regressing
non-English native accounts (JP/ja, DE/de, etc.). Keep server-authoritative
country from fallbackRegion; take language from cloud_store_locale parse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Qt SetNpssoToken and Android saveNpssoToken dropped the 24h catalog cache
unconditionally on every write. Re-auth paths re-save the same npsso (e.g.
token re-exchange after an expired access token), which is not an account
change, so the cache was needlessly wiped and the next Cloud Play open paid
a full multi-second re-fetch. Guard both on a value change, matching the
iOS SecureStore behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rse)

Expose the base_url container-locale parser via cloudcatalog_internal.h
(lib-internal, not the public API) and add a munit case. Covers the happy
path, a non-English native account (FI/fi), and the fail-closed cases
(no /container/ segment, empty country, missing language slash, country
longer than its buffer) so a malformed base_url can never feed a broken
step0_5d container URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Homebrew migrated the `sdl2` formula to an alias for `sdl2-compat`, an SDL2
API shim that dlopens SDL3 at runtime via @loader_path/libSDL3.dylib. macdeployqt
does not copy SDL3 (it's loaded via dlopen, not linked), so the app aborted on
launch with "Failed loading SDL3 library" before any of our code ran -- on any
build after the Homebrew migration, local and the App Store CI alike.

Bundle SDL3 next to libSDL2 under exactly the name sdl2-compat looks for
(libSDL3.dylib, NOT libSDL3.0.dylib -- the latter only the bare-name fallback
finds, masking the bug on dev machines that have Homebrew SDL3). Added to both
scripts/build-macos.sh (sign_app_bundle, covers --iterate + full build) and the
deploy-macos.yml App Store workflow (per-arch, lipo-merged + signed). Proven
machine-independent via the Homebrew API: sdl2-compat has aliases: ['sdl2'].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
For an owned PS Now title the unified catalog already carries the resolved
streaming entitlement, so there is nothing to look up or acquire. When the
catalog provides an entitlementId for the launched game, PSKamajiSession skips
the entire entitlement path (0.5b anonymous session, 0.5d product->entitlement
resolve, 0.5e check/acquire) and goes straight to the authenticated session
(step5/6). This is the correctness fix for storefront-less regions where 0.5d/
0.5e 404 and the acquire always fails even though the entitlement is owned.

Safety: if Gaikai rejects the fast-path entitlement (noGameForEntitlementId at
session start), CloudStreamingBackend retries exactly once with
forceFullEntitlementFlow=true -- the normal resolve/acquire path -- which can't
loop because the fast-path is disabled on the retry. Unowned titles are
unaffected: no catalog entitlementId -> full flow as before.

Also fixes a real bug this surfaced: Gaikai's Step 8 (sessions/start) error
dropped the response body, so the noGameForEntitlementId marker never reached
the fallback check; include the body in the AllocationError.

Validated on live streams: owned (Ghost of Tsushima) fast-paths and streams;
unowned (RESOGUN) runs the full $0-acquire flow; a forced bad entitlement
(Celeste) rejects -> one-shot fallback -> acquires -> streams.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 (c669fb3) renamed the iOS constant kCloudFallbackRegion to
kCloudResolvedStoreCountry / kLegacyCloudFallbackRegion but left a dangling
reference in SecureStore.clearAll(), so the iOS target had not compiled since
Phase 1 (Phase 1/2 were never actually built or run on iOS until now). Clear all
three current cloud-region keys instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the Qt owned-PSNOW fast-path (a90926d) to Android and iOS. For an owned
PS Now title the unified catalog already carries the resolved streaming
entitlement, so PSKamajiSession skips the entire entitlement path (0.5b anonymous
session, 0.5d product->entitlement resolve, 0.5e check/acquire) and goes straight
to the authenticated session. This is the correctness fix for storefront-less
regions where 0.5d/0.5e 404 and the acquire fails even though the entitlement is
owned.

Safety: if Gaikai rejects the fast-path entitlement (noGameForEntitlementId at
session start), the orchestrator retries exactly once with the full resolve/
acquire flow (one-shot; the fast-path is disabled on the retry, so it can't loop).
Unowned titles are unaffected (empty catalog entitlementId -> full flow). Also
surfaces the Gaikai step8 response body so the noGameForEntitlementId marker
reaches the fallback. CloudPlayFragment.kt / CloudPlayView.swift pass the
catalog entitlementId + platform from the launched game.

Validated on a real device + simulator: owned (Ghost of Tsushima) fast-paths and
allocates; unowned (Gitaroo Man / Bomber Crew) runs the full $0-acquire; a forced
bad entitlement (Hollow Knight / Tekken 6) rejects -> one-shot fallback -> resolves
+ acquires the real entitlement -> allocates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
step0_5d's /container/{CC}/{lang}/ entitlement lookup 404s on the wrong
language for a non-English native store (NL needs /NL/nl/, rejects
/NL/en/). The imagic catalog can settle its locale to English while the
Kamaji store still serves the native language, so the locale-derived
proxy step0_5d used was unreliable.

Parse the store language from the /user/stores base_url (same source we
already use for the store country) and emit it as a new "resolvedStoreLang"
field. step0_5d now prefers it over the locale proxy, falling back to the
proxy only in fallback/foreign mode where the field is empty. Mirrored
across libchiaki plus Qt, Android and iOS (setting + persist + step0_5d).

Bump schemaVersion 2 -> 3 so existing caches (24h TTL) refetch and pick up
the field immediately rather than after expiry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "PlayStation cloud isn't offered natively in your region" banner fired
whenever nativeMode=false -- including when the native probe failed for an
auth reason (missing/expired npsso). In that case the region was never
actually determined, so the banner was misleading; the login/expired
banner is the real message. It also flashed during catalog load because
the persisted nativeMode held a stale value mid-fetch.

Show the region banner only when nativeMode=false AND there is no auth
warning AND the catalog is not loading. Applied to Qt, Android and iOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
forward-technologies and others added 4 commits June 30, 2026 00:21
…review

R1 - store country/language locale fallback was lost. The originals (Qt/iOS
PSKamajiSession step0_5d) derived country/language from the store locale (e.g.
"de-DE" -> DE/de) when the server-authoritative resolvedStoreCountry/Lang were
empty; the unified flow hardcoded US/en, so a non-English native-store user 404'd
on the wrong container URL. Restore the fallback on all three wrappers (parse the
store locale, prefer resolved else locale-derived) -- the C keeps US/en only as the
absolute last resort.

R2 - the noGameForEntitlementId one-shot fallback only fired on Gaikai step8, not
step9. The originals captured the reject body from both start (step8) and authorize
(step9); the new gk_step9_authorize only scanned for the PS+ marker. Now it forwards
the reject body (unless it's the 002.2001 PS+ marker) so the owned-fast-path retry
fires on a step9 rejection too.

Also: authorizeCheck now accepts any 2xx (was 200/204; originals accepted all
NoError), and chiaki_cloud_provision_session inits the result before the cfg check
so a caller's result_fini is always safe. All three build; lib 108/108; live
provision still err=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Its only caller, cloudplay/ping/DatacenterPing.kt, was deleted with the old
provisioning classes (e20bf92); the native export + the PingResult it built were
left behind (datacenter pinging lives in libchiaki now). 187 lines of dead,
unreachable JNI removed. Android builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inal

The R1 fix used independent country/language fallbacks; equivalent in the common
case but not structurally identical to the old Kamaji step0_5d (commit a43e8af).
Replace with that commit's exact branching on all three wrappers: native mode
(resolvedStoreCountry empty -- the normal state for a natively-supported non-US
region) derives BOTH country and language from the store locale; fallback mode uses
the resolved country and the resolved-else-locale language. No behavioral guesswork
left. All three build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ud-provisioning

Unify PS Plus cloud provisioning into libchiaki
@forward-technologies

Copy link
Copy Markdown
Collaborator Author

@nyakaspeter @Chazq2023 @Leeiiiiiii

I've incorporated most of your changes into a single unified PR here.

One of the main issues we all ran into with the game library was that it was implemented separately on each platform — so the same logic had to be duplicated across Qt, Android, and iOS and tested three times. I've simplified this by pulling the game-loading and matching logic into one shared C library (libchiaki) that every platform now calls. So any change to the game-matching logic can be made in a single place instead of three.

I haven't been able to test this end-to-end, especially in your regions, so I can't confirm it's actually fixed. ]I'm hoping you can help with that: please check out this branch, see whether it works in your region, and if not, have your LLM make the fix in the shared location. That should be much easier now that everything lives in one place.

The two areas I'm least sure about outside my own region:

@nyakaspeter's region fallback — I can't tell whether it holds all the way through to streaming in an unsupported region.
Language handling — whether the right store language and game language actually reach the stream in non-English regions.

@Leeiiiiiii — I dropped the logic that hardcodes a specific language to a datacenter, in favor of offering a language list the user picks from (with a note that a language only applies on a datacenter that serves it). I also ported a unified version of your performance overlay to all three platforms, though there are still a few fields I haven't added yet.

These changes also merge the cloud catalog and library into a single view to keep things simpler (most people don't understand the distinction anyway).

So overall: I think it's pretty close, but I really can't tell from here — I'd appreciate those of you in other regions testing it and confirming what works and what doesn't.

Where possible, I've tried to avoid:

  • Hardcoded mappings — it's hard to predict every region and they can change, so I prefer a dynamic solution where one exists.
  • Regex-matching on game-title patterns — different regions can use different title prefixes/patterns, so try to match on structural fields (e.g. the device list) instead.
  • Platform-specific matching logic — now that the list comes from one shared location, this should live there too.
  • Broad rewrites for the region/language issues — keeping those targeted for now.

Direct apk download link created via github action if you want to just install the apk and see if it works or not (since I think most of you are on android)without building locally: Download Link

forward-technologies and others added 24 commits July 3, 2026 01:00
…r-free)

Add setSettings() to CloudStreamingBackend and call it in profileChanged()
immediately after cloud_catalog_backend is rebound, preventing a dangling
Settings pointer if the backend outlives the deleted profile Settings object.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace nested strtok(buf,"-") outer / strtok(sub,"_") inner loops with a
single strtok(buf,"-_") pass to fix the re-entrancy bug that caused the outer
loop to terminate after one token, making stable_key always return "".
Rename stable_key → cc_stable_key (remove static) and declare in
cloudcatalog_internal.h; add test_stable_key to the unit suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cloudsession_internal.h: add CC_MS_SLEEP macro (Sleep/usleep), cc_strcasestr
inline helper, and conditional <windows.h>/<unistd.h> includes.
cloudsession_gaikai.c: drop bare <unistd.h>; replace usleep(100000) with
CC_MS_SLEEP(100); replace tm_gmtoff with portable difftime(mktime) approach.
cloudsession_kamaji.c: replace three strcasestr() calls with cc_strcasestr().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rash/UB)

Add pending_callbacks (QHash<quint64,QJSValue>) + next_request_id to both
backend classes. In the async methods: hoist callback to pending_callbacks
before spawning, capture QPointer<self> + reqId instead of callback/this;
invoke via qApp (not this) in a Qt::QueuedConnection; guard with if(!self).
This ensures QJSValue is only created/fetched/invoked/destroyed on the GUI
thread. Fix provisionProgressThunk to accept QPointer<CloudStreamingBackend>*
instead of a raw this pointer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add chiaki_stream_connection_video_resolution() which locks state_mutex
before reading video_receiver->profiles[profile_cur], preventing the race
with chiaki_video_receiver_free() (called under the same mutex by
chiaki_stream_connection_run cleanup). Qt GetResolution() and Android
sessionGetMetrics() vals[5]/vals[6] now use the new accessor.

Deviation: vals[2] (cumulative_frames_lost) still read via raw video_receiver
pointer — pre-existing issue not addressed by this item (plan only targets
vals[5]/vals[6]; a separate API for that counter is out of scope).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When all pings fail, every row is measured=false and rtt=999. The old code
only checked (measured && rtt>80), so the gate was skipped and provisioning
proceeded with rtt=999. Add an explicit !measured check first so an
all-fail run sets ping_timeout=true and returns CHIAKI_ERR_UNKNOWN.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…now_catalog

Change 1: switch getGameLandscapeImageFromCache psnow branch from
"psnow_catalog" (nothing writes it) to "unified_catalog_v3".
Change 2: broaden psnow match to check id, storeProductId, and productId
(unified rows may carry the identifier under different keys).
Change 3: imageUrl flat fallback was already present; no code change needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GkPingJob now owns its strings (strdup), carries atomic_int handoff for
ownership handoff so late threads cannot touch freed memory. jobs becomes
GkPingJob** (individual heap allocs). gk_ping_thread calls atomic_exchange
at exit; if the owner already abandoned it (exchanged to 1 first) the thread
frees the job. Replace the unconditional join loop with a 15s deadline +
100ms timed-join + gk_cancelled poll; abandoned slots get detached via new
chiaki_thread_detach (thread frees). Cancel path returns CHIAKI_ERR_CANCELED.

Note: chiaki_thread_timedjoin is guarded #if !__APPLE__ in thread.c (macOS
uses blocking join). On this macOS build the new deadline code is built but
chiaki_thread_timedjoin falls back to blocking join per the existing guard,
meaning the 15s cap won't take effect on macOS — correct on Linux/Android.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…talog

psnow_fetch_category: return bool (false on HTTP/parse failure).
Category loop: add CC_MS_SLEEP(100) between categories; retry failed
category once after CC_MS_SLEEP(500); track bool complete.
cc_fetch_psnow_native: add bool *out_complete parameter.
Owned-entitlements pagination: add CC_MS_SLEEP(100) between pages.
cloudcatalog_unified.c: require apollo_complete in cache-write guard.
cloudcatalog_internal.h: add CC_MS_SLEEP portable macro.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In the success branch of the provisioning completion lambda, call
SetAccountAttributesCheckPassed(true) when attrPassed was false — a
successful provision with skip_account_attr_check=false implies the check
passed. attrPassed added to the invokeMethod capture list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add char **out_error to km_step0_5d_resolve (mirrors km_step0_5e_check_acquire).
404 path: set "Game not found: Product ID '%s' does not exist or is not
available for cloud streaming". No-entitlement path: set "Could not determine
Entitlement ID from Product ID '%s'. Game may not be available for cloud
streaming." Both byte-match the original Qt pskamajisession.cpp error texts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
gk_build_picker: after populating from the API list, append any prior-stored
datacenter not already in the output so previously-measured RTTs for
non-API datacenters are preserved (mirrors the old per-platform merge).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At the top of chiaki_cloud_provision_session, copy cfg to a local struct
and replace all NULL string fields with "" using a CC_NZ macro, matching
the documented contract that callers may omit optional fields as NULL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use int64_t + difftime() instead of long arithmetic for cache age to
avoid 32-bit overflow on platforms where long is 32 bits; update the
two log format strings to %lld with (long long) cast for portability.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three call sites passed json_object_new_array() directly into cc_json_clone();
the freshly-allocated temporary array was never put(), leaking one array per
cache hit in the v6-browse (x2) and owned-library paths. Introduce local fb /
fb2 temporaries so the transient object is released immediately after cloning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The onProgress callback is invoked from the native provisioning thread.
After fragment detach, requireActivity() throws IllegalStateException on a
background thread, crashing the process. Switch to activity?.runOnUiThread
so the call is a no-op when the fragment is detached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove invalidatePs5CatalogCache() (zero callers; cache is now owned
  exclusively by libchiaki via cc_cache_remove / cc_cache_invalidate_all).
- Remove queuePosition Q_PROPERTY, getQueuePosition(), queuePositionChanged
  signal, queue_position member, and the defaulted queuePosition parameter of
  onAllocationProgress (zero QML bindings; the progress string already carries
  all user-visible information).
- Extract the duplicated PSCloud entitlement→productId library lookup from
  getGameLandscapeImageFromCache and createCloudSteamShortcut into
  findProductIdForEntitlement(); both call sites now delegate to it.
  No behavior change.

Qt GUI not compile-checked (no Qt in cmake config); changes are
straightforward refactors with no logic mutations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove gaikaiLanguageForLocale from CloudCatalogBridge.h and .m: the
  function wrapped chiaki_cloud_gaikai_language but had zero callers after the
  C lib took ownership of language resolution.
- Remove @published var fallbackRegion and its assignment in applyLoadedGames
  in CloudPlayView.swift: catalogIsForeign is the property actually rendered
  in the UI; fallbackRegion was a redundant mirror of cloudResolvedStoreCountry
  with no SwiftUI bindings.

iOS not compile-checked (no Xcode environment); changes are pure dead-code
removal with no behavior impact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…loop

chiaki_thread_timedjoin is compiled out entirely on __APPLE__/__SWITCH__
(the `#if !__APPLE__ && !__SWITCH__` guard in lib/src/thread.c), so after F8
libchiaki.a on macOS contained an UNDEFINED _chiaki_thread_timedjoin
referenced by cloudsession_gaikai.o. chiaki-unit linked only because the
test binary never pulls gaikai.o from the archive; the real macOS/iOS apps
link chiaki_cloud_provision_session -> pull gaikai.o -> hard link failure.
And on Android the existing implementation ignores the timeout (blocking
pthread_join fallback since minSdk 24 < 26), so the 15s deadline and cancel
polling silently didn't work on our primary platform.

Rework the collection into two phases with no timed join at all:
- GkPingJob gains an atomic_int done, set by gk_ping_thread right before
  its handoff exchange (also covers the inline thread-create-failure path,
  whose now-redundant owner-side handoff exchange is removed).
- Phase 1 polls the done flags with CC_MS_SLEEP(100) against the 15s
  deadline, checking gk_cancelled each tick.
- Phase 2 joins only jobs that signalled done (join returns almost
  immediately) and appends their rows; unfinished jobs are detached and
  ownership resolved via the existing handoff atomic_exchange (a job that
  finished inside the race window is still collected, without joining the
  detached thread; otherwise the thread frees the job and the row falls
  back to the dcs entry).

thread.c's timedjoin is left untouched; chiaki_thread_detach stays.

Verified: ctest 109/109; no chiaki_thread_timedjoin reference left in
cloudsession_gaikai.c; nm shows no undefined _chiaki_thread_timedjoin in
libchiaki.a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cessor

android/app/src/main/cpp/chiaki-jni.c sessionGetMetrics still read
sc->video_receiver->cumulative_frames_lost into vals[2] without holding
state_mutex -- the same use-after-free race F5 fixed for vals[5]/vals[6]
(video_receiver is freed on the stream thread while the overlay polls).

- Add chiaki_stream_connection_video_frames_lost() to streamconnection.h
  (same doc style as the resolution accessor) and implement it in
  streamconnection.c under state_mutex; returns 0 when no receiver.
- chiaki-jni.c vals[2] now calls the accessor. vals[] is zero-initialized,
  so the no-receiver case (accessor returns 0) is behavior-identical to the
  old guarded read that left vals[2] untouched.
- Updated the stale "no locking" comment above sessionGetMetrics to match.

Sweep: `git grep -n 'video_receiver' -- gui/ android/ ios/` shows no direct
dereference outside libchiaki (one comment mention remains).

Verified: ctest 109/109; _chiaki_stream_connection_video_frames_lost is a
defined (T) symbol in libchiaki.a.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eads

On Windows the OS thread wrapper (thread.c win32_thread_func) writes thread->ret
into its ChiakiThread AFTER the ping body returns. F8's new abandon path detaches
a still-running thread and then free()s the threads[] array, so a datacenter hung
past the 15s deadline would write into freed memory. Leak threads[] on the abandon
path on Windows only (bounded, rare); POSIX is unaffected (pthread_t not written
post-detach) and still frees normally. Found by fresh-eyes review of PR #30.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cessors

chiaki_session_get_stream_metrics_ex (the iOS overlay's metrics path) still read
video_receiver->cumulative_frames_lost / profiles[profile_cur] with no state_mutex
-- the exact use-after-free race F5 fixed for Qt and Android, left live on iOS.
Use chiaki_stream_connection_video_frames_lost/_video_resolution. Found by
fresh-eyes review of PR #30.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ress thunk

QPointer copy off the GUI thread is not thread-safe; it's safe here only because
CloudStreamingBackend outlives every provision. Document the real constraint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…findings

Fix 8 bugs + 10 cleanups from the cloud-provisioning review
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants