Skip to content

Release/beta#25

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

Release/beta#25
forward-technologies wants to merge 33 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 3 commits June 28, 2026 02:00
…ud-games-page

Unified cloud games page: cross-platform catalog parity, streaming stats, and Android test-APK CI
assembleRelease with -Pandroid.injected.build.abi produces app-release.apk
(no ABI token in the name), so `find -name "*arm64-v8a*.apk"` matched
nothing. The pipe `find ... | xargs -0 ls -t` then ran `ls` with no args,
listing the cwd and returning the `app` directory — a bogus non-empty value
that skipped the *.apk fallback and the "No APK produced" guard, so the
final `cp "app" ...` failed with "cp: -r not specified; omitting directory".

Add `-r` (--no-run-if-empty) to both xargs calls so an empty find yields an
empty APK var, letting the fallback find app-release.apk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The build_apk step used `-Pandroid.injected.build.abi`, which is Android
Studio's deploy mechanism and marks the APK testOnly=true. Such an APK can
only be installed via `adb install -t` and is rejected by the normal
package installer when sideloaded ("can't install on this device" /
INSTALL_FAILED_TEST_ONLY).

Build a plain assembleRelease/Debug instead — abiFilters already limits the
native libs to the built ABI (arm64-v8a), so the APK stays effectively
arm64 but is now installable from a file manager / Downloads. Also locate
the APK in outputs/ only, so we never grab an intermediate unsigned APK.

Only touches the sideloadable-APK path; the AAB build and Google Play
upload steps are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@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

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