Add initial boot scene to check/prompt for save data before overworld

A new SCENE_TYPE_INITIAL now runs before the overworld: it checks
save-device availability and existing save data, then shows one of two
new dedicated modals - "no save device found" (Retry / Continue Anyway)
or "no save data found, create one?" (Yes / No) - before handing off to
the overworld. Both modals are self-contained UI elements mirroring
uiconfirm.h's shape, registered like any other global UI element.

Choosing "Continue Anyway" marks the session temporary (SAVE.temporary,
folded into saveIsAvailable()) so saving stays disabled for the rest of
the session instead of silently retrying, and the game menu's Save
action now reports that distinctly instead of the generic "no device"
message.

Also removes rpg.c's leftover TEST block (unconditional player-name
stamp + save write on every boot) now that this real flow owns save
creation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 20:53:01 -05:00
parent aa0180571e
commit e2a9442aa6
18 changed files with 649 additions and 18 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
consolePrint("Assertions real"); consolePrint("Assertions real");
#endif #endif
sceneSet(SCENE_TYPE_OVERWORLD); sceneSet(SCENE_TYPE_INITIAL);
errorOk(); errorOk();
-12
View File
@@ -16,7 +16,6 @@
#include "time/time.h" #include "time/time.h"
#include "rpgcamera.h" #include "rpgcamera.h"
#include "util/memory.h" #include "util/memory.h"
#include "util/string.h"
#include "assert/assert.h" #include "assert/assert.h"
#include "save/save.h" #include "save/save.h"
#include "error/error.h" #include "error/error.h"
@@ -24,10 +23,6 @@
#include "ui/rpg/uiemoji.h" #include "ui/rpg/uiemoji.h"
#include "rpg/story/storyflag.h" #include "rpg/story/storyflag.h"
static void rpgTestSaveComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
}
errorret_t rpgInit(void) { errorret_t rpgInit(void) {
memoryZero(ENTITIES, sizeof(ENTITIES)); memoryZero(ENTITIES, sizeof(ENTITIES));
memoryZero(MAP_AREAS, sizeof(MAP_AREAS)); memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
@@ -71,13 +66,6 @@ errorret_t rpgInit(void) {
backpackAdd(ITEM_ID_POTATO, 3); backpackAdd(ITEM_ID_POTATO, 3);
backpackAdd(ITEM_ID_APPLE, 8); backpackAdd(ITEM_ID_APPLE, 8);
// TEST: Verify the save system round-trips real game data, not just the
// header/version. Remove once there's an actual name-entry flow. On PSP
// this shows the real native save dialog every boot - expected while
// testing that path, not something to ship as-is.
stringCopy(saveSlot->playerName, "Dusk", SAVE_PLAYER_NAME_MAX);
saveWriteSlot(SAVE_ACTIVE_SLOT, rpgTestSaveComplete, NULL);
// All Good! // All Good!
errorOk(); errorOk();
} }
+9 -1
View File
@@ -51,7 +51,15 @@ errorret_t saveInit(void) {
} }
bool_t saveIsAvailable(void) { bool_t saveIsAvailable(void) {
return SAVE.available; return SAVE.available && !SAVE.temporary;
}
void saveMarkTemporary(void) {
SAVE.temporary = true;
}
bool_t saveIsTemporary(void) {
return SAVE.temporary;
} }
errorret_t saveDispose(void) { errorret_t saveDispose(void) {
+34 -4
View File
@@ -26,6 +26,15 @@ typedef struct {
* conditions here, not fatal errors - see saveIsAvailable(). * conditions here, not fatal errors - see saveIsAvailable().
*/ */
bool_t available; bool_t available;
/**
* True once the player has explicitly acknowledged there's no save
* device and chosen to continue anyway (see the initial scene's "no
* card" prompt) - sticky for the rest of the session, folded into
* saveIsAvailable() so every existing/future call site that already
* gates on that automatically refuses to save/load from here on. See
* saveMarkTemporary()/saveIsTemporary().
*/
bool_t temporary;
/** /**
* Scratch error state used by platforms whose save/load completes * Scratch error state used by platforms whose save/load completes
* asynchronously (see saveIsBusy()) to construct a result to hand to a * asynchronously (see saveIsBusy()) to construct a result to hand to a
@@ -60,14 +69,35 @@ errorret_t saveInit(void);
/** /**
* Checks whether the save medium was reachable as of the last load/write * Checks whether the save medium was reachable as of the last load/write
* attempt (or saveInit(), if none has been attempted yet). Intended for UI * attempt (or saveInit(), if none has been attempted yet), AND the
* to decide whether to offer saving/loading at all, or to explain why it * session hasn't been marked temporary (see saveMarkTemporary()).
* isn't available right now - e.g. "No memory card inserted". * Intended for UI to decide whether to offer saving/loading at all, or to
* explain why it isn't available right now - e.g. "No memory card
* inserted".
* *
* @return true if the save medium was available last time it was checked. * @return true if saving/loading can be attempted right now.
*/ */
bool_t saveIsAvailable(void); bool_t saveIsAvailable(void);
/**
* Marks this session as temporary - the player explicitly acknowledged
* there's no save device and chose to continue anyway. One-way: nothing
* currently re-probes the save medium mid-session (see saveInit()'s doc
* comment), so there's no legitimate way to un-stick this once set short
* of restarting the game.
*/
void saveMarkTemporary(void);
/**
* True once saveMarkTemporary() has been called this session. Distinct
* from saveIsAvailable() so UI can give a more specific message (e.g.
* "this session is temporary" rather than the generic "no save device
* found") once the player has already made that choice.
*
* @return true if this session has been marked temporary.
*/
bool_t saveIsTemporary(void);
/** /**
* Disposes of the save system. * Disposes of the save system.
* *
+1
View File
@@ -10,5 +10,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
) )
# Subdirs # Subdirs
add_subdirectory(initial)
add_subdirectory(overworld) add_subdirectory(overworld)
add_subdirectory(battle) add_subdirectory(battle)
+9
View File
@@ -0,0 +1,9 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
sceneinitial.c
)
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "scene/scene.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "error/error.h"
#include "save/save.h"
#include "ui/frame/initial/uiinitialnocard.h"
#include "ui/frame/initial/uiinitialcreatesave.h"
static void sceneInitialCheckSave(void);
static void sceneInitialCreateSaveWriteComplete(errorret_t result, void *user) {
if(errorIsNotOk(result)) errorCatch(errorPrint(result));
sceneSet(SCENE_TYPE_OVERWORLD);
}
static void sceneInitialCreateSaveResult(const bool_t create, void *user) {
if(!create) {
sceneSet(SCENE_TYPE_OVERWORLD);
return;
}
saveWriteSlot(SAVE_ACTIVE_SLOT, sceneInitialCreateSaveWriteComplete, NULL);
}
static void sceneInitialNoCardResult(const bool_t retry, void *user) {
if(retry) {
sceneInitialCheckSave();
return;
}
// The player explicitly acknowledged there's no save device and chose
// to proceed anyway - stick with that for the rest of the session (see
// saveMarkTemporary()'s doc comment for why this can't be un-set later).
saveMarkTemporary();
sceneSet(SCENE_TYPE_OVERWORLD);
}
static void sceneInitialLoadComplete(errorret_t result, void *user) {
if(errorIsNotOk(result) || !saveIsAvailable()) {
errorCatch(result);
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
return;
}
if(saveSlotExists(SAVE_ACTIVE_SLOT)) {
sceneSet(SCENE_TYPE_OVERWORLD);
return;
}
uiInitialCreateSaveOpen(sceneInitialCreateSaveResult, NULL);
}
static void sceneInitialCheckSave(void) {
if(!saveIsAvailable()) {
uiInitialNoCardOpen(sceneInitialNoCardResult, NULL);
return;
}
if(saveIsBusy()) return;
saveLoadSlot(SAVE_ACTIVE_SLOT, sceneInitialLoadComplete, NULL);
}
errorret_t sceneInitialInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
sceneInitialCheckSave();
errorOk();
}
errorret_t sceneInitialUpdate(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
errorOk();
}
errorret_t sceneInitialRender(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
errorOk();
}
errorret_t sceneInitialDispose(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
errorOk();
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "scene/scenebase.h"
// No per-scene state needed - the save globals and the two modal UI
// elements (see ui/frame/initial/) carry everything this scene cares
// about. A byte placeholder keeps the struct non-empty for portability.
typedef struct {
uint8_t reserved;
} sceneinitial_t;
/**
* Initialises the initial (boot) scene - kicks off the save
* availability/existence check that decides which prompt, if any, to
* show before proceeding to the overworld.
*
* @param sceneData The scene data used for this scene.
* @return An error if the init failed, or errorOk() if it succeeded.
*/
errorret_t sceneInitialInit(scenedata_t *sceneData);
/**
* Updates the initial scene. Currently a no-op - all the work happens in
* save callbacks and the two modal UI elements' own button handling.
*
* @param sceneData The scene data used for this scene.
* @return An error if the update failed, or errorOk() if it succeeded.
*/
errorret_t sceneInitialUpdate(scenedata_t *sceneData);
/**
* Renders the initial scene. Currently a no-op - the modals draw
* themselves via the global UI element pipeline.
*
* @param sceneData The scene data used for this scene.
* @return An error if the render failed, or errorOk() if it succeeded.
*/
errorret_t sceneInitialRender(scenedata_t *sceneData);
/**
* Disposes the initial scene.
*
* @param sceneData The scene data used for this scene.
* @return An error if the dispose failed, or errorOk() if it succeeded.
*/
errorret_t sceneInitialDispose(scenedata_t *sceneData);
+7
View File
@@ -10,6 +10,13 @@
scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = { scenecallbacks_t SCENE_TYPES[SCENE_TYPE_COUNT] = {
[SCENE_TYPE_NULL] = { 0 }, [SCENE_TYPE_NULL] = { 0 },
[SCENE_TYPE_INITIAL] = {
.init = sceneInitialInit,
.update = sceneInitialUpdate,
.render = sceneInitialRender,
.dispose = sceneInitialDispose
},
[SCENE_TYPE_OVERWORLD] = { [SCENE_TYPE_OVERWORLD] = {
.init = sceneOverworldInit, .init = sceneOverworldInit,
.update = sceneOverworldUpdate, .update = sceneOverworldUpdate,
+3
View File
@@ -7,10 +7,12 @@
#pragma once #pragma once
#include "scene/scenebase.h" #include "scene/scenebase.h"
#include "scene/initial/sceneinitial.h"
#include "scene/overworld/sceneoverworld.h" #include "scene/overworld/sceneoverworld.h"
#include "scene/battle/scenebattle.h" #include "scene/battle/scenebattle.h"
typedef union scenedata_u { typedef union scenedata_u {
sceneinitial_t initial;
sceneoverworld_t overworld; sceneoverworld_t overworld;
scenebattle_t battle; scenebattle_t battle;
} scenedata_t; } scenedata_t;
@@ -27,6 +29,7 @@ typedef struct {
typedef enum { typedef enum {
SCENE_TYPE_NULL, SCENE_TYPE_NULL,
SCENE_TYPE_INITIAL,
SCENE_TYPE_OVERWORLD, SCENE_TYPE_OVERWORLD,
SCENE_TYPE_BATTLE, SCENE_TYPE_BATTLE,
+1
View File
@@ -13,3 +13,4 @@ add_subdirectory(game)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(battle) add_subdirectory(battle)
add_subdirectory(backpack) add_subdirectory(backpack)
add_subdirectory(initial)
+7
View File
@@ -81,6 +81,13 @@ static void uiGameMenuSaveCheckComplete(errorret_t result, void *user) {
} }
static void uiGameMenuSave(void) { static void uiGameMenuSave(void) {
if(saveIsTemporary()) {
uiTextboxMainSetText(
"This session is temporary - no save device was found, so saving "
"is disabled."
);
return;
}
if(!saveIsAvailable()) { if(!saveIsAvailable()) {
uiTextboxMainSetText("Can't save - no save device found."); uiTextboxMainSetText("Can't save - no save device found.");
return; return;
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uiinitialnocard.c
uiinitialcreatesave.c
)
@@ -0,0 +1,127 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiinitialcreatesave.h"
#include "ui/frame/uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "display/color.h"
#include "display/spritebatch/spritebatch.h"
#include "display/texture/texture.h"
#include "display/shader/shaderunlit.h"
#define UI_INITIAL_CREATE_SAVE_BACKDROP_COLOR color4b(0, 0, 0, 160)
#define UI_INITIAL_CREATE_SAVE_TEXT "No save data found. Create a new save?"
uiinitialcreatesave_t UI_INITIAL_CREATE_SAVE;
static void uiInitialCreateSaveSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
UI_INITIAL_CREATE_SAVE.create = index == UI_INITIAL_CREATE_SAVE_INDEX_YES;
uiInitialCreateSaveClose();
}
static void uiInitialCreateSaveClosed(const uimenu_t *menu) {
if(UI_INITIAL_CREATE_SAVE.callback != NULL) {
UI_INITIAL_CREATE_SAVE.callback(
UI_INITIAL_CREATE_SAVE.create, UI_INITIAL_CREATE_SAVE.user
);
}
}
errorret_t uiInitialCreateSaveInit(void) {
memoryZero(&UI_INITIAL_CREATE_SAVE, sizeof(uiinitialcreatesave_t));
MENU_BEGIN(
&UI_INITIAL_CREATE_SAVE.menu, UI_INITIAL_CREATE_SAVE.items,
uiInitialCreateSaveSelected, uiInitialCreateSaveClosed, NULL
);
MENU_BUTTON("Yes");
MENU_BUTTON("No");
MENU_END(UI_INITIAL_CREATE_SAVE.items, menuIndex);
errorOk();
}
errorret_t uiInitialCreateSaveDraw(void) {
if(!uiMenuIsActive(&UI_INITIAL_CREATE_SAVE.menu)) errorOk();
spritebatchsprite_t backdropSprite = {
.min = { 0.0f, 0.0f, 0.0f },
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
shadermaterial_t backdropMaterial = {
.unlit = {
.color = UI_INITIAL_CREATE_SAVE_BACKDROP_COLOR,
.texture = &TEXTURE_WHITE
}
};
errorChain(
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
);
errorChain(spriteBatchFlush());
int32_t textW, textH;
textMeasure(UI_INITIAL_CREATE_SAVE_TEXT, &FONT_DEFAULT, &textW, &textH);
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
float_t width = mathMax(
(float_t)textW + (UI_FRAME_START_X * 2), UI_INITIAL_CREATE_SAVE_MIN_WIDTH
);
float_t height = (UI_FRAME_START_Y * 2) + rowHeight + UI_FRAME_PADDING_Y +
rowHeight;
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
float_t contentX = x + UI_FRAME_START_X;
float_t contentY = y + UI_FRAME_START_Y;
float_t contentWidth = width - (UI_FRAME_START_X * 2);
errorChain(textDraw(
contentX, contentY, UI_INITIAL_CREATE_SAVE_TEXT, COLOR_WHITE, &FONT_DEFAULT
));
float_t buttonsY = contentY + rowHeight + UI_FRAME_PADDING_Y;
errorChain(uiMenuDraw(
&UI_INITIAL_CREATE_SAVE.menu, contentX, buttonsY, contentWidth, rowHeight
));
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiInitialCreateSaveIsOpen(void) {
return uiMenuIsActive(&UI_INITIAL_CREATE_SAVE.menu);
}
void uiInitialCreateSaveOpen(
uiinitialcreatesavecallback_t callback, void *user
) {
UI_INITIAL_CREATE_SAVE.callback = callback;
UI_INITIAL_CREATE_SAVE.user = user;
UI_INITIAL_CREATE_SAVE.create = false;
uiMenuOpen(&UI_INITIAL_CREATE_SAVE.menu);
}
void uiInitialCreateSaveClose(void) {
uiMenuClose(&UI_INITIAL_CREATE_SAVE.menu);
}
errorret_t uiInitialCreateSaveDispose(void) {
errorOk();
}
@@ -0,0 +1,79 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#define UI_INITIAL_CREATE_SAVE_INDEX_YES 0
#define UI_INITIAL_CREATE_SAVE_INDEX_NO 1
#define UI_INITIAL_CREATE_SAVE_ITEM_COUNT 2
#define UI_INITIAL_CREATE_SAVE_MIN_WIDTH 160.0f
/**
* Callback invoked once the create-save modal is dismissed.
*
* @param create True if Yes was selected, false if No was selected or
* the dialog was backed out of.
* @param user Arbitrary pointer passed to uiInitialCreateSaveOpen.
*/
typedef void (*uiinitialcreatesavecallback_t)(const bool_t create, void *user);
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_INITIAL_CREATE_SAVE_ITEM_COUNT];
uiinitialcreatesavecallback_t callback;
void *user;
bool_t create;
} uiinitialcreatesave_t;
extern uiinitialcreatesave_t UI_INITIAL_CREATE_SAVE;
/**
* Initializes the create-save modal.
*
* @return Any error that occurs.
*/
errorret_t uiInitialCreateSaveInit(void);
/**
* Draws the create-save modal: a semi-transparent black backdrop covering
* the whole screen, then its own centered frame with a fixed message and
* Yes/No buttons. No-op when not open.
*
* @return Any error that occurs.
*/
errorret_t uiInitialCreateSaveDraw(void);
/**
* Returns true when the modal is currently open.
*
* @return True if open.
*/
bool_t uiInitialCreateSaveIsOpen(void);
/**
* Opens the create-save modal. callback is invoked exactly once with the
* result, whether dismissed by selecting a button or backing out.
*
* @param callback Called with the result once the modal closes. May be
* NULL.
* @param user Arbitrary pointer passed through to callback.
*/
void uiInitialCreateSaveOpen(uiinitialcreatesavecallback_t callback, void *user);
/**
* Closes the modal. No-op when already closed.
*/
void uiInitialCreateSaveClose(void);
/**
* Disposes of the modal.
*
* @return Any error that occurs.
*/
errorret_t uiInitialCreateSaveDispose(void);
+124
View File
@@ -0,0 +1,124 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "uiinitialnocard.h"
#include "ui/frame/uiframe.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/math.h"
#include "display/screen/screen.h"
#include "display/text/text.h"
#include "display/color.h"
#include "display/spritebatch/spritebatch.h"
#include "display/texture/texture.h"
#include "display/shader/shaderunlit.h"
#define UI_INITIAL_NO_CARD_BACKDROP_COLOR color4b(0, 0, 0, 160)
#define UI_INITIAL_NO_CARD_TEXT \
"No save device found. You can continue, but\nprogress will not be saved."
uiinitialnocard_t UI_INITIAL_NO_CARD;
static void uiInitialNoCardSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
UI_INITIAL_NO_CARD.retry = index == UI_INITIAL_NO_CARD_INDEX_RETRY;
uiInitialNoCardClose();
}
static void uiInitialNoCardClosed(const uimenu_t *menu) {
if(UI_INITIAL_NO_CARD.callback != NULL) {
UI_INITIAL_NO_CARD.callback(UI_INITIAL_NO_CARD.retry, UI_INITIAL_NO_CARD.user);
}
}
errorret_t uiInitialNoCardInit(void) {
memoryZero(&UI_INITIAL_NO_CARD, sizeof(uiinitialnocard_t));
MENU_BEGIN(
&UI_INITIAL_NO_CARD.menu, UI_INITIAL_NO_CARD.items,
uiInitialNoCardSelected, uiInitialNoCardClosed, NULL
);
MENU_BUTTON("Retry");
MENU_BUTTON("Continue Anyway");
MENU_END(UI_INITIAL_NO_CARD.items, menuIndex);
errorOk();
}
errorret_t uiInitialNoCardDraw(void) {
if(!uiMenuIsActive(&UI_INITIAL_NO_CARD.menu)) errorOk();
spritebatchsprite_t backdropSprite = {
.min = { 0.0f, 0.0f, 0.0f },
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
shadermaterial_t backdropMaterial = {
.unlit = {
.color = UI_INITIAL_NO_CARD_BACKDROP_COLOR,
.texture = &TEXTURE_WHITE
}
};
errorChain(
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
);
errorChain(spriteBatchFlush());
int32_t textW, textH;
textMeasure(UI_INITIAL_NO_CARD_TEXT, &FONT_DEFAULT, &textW, &textH);
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
float_t width = mathMax(
(float_t)textW + (UI_FRAME_START_X * 2), UI_INITIAL_NO_CARD_MIN_WIDTH
);
float_t height = (UI_FRAME_START_Y * 2) + (float_t)textH +
UI_FRAME_PADDING_Y + rowHeight;
float_t x = (float_t)SCREEN.scanX + ((float_t)SCREEN.scanWidth - width) * 0.5f;
float_t y = (float_t)SCREEN.scanY + ((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
float_t contentX = x + UI_FRAME_START_X;
float_t contentY = y + UI_FRAME_START_Y;
float_t contentWidth = width - (UI_FRAME_START_X * 2);
errorChain(textDraw(
contentX, contentY, UI_INITIAL_NO_CARD_TEXT, COLOR_WHITE, &FONT_DEFAULT
));
float_t buttonsY = contentY + (float_t)textH + UI_FRAME_PADDING_Y;
errorChain(uiMenuDraw(
&UI_INITIAL_NO_CARD.menu, contentX, buttonsY, contentWidth, rowHeight
));
errorChain(spriteBatchFlush());
errorOk();
}
bool_t uiInitialNoCardIsOpen(void) {
return uiMenuIsActive(&UI_INITIAL_NO_CARD.menu);
}
void uiInitialNoCardOpen(uiinitialnocardcallback_t callback, void *user) {
UI_INITIAL_NO_CARD.callback = callback;
UI_INITIAL_NO_CARD.user = user;
UI_INITIAL_NO_CARD.retry = false;
uiMenuOpen(&UI_INITIAL_NO_CARD.menu);
}
void uiInitialNoCardClose(void) {
uiMenuClose(&UI_INITIAL_NO_CARD.menu);
}
errorret_t uiInitialNoCardDispose(void) {
errorOk();
}
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
#define UI_INITIAL_NO_CARD_INDEX_RETRY 0
#define UI_INITIAL_NO_CARD_INDEX_CONTINUE 1
#define UI_INITIAL_NO_CARD_ITEM_COUNT 2
#define UI_INITIAL_NO_CARD_MIN_WIDTH 220.0f
/**
* Callback invoked once the no-save-device modal is dismissed.
*
* @param retry True if Retry was selected, false if Continue Anyway was.
* @param user Arbitrary pointer passed to uiInitialNoCardOpen.
*/
typedef void (*uiinitialnocardcallback_t)(const bool_t retry, void *user);
typedef struct {
uimenu_t menu;
uimenuitem_t items[UI_INITIAL_NO_CARD_ITEM_COUNT];
uiinitialnocardcallback_t callback;
void *user;
bool_t retry;
} uiinitialnocard_t;
extern uiinitialnocard_t UI_INITIAL_NO_CARD;
/**
* Initializes the no-save-device modal.
*
* @return Any error that occurs.
*/
errorret_t uiInitialNoCardInit(void);
/**
* Draws the no-save-device modal: a semi-transparent black backdrop
* covering the whole screen, then its own centered frame with a fixed
* message and Retry/Continue Anyway buttons. No-op when not open.
*
* @return Any error that occurs.
*/
errorret_t uiInitialNoCardDraw(void);
/**
* Returns true when the modal is currently open.
*
* @return True if open.
*/
bool_t uiInitialNoCardIsOpen(void);
/**
* Opens the no-save-device modal. callback is invoked exactly once with
* the result, whether dismissed by selecting a button or backing out.
*
* @param callback Called with the result once the modal closes. May be
* NULL.
* @param user Arbitrary pointer passed through to callback.
*/
void uiInitialNoCardOpen(uiinitialnocardcallback_t callback, void *user);
/**
* Closes the modal. No-op when already closed.
*/
void uiInitialNoCardClose(void);
/**
* Disposes of the modal.
*
* @return Any error that occurs.
*/
errorret_t uiInitialNoCardDispose(void);
+14
View File
@@ -21,6 +21,8 @@
#include "ui/frame/battle/uibattlemenu.h" #include "ui/frame/battle/uibattlemenu.h"
#include "ui/frame/backpack/uibackpack.h" #include "ui/frame/backpack/uibackpack.h"
#include "ui/frame/uiconfirm.h" #include "ui/frame/uiconfirm.h"
#include "ui/frame/initial/uiinitialnocard.h"
#include "ui/frame/initial/uiinitialcreatesave.h"
#include "ui/rpg/textbox/uitextboxmain.h" #include "ui/rpg/textbox/uitextboxmain.h"
#include "ui/rpg/textbox/uitextboxminilist.h" #include "ui/rpg/textbox/uitextboxminilist.h"
#include "ui/rpg/uiemoji.h" #include "ui/rpg/uiemoji.h"
@@ -79,6 +81,18 @@ uielement_t UI_ELEMENTS[] = {
.dispose = uiConfirmDispose .dispose = uiConfirmDispose
}, },
{
.init = uiInitialNoCardInit,
.draw = uiInitialNoCardDraw,
.dispose = uiInitialNoCardDispose
},
{
.init = uiInitialCreateSaveInit,
.draw = uiInitialCreateSaveDraw,
.dispose = uiInitialCreateSaveDispose
},
{ {
.init = uiTextboxMainInit, .init = uiTextboxMainInit,
.update = uiTextboxMainUpdate, .update = uiTextboxMainUpdate,