Skip to content
Open
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 android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

<activity android:name="com.metallic.chiaki.main.MainActivity"
android:exported="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ object CloudLocale
return "$lang-${cty.uppercase()}"
}

/**
* Ordered store locales to try when fetching the catalog. Sony serves a fixed set of
* language-COUNTRY combinations: the country is always valid but the language may not be
* (a Hungarian-language account yields "hu-HU", which 404s, while "en-HU" works). Fall back
* to English for the same country, then en-US, so the catalog loads in every region.
* Each pair is (canonical "ll-CC" for storage, lowercased "ll-cc" for the imagic URL).
*/
fun fallbackChain(stored: String): List<Pair<String, String>>
{
val (country, language) = parseStorePath(stored)
val seen = LinkedHashSet<String>()
val chain = mutableListOf<Pair<String, String>>()
fun add(lang: String, ctry: String)
{
val canonical = "$lang-$ctry"
val imagic = canonical.lowercase()
if (seen.add(imagic))
chain.add(canonical to imagic)
}
add(language, country)
add("en", country)
add("en", "US")
return chain
}

/** Non-fatal warning when locale could not be learned from Kamaji (catalog may use en-US). */
fun unconfiguredWarning(): String =
"Could not detect your PlayStation region. The catalog may not match your store."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,43 @@ object PsnApiConstants
const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) playstation-now/0.0.0 Chrome/83.0.4103.104 Electron/9.0.4 Safari/537.36 gkApollo"

const val PS4_SCOPES = "kamaji:commerce_native kamaji:commerce_container kamaji:lists kamaji:s2s.subscriptionsPremium.get"

const val ROOT_CONTAINER_ID = "STORE-MSF75508-PSNOWALLGAMES"
}

/**
* PS3 / Classics pcnow store helpers, by account region group.
* Mirrors KamajiConsts (gui/include/cloudstreaming/pskamajisession.h) exactly.
*
* pcnow (the PS Plus PC "Apollo" backend) has only TWO Classics id families:
* - SCEA / Americas -> store MSF192018, US-region ids (UP/NPUA/BLUS),
* PS3 child container "APOLLOPS3GAMES"
* - SCEE / PAL (rest) -> store MSF192014, EU-region ids (EP/NPEA/NPEB/BLES),
* PS3 child container "APOLLOPS3"
* JP / Asia have no Apollo store (the PC app isn't offered there), so they fall back to
* PAL. A PS Plus account is authorized at Gaikai only for the id family of its own region
* group, so the catalog must be browsed + resolved in the account's group. Region is keyed
* by the ACCOUNT's region group, NOT by parsing the product-id prefix.
*/
object KamajiClassics
{
private val AMERICAS = setOf(
"US", "CA", "MX", "BR", "AR", "CL", "CO", "PE", "EC", "BO", "PY", "UY",
"CR", "GT", "HN", "NI", "PA", "SV", "DO"
)

fun isAmericasClassicsRegion(countryCode: String): Boolean =
AMERICAS.contains(countryCode.uppercase())

/** Country path to use for container/conversion calls (US for Americas, GB for PAL). */
fun classicsStoreCountry(accountCountry: String): String =
if (isAmericasClassicsRegion(accountCountry)) "US" else "GB"

/** Fully-qualified PS3 catalog container id for the account's region group. */
fun classicsPs3ContainerId(accountCountry: String): String =
if (isAmericasClassicsRegion(accountCountry))
"STORE-MSF192018-APOLLOPS3GAMES"
else
"STORE-MSF192014-APOLLOPS3"
}

