mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Make the auto-save options and save-state import do what they say
Auto-save on exit, auto-load on boot and the interval auto-save were ARMSX2 shims returning false that were never ported, so all three toggles persisted and read back while doing nothing -- the interval job woke on schedule to call a function that always failed. They now use a reserved slot above the ten the picker shows, reusing the numbered-slot path rather than growing a second mechanism. Import treated getGamePathSlot as a file path, but it answers with the title id: File(id).exists() was false for every slot, so the first OCCUPIED slot read as free and the destination resolved against the process working directory. It copied the file nowhere useful and reported the slot it had not written. Occupancy now comes from the core and the destination from the real path. Also says how large a state is before the storage bill arrives, and stops the interval description promising a pause when a PS3 save is a stop and a reload.
This commit is contained in:
@@ -1681,7 +1681,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"renderer.upscale.label" to "Upscale",
|
||||
"renderer.vsync.description" to "Sync presentation to the display refresh — less tearing/smoother, slightly more latency. Restart the game to apply.",
|
||||
"savestate.autoLoadOnBoot" to "Auto-load last state on boot",
|
||||
"savestate.autoSaveInterval.description" to "Save automatically while you play, so a crash or a flat battery costs at most this much progress. It writes the same auto-save slot as the option above, so your numbered slots stay yours. Saving pauses the game for a moment, so a short interval is felt — 5 minutes is a good starting point.",
|
||||
"savestate.autoSaveInterval.description" to "Save automatically while you play, so a crash or a flat battery costs at most this much progress. It writes the same auto-save slot as the option above, so your numbered slots stay yours. Saving a PS3 state stops and reloads the game, which takes several seconds each time — keep the interval long, 15 minutes or more.",
|
||||
"savestate.autoSaveInterval.every" to "Every %d min",
|
||||
"savestate.autoSaveInterval.label" to "Auto-save while playing",
|
||||
"savestate.autoSaveInterval.off" to "Off",
|
||||
@@ -1692,6 +1692,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"savestate.backup" to "Backup",
|
||||
"savestate.import" to "Import",
|
||||
"savestate.hint" to "Choose a slot. Hold a slot, or use the trash button, to delete it.",
|
||||
"savestate.sizeWarning" to "Save states are big — usually 25–40 MB each, and it varies by game. Ten slots per game adds up, so keep an eye on storage.",
|
||||
"savestate.delete.mode" to "Delete a save",
|
||||
"savestate.delete.modeHint" to "Delete mode: choose a save to delete. Tap the trash button again to cancel.",
|
||||
"savestate.delete.title" to "Delete save state",
|
||||
|
||||
@@ -228,11 +228,15 @@ internal fun importSaveStateToNextFreeSlot(context: android.content.Context, uri
|
||||
val active = MainActivityRuntime.currentGame.value
|
||||
if (active == null || active.serial.isNullOrBlank()) return SS_IMPORT_NO_GAME
|
||||
return runCatching {
|
||||
val free = (0 until 10).firstOrNull { s ->
|
||||
val p = NativeApp.getGamePathSlot(s)
|
||||
p.isNullOrBlank() || !File(p).exists()
|
||||
} ?: return@runCatching SS_IMPORT_SLOTS_FULL
|
||||
val destPath = NativeApp.getGamePathSlot(free)?.takeIf(String::isNotBlank) ?: return@runCatching SS_IMPORT_FAILED
|
||||
// Occupancy comes from the core. getGamePathSlot answers with the TITLE ID, so
|
||||
// File(it).exists() was false for every slot: the first OCCUPIED slot read as free,
|
||||
// and the destination built from the same value was a relative name that landed in
|
||||
// the process working directory. The import then reported the slot it had not
|
||||
// written, which is worse than failing.
|
||||
val free = (0 until 10).firstOrNull { !NativeApp.hasStateInSlot(it) }
|
||||
?: return@runCatching SS_IMPORT_SLOTS_FULL
|
||||
val destPath = NativeApp.getSlotFilePath(free)?.takeIf(String::isNotBlank)
|
||||
?: return@runCatching SS_IMPORT_FAILED
|
||||
val dest = File(destPath)
|
||||
dest.parentFile?.mkdirs()
|
||||
val ok = context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
|
||||
@@ -150,6 +150,16 @@ fun SaveStatePickerScreen(mode: SaveMode, onBack: () -> Unit) {
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
// Worth saying before the storage bill arrives rather than after: one state is
|
||||
// tens of megabytes, it varies with the game, and there are ten slots for each.
|
||||
if (!deleteMode) {
|
||||
Text(
|
||||
str("savestate.sizeWarning"),
|
||||
color = Color(0xFF9AA0A6),
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
}
|
||||
failure?.let { key ->
|
||||
Text(
|
||||
str(key),
|
||||
|
||||
@@ -129,9 +129,16 @@ public final class NativeApp {
|
||||
public static String getGamePathSlot(int slot) { return Rpcs3Bridge.gamePathForSlot(slot); }
|
||||
|
||||
/** [TODO] Autosave is an ARMSX2 feature layered on PCSX2 savestates. */
|
||||
public static boolean hasAutosaveState() { Unsupported.note("hasAutosaveState"); return false; }
|
||||
public static boolean saveAutosaveState() { Unsupported.note("saveAutosaveState"); return false; }
|
||||
public static boolean loadAutosaveState() { Unsupported.note("loadAutosaveState"); return false; }
|
||||
/** [MAPPED] Auto-save state, kept in a reserved slot above the ten the picker shows. */
|
||||
public static boolean hasAutosaveState() { return Rpcs3Bridge.hasAutosaveState(); }
|
||||
public static boolean saveAutosaveState() { return Rpcs3Bridge.saveAutosaveState(); }
|
||||
public static boolean loadAutosaveState() { return Rpcs3Bridge.loadAutosaveState(); }
|
||||
|
||||
/** [MAPPED] Where a slot's state file lives. Not getGamePathSlot, which answers a title id. */
|
||||
public static String getSlotFilePath(int slot) { return Rpcs3Bridge.slotFilePath(slot); }
|
||||
|
||||
/** [MAPPED] Whether a slot holds a state. Ask this rather than probing a path. */
|
||||
public static boolean hasStateInSlot(int slot) { return Rpcs3Bridge.hasState(slot); }
|
||||
public static String getAutosaveGamePath() { Unsupported.note("getAutosaveGamePath"); return ""; }
|
||||
public static byte[] getAutosaveImage() { Unsupported.note("getAutosaveImage"); return null; }
|
||||
|
||||
|
||||
@@ -616,6 +616,44 @@ object Rpcs3Bridge {
|
||||
fun hasState(slot: Int): Boolean =
|
||||
runCatching { RPCSX.instance.hasStateInSlot(slot) }.getOrDefault(false)
|
||||
|
||||
/**
|
||||
* The auto-save lives one slot above the ten the picker shows.
|
||||
*
|
||||
* Auto-save-on-exit, auto-load-on-boot and the interval auto-save were ARMSX2 shims that
|
||||
* returned false and were never ported, so all three toggles persisted, read back, and
|
||||
* did nothing -- the interval job woke on schedule for a function that always failed.
|
||||
* Nothing bounds a slot number on either side of the JNI, so they reuse the numbered-slot
|
||||
* path that works rather than growing a second mechanism, and the user's ten stay theirs.
|
||||
*/
|
||||
private const val AUTOSAVE_SLOT = 10
|
||||
|
||||
@JvmStatic
|
||||
fun hasAutosaveState(): Boolean = hasState(AUTOSAVE_SLOT)
|
||||
|
||||
@JvmStatic
|
||||
fun saveAutosaveState(): Boolean = saveState(AUTOSAVE_SLOT)
|
||||
|
||||
@JvmStatic
|
||||
fun loadAutosaveState(): Boolean = loadState(AUTOSAVE_SLOT)
|
||||
|
||||
/**
|
||||
* Absolute path a slot's state file would occupy, whether or not one is there.
|
||||
*
|
||||
* gamePathForSlot answers with the TITLE ID -- the picker wants it as a subtitle -- so
|
||||
* anything treating it as a path gets a relative name that resolves against the process
|
||||
* working directory. Import did exactly that. This is the real location, built the way
|
||||
* armsx3_slot_dir builds it natively.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun slotFilePath(slot: Int): String? = runCatching {
|
||||
val title = RPCSX.instance.getTitleId().takeIf { it.isNotEmpty() } ?: return null
|
||||
val root = com.armsx2.runtime.MainActivityRuntime.systemDirPosix()
|
||||
?: com.armsx2.runtime.MainActivityRuntime.instance
|
||||
?.applicationContext?.getExternalFilesDir(null)?.absolutePath
|
||||
?: return null
|
||||
java.io.File(root, "config/savestates/$title/armsx3_slots/slot$slot.SAVESTAT.zst").absolutePath
|
||||
}.getOrNull()
|
||||
|
||||
/**
|
||||
* Occupancy for the slot picker, which treats a non-empty string as "this slot has
|
||||
* something in it" and shows the last path segment as the tile's subtitle.
|
||||
|
||||
Reference in New Issue
Block a user