Skip to content

Commit f4383e7

Browse files
gantoineclaude
andcommitted
fix: avoid overwriting an unpicked save slot when resuming from a state
Only auto-bind a default save as the "Save & Quit" write-back target when the choice is unambiguous (a single save, or no compatible state armed). With multiple saves and a state armed, loading the state injects a different SRAM timeline, so binding an arbitrary user_saves[0] let Save & Quit PUT over a named slot the user never selected. In that case leave the save unbound and require an explicit slot pick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 90eaf23 commit f4383e7

2 files changed

Lines changed: 16 additions & 17 deletions

File tree

frontend/src/v2/components/shared/AssetList.stories.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ function makeSave(i: number, overrides: Partial<SaveSchema> = {}): SaveSchema {
3737
created_at: new Date(now - (i + 2) * 86400 * 1000).toISOString(),
3838
updated_at: new Date(now - (i + 1) * 4 * 3600 * 1000).toISOString(),
3939
emulator: i % 2 === 0 ? "snes9x" : null,
40-
// Every third save is slot-less (mirrors web-player saves, which never
41-
// carry a slot); the rest are named client slots so the slot chip shows.
42-
slot: i % 3 === 0 ? null : slot,
40+
slot: i % 3 === 0 ? null : slot, // Every third save is slot-less
4341
screenshot: null,
4442
...overrides,
4543
} as SaveSchema;

frontend/src/v2/views/Player/EmulatorJS.vue

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -300,10 +300,6 @@ async function onPlay() {
300300
}
301301
}
302302
303-
// A save file and a savestate are orthogonal artifacts, so selecting one
304-
// no longer clears the other — both can be armed at launch. Keeping the
305-
// save bound is what lets "Save & Quit" update it in place (PUT) instead
306-
// of spawning a new slot-less file.
307303
function selectSave(save: SaveSchema) {
308304
selectedSave.value = save;
309305
localStorage.setItem(
@@ -376,22 +372,27 @@ onMounted(async () => {
376372
});
377373
}
378374
379-
// Default selection — save and state are independent, so preselect both
380-
// when present. The origin save stays bound even when the view opens on
381-
// the States tab, so "Save & Quit" writes back to it instead of creating
382-
// a new slot-less save. The visible tab still defaults to States when a
383-
// compatible state exists (quick-resume), else Saves.
375+
// Default selection — save and state are independent, so both can be
376+
// armed at once. The bound save is the write-back target for "Save &
377+
// Quit" (PUT in place), so we only auto-bind it when the choice is
378+
// unambiguous: never silently pick a slot when a state is armed and
379+
// there are multiple saves, since loading the state injects a different
380+
// SRAM timeline that would overwrite an arbitrary save the user never
381+
// picked. In that case the user must select the save slot explicitly.
384382
const initiallyCompatibleStates = rom.value.user_states.filter(
385383
(s) => !s.emulator || s.emulator === supportedCores.value[0],
386384
);
385+
const hasCompatibleState = initiallyCompatibleStates.length > 0;
387386
388-
if (rom.value.user_saves.length > 0) {
389-
selectedSave.value = rom.value.user_saves[0];
390-
}
391-
if (initiallyCompatibleStates.length > 0) {
387+
if (hasCompatibleState) {
392388
selectedState.value = initiallyCompatibleStates[0];
393389
}
394-
isSavesTabSelected.value = initiallyCompatibleStates.length === 0;
390+
const safeToBindSave =
391+
rom.value.user_saves.length === 1 || !hasCompatibleState;
392+
if (rom.value.user_saves.length > 0 && safeToBindSave) {
393+
selectedSave.value = rom.value.user_saves[0];
394+
}
395+
isSavesTabSelected.value = !hasCompatibleState;
395396
396397
const storedDisc = localStorage.getItem(`player:${rom.value.id}:disc`);
397398
if (storedDisc) {

0 commit comments

Comments
 (0)