Add autosave system with forced no-save-device prompt on failure

autoSaveQueue() is a single callable entry point for requesting an
autosave; autoSaveUpdate() pumps the queue each frame and, if the write
fails (e.g. no memory card), forces the existing no-card modal back
open and pauses world simulation until the player retries or accepts a
temporary session. A small overlay shows "SAVING" while a write is in
flight.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 22:05:43 -05:00
parent 7357b4a5df
commit 9aaffff7a8
9 changed files with 193 additions and 2 deletions
+2 -1
View File
@@ -20,6 +20,7 @@
#include "system/system.h"
#include "console/console.h"
#include "save/save.h"
#include "save/autosave.h"
engine_t ENGINE;
@@ -54,7 +55,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
#endif
sceneSet(SCENE_TYPE_INITIAL);
errorOk();
}
@@ -63,6 +63,7 @@ errorret_t engineUpdate(void) {
// Order here is important.
errorChain(networkUpdate());
errorChain(saveUpdate());
autoSaveUpdate();
timeUpdate();
inputUpdate();
consoleUpdate();
+7
View File
@@ -18,6 +18,7 @@
#include "util/memory.h"
#include "assert/assert.h"
#include "save/save.h"
#include "save/autosave.h"
#include "error/error.h"
#include "scene/scene.h"
@@ -78,6 +79,12 @@ errorret_t rpgUpdate(void) {
}
#endif
// A failed autosave forces a no-save-device prompt (see autosave.h) -
// freeze the world entirely until the player resolves it, rather than
// letting NPCs/cutscenes/camera keep running behind a modal that's
// supposed to be blocking.
if(autoSaveIsBlocking()) errorOk();
// TODO: Do not update if the scene is not the map scene?
errorChain(mapUpdate());
+1
View File
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
save.c
savestream.c
autosave.c
)
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "autosave.h"
#include "save.h"
#include "error/error.h"
#include "ui/frame/initial/uiinitialnocard.h"
autosave_t AUTO_SAVE;
static void autoSaveNoCardResult(const bool_t retry, void *user) {
AUTO_SAVE.blocking = false;
// "Retry" just re-queues the write for the next tick - there's no way
// to force a re-probe of the hardware mid-session (see saveInit()'s doc
// comment), so this only actually helps for a transient failure.
if(retry) {
AUTO_SAVE.pending = true;
return;
}
// The player explicitly acknowledged there's no save device and chose
// to proceed anyway - sticks for the rest of the session, so future
// autoSaveUpdate() calls silently skip instead of prompting again.
saveMarkTemporary();
}
static void autoSaveWriteComplete(errorret_t result, void *user) {
AUTO_SAVE.saving = false;
if(errorIsOk(result)) return;
errorCatch(errorPrint(result));
// The write failed - most likely no save medium is present right now
// (a GameCube with no memory card inserted, for example). Pause world
// simulation and force the same prompt the initial boot scene uses so
// the player can insert a card and retry, or explicitly accept a
// temporary, unsaved session.
AUTO_SAVE.blocking = true;
uiInitialNoCardOpen(autoSaveNoCardResult, NULL);
}
void autoSaveQueue(void) {
AUTO_SAVE.pending = true;
}
void autoSaveUpdate(void) {
if(AUTO_SAVE.blocking) return;
if(!AUTO_SAVE.pending) return;
if(saveIsBusy()) return;
AUTO_SAVE.pending = false;
// Already opted out of saving for this session - nothing to do, and
// definitely don't reprompt every time an autosave is queued.
if(saveIsTemporary()) return;
AUTO_SAVE.saving = true;
saveWriteSlot(SAVE_ACTIVE_SLOT, autoSaveWriteComplete, NULL);
}
bool_t autoSaveIsSaving(void) {
return AUTO_SAVE.saving;
}
bool_t autoSaveIsBlocking(void) {
return AUTO_SAVE.blocking;
}
+57
View File
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
typedef struct {
/** True from autoSaveQueue() until the queued write starts. */
bool_t pending;
/** True while the queued write is actually in flight. */
bool_t saving;
/**
* True while the forced no-save-device prompt is up after a queued
* write failed - see autoSaveUpdate(). World simulation must pause
* while this is true (see rpgUpdate()).
*/
bool_t blocking;
} autosave_t;
extern autosave_t AUTO_SAVE;
/**
* Queues an autosave to run on a future engine tick. Safe to call as
* often as needed (e.g. after every map transition or story event) -
* repeated calls before the queued save starts just collapse into one.
*/
void autoSaveQueue(void);
/**
* Pumps the queued autosave: starts the write once nothing else is
* using the save system, and if that write fails (e.g. a GameCube with
* no memory card inserted), forces the same no-save-device prompt the
* initial boot scene uses and pauses world simulation until the player
* resolves it. Must be called every engine frame, after saveUpdate().
*/
void autoSaveUpdate(void);
/**
* True while a queued autosave's write is actually in progress. Intended
* for UI to show a "Saving" indicator.
*
* @return true if an autosave write is currently in flight.
*/
bool_t autoSaveIsSaving(void);
/**
* True while a failed autosave is forcing the no-save-device prompt and
* waiting on the player's decision. Intended for the world simulation to
* pause while this is true.
*
* @return true if an autosave is currently blocking on player input.
*/
bool_t autoSaveIsBlocking(void);
+1
View File
@@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
uicrop.c
uifullbox.c
uiloading.c
uiautosave.c
)
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiautosave.h"
#include "save/autosave.h"
#include "display/text/text.h"
#include "display/screen/screen.h"
#include "display/spritebatch/spritebatch.h"
// TODO: Localize once this grows beyond a placeholder indicator.
#define UI_AUTOSAVE_TEXT "SAVING"
errorret_t uiAutoSaveDraw(void) {
if(!autoSaveIsSaving()) errorOk();
int32_t textW, textH;
textMeasure(UI_AUTOSAVE_TEXT, &FONT_DEFAULT, &textW, &textH);
float_t x = (float_t)SCREEN.scanX + UI_AUTOSAVE_MARGIN;
float_t y = (float_t)(SCREEN.scanY + SCREEN.scanHeight) -
(float_t)textH - UI_AUTOSAVE_MARGIN;
errorChain(textDraw(x, y, UI_AUTOSAVE_TEXT, COLOR_WHITE, &FONT_DEFAULT));
return spriteBatchFlush();
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#define UI_AUTOSAVE_MARGIN 8.0f
/**
* Draws a "Saving" indicator in the bottom-left corner of the screen
* while an autosave write is in flight (see save/autosave.h). No-op
* otherwise.
*
* @return Any error that occurs.
*/
errorret_t uiAutoSaveDraw(void);
+4 -1
View File
@@ -12,6 +12,7 @@
#include "engine/engine.h"
#include "ui/overlay/uifullbox.h"
#include "ui/overlay/uiloading.h"
#include "ui/overlay/uiautosave.h"
#include "ui/debug/uiplayerpos.h"
#include "ui/overlay/uicrop.h"
#include "ui/transition/uitransition.h"
@@ -129,11 +130,13 @@ uielement_t UI_ELEMENTS[] = {
.draw = uiCropDraw
},
{ .draw = uiAutoSaveDraw },
// Debug items
{ .draw = uiConsoleDraw },
{ .draw = uiFPSDraw },
{ .draw = uiPlayerPosDraw },
{ 0 } // Null terminator
};