mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Library: a filename-derived PS3 serial gets a hyphen and loses its cover
FilenameParser reconstructs every serial in the PS2 dump shape -- four letters,
a hyphen, five digits (SLUS-20312) -- because that is the convention its regex
was written for. A PS3 title ID has no separator, so a game whose serial comes
off the filename rather than the disc is recorded as BLUS-30917.
That serial matches nothing. Cover art is fetched as COV/<TITLE_ID>.JPG, keyed
by exactly the id PARAM.SFO gives us, and the extracted-icon fallback is
disc-icons/<TITLE_ID>.png:
COV/BLUS30917.JPG -> HTTP 200
COV/BLUS-30917.JPG -> HTTP 404
so the card shows a text placeholder. The filename path is taken whenever the
disc was not probed -- probeDiscInfo answers "{}" while a game is loaded. Once
that has happened the entry cannot recover: the cached serial is re-seeded into
discInfoCache at the start of every scan and comes back as disc.titleId, which
has top priority.
Normalise the resolved serial rather than the parser, which is what repairs the
already-cached entries since they arrive through the same expression.
Three things this has to get right beyond the cover itself.
NOT EVERY 4+5 TOKEN IS A TITLE ID. FilenameParser takes the first four-letter
plus five-digit token it finds anywhere in the name, so what arrives may be a
release tag or an id belonging to a different game. Left hyphenated a bad guess
matches nothing and the card shows a placeholder -- visibly wrong, and safe.
Stripped, it would become a WELL-FORMED id and quietly resolve whatever is filed
under it: another game's cover, its curated name, and its config_db entry, which
the core applies at boot. So normalise only what carries a real PS3 prefix, B
for disc releases and N for PSN. Deliberately not gated on GamePlatform: that
enum comes from the same probe that produced the serial, so in the one case this
exists for -- probe failed, name came off the filename -- it is always null and
the guard would be constant-true.
THE SERIAL IS NOT ONLY THE COVER KEY. It also keys config.game.<serial>, per-game
core overrides, touch layouts and profiles, pad bindings, play time, the pinned
name and the custom cover file. Renaming the game without moving those resets
every one of them silently, and nothing prunes the old keys, so they become
unreachable rather than merely unused -- the custom cover worst of all, since
CustomCovers.remove resolves through the same name and cannot delete the orphan.
migrateSerialKeys moves them, and CustomCovers.renameSerial follows the file.
THE REPAIR HAS TO REACH EXISTING INSTALLS. cacheKey embeds ScanSchemaVersion, and
HomeViewModel only schedules a scan when that key changes. Without a bump an
upgraded install keeps serving the cached hyphenated ids and never rescans, so
the covers stay broken until the user finds the refresh button. Bumped 7 -> 8;
the constant's own contract asks for this whenever a stored field changes, and a
changed VALUE has the same staleness signature as a new field.
Verified on device, 14-game library with three affected ISOs. Seeded the broken
state (hyphenated serial in the cache, a pinned name and play time under the old
id, cached key at v7), then launched WITHOUT touching the UI:
load(first): cachedKey=v7|... newKey=v8|... pending=true
scan start: 1 dir(s), rawStorage=true
serial 'BLUS-30917' -> 'BLUS30917' (3 pref key(s) moved)
pending=true is the field that read false before the bump. Afterwards no
hyphenated key or serial remained anywhere in the preferences, the pinned name
was live on the card under the new id, and Lollipop Chainsaw, Ratchet & Clank:
Full Frontal Assault and Virtua Tennis 4 all render their covers.
Not fixed here: those discs still have no extracted ICON0.PNG, so their offline
fallback stays missing, and the re-probe that would create one is folder-only --
re-probing an ISO needs a vfs::mount, which the seeding loop deliberately avoids.
Two library entries that resolve to the same id can also cross-write each other's
per-serial data; that is the intended merge for a genuine duplicate, but nothing
models it.
This commit is contained in:
@@ -469,6 +469,21 @@ object CustomCovers {
|
||||
(target.isFile && target.length() > 0L).also { if (it) version.value++ }
|
||||
}.getOrDefault(false)
|
||||
|
||||
/**
|
||||
* Follow a game's custom cover across an identity correction.
|
||||
*
|
||||
* The file is named after the serial, so a game whose id is corrected stops matching its
|
||||
* own cover -- and because [remove] resolves through the same name, the orphan cannot be
|
||||
* deleted from the app either. Skips when a cover already exists under the new id, so a
|
||||
* deliberate choice is never overwritten by a stale one.
|
||||
*/
|
||||
fun renameSerial(context: Context, old: String, new: String): Boolean = runCatching {
|
||||
val from = File(dir(context), sanitize(old) + ".png")
|
||||
val to = File(dir(context), sanitize(new) + ".png")
|
||||
if (!from.isFile || to.exists()) return@runCatching false
|
||||
from.renameTo(to).also { if (it) version.value++ }
|
||||
}.getOrDefault(false)
|
||||
|
||||
fun remove(context: Context, game: GameInfo): Boolean {
|
||||
val f = fileFor(context, game) ?: return false
|
||||
return f.delete().also { if (it) version.value++ }
|
||||
|
||||
+93
-4
@@ -16,6 +16,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.armsx2.CustomCovers
|
||||
import com.armsx2.DiscIcons
|
||||
import com.armsx3.NativeApp
|
||||
import net.rpcsx.GameFlag
|
||||
@@ -507,6 +508,73 @@ class GameLibraryRepository(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the separator, but only from something that actually looks like a title ID.
|
||||
*
|
||||
* The check is not about which console the game is for -- this emulator runs PS3 titles
|
||||
* and nothing else. It is about not manufacturing an identity out of a guess.
|
||||
* FilenameParser takes the first four-letters + five-digits token it finds ANYWHERE in
|
||||
* the name and reconstructs it in the PS2 dump shape it was written for, so what arrives
|
||||
* here may be a real id (BLUS-30917), a token out of a release tag, or an id belonging to
|
||||
* a different game entirely.
|
||||
*
|
||||
* Left hyphenated, a bad guess matches nothing: no cover, no disc icon, no config, and
|
||||
* the card shows a placeholder. That is a visible failure and it is the safe one.
|
||||
* Stripped, the same guess becomes a WELL-FORMED title id and quietly resolves whatever
|
||||
* is filed under it -- another game's cover and curated name, and its config_db entry,
|
||||
* which the core applies at boot. So normalise only what carries a real PS3 prefix:
|
||||
* B for disc releases (BLUS/BLES/BCUS...), N for PSN (NPUB/NPEB...).
|
||||
*
|
||||
* Deliberately NOT gated on [GamePlatform]: that enum comes from the same probe that
|
||||
* produced the serial, so in the one case this function exists for -- the probe failed
|
||||
* and the name came off the filename -- it carries no information at all.
|
||||
*/
|
||||
private fun normalizeSerial(raw: String): String {
|
||||
val stripped = raw.replace("-", "")
|
||||
return if (ps3SerialRegex.matches(stripped)) stripped else raw
|
||||
}
|
||||
|
||||
/**
|
||||
* Carry a game's per-serial data across an identity correction.
|
||||
*
|
||||
* The serial is not just the cover key: it keys config.game.<serial>, per-game core
|
||||
* overrides, touch layouts and profiles, pad bindings, play time, the pinned name and
|
||||
* the custom cover file. Renaming the game without moving those silently resets every
|
||||
* one of them, and nothing prunes the old keys afterwards, so they become unreachable
|
||||
* rather than merely unused.
|
||||
*
|
||||
* A same-named key already under the new id is overwritten. It can only have come from
|
||||
* an earlier scan that probed the disc successfully, before this entry regressed to a
|
||||
* filename-derived id; the hyphenated one is what the game has actually been running
|
||||
* with since, so it is the live value and the older one is stale.
|
||||
*/
|
||||
private fun migrateSerialKeys(old: String, new: String) {
|
||||
runCatching {
|
||||
val prefs = MainActivityRuntime.prefs
|
||||
val snapshot = prefs.all
|
||||
val moved = snapshot.keys.filter { it.contains(old) }
|
||||
if (moved.isNotEmpty()) {
|
||||
prefs.edit().apply {
|
||||
moved.forEach { key ->
|
||||
when (val value = snapshot[key]) {
|
||||
is String -> putString(key.replace(old, new), value)
|
||||
is Int -> putInt(key.replace(old, new), value)
|
||||
is Long -> putLong(key.replace(old, new), value)
|
||||
is Boolean -> putBoolean(key.replace(old, new), value)
|
||||
is Float -> putFloat(key.replace(old, new), value)
|
||||
is Set<*> -> @Suppress("UNCHECKED_CAST")
|
||||
putStringSet(key.replace(old, new), value as Set<String>)
|
||||
else -> return@forEach
|
||||
}
|
||||
remove(key)
|
||||
}
|
||||
}.apply()
|
||||
}
|
||||
CustomCovers.renameSerial(context, old, new)
|
||||
android.util.Log.i(ScanTag, "serial '$old' -> '$new' (${moved.size} pref key(s) moved)")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createGame(
|
||||
uri: Uri,
|
||||
name: String,
|
||||
@@ -516,9 +584,22 @@ class GameLibraryRepository(private val context: Context) {
|
||||
): GameInfo {
|
||||
val (probeSerial, probePlatform) = parseProbe(rawProbe)
|
||||
val (fileTitle, fileSerial) = FilenameParser.parse(name)
|
||||
val platform = if (disc != null) GamePlatform.PS3 else probePlatform ?: GamePlatform.PS3
|
||||
// The disc's own PARAM.SFO wins: it is the authoritative title ID, where
|
||||
// a filename-derived one is a guess off a dump's naming convention.
|
||||
val serial = disc?.titleId ?: probeSerial ?: fileSerial
|
||||
//
|
||||
// Normalise the result: FilenameParser reconstructs every serial in the PS2 dump
|
||||
// shape (SLUS-20312) because that is the convention its regex was written for, so a
|
||||
// PS3 game whose serial came off the filename is recorded as BLUS-30917 and matches
|
||||
// nothing -- not the cover repo (keyed by the exact PARAM.SFO id), not disc-icons,
|
||||
// not config.game.<serial>. Doing it here rather than in FilenameParser is what
|
||||
// repairs the entries already cached: they are re-seeded into discInfoCache on every
|
||||
// scan and arrive back here as disc.titleId, which has top priority.
|
||||
val rawSerial = disc?.titleId ?: probeSerial ?: fileSerial
|
||||
val serial = rawSerial?.let(::normalizeSerial)
|
||||
if (rawSerial != null && serial != null && rawSerial != serial) {
|
||||
migrateSerialKeys(rawSerial, serial)
|
||||
}
|
||||
val compatibility = serial
|
||||
?.let { runCatching { NativeApp.getCompatibilityForSerial(it) }.getOrDefault(0) }
|
||||
?.minus(1)
|
||||
@@ -537,7 +618,7 @@ class GameLibraryRepository(private val context: Context) {
|
||||
serial = serial,
|
||||
compatibility = compatibility,
|
||||
extension = extension.uppercase(),
|
||||
platform = if (disc != null) GamePlatform.PS3 else probePlatform ?: GamePlatform.PS3,
|
||||
platform = platform,
|
||||
// Only meaningful alongside a DB title; a filename-derived one has no sort key
|
||||
// and is not a translation of anything.
|
||||
titleSort = db?.sort.orEmpty(),
|
||||
@@ -669,8 +750,16 @@ class GameLibraryRepository(private val context: Context) {
|
||||
/** v2: PS3 title ID + title + ICON0.PNG read from the disc's PARAM.SFO.
|
||||
* v5: folder-format games (JB folder / installed game folder).
|
||||
* v6: PARAM.SFO CATEGORY read, to drop game-data installs.
|
||||
* v7: licence-locked state, asked of the core per installed title. */
|
||||
const val ScanSchemaVersion = 7
|
||||
* v7: licence-locked state, asked of the core per installed title.
|
||||
* v8: PS3 serials normalised (BLUS-30917 -> BLUS30917). The scanner does not
|
||||
* extract a NEW field here, it changes the VALUE of one it already stored, which
|
||||
* has the same staleness signature: without a bump an existing install keeps
|
||||
* serving the cached hyphenated ids and never rescans, so the repair never
|
||||
* reaches the libraries that need it. */
|
||||
/** PS3 disc ids are B***, PSN ids N***, both four letters and five digits. */
|
||||
val ps3SerialRegex = Regex("^[BN][A-Z]{3}[0-9]{5}$")
|
||||
|
||||
const val ScanSchemaVersion = 8
|
||||
const val ScanTag = "ARMSX3-Scan"
|
||||
/** Staging name for an extracted icon, renamed once the title ID is known. */
|
||||
const val PendingIcon = "__pending"
|
||||
|
||||
Reference in New Issue
Block a user