First pass of save

This commit is contained in:
2026-08-14 14:19:44 -05:00
parent 33f50a2c69
commit 45331c2a60
23 changed files with 577 additions and 510 deletions
+3
View File
@@ -10,6 +10,9 @@ msgid "ui.title"
msgstr ""
"Welcome"
msgid "save.linux.mkdirp_failed"
msgstr "Failed to create save directory, check the disk is not full or write-protected."
#: src/dusk/ui/frame/settings/uisettings.c
msgid "ui.settings.tabs.general"
msgstr "General"
+1 -1
View File
@@ -67,7 +67,7 @@ errorret_t engineUpdate(void) {
#ifdef DUSK_NETWORK
errorChain(networkUpdate());
#endif
// errorChain(saveManagerUpdate());
errorChain(saveManagerUpdate());
timeUpdate();
inputUpdate();
consoleUpdate();
+1
View File
@@ -7,4 +7,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savemanager.c
savedevice.c
)
+86
View File
@@ -0,0 +1,86 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/savedevice.h"
#include "assert/assert.h"
#include "util/memory.h"
errorret_t saveDeviceInit(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
memoryZero(device, sizeof(savedevice_t));
device->state = 0xFF;// We set this because the platform must define it.
errorChain(saveDevicePlatformInit(device));
// The platform must put the device into a state we can work with.
assertTrue(
device->state == SAVE_DEVICE_STATE_UNKNOWN ||
device->state == SAVE_DEVICE_STATE_UNAVAILABLE ||
device->state == SAVE_DEVICE_STATE_AVAILABLE ||
device->state == SAVE_DEVICE_STATE_ERRORED,
"Save device must be in a state that allows checking availability"
);
errorOk();
}
errorret_t saveDeviceUpdate(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
assertIsMainThread("Invalid thread");
// Perform a device update.
errorChain(saveDevicePlatformUpdate(device));
// Fire the callback if desired.
if(device->fireCallback) {
device->fireCallback = false;
if(device->stateCallback) device->stateCallback(device, device->user);
}
errorOk();
}
void saveDeviceFireCallback(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
assertIsMainThread("Invalid thread");
assertFalse(device->fireCallback, "Device callback already fired?");
device->fireCallback = true;
}
void saveDeviceCheckAvailability(
savedevice_t *device,
savedevicestatecallback_t callback,
void *user
) {
assertNotNull(device, "device cannot be null");
assertNotNull(callback, "callback cannot be null");
assertIsMainThread("Invalid thread");
// The device must be in a state that we allow checking the availability.
assertTrue(
device->state == SAVE_DEVICE_STATE_UNKNOWN ||
device->state == SAVE_DEVICE_STATE_UNAVAILABLE ||
device->state == SAVE_DEVICE_STATE_AVAILABLE ||
device->state == SAVE_DEVICE_STATE_ERRORED,
"Save device must be in a state that allows checking availability"
);
// Set state data and callback.
device->state = SAVE_DEVICE_STATE_CHECKING_AVAILABILITY;
device->stateCallback = callback;
device->user = user;
// Handoff to the platform to do its checks, it can opt to do this either
// synchronously or asynchronously.
saveDeviceCheckAvailabilityPlatform(device);
}
errorret_t saveDeviceDispose(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
errorChain(saveDevicePlatformDispose(device));
errorOk();
}
+62 -3
View File
@@ -7,16 +7,75 @@
#pragma once
#include "error/error.h"
#include "save/savedeviceplatform.h"
// Save device count e.g. Memory Cards count.
#ifndef SAVE_DEVICE_COUNT
#error "SAVE_DEVICE_COUNT must be defined"
#endif
typedef struct savedevice_s savedevice_t;
typedef void (*savedevicestatecallback_t)(savedevice_t *device, void *user);
typedef enum {
SAVE_DEVICE_STATE_UNKNOWN,
SAVE_DEVICE_STATE_CHECKING_AVAILABILITY,
SAVE_DEVICE_STATE_UNAVAILABLE,
SAVE_DEVICE_STATE_AVAILABLE,
SAVE_DEVICE_STATE_MOUNTING,
SAVE_DEVICE_STATE_MOUNTED,
SAVE_DEVICE_STATE_ERRORED
} savedevicestate_t;
typedef struct {
typedef struct savedevice_s {
savedevicestate_t state;
savedeviceplatform_t platform;
const char_t *reasonKey;
bool_t fireCallback;
void *user;
savedevicestatecallback_t stateCallback;
} savedevice_t;
/**
* Initializes the save device.
*
* @param device The save device to initialize.
* @return Error state if any.
*/
errorret_t saveDeviceInit(savedevice_t *device);
/**
* Updates the save device.
*
* @param device The save device to update.
* @return Error state if any.
*/
errorret_t saveDeviceUpdate(savedevice_t *device);
/**
* Internal method to fire the save callback, does it at the appropriate time.
*
* @param device The save device to fire the callback for.
*/
void saveDeviceFireCallback(savedevice_t *device);
/**
* Requests the device to check its availability, this will call the callback
* whence completed.
*
* @param device The save device to check availability.
* @param callback The callback to call when the availability check is complete.
* @param user User data to pass to the callback.
*/
void saveDeviceCheckAvailability(
savedevice_t *device,
savedevicestatecallback_t callback,
void *user
);
/**
* Disposes of the save device.
*
* @param device The save device to dispose.
* @return Error state if any.
*/
errorret_t saveDeviceDispose(savedevice_t *device);
+1
View File
@@ -8,3 +8,4 @@
#pragma once
#include "dusk.h"
typedef uint8_t saveslot_t;
+136
View File
@@ -6,11 +6,147 @@
*/
#include "savemanager.h"
#include "util/memory.h"
#include "assert/assert.h"
savemanager_t SAVE_MANAGER;
errorret_t saveManagerInit() {
memoryZero(&SAVE_MANAGER, sizeof(savemanager_t));
SAVE_MANAGER.deviceCurrent = 0xFF;// No current device.
// Start by initializing each of the save devices.
savedevice_t *device = &SAVE_MANAGER.devices[0];
do {
saveDeviceInit(device);
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
errorOk();
}
errorret_t saveManagerUpdate() {
assertIsMainThread("Invalid thread");
// Start by updating each device
savedevice_t *device = &SAVE_MANAGER.devices[0];
do {
saveDeviceUpdate(device);
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
// We may be waiting for a device to become available
if(SAVE_MANAGER.findingAvailableDevice) {
assertNotNull(
SAVE_MANAGER.findAvailableCallback,
"Callback cannot be null while looking for devices"
);
// Yes, we have a device available, fire the callback.
if(SAVE_MANAGER.deviceCurrent == 0xFF) {
// Are we still looking or did we fail to find anything?
if(SAVE_MANAGER.noAvailableDeviceFound) {
SAVE_MANAGER.findingAvailableDevice = false;
SAVE_MANAGER.findAvailableCallback(
NULL,
SAVE_MANAGER.findAvailableUser
);
}
// Still searching
} else {
// Device was found!
SAVE_MANAGER.findingAvailableDevice = false;
SAVE_MANAGER.findAvailableCallback(
&SAVE_MANAGER.devices[SAVE_MANAGER.deviceCurrent],
SAVE_MANAGER.findAvailableUser
);
}
}
errorOk();
}
void saveManagerFindAvailableDevice(
savedevicestatecallback_t callback,
void *user
) {
assertIsMainThread("Invalid thread");
assertNotNull(callback, "Callback cannot be null");
assertFalse(
SAVE_MANAGER.findingAvailableDevice,
"Already finding an available device"
);
SAVE_MANAGER.findingAvailableDevice = true;
SAVE_MANAGER.findAvailableCallback = callback;
SAVE_MANAGER.findAvailableUser = user;
SAVE_MANAGER.noAvailableDeviceFound = false;
// Is there an available device marked already?
if(SAVE_MANAGER.deviceCurrent != 0xFF) {
// Yes, fire the callback next tick.
return;
}
// Do we have an available device not yet marked as current?
savedevice_t *device = &SAVE_MANAGER.devices[0];
do {
if(device->state != SAVE_DEVICE_STATE_AVAILABLE) continue;
// Yes this device is available, set as current device.
SAVE_MANAGER.deviceCurrent = (uint8_t)(device - &SAVE_MANAGER.devices[0]);
return;// Next tick will fire the callback.
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
// No currently available device, check them IN ORDER, starting at 0
savedevice_t *deviceToCheck = &SAVE_MANAGER.devices[0];
assertNotNull(deviceToCheck, "Device to check cannot be null");
saveDeviceCheckAvailability(
deviceToCheck,
saveManagerOnDeviceAvailabilityChecked,
NULL
);
}
void saveManagerOnDeviceAvailabilityChecked(savedevice_t *device, void *user) {
assertNotNull(device, "Device cannot be null");
assertTrue(
SAVE_MANAGER.findingAvailableDevice,
"Not currently finding an available device"
);
// Did we already find a device?
if(SAVE_MANAGER.deviceCurrent != 0xFF) return;
// Is this device available?
if(device->state != SAVE_DEVICE_STATE_AVAILABLE) {
// Since it's unavailable we should tell the next device to check its
// availability. If there is no next device we give up.
savedevice_t *nextDevice = device + 1;
if(nextDevice >= &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT]) {
// No more devices to check, we give up.
SAVE_MANAGER.noAvailableDeviceFound = true;
return;
}
// Check the next device's availability.
saveDeviceCheckAvailability(
nextDevice,
saveManagerOnDeviceAvailabilityChecked,
NULL
);
return;
}
// Yes this device is available, set as current device and next tick will
// invoke callback
SAVE_MANAGER.deviceCurrent = (uint8_t)(device - &SAVE_MANAGER.devices[0]);
}
errorret_t saveManagerDispose() {
// Dispose each device.
savedevice_t *device = &SAVE_MANAGER.devices[0];
do {
saveDeviceDispose(device);
} while(device++ < &SAVE_MANAGER.devices[SAVE_DEVICE_COUNT - 1]);
errorOk();
}
+40 -2
View File
@@ -6,10 +6,16 @@
*/
#pragma once
#include "error/error.h"
#include "savedevice.h"
typedef struct {
void *nothing;
savedevice_t devices[SAVE_DEVICE_COUNT];
uint8_t deviceCurrent;
bool_t findingAvailableDevice;
bool_t noAvailableDeviceFound;
savedevicestatecallback_t findAvailableCallback;
void *findAvailableUser;
} savemanager_t;
extern savemanager_t SAVE_MANAGER;
@@ -17,10 +23,42 @@ extern savemanager_t SAVE_MANAGER;
/**
* Initializes the save manager, this does not do anything related to mounting
* files, memory cards, etc, this is entirely prepping for that capability.
*
* @return Error state if any.
*/
errorret_t saveManagerInit();
/**
* Updates the save manager.
*
* @return Error state if any.
*/
errorret_t saveManagerUpdate();
/**
* Requests the save manager to try and find an available device. This will fire
* the callback whenever an avaialble device is found, or will fire with a NULL
* save device if no available save devices are found.
*
* @param callback The callback to fire when an available device is found.
* @param user The user data to pass to the callback.
*/
void saveManagerFindAvailableDevice(
savedevicestatecallback_t callback,
void *user
);
/**
* Internal method to fire the save callback, does it at the appropriate time.
*
* @param device The save device to fire the callback for.
* @param user Unused, present to match savedevicestatecallback_t.
*/
void saveManagerOnDeviceAvailabilityChecked(savedevice_t *device, void *user);
/**
* Disposes of the save manager.
*
* @return Error state if any.
*/
errorret_t saveManagerDispose();
+19 -2
View File
@@ -9,13 +9,30 @@
#include "assert/assert.h"
#include "util/memory.h"
#include "error/error.h"
#include "ui/frame/initial/uiinitialnocard.h"
#include "ui/frame/initial/uiinitialcreatesave.h"
#include "console/console.h"
#include "save/savemanager.h"
int32_t testData = 69;
void testCallback(savedevice_t *device, void *user) {
if(device == NULL) {
consolePrint("No save device found.");
} else {
consolePrint("Found save device: %s", device->reasonKey);
}
}
errorret_t sceneInitialInit(scenedata_t *sceneData) {
assertNotNull(sceneData, "Scene data cannot be null");
memoryZero(&sceneData->initial, sizeof(sceneinitial_t));
consolePrint("Going to find a save device.");
saveManagerFindAvailableDevice(
testCallback,
&testData
);
errorOk();
}
-1
View File
@@ -13,4 +13,3 @@ add_subdirectory(game)
add_subdirectory(mainmenu)
add_subdirectory(battle)
add_subdirectory(backpack)
add_subdirectory(initial)
@@ -1,150 +0,0 @@
/**
* 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"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#define UI_INITIAL_CREATE_SAVE_BACKDROP_COLOR color4b(0, 0, 0, 160)
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));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.initial.create_save.message",
0,
UI_INITIAL_CREATE_SAVE.text,
UI_INITIAL_CREATE_SAVE_TEXT_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.initial.create_save.yes",
0,
UI_INITIAL_CREATE_SAVE.yesLabel,
UI_INITIAL_CREATE_SAVE_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.initial.create_save.no",
0,
UI_INITIAL_CREATE_SAVE.noLabel,
UI_INITIAL_CREATE_SAVE_LABEL_MAX
));
MENU_BEGIN(
&UI_INITIAL_CREATE_SAVE.menu, UI_INITIAL_CREATE_SAVE.items,
uiInitialCreateSaveSelected, uiInitialCreateSaveClosed, NULL
);
MENU_BUTTON(UI_INITIAL_CREATE_SAVE.yesLabel);
MENU_BUTTON(UI_INITIAL_CREATE_SAVE.noLabel);
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();
}
@@ -1,84 +0,0 @@
/**
* 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
#define UI_INITIAL_CREATE_SAVE_TEXT_MAX 256
#define UI_INITIAL_CREATE_SAVE_LABEL_MAX 32
/**
* 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;
char_t text[UI_INITIAL_CREATE_SAVE_TEXT_MAX];
char_t yesLabel[UI_INITIAL_CREATE_SAVE_LABEL_MAX];
char_t noLabel[UI_INITIAL_CREATE_SAVE_LABEL_MAX];
} 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);
-146
View File
@@ -1,146 +0,0 @@
/**
* 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"
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#define UI_INITIAL_NO_CARD_BACKDROP_COLOR color4b(0, 0, 0, 160)
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));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.initial.no_card.message",
0,
UI_INITIAL_NO_CARD.text,
UI_INITIAL_NO_CARD_TEXT_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.initial.no_card.retry",
0,
UI_INITIAL_NO_CARD.retryLabel,
UI_INITIAL_NO_CARD_LABEL_MAX
));
errorChain(assetLocaleGetString(
&LOCALE.entry->data.locale,
"ui.initial.no_card.continue",
0,
UI_INITIAL_NO_CARD.continueLabel,
UI_INITIAL_NO_CARD_LABEL_MAX
));
MENU_BEGIN(
&UI_INITIAL_NO_CARD.menu, UI_INITIAL_NO_CARD.items,
uiInitialNoCardSelected, uiInitialNoCardClosed, NULL
);
MENU_BUTTON(UI_INITIAL_NO_CARD.retryLabel);
MENU_BUTTON(UI_INITIAL_NO_CARD.continueLabel);
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();
}
@@ -1,83 +0,0 @@
/**
* 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
#define UI_INITIAL_NO_CARD_TEXT_MAX 256
#define UI_INITIAL_NO_CARD_LABEL_MAX 32
/**
* 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;
char_t text[UI_INITIAL_NO_CARD_TEXT_MAX];
char_t retryLabel[UI_INITIAL_NO_CARD_LABEL_MAX];
char_t continueLabel[UI_INITIAL_NO_CARD_LABEL_MAX];
} 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);
+38 -33
View File
@@ -20,26 +20,22 @@
#include "ui/frame/battle/uibattlehud.h"
#include "ui/frame/backpack/uibackpack.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/uitextboxminilist.h"
#include "ui/rpg/uiemoji.h"
uielement_t UI_ELEMENTS[] = {
// ===========================================================================
// Non-Rendered Components, just needs init and deinit
// ===========================================================================
{
.init = uiFrameInit,
.dispose = uiFrameDispose
},
// Fullbox under: above scene, below system UI.
{
.init = uiFullboxUnderInit,
.update = uiFullboxUnderUpdate,
.draw = uiFullboxUnderDraw
},
// in world stuff
// ===========================================================================
// In-World Gameplay Components, sits below all UI but above gameplay.
// ===========================================================================
{
.init = uiEmojiInit,
.update = uiEmojiUpdate,
@@ -47,53 +43,53 @@ uielement_t UI_ELEMENTS[] = {
.dispose = uiEmojiDispose
},
// Ingame menus
// ===========================================================================
// Low rendered components, usually gameplay non-world components.
// ===========================================================================
{
.init = uiFullboxUnderInit,
.update = uiFullboxUnderUpdate,
.draw = uiFullboxUnderDraw
},
// ===========================================================================
// Most menus and non-gameplay components. Hierarchy is very important.
// ===========================================================================
{
.init = uiGameMenuInit,
.draw = uiGameMenuDraw,
.dispose = uiGameMenuDispose
},
{
.init = uiMainMenuInit,
.update = uiMainMenuUpdate,
.draw = uiMainMenuDraw,
.dispose = uiMainMenuDispose
},
{ .init = uiBattleHudInit, .draw = uiBattleHudDraw },
{
.init = uiBattleHudInit,
.draw = uiBattleHudDraw
},
{
.init = uiBattleMenuInit,
.update = uiBattleMenuUpdate,
.draw = uiBattleMenuDraw
},
{
.init = uiBackpackInit,
.draw = uiBackpackDraw,
.dispose = uiBackpackDispose
},
// Text stuffs
// ===========================================================================
// Modals, Popups and Dialogs, above most other UI things.
// ===========================================================================
{
.init = uiConfirmInit,
.draw = uiConfirmDraw,
.dispose = uiConfirmDispose
},
{
.init = uiInitialNoCardInit,
.draw = uiInitialNoCardDraw,
.dispose = uiInitialNoCardDispose
},
{
.init = uiInitialCreateSaveInit,
.draw = uiInitialCreateSaveDraw,
.dispose = uiInitialCreateSaveDispose
},
{
.init = uiTextboxMainInit,
.update = uiTextboxMainUpdate,
@@ -106,13 +102,14 @@ uielement_t UI_ELEMENTS[] = {
.draw = uiTextboxMiniListDraw
},
// ===========================================================================
// Overlayed components, can even outdraw all UI things.
// ===========================================================================
{
.init = uiTransitionInit,
.update = uiTransitionUpdate,
.draw = uiTransitionDraw
},
// Fullbox over: above absolutely everything.
{
.init = uiFullboxOverInit,
.update = uiFullboxOverUpdate,
@@ -125,15 +122,23 @@ uielement_t UI_ELEMENTS[] = {
.draw = uiLoadingDraw
},
// ===========================================================================
// Important rendering components, lives outside the primary game engine
// ===========================================================================
{
.init = uiCropInit,
.draw = uiCropDraw
},
// Debug items
// ===========================================================================
// Debug components, disregards everything else.
// ===========================================================================
{ .draw = uiConsoleDraw },
{ .draw = uiFPSDraw },
{ .draw = uiPlayerPosDraw },
{ 0 } // Null terminator
// ===========================================================================
// Null terminator
// ===========================================================================
{ 0 }
};
+2
View File
@@ -16,5 +16,7 @@ add_subdirectory(input)
if(DUSK_NETWORK)
add_subdirectory(network)
endif()
add_subdirectory(save)
add_subdirectory(system)
add_subdirectory(time)
add_subdirectory(util)
@@ -5,6 +5,5 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uiinitialnocard.c
uiinitialcreatesave.c
savedevicelinux.c
)
+51
View File
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/savedevice.h"
#include "save/savedevicelinux.h"
#include "assert/assert.h"
#include "util/mkdirp.h"
errorret_t saveDeviceLinuxInit(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
device->state = SAVE_DEVICE_STATE_UNKNOWN;
errorOk();
}
errorret_t saveDeviceLinuxUpdate(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
assertIsMainThread("Invalid thread");
errorOk();
}
void saveDeviceLinuxCheckAvailability(savedevice_t *device) {
assertNotNull(device, "device cannot be null");
assertIsMainThread("Invalid thread");
assertTrue(
device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY,
"Device state incorrect?"
);
// Mkdirp the save directory.
int mkdirpResult = mkdirp(SAVE_DEVICE_LINUX_DIRECTORY, 0700);
if(mkdirpResult != 0) {
device->state = SAVE_DEVICE_STATE_UNAVAILABLE;
device->reasonKey = "save.linux.mkdirp_failed";
return saveDeviceFireCallback(device);
}
// The save device is available.
device->state = SAVE_DEVICE_STATE_AVAILABLE;
device->reasonKey = NULL;
saveDeviceFireCallback(device);
}
errorret_t saveDeviceLinuxDispose(savedevice_t *device) {
errorOk();
}
+50
View File
@@ -0,0 +1,50 @@
/**
* 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 SAVE_DEVICE_LINUX_DIRECTORY "~/.dusk/saves"
#define SAVE_DEVICE_LINUX_FILE "~/.dusk/saves/save.dat"
typedef struct {
void *nothing;
} savedeviceplatform_t;
typedef struct savedevice_s savedevice_t;
/**
* Initializes the save device platform.
*
* @param device The save device platform to initialize.
* @return Error state if any.
*/
errorret_t saveDeviceLinuxInit(savedevice_t *device);
/**
* Updates the save device platform.
*
* @param device The save device platform to update.
* @return Error state if any.
*/
errorret_t saveDeviceLinuxUpdate(savedevice_t *device);
/**
* Requests the device to check its availability, this will call the callback
* whence completed.
*
* @param device The save device platform to check availability.
*/
void saveDeviceLinuxCheckAvailability(savedevice_t *device);
/**
* Disposes of the save device platform.
*
* @param device The save device platform to dispose.
* @return Error state if any.
*/
errorret_t saveDeviceLinuxDispose(savedevice_t *device);
+16
View File
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "savedevicelinux.h"
#define SAVE_DEVICE_COUNT 1
#define saveDevicePlatformInit saveDeviceLinuxInit
#define saveDevicePlatformUpdate saveDeviceLinuxUpdate
#define saveDeviceCheckAvailabilityPlatform saveDeviceLinuxCheckAvailability
#define saveDevicePlatformDispose saveDeviceLinuxDispose
+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
mkdirp.c
)
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "util/mkdirp.h"
#include "util/string.h"
#include "assert/assert.h"
#include <sys/stat.h>
#include <limits.h>
int_t mkdirp(const char_t *path, const int_t mode) {
assertNotNull(path, "path cannot be null");
assertStrLenMax(path, PATH_MAX, "path is too long");
char_t buffer[PATH_MAX];
stringCopy(buffer, path, sizeof(buffer) - 1);
size_t len = strlen(buffer);
if(len == 0) return -1;
// Strip a trailing slash so the loop below doesn't try to create the same
// directory twice.
if(buffer[len - 1] == '/') buffer[len - 1] = '\0';
for(char_t *p = buffer + 1; *p != '\0'; p++) {
if(*p != '/') continue;
*p = '\0';
if(mkdir(buffer, mode) != 0 && errno != EEXIST) return -1;
*p = '/';
}
if(mkdir(buffer, mode) != 0 && errno != EEXIST) return -1;
return 0;
}
+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 "dusk.h"
/**
* Recursively creates a directory and any missing parent directories, similar
* to the shell command `mkdir -p`. If the directory already exists, this is
* considered a success.
*
* @param path The path of the directory to create.
* @param mode The permissions to create the directories with.
* @return 0 on success, -1 on failure (errno will be set).
*/
int_t mkdirp(const char_t *path, const int_t mode);