From 9aaffff7a8efb54d9c3645f63b327d026f50cf84 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Tue, 4 Aug 2026 22:05:43 -0500 Subject: [PATCH] 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 --- src/dusk/engine/engine.c | 3 +- src/dusk/rpg/rpg.c | 7 +++ src/dusk/save/CMakeLists.txt | 1 + src/dusk/save/autosave.c | 72 ++++++++++++++++++++++++++++++ src/dusk/save/autosave.h | 57 +++++++++++++++++++++++ src/dusk/ui/overlay/CMakeLists.txt | 1 + src/dusk/ui/overlay/uiautosave.c | 29 ++++++++++++ src/dusk/ui/overlay/uiautosave.h | 20 +++++++++ src/dusk/ui/uielement.c | 5 ++- 9 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/dusk/save/autosave.c create mode 100644 src/dusk/save/autosave.h create mode 100644 src/dusk/ui/overlay/uiautosave.c create mode 100644 src/dusk/ui/overlay/uiautosave.h diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index f168b077..12714e33 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -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(); diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index be1659e2..de235212 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -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()); diff --git a/src/dusk/save/CMakeLists.txt b/src/dusk/save/CMakeLists.txt index 55b35252..d2b6fc74 100644 --- a/src/dusk/save/CMakeLists.txt +++ b/src/dusk/save/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC save.c savestream.c + autosave.c ) diff --git a/src/dusk/save/autosave.c b/src/dusk/save/autosave.c new file mode 100644 index 00000000..52bf54a2 --- /dev/null +++ b/src/dusk/save/autosave.c @@ -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; +} diff --git a/src/dusk/save/autosave.h b/src/dusk/save/autosave.h new file mode 100644 index 00000000..1bfcf826 --- /dev/null +++ b/src/dusk/save/autosave.h @@ -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); diff --git a/src/dusk/ui/overlay/CMakeLists.txt b/src/dusk/ui/overlay/CMakeLists.txt index d6c051b3..1aed33ef 100644 --- a/src/dusk/ui/overlay/CMakeLists.txt +++ b/src/dusk/ui/overlay/CMakeLists.txt @@ -8,4 +8,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} uicrop.c uifullbox.c uiloading.c + uiautosave.c ) diff --git a/src/dusk/ui/overlay/uiautosave.c b/src/dusk/ui/overlay/uiautosave.c new file mode 100644 index 00000000..20bf9be1 --- /dev/null +++ b/src/dusk/ui/overlay/uiautosave.c @@ -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(); +} diff --git a/src/dusk/ui/overlay/uiautosave.h b/src/dusk/ui/overlay/uiautosave.h new file mode 100644 index 00000000..2bfe2148 --- /dev/null +++ b/src/dusk/ui/overlay/uiautosave.h @@ -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); diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index e85767e5..7194973c 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -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 };