diff --git a/cmake/targets/wii.cmake b/cmake/targets/wii.cmake index af29731b..5b2ae662 100644 --- a/cmake/targets/wii.cmake +++ b/cmake/targets/wii.cmake @@ -4,6 +4,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC DUSK_WII ) +# Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h. +set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING + "Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\ +compatible memory card emulation), or SD (SD card via libfat)" +) +set_property(CACHE DUSK_SAVE_WII_METHOD PROPERTY STRINGS "NAND" "CARD" "SD") + +target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + DUSK_SAVE_WII_METHOD_${DUSK_SAVE_WII_METHOD} +) + # Generate Homebrew Channel meta.xml from project identity variables string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC) configure_file( diff --git a/src/duskdolphin/CMakeLists.txt b/src/duskdolphin/CMakeLists.txt index 8b6630b0..a5323674 100644 --- a/src/duskdolphin/CMakeLists.txt +++ b/src/duskdolphin/CMakeLists.txt @@ -22,5 +22,6 @@ 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 diff --git a/src/duskdolphin/save/CMakeLists.txt b/src/duskdolphin/save/CMakeLists.txt new file mode 100644 index 00000000..4b7de25d --- /dev/null +++ b/src/duskdolphin/save/CMakeLists.txt @@ -0,0 +1,26 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Only one save device implementation is ever compiled in - it must match +# whichever one savedeviceplatform.h dispatches to. In particular, the NAND +# (ISFS) device can't even compile on GameCube - libogc's isfs.h declares +# nothing at all outside of Wii (HW_RVL) builds. +if(DUSK_TARGET_SYSTEM STREQUAL "wii" AND DUSK_SAVE_WII_METHOD STREQUAL "NAND") + target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + savedevicedolphinnand.c + ) +elseif(DUSK_TARGET_SYSTEM STREQUAL "wii" AND DUSK_SAVE_WII_METHOD STREQUAL "SD") + target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + savedevicedolphinsd.c + ) +else() + # GameCube, or Wii with DUSK_SAVE_WII_METHOD=CARD. + target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + savedevicedolphincard.c + ) +endif() diff --git a/src/duskdolphin/save/savedevicedolphincard.c b/src/duskdolphin/save/savedevicedolphincard.c new file mode 100644 index 00000000..e0fab1e4 --- /dev/null +++ b/src/duskdolphin/save/savedevicedolphincard.c @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/savedevice.h" +#include "save/savedevicedolphincard.h" +#include "save/savemanager.h" +#include "assert/assert.h" + +errorret_t saveDeviceDolphinCardInit(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + + device->state = SAVE_DEVICE_STATE_UNKNOWN; + + // Each savedevice_t in SAVE_MANAGER.devices maps to one physical memory + // card slot - device 0 is slot A, device 1 is slot B, determined by this + // device's position in that array (the same index math savemanager.c + // itself uses to identify a device). + uint8_t index = (uint8_t)(device - &SAVE_MANAGER.devices[0]); + device->platform.channel = index == 0 ? CARD_SLOTA : CARD_SLOTB; + + errorOk(); +} + +errorret_t saveDeviceDolphinCardUpdate(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + errorOk(); +} + +void saveDeviceDolphinCardCheckAvailability(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + assertTrue( + device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, + "Device state incorrect?" + ); + + savedeviceplatform_t *platform = &device->platform; + + // CARD_Init sets up card_inited, the per-channel control blocks (wait + // queues, alarms) CARD_Mount reads, and initializes the DSP (needed for + // the card unlock sequence) - safe to call more than once, and cheap + // enough to just always call before every mount attempt. + int32_t result = CARD_Init(SAVE_DEVICE_DOLPHIN_GAME_CODE, NULL); + if(result < 0) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.dolphin.card_init_failed"; + return saveDeviceFireCallback(device); + } + + do { + result = CARD_Mount(platform->channel, platform->cardBuffer, NULL); + } while(result == CARD_ERROR_BUSY); + + if(result < 0) { + platform->mounted = false; + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + switch(result) { + case CARD_ERROR_NOCARD: + device->reasonKey = "save.dolphin.no_card"; + break; + case CARD_ERROR_WRONGDEVICE: + device->reasonKey = "save.dolphin.wrong_device"; + break; + case CARD_ERROR_BROKEN: + device->reasonKey = "save.dolphin.broken"; + break; + default: + device->reasonKey = "save.dolphin.mount_failed"; + break; + } + return saveDeviceFireCallback(device); + } + platform->mounted = true; + + if(!saveDeviceDolphinCardHasFreeSpace(platform->channel)) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.dolphin.card_full"; + return saveDeviceFireCallback(device); + } + + device->state = SAVE_DEVICE_STATE_AVAILABLE; + device->reasonKey = NULL; + saveDeviceFireCallback(device); +} + +bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) { + uint16_t blockCount = 0; + if(CARD_GetBlockCount(channel, &blockCount) < 0) return false; + + uint32_t sectorSize = 0; + if(CARD_GetSectorSize(channel, §orSize) < 0 || sectorSize == 0) { + return false; + } + + // showall=true - free space is a card-wide resource, not just what this + // game itself has already written. + card_dir entries[CARD_MAXFILES]; + int32_t count = 0; + if(CARD_GetDirectory(channel, entries, &count, true) < 0) return false; + if(count >= CARD_MAXFILES) return false;// No free file entry left either. + + uint32_t usedBlocks = 0; + for(int32_t i = 0; i < count; i++) { + usedBlocks += (entries[i].filelen + sectorSize - 1) / sectorSize; + } + + return usedBlocks < (uint32_t)blockCount; +} + +errorret_t saveDeviceDolphinCardDispose(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + if(device->platform.mounted) { + CARD_Unmount(device->platform.channel); + device->platform.mounted = false; + } + errorOk(); +} diff --git a/src/duskdolphin/save/savedevicedolphincard.h b/src/duskdolphin/save/savedevicedolphincard.h new file mode 100644 index 00000000..8c3c2f29 --- /dev/null +++ b/src/duskdolphin/save/savedevicedolphincard.h @@ -0,0 +1,75 @@ +/** + * 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 + +// GameCube always uses this device (slot A + slot B). On Wii it's also used +// when DUSK_SAVE_WII_METHOD_CARD is selected - libogc's CARD API +// transparently redirects to Wii's NAND-based GameCube memory card +// emulation, so the exact same code path works on both. +#ifndef SAVE_DEVICE_DOLPHIN_GAME_CODE + #define SAVE_DEVICE_DOLPHIN_GAME_CODE "DUSK" +#endif + +typedef struct { + // Set from this device's index into SAVE_MANAGER.devices - device 0 is + // CARD_SLOTA, device 1 is CARD_SLOTB. + int32_t channel; + uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32))); + bool_t mounted; +} 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 saveDeviceDolphinCardInit(savedevice_t *device); + +/** + * Updates the save device platform. + * + * @param device The save device platform to update. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinCardUpdate(savedevice_t *device); + +/** + * Requests the device to check its availability, this will call the callback + * whence completed. Mounts the memory card in this device's slot and + * confirms there's at least one free block and one free file entry left to + * write a save into. + * + * @param device The save device platform to check availability. + */ +void saveDeviceDolphinCardCheckAvailability(savedevice_t *device); + +/** + * Returns whether the mounted memory card in the given slot has at least + * one block of free space and one free file entry remaining, by summing + * the block usage of every file already on the card (CARD_GetDirectory) + * against the card's total block count (CARD_GetBlockCount) - libogc + * doesn't expose a single "free blocks remaining" call. + * + * @param channel The CARD slot (CARD_SLOTA or CARD_SLOTB) to check. Must + * already be mounted. + * @return True if there's room to create at least one more save file. + */ +bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel); + +/** + * Disposes of the save device platform. + * + * @param device The save device platform to dispose. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinCardDispose(savedevice_t *device); diff --git a/src/duskdolphin/save/savedevicedolphinnand.c b/src/duskdolphin/save/savedevicedolphinnand.c new file mode 100644 index 00000000..c668d4ae --- /dev/null +++ b/src/duskdolphin/save/savedevicedolphinnand.c @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/savedevice.h" +#include "save/savedevicedolphinnand.h" +#include "assert/assert.h" + +errorret_t saveDeviceDolphinNandInit(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + + device->state = SAVE_DEVICE_STATE_UNKNOWN; + + errorOk(); +} + +errorret_t saveDeviceDolphinNandUpdate(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + errorOk(); +} + +void saveDeviceDolphinNandCheckAvailability(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + assertTrue( + device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, + "Device state incorrect?" + ); + + int32_t result = ISFS_Initialize(); + if(result != ISFS_OK) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.dolphin.nand_init_failed"; + return saveDeviceFireCallback(device); + } + device->platform.initialized = true; + + // Best-effort create - libogc's isfs.h doesn't expose a named "already + // exists" result, so the real confirmation is the ISFS_GetUsage() call + // below rather than this return value. + ISFS_CreateDir( + SAVE_DEVICE_DOLPHIN_NAND_DIR, 0, ISFS_OPEN_RW, ISFS_OPEN_RW, ISFS_OPEN_RW + ); + + uint32_t usedBlocks = 0; + uint32_t usedInodes = 0; + result = ISFS_GetUsage( + SAVE_DEVICE_DOLPHIN_NAND_DIR, &usedBlocks, &usedInodes + ); + if(result != ISFS_OK) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.dolphin.nand_access_denied"; + return saveDeviceFireCallback(device); + } + + device->state = SAVE_DEVICE_STATE_AVAILABLE; + device->reasonKey = NULL; + saveDeviceFireCallback(device); +} + +errorret_t saveDeviceDolphinNandDispose(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + if(device->platform.initialized) { + ISFS_Deinitialize(); + device->platform.initialized = false; + } + errorOk(); +} diff --git a/src/duskdolphin/save/savedevicedolphinnand.h b/src/duskdolphin/save/savedevicedolphinnand.h new file mode 100644 index 00000000..79386fb7 --- /dev/null +++ b/src/duskdolphin/save/savedevicedolphinnand.h @@ -0,0 +1,60 @@ +/** + * 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 + +// A private namespace under /title on the Wii's internal NAND, used purely +// as ISFS storage - not a real installed title/ticket. Only reachable +// because the Homebrew Channel's grants full hardware access, +// which also disables IOS's normal ES rights checking for the running app; +// this hasn't been confirmed against real hardware yet (see project memory +// for the caveat - Dolphin's NAND emulation may not reproduce the same +// rights-check behavior as a real Wii/vWii). +#ifndef SAVE_DEVICE_DOLPHIN_NAND_DIR + #define SAVE_DEVICE_DOLPHIN_NAND_DIR "/title/00010001/44555348/data" +#endif + +typedef struct { + bool_t initialized; +} 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 saveDeviceDolphinNandInit(savedevice_t *device); + +/** + * Updates the save device platform. + * + * @param device The save device platform to update. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinNandUpdate(savedevice_t *device); + +/** + * Requests the device to check its availability, this will call the callback + * whence completed. Brings up ISFS and confirms our private save directory + * exists (creating it on first run). + * + * @param device The save device platform to check availability. + */ +void saveDeviceDolphinNandCheckAvailability(savedevice_t *device); + +/** + * Disposes of the save device platform. + * + * @param device The save device platform to dispose. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinNandDispose(savedevice_t *device); diff --git a/src/duskdolphin/save/savedevicedolphinsd.c b/src/duskdolphin/save/savedevicedolphinsd.c new file mode 100644 index 00000000..98fa2c20 --- /dev/null +++ b/src/duskdolphin/save/savedevicedolphinsd.c @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/savedevice.h" +#include "save/savedevicedolphinsd.h" +#include "assert/assert.h" +#include +#include +#include + +errorret_t saveDeviceDolphinSDInit(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + + device->state = SAVE_DEVICE_STATE_UNKNOWN; + + errorOk(); +} + +errorret_t saveDeviceDolphinSDUpdate(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + errorOk(); +} + +void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + assertTrue( + device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, + "Device state incorrect?" + ); + + // Safe to call more than once - returns true if already mounted. + if(!fatInitDefault()) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.dolphin.sd_fat_init_failed"; + return saveDeviceFireCallback(device); + } + + // mkdir() only ever creates one level at a time - create the parent + // ("/dusk") before the save directory itself. + if(mkdir("/dusk", 0700) != 0 && errno != EEXIST) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.dolphin.sd_mkdir_failed"; + return saveDeviceFireCallback(device); + } + + if( + mkdir(SAVE_DEVICE_DOLPHIN_SD_DIRECTORY, 0700) != 0 && errno != EEXIST + ) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.dolphin.sd_mkdir_failed"; + return saveDeviceFireCallback(device); + } + + device->state = SAVE_DEVICE_STATE_AVAILABLE; + device->reasonKey = NULL; + saveDeviceFireCallback(device); +} + +errorret_t saveDeviceDolphinSDDispose(savedevice_t *device) { + errorOk(); +} diff --git a/src/duskdolphin/save/savedevicedolphinsd.h b/src/duskdolphin/save/savedevicedolphinsd.h new file mode 100644 index 00000000..ad9967b2 --- /dev/null +++ b/src/duskdolphin/save/savedevicedolphinsd.h @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +// Root directory on whichever device libfat's fatInitDefault() picks as the +// default (SD slot on Wii, SD Gecko/Slot2 on GameCube) - same mechanism +// already used for Dolphin asset loading, see assetdolphinfat.c. +#ifndef SAVE_DEVICE_DOLPHIN_SD_DIRECTORY + #define SAVE_DEVICE_DOLPHIN_SD_DIRECTORY "/dusk/save" +#endif + +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 saveDeviceDolphinSDInit(savedevice_t *device); + +/** + * Updates the save device platform. + * + * @param device The save device platform to update. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinSDUpdate(savedevice_t *device); + +/** + * Requests the device to check its availability, this will call the callback + * whence completed. Initializes libfat and confirms the save directory + * exists (creating it on first run). + * + * @param device The save device platform to check availability. + */ +void saveDeviceDolphinSDCheckAvailability(savedevice_t *device); + +/** + * Disposes of the save device platform. + * + * @param device The save device platform to dispose. + * @return Error state if any. + */ +errorret_t saveDeviceDolphinSDDispose(savedevice_t *device); diff --git a/src/duskdolphin/save/savedeviceplatform.h b/src/duskdolphin/save/savedeviceplatform.h new file mode 100644 index 00000000..d3a23c95 --- /dev/null +++ b/src/duskdolphin/save/savedeviceplatform.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once + +// GameCube only ever has the CARD_SLOTA/CARD_SLOTB memory card devices. +// Wii additionally picks between three storage methods, selected by +// DUSK_SAVE_WII_METHOD in cmake/targets/wii.cmake: +// - NAND (default): real internal storage via ISFS, see +// savedevicedolphinnand.h for the caveats around this. +// - CARD: reuses the exact same GameCube memory card code, since libogc's +// CARD API transparently redirects to Wii's NAND-based GameCube memory +// card emulation. +// - SD: the SD card via libfat, same mechanism this project already uses +// for Dolphin/Wii asset loading (see assetdolphinfat.c). +#if defined(DUSK_WII) && defined(DUSK_SAVE_WII_METHOD_NAND) + #include "savedevicedolphinnand.h" + #define SAVE_DEVICE_COUNT 1 + #define saveDevicePlatformInit saveDeviceDolphinNandInit + #define saveDevicePlatformUpdate saveDeviceDolphinNandUpdate + #define saveDeviceCheckAvailabilityPlatform \ + saveDeviceDolphinNandCheckAvailability + #define saveDevicePlatformDispose saveDeviceDolphinNandDispose +#elif defined(DUSK_WII) && defined(DUSK_SAVE_WII_METHOD_SD) + #include "savedevicedolphinsd.h" + #define SAVE_DEVICE_COUNT 1 + #define saveDevicePlatformInit saveDeviceDolphinSDInit + #define saveDevicePlatformUpdate saveDeviceDolphinSDUpdate + #define saveDeviceCheckAvailabilityPlatform \ + saveDeviceDolphinSDCheckAvailability + #define saveDevicePlatformDispose saveDeviceDolphinSDDispose +#else + #include "savedevicedolphincard.h" + #define SAVE_DEVICE_COUNT 2 + #define saveDevicePlatformInit saveDeviceDolphinCardInit + #define saveDevicePlatformUpdate saveDeviceDolphinCardUpdate + #define saveDeviceCheckAvailabilityPlatform \ + saveDeviceDolphinCardCheckAvailability + #define saveDevicePlatformDispose saveDeviceDolphinCardDispose +#endif diff --git a/src/duskdolphin/system/systemdolphin.c b/src/duskdolphin/system/systemdolphin.c index b1e7d419..429a5ade 100644 --- a/src/duskdolphin/system/systemdolphin.c +++ b/src/duskdolphin/system/systemdolphin.c @@ -12,6 +12,11 @@ #include "console/console.h" errorret_t systemInitDolphin(void) { + // Routes newlib stdout/stderr (printf, and this project's logDebug/ + // logError) through OSReport, which Dolphin's logger captures under the + // OSREPORT category - without this, guest console output goes nowhere. + SYS_STDIO_Report(true); + char_t *names[] = { "4:3", "16:9", @@ -43,8 +48,8 @@ int32_t systemGetLanguageDolphin(void) { const localeinfo_t * systemGetLocaleDolphin(void) { switch(systemGetLanguageDolphin()) { - case CONF_LANG_JAPANESE: return &LOCALE_INFO_JP_JP; - case CONF_LANG_SPANISH: return &LOCALE_INFO_ES_MX; + case SYS_LANG_JAPANESE: return &LOCALE_INFO_JP_JP; + case SYS_LANG_SPANISH: return &LOCALE_INFO_ES_MX; default: return &LOCALE_DEFAULT; } } \ No newline at end of file diff --git a/src/duskpsp/CMakeLists.txt b/src/duskpsp/CMakeLists.txt index 9e10f8d5..5ca3534c 100644 --- a/src/duskpsp/CMakeLists.txt +++ b/src/duskpsp/CMakeLists.txt @@ -21,5 +21,6 @@ add_subdirectory(log) if(DUSK_NETWORK) add_subdirectory(network) endif() +add_subdirectory(save) add_subdirectory(system) add_subdirectory(time) \ No newline at end of file diff --git a/src/duskpsp/save/CMakeLists.txt b/src/duskpsp/save/CMakeLists.txt new file mode 100644 index 00000000..d0fd4fe5 --- /dev/null +++ b/src/duskpsp/save/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 + savedevicepsp.c +) diff --git a/src/duskpsp/save/savedeviceplatform.h b/src/duskpsp/save/savedeviceplatform.h new file mode 100644 index 00000000..9918d51f --- /dev/null +++ b/src/duskpsp/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 "savedevicepsp.h" + +#define SAVE_DEVICE_COUNT 1 + +#define saveDevicePlatformInit saveDevicePSPInit +#define saveDevicePlatformUpdate saveDevicePSPUpdate +#define saveDeviceCheckAvailabilityPlatform saveDevicePSPCheckAvailability +#define saveDevicePlatformDispose saveDevicePSPDispose diff --git a/src/duskpsp/save/savedevicepsp.c b/src/duskpsp/save/savedevicepsp.c new file mode 100644 index 00000000..848508bd --- /dev/null +++ b/src/duskpsp/save/savedevicepsp.c @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "save/savedevice.h" +#include "save/savedevicepsp.h" +#include "assert/assert.h" +#include + +errorret_t saveDevicePSPInit(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + + device->state = SAVE_DEVICE_STATE_UNKNOWN; + + errorOk(); +} + +errorret_t saveDevicePSPUpdate(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + errorOk(); +} + +void saveDevicePSPCheckAvailability(savedevice_t *device) { + assertNotNull(device, "device cannot be null"); + assertIsMainThread("Invalid thread"); + assertTrue( + device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, + "Device state incorrect?" + ); + + // Confirm the memory stick is actually reachable - the real save/load + // (via sceUtilitySavedata) only reports failure once a save/load is + // attempted, so this is checked separately up front. + SceIoStat stat; + if(sceIoGetstat(SAVE_DEVICE_PSP_ROOT, &stat) < 0) { + device->state = SAVE_DEVICE_STATE_UNAVAILABLE; + device->reasonKey = "save.psp.no_memory_stick"; + return saveDeviceFireCallback(device); + } + + device->state = SAVE_DEVICE_STATE_AVAILABLE; + device->reasonKey = NULL; + saveDeviceFireCallback(device); +} + +errorret_t saveDevicePSPDispose(savedevice_t *device) { + errorOk(); +} diff --git a/src/duskpsp/save/savedevicepsp.h b/src/duskpsp/save/savedevicepsp.h new file mode 100644 index 00000000..76f9f1d7 --- /dev/null +++ b/src/duskpsp/save/savedevicepsp.h @@ -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 "error/error.h" + +// PSP only ever exposes one save device - the memory stick. +#ifndef SAVE_DEVICE_PSP_ROOT + #define SAVE_DEVICE_PSP_ROOT "ms0:/" +#endif + +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 saveDevicePSPInit(savedevice_t *device); + +/** + * Updates the save device platform. + * + * @param device The save device platform to update. + * @return Error state if any. + */ +errorret_t saveDevicePSPUpdate(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 saveDevicePSPCheckAvailability(savedevice_t *device); + +/** + * Disposes of the save device platform. + * + * @param device The save device platform to dispose. + * @return Error state if any. + */ +errorret_t saveDevicePSPDispose(savedevice_t *device);