Original file line number Diff line number Diff line change
Expand Up @@ -309,8 +309,23 @@ class PSKamajiSession(
try
{
val localeSetting = preferences.getCloudLanguage()
val (country, language) = com.metallic.chiaki.cloudplay.CloudLocale.parseStorePath(localeSetting)
var (country, language) = com.metallic.chiaki.cloudplay.CloudLocale.parseStorePath(localeSetting)
Log.i(TAG, "Using locale from settings: $localeSetting -> country=$country, language=$language")

// PS3 / Classics product ids (NPEA/NPEB/BLES or NPUA/NPUB/BLUS -- anything that isn't a modern
// CUSA/PPSA id) come from the public Apollo catalog, which we walk in the account's region
// group (Americas -> US store, everything else -> PAL/GB). Resolve them against that SAME
// region's container so the lookup finds the product and returns the PSNW entitlement the
// account is authorized for at Gaikai. The account's own locale country can be a region with
// no pcnow storefront (e.g. Hungary -> "Storefront not found") and the raw locale 404s, so map
// to the region-group store. Must match the PS3 catalog source (fetchPs3ClassicsCatalog).
val isLegacyClassicsId = !productId.contains("CUSA") && !productId.contains("PPSA")
if (isLegacyClassicsId)
{
country = com.metallic.chiaki.cloudplay.KamajiClassics.classicsStoreCountry(country)
language = "en"
Log.i(TAG, "Legacy Classics id -> region-group container: country=$country, language=$language")
}
val url = "$storeBase/container/$country/$language/19/$productId?useOffers=true&gkb=1&gkb2=1"

Log.d(TAG, "Step 0.5d: Convert Product ID")
Expand Down Expand Up @@ -412,6 +427,44 @@ class PSKamajiSession(
}
}


// Full-game fallback: PS Plus catalog titles have no license_type==4 entitlement; their
// full-game entitlement is license_type 0 with packageType "*GD". Prefer the one whose id
// matches the requested product's title id so cross-gen picks the consistent platform.
if (streamingEntitlementId.isEmpty())
{
val requestedTitleId = productId.split("-").getOrNull(1)?.split("_")?.firstOrNull() ?: ""
fun pickFullGameEntitlement(skuObj: JSONObject, requireTitleMatch: Boolean): Boolean
{
val ents = skuObj.optJSONArray("entitlements") ?: return false
for (j in 0 until ents.length())
{
val ent = ents.getJSONObject(j)
val entId = ent.optString("id", "")
val pkg = ent.optString("packageType", "")
if (entId.isEmpty() || !pkg.endsWith("GD")) continue
if (requireTitleMatch && requestedTitleId.isNotEmpty() && !entId.contains(requestedTitleId)) continue
streamingEntitlementId = entId
sku = skuObj.optString("id", "")
Log.i(TAG, "Found full-game Entitlement ID (PS Plus catalog fallback): $streamingEntitlementId packageType: $pkg titleMatch: $requireTitleMatch")
return true
}
return false
}
for (requireTitleMatch in listOf(true, false))
{
if (json.has("default_sku") && pickFullGameEntitlement(json.getJSONObject("default_sku"), requireTitleMatch)) break
if (streamingEntitlementId.isEmpty() && json.has("skus"))
{
val skus = json.getJSONArray("skus")
for (i in 0 until skus.length())
{
if (pickFullGameEntitlement(skus.getJSONObject(i), requireTitleMatch)) break
}
}
if (streamingEntitlementId.isNotEmpty()) break
}
}
// Try to extract platform from playable_platform
if (json.has("playable_platform"))
{
Expand Down Expand Up @@ -522,9 +575,24 @@ class PSKamajiSession(
return true
}

// User doesn't have entitlement (404), try to acquire it
// User doesn't have the per-game entitlement on the account (404).
// PS3 / Classics: the streaming entitlement is granted by the PS Plus subscription (a free
// 100%-off checkout), but that checkout requires a pcnow storefront in the account's region
// -- which many regions (e.g. Hungary) don't have, so the acquire fails with "Against
// Eligibility Rule". On a real PS5 the subscription alone grants streaming with no purchase,
// so skip the acquire and let Gaikai validate the Premium subscription directly. If Gaikai
// genuinely needs the entitlement, it returns noGameForEntitlementId downstream.
// CUSA/PS4 and PPSA/PS5 keep the existing acquire behavior.
val isLegacyClassicsId = !productId.contains("CUSA") && !productId.contains("PPSA")
if (isLegacyClassicsId)
{
Log.i(TAG, "Kamaji Step 0.5e.2 - Entitlement not found (404); legacy Classics id -> skipping acquire, proceeding to Gaikai")
return true
}

// PS4/PS5 catalog: try to acquire it via checkout.
Log.i(TAG, "Kamaji Step 0.5e.2 - Entitlement not found (404), will attempt to acquire")

// Step 0.5e.3: Checkout preview
// Throws PsPlusSubscriptionException if user doesn't have required subscription
val previewOk = step0_5e3_CheckoutPreview(sessionId)
Expand Down
Loading