Commit c6dc57d
authored
Built-in libretro saves & states: flat layout, honor explicit restores, surface silent failures (#266)
* saves: flush built-in SRAM to disk before caching on quit
The in-game quit handler cached the session save (cacheCurrentSessionForQuit reads the .srm off disk) before saveSram() flushed the live SRAM, so the cache captured the pre-session copy and a later resume could restore that stale save over the fresh one. Flush SRAM first, then cache.
* states: flatten built-in save-state layout; channels are cache-only
Built-in libretro wrote live save-states into per-channel subdirs ({statesDir}/{channel}/rom.state.X) while every cache/restore path that writes the live disk wrote flat and, for built-in, used StatePathRegistry's unexpanded '{filesDir}/libretro/states' base -- reconciled by a flat->default/ migration. SRAM and external emulators are already flat. Unify on flat, channels only in the cache:
- LibretroStateSlots: single source of truth for slot<->filename.
- LibretroStatePathResolver: flat live-state base dir with the real filesDir and the same override precedence GameLauncher uses (fixes the unexpanded {filesDir} bug for built-in).
- SaveStateManager: flat live paths; migration inverted to lift legacy channel states up to flat.
- StateCacheManager / RestoreCachedStatesUseCase / RestoreStateUseCase route through the resolver; deleteAutoStateFromDisk -> deleteAutoResumeStatesFromDisk now clears both auto and resume so an explicitly-restored save loads instead of being overridden by a stale auto-resume state.
Also: RESUME_HARDCORE falls back to the active SRAM save instead of starting fresh when no hardcore-tagged save exists (RetroAchievements allows SRAM in hardcore). No DB schema change (disk-layout migration only).
* states: resolve GameLauncher's built-in state dir via LibretroStatePathResolver
GameLauncher computed the live state dir inline (platformOverride?.statePath ?: customStatePath), mirroring the resolver instead of using it. Route it through LibretroStatePathResolver.liveStateBaseDir so the launch path and the cache/restore path share one source of truth. LibretroActivity still layers variant isolation on top of this base. No behavior change (the resolver replicates the prior precedence, with an added blank-path guard).
* settings: mark save-state toggles as casual-only in built-in settings
Save states are disabled in hardcore (checkStateSupport bails on hardcoreMode), so 'Save State on Exit', 'Restore State on Launch', and 'Hardware-Core Save States' have no effect in a hardcore session. Add an 'Applies to casual games only' subtitle to each so that's clear in the UI.
* gamedetail: offer Continue-in-Hardcore when only a casual save exists
RESUME_HARDCORE now falls back to loading the active SRAM, so a game with only casual saves can be continued in hardcore. But the Play Options modal only showed the hardcore continue row when a hardcore-tagged save existed, so hardcore could never resume such a game (only New Game offered hardcore). Show the row whenever hardcore is available (RA logged in) and there is any resumable save; the condition is centralized on PlayOptionsState.showResumeHardcore so the delegate index math and the modal rendering stay in sync.
* settings: add global "Default to Hardcore" launch preference
New built-in setting (default off), 'When launching a game, default to hardcore instead of casual'. When on and hardcore is available, the Play Options modal pre-focuses the Continue-in-Hardcore row so A launches hardcore without reaching for the secondary action. Wired through BuiltinEmulatorSettings + the prefs repo + the LibretroSettingDef built-in settings UI (accessor/section/routers), read in PlayOptionsDelegate.showPlayOptions to choose the default focus.
* saves: refresh save panel in place after restore instead of closing
Restoring a save (activateSlot / restoreSave) set isVisible=false, so the panel closed and never showed the restore taking effect -- and a server restore left activeSaveCacheId null with nothing marked active, so the panel kept showing the local save as current. Keep the panel open and call refreshEntries() on success; the active marker falls back to the recorded timestamp, so server (cloud) restores now show as the active point too.
* emulator: surface core-fallback + SRAM-load failures, register vba_next
Three related fixes so save/core problems stop being silent:
- resolveBuiltinCoreId warned to log and silently substituted mGBA when the configured core wasn't a valid built-in core. Now it shows a 'Failed to load X, using Y instead' notification.
- The built-in registry only listed mgba/vbam for GBA, so a vba_next selection was rejected and fell back to mGBA (making an incompatible 64KB save). Register vba_next so it downloads from the buildbot and matches vba_next cloud saves exactly.
- unserializeSRAM returns false on a size mismatch (e.g. a 128KB save into a 64KB core) but the result was ignored, so the game booted a fresh save silently. GLRetroView now records it and LibretroActivity shows 'Failed to load save, using new save instead' in-game.
* saves: honor an explicit save restore over the default resume pick
A save-management restore writes the chosen save to the live .srm and sets activeSaveApplied, but restoreSaveForLaunchMode ignored that and re-derived the SRAM from the cache -- RESUME_HARDCORE via getLatestHardcoreSave, RESUME via getByTimestamp. So a Continue / Continue-Hardcore launch right after a restore silently loaded a different save (e.g. the latest hardcore autosave) and clobbered the user's choice, with no error since the SRAM itself loaded fine. When activeSaveApplied is set (cleared on session end) load the on-disk .srm as-is for RESUME/RESUME_HARDCORE. Consistent with ConflictAutoResolver already treating activeSaveApplied as keep-local.
* cleanup: dedup play-options ordering, reuse resolver inputs + core name helper
Post-review simplifications (no behavior change):
- PlayOptionsDelegate: derive row count, confirm mapping and default focus from one visibleActions() list instead of three parallel index computations; drop the redundant defaultFocusIndex helper.
- PlayOptionsModal: the hardcore-continue row's (hasSaves || hasHardcoreSave) is always true inside the CONTINUE section -> gate on showHardcoreOptions only.
- LibretroStatePathResolver: add a (platformStatePath, customStatePath) overload so GameLauncher reuses the game/platform/prefs it already loaded instead of 2 extra Room reads + a DataStore collect per launch.
- SaveStateManager.restoreSaveForLaunchMode: read the game row once and thread it into restoreResumeSave instead of fetching it twice on RESUME.
- LibretroCoreRegistry.displayNameFor(): shared coreId->name fallback, reused by GameLauncher and CoreCrashController.
* saves: recognize server-only cloud saves in Start Game + delete
For a freshly synced RomM game the save lives only on the server (no local cache), and two paths only looked at the local cache: Start Game (showPlayOptions + shouldShowModeSelection) showed New Game with no Continue -- and New Game overwrites the cloud save on launch -- while deleting a save slot removed only the local cache, reporting success as the server copy re-synced back. Start Game now derives its options from GetUnifiedSavesUseCase (cache + server, offline-safe via checkSavesForGame returning empty), and confirmDeleteChannel now deletes the server copies too, matching confirmDeleteLegacyChannel.
* saves: apply autosave autocleanup when the channel is the string "autosave"
autocleanup (server-side pruning to the newest 10) was gated on channelName == null, but the autosave channel is also represented as the literal string "autosave" -- a restore sets it via targetChannel = entry.channelName ?: AUTOSAVE_SLOT_NAME. In that string form autocleanup was skipped, so the autosave slot's server-side history grew unbounded after a restore. Recognize both forms (matching SaveSyncApiClient's own autosave normalization).
* gamedetail: apply Default to Hardcore on the direct-launch + fresh paths
Default to Hardcore only set the play-options modal's focus, so the direct launch (playGame -> gameLaunchDelegate.launchGame with forResume=true) and the fresh-game mode picker ignored it and started/resumed casual -- which is what a game with a save actually hits. playGame now passes overrideLaunchMode=RESUME_HARDCORE for a built-in game when the setting is on and RetroAchievements is signed in; showFreshGameModeSelection pre-focuses New Hardcore. New shouldDefaultToHardcoreResume() centralizes the decision (setting + RA login). Verified on-device that the pref itself was persisted true, so this was purely the launch path not consulting it.
* launch: centralize Default to Hardcore in the shared launch path
The default-to-hardcore decision only lived in GameDetailViewModel.playGame, so launching from the home screen, library, dual-screen, or secondary home (which all call GameLaunchDelegate.launchGame directly) still resumed casual. Move it into GameLaunchDelegate's launch-mode when() alongside isActiveSaveHardcore: a built-in game defaults to RESUME_HARDCORE when the setting is on and RetroAchievements is signed in. Every launch entry point now honors it. Revert the per-call-site check in playGame and drop the now-unused PlayOptionsDelegate.shouldDefaultToHardcoreResume; explicit modal choices (overrideLaunchMode) still take precedence.
* cleanup: dedup channel-delete, autosave-channel check, and modal-open
Post-review /simplify pass on the server-save + default-to-hardcore changes: extract SaveChannelSavesDelegate.deleteChannelEverywhere (was copy-pasted between confirmDeleteChannel and confirmDeleteLegacyChannel); add SaveSyncApiClient.isAutosaveChannel() for the null-or-"autosave" check, used by SaveUploader's autocleanup gate and computeUploadFileName; extract PlayOptionsDelegate.openModal(state, preferred) shared by showPlayOptions and showFreshGameModeSelection; and gate shouldShowModeSelection on the cheap raRepository.isLoggedIn() before the networked getUnifiedSaves.
* review: address maintainer feedback on the save/state PR
- cores: restrict vba_next to platforms = setOf("gba"). VBA Next only loads .gba, so listing gb/gbc made it a dead-end selectable core and let a stale vba_next selection on a GB/GBC game pass resolveBuiltinCoreId validation, suppressing the fallback toast.
- launch: gate Default to Hardcore on game.achievementCount > 0. hardcoreMode disables save states, rewind, and cheats regardless of whether an RA session resolves, so forcing it on an achievement-less game only removes features, and its hardcore-tagged save would ratchet future resumes into hardcore even after the setting is turned off.
- gamedetail: skip the Start Game server fetch when offline. GetUnifiedSavesUseCase gains includeServer; showPlayOptions and shouldShowModeSelection pass NetworkUtils.isOnline so an offline launch no longer waits out checkSavesForGame's 30s connect timeout. The blocking fetch is kept when online.
- saves: correct the slot-delete dialog copy to "from the server and local storage. This cannot be undone.", matching the delete now removing the server copy.
- libretro: surface the casual-into-hardcore fallback. RestoreResult gains casualSaveInHardcore, set when RESUME_HARDCORE falls back to a non-hardcore save, and LibretroActivity shows "Continuing casual save in hardcore".
- test: cover the activeSaveApplied early-return in restoreSaveForLaunchMode (the on-disk .srm must win over the cache's hardcore pick).
- cleanup: remove inline narration comments per project standard; move the genuinely-novel rationale into docblocks.1 parent 39a604d commit c6dc57d
34 files changed
Lines changed: 618 additions & 178 deletions
File tree
- app/src
- main/kotlin/com/nendo/argosy
- core/emulator
- data
- emulator
- preferences
- repository
- domain/usecase
- save
- state
- libretro
- ui
- common/savechannel
- screens
- common
- gamedetail
- delegates
- modals
- settings
- libretro
- sections
- input
- test/kotlin/com/nendo/argosy
- data/emulator
- libretro
- libretrodroid/src/main/java/com/swordfish/libretrodroid
Lines changed: 13 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
| 147 | + | |
147 | 148 | | |
148 | 149 | | |
149 | 150 | | |
150 | 151 | | |
151 | 152 | | |
152 | 153 | | |
153 | 154 | | |
| 155 | + | |
154 | 156 | | |
155 | 157 | | |
156 | 158 | | |
157 | 159 | | |
158 | 160 | | |
159 | 161 | | |
160 | 162 | | |
161 | | - | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
162 | 172 | | |
163 | 173 | | |
164 | 174 | | |
| |||
183 | 193 | | |
184 | 194 | | |
185 | 195 | | |
186 | | - | |
| 196 | + | |
| 197 | + | |
187 | 198 | | |
188 | 199 | | |
189 | 200 | | |
| |||
Lines changed: 19 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
| |||
521 | 523 | | |
522 | 524 | | |
523 | 525 | | |
524 | | - | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
525 | 529 | | |
526 | 530 | | |
527 | 531 | | |
| |||
535 | 539 | | |
536 | 540 | | |
537 | 541 | | |
538 | | - | |
| 542 | + | |
539 | 543 | | |
540 | 544 | | |
541 | 545 | | |
| |||
1061 | 1065 | | |
1062 | 1066 | | |
1063 | 1067 | | |
| 1068 | + | |
1064 | 1069 | | |
1065 | 1070 | | |
1066 | 1071 | | |
1067 | 1072 | | |
1068 | 1073 | | |
| 1074 | + | |
1069 | 1075 | | |
1070 | 1076 | | |
1071 | 1077 | | |
| |||
1079 | 1085 | | |
1080 | 1086 | | |
1081 | 1087 | | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
1082 | 1098 | | |
1083 | 1099 | | |
1084 | 1100 | | |
| |||
Lines changed: 56 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
Lines changed: 6 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| |||
101 | 102 | | |
102 | 103 | | |
103 | 104 | | |
| 105 | + | |
104 | 106 | | |
105 | 107 | | |
106 | 108 | | |
| |||
277 | 279 | | |
278 | 280 | | |
279 | 281 | | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
280 | 286 | | |
281 | 287 | | |
282 | 288 | | |
| |||
Lines changed: 1 addition & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
421 | 421 | | |
422 | 422 | | |
423 | 423 | | |
| 424 | + | |
424 | 425 | | |
425 | 426 | | |
426 | 427 | | |
| |||
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
128 | 131 | | |
129 | 132 | | |
130 | 133 | | |
| |||
Lines changed: 9 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
463 | 463 | | |
464 | 464 | | |
465 | 465 | | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
466 | 473 | | |
467 | 474 | | |
468 | | - | |
469 | | - | |
470 | | - | |
| 475 | + | |
| 476 | + | |
471 | 477 | | |
472 | 478 | | |
473 | 479 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
313 | 313 | | |
314 | 314 | | |
315 | 315 | | |
316 | | - | |
| 316 | + | |
317 | 317 | | |
318 | 318 | | |
319 | 319 | | |
| |||
Lines changed: 29 additions & 21 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
65 | 66 | | |
66 | 67 | | |
67 | 68 | | |
| |||
134 | 135 | | |
135 | 136 | | |
136 | 137 | | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
| 138 | + | |
144 | 139 | | |
145 | 140 | | |
146 | 141 | | |
| |||
487 | 482 | | |
488 | 483 | | |
489 | 484 | | |
| 485 | + | |
490 | 486 | | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
491 | 491 | | |
492 | 492 | | |
493 | 493 | | |
| |||
560 | 560 | | |
561 | 561 | | |
562 | 562 | | |
563 | | - | |
| 563 | + | |
564 | 564 | | |
565 | 565 | | |
566 | 566 | | |
567 | 567 | | |
568 | | - | |
| 568 | + | |
| 569 | + | |
569 | 570 | | |
570 | 571 | | |
571 | 572 | | |
572 | | - | |
| 573 | + | |
573 | 574 | | |
574 | 575 | | |
575 | 576 | | |
| |||
590 | 591 | | |
591 | 592 | | |
592 | 593 | | |
| 594 | + | |
| 595 | + | |
593 | 596 | | |
594 | 597 | | |
595 | 598 | | |
596 | | - | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
597 | 605 | | |
| 606 | + | |
598 | 607 | | |
599 | 608 | | |
600 | 609 | | |
601 | | - | |
602 | | - | |
603 | | - | |
604 | | - | |
605 | | - | |
606 | | - | |
607 | | - | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
608 | 617 | | |
609 | 618 | | |
610 | | - | |
611 | | - | |
612 | | - | |
| 619 | + | |
| 620 | + | |
613 | 621 | | |
614 | 622 | | |
615 | 623 | | |
| |||
Lines changed: 3 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
34 | | - | |
| 35 | + | |
35 | 36 | | |
36 | 37 | | |
37 | 38 | | |
| |||
0 commit comments