diff --git a/assets/locale/en_US.po b/assets/locale/en_US.po index 68be31f6..2b7f69e3 100644 --- a/assets/locale/en_US.po +++ b/assets/locale/en_US.po @@ -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" diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 5a986a25..0d56d967 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -67,7 +67,7 @@ errorret_t engineUpdate(void) { #ifdef DUSK_NETWORK errorChain(networkUpdate()); #endif - // errorChain(saveManagerUpdate()); + errorChain(saveManagerUpdate()); timeUpdate(); inputUpdate(); consoleUpdate(); diff --git a/src/dusk/save/CMakeLists.txt b/src/dusk/save/CMakeLists.txt index c31cadaf..aee88303 100644 --- a/src/dusk/save/CMakeLists.txt +++ b/src/dusk/save/CMakeLists.txt @@ -7,4 +7,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC savemanager.c + savedevice.c ) diff --git a/src/dusk/save/savedevice.c b/src/dusk/save/savedevice.c new file mode 100644 index 00000000..ed7e9f57 --- /dev/null +++ b/src/dusk/save/savedevice.c @@ -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(); +} \ No newline at end of file diff --git a/src/dusk/save/savedevice.h b/src/dusk/save/savedevice.h index 724cbda2..c0f1f4d9 100644 --- a/src/dusk/save/savedevice.h +++ b/src/dusk/save/savedevice.h @@ -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; -} savedevice_t; \ No newline at end of file + 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); \ No newline at end of file diff --git a/src/dusk/save/savefile.h b/src/dusk/save/savefile.h index b592deeb..a7044bb3 100644 --- a/src/dusk/save/savefile.h +++ b/src/dusk/save/savefile.h @@ -8,3 +8,4 @@ #pragma once #include "dusk.h" +typedef uint8_t saveslot_t; \ No newline at end of file diff --git a/src/dusk/save/savemanager.c b/src/dusk/save/savemanager.c index 22e7c69a..63504a00 100644 --- a/src/dusk/save/savemanager.c +++ b/src/dusk/save/savemanager.c @@ -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(); } \ No newline at end of file diff --git a/src/dusk/save/savemanager.h b/src/dusk/save/savemanager.h index 9ef7199c..db22da8c 100644 --- a/src/dusk/save/savemanager.h +++ b/src/dusk/save/savemanager.h @@ -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(); \ No newline at end of file diff --git a/src/dusk/scene/initial/sceneinitial.c b/src/dusk/scene/initial/sceneinitial.c index 341e11c2..25eb42e9 100644 --- a/src/dusk/scene/initial/sceneinitial.c +++ b/src/dusk/scene/initial/sceneinitial.c @@ -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(); } diff --git a/src/dusk/ui/frame/CMakeLists.txt b/src/dusk/ui/frame/CMakeLists.txt index a94d68bd..39047650 100644 --- a/src/dusk/ui/frame/CMakeLists.txt +++ b/src/dusk/ui/frame/CMakeLists.txt @@ -12,5 +12,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} add_subdirectory(game) add_subdirectory(mainmenu) add_subdirectory(battle) -add_subdirectory(backpack) -add_subdirectory(initial) +add_subdirectory(backpack) \ No newline at end of file diff --git a/src/dusk/ui/frame/initial/uiinitialcreatesave.c b/src/dusk/ui/frame/initial/uiinitialcreatesave.c deleted file mode 100644 index ade0fa29..00000000 --- a/src/dusk/ui/frame/initial/uiinitialcreatesave.c +++ /dev/null @@ -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(); -} diff --git a/src/dusk/ui/frame/initial/uiinitialcreatesave.h b/src/dusk/ui/frame/initial/uiinitialcreatesave.h deleted file mode 100644 index 8eb3a04b..00000000 --- a/src/dusk/ui/frame/initial/uiinitialcreatesave.h +++ /dev/null @@ -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); diff --git a/src/dusk/ui/frame/initial/uiinitialnocard.c b/src/dusk/ui/frame/initial/uiinitialnocard.c deleted file mode 100644 index 35ebd9dc..00000000 --- a/src/dusk/ui/frame/initial/uiinitialnocard.c +++ /dev/null @@ -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(); -} diff --git a/src/dusk/ui/frame/initial/uiinitialnocard.h b/src/dusk/ui/frame/initial/uiinitialnocard.h deleted file mode 100644 index 3b09c142..00000000 --- a/src/dusk/ui/frame/initial/uiinitialnocard.h +++ /dev/null @@ -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); diff --git a/src/dusk/ui/uielementlist.c b/src/dusk/ui/uielementlist.c index 585798f7..07e867e1 100644 --- a/src/dusk/ui/uielementlist.c +++ b/src/dusk/ui/uielementlist.c @@ -20,80 +20,76 @@ #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, .draw = uiEmojiDraw, .dispose = uiEmojiDispose }, + + // =========================================================================== + // Low rendered components, usually gameplay non-world components. + // =========================================================================== + { + .init = uiFullboxUnderInit, + .update = uiFullboxUnderUpdate, + .draw = uiFullboxUnderDraw + }, - // Ingame menus + // =========================================================================== + // 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 } }; diff --git a/src/dusklinux/CMakeLists.txt b/src/dusklinux/CMakeLists.txt index 9aff40e1..2e468c9d 100644 --- a/src/dusklinux/CMakeLists.txt +++ b/src/dusklinux/CMakeLists.txt @@ -16,5 +16,7 @@ add_subdirectory(input) if(DUSK_NETWORK) add_subdirectory(network) endif() +add_subdirectory(save) add_subdirectory(system) -add_subdirectory(time) \ No newline at end of file +add_subdirectory(time) +add_subdirectory(util) \ No newline at end of file diff --git a/src/dusk/ui/frame/initial/CMakeLists.txt b/src/dusklinux/save/CMakeLists.txt similarity index 79% rename from src/dusk/ui/frame/initial/CMakeLists.txt rename to src/dusklinux/save/CMakeLists.txt index a711dfa1..85be1d9f 100644 --- a/src/dusk/ui/frame/initial/CMakeLists.txt +++ b/src/dusklinux/save/CMakeLists.txt @@ -5,6 +5,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC - uiinitialnocard.c - uiinitialcreatesave.c + savedevicelinux.c ) diff --git a/src/dusklinux/save/savedevicelinux.c b/src/dusklinux/save/savedevicelinux.c new file mode 100644 index 00000000..07bd8e22 --- /dev/null +++ b/src/dusklinux/save/savedevicelinux.c @@ -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(); +} \ No newline at end of file diff --git a/src/dusklinux/save/savedevicelinux.h b/src/dusklinux/save/savedevicelinux.h new file mode 100644 index 00000000..2e18b651 --- /dev/null +++ b/src/dusklinux/save/savedevicelinux.h @@ -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); \ No newline at end of file diff --git a/src/dusklinux/save/savedeviceplatform.h b/src/dusklinux/save/savedeviceplatform.h new file mode 100644 index 00000000..23f99b2f --- /dev/null +++ b/src/dusklinux/save/savedeviceplatform.h @@ -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 \ No newline at end of file diff --git a/src/dusklinux/util/CMakeLists.txt b/src/dusklinux/util/CMakeLists.txt new file mode 100644 index 00000000..b60e7416 --- /dev/null +++ b/src/dusklinux/util/CMakeLists.txt @@ -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 +) diff --git a/src/dusklinux/util/mkdirp.c b/src/dusklinux/util/mkdirp.c new file mode 100644 index 00000000..2c086ea8 --- /dev/null +++ b/src/dusklinux/util/mkdirp.c @@ -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 +#include + +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; +} diff --git a/src/dusklinux/util/mkdirp.h b/src/dusklinux/util/mkdirp.h new file mode 100644 index 00000000..c752ef28 --- /dev/null +++ b/src/dusklinux/util/mkdirp.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 "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);