From 3c3dc35e1dd40e88448bfd96ef608e3254982f8a Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Fri, 4 Sep 2026 09:43:00 -0500 Subject: [PATCH] Default Wii saves to SD card, store saves next to dusk.dsk/EBOOT.PBP Wii NAND save storage is blocked by Dolphin's IOS ticket-rights check for homebrew (documented earlier this session as a known, non-code limitation), so Wii now defaults DUSK_SAVE_WII_METHOD to SD instead of NAND. Also: asset_t gains ASSET.baseDirectory, populated by each platform's asset loader with the real directory dusk.dsk was opened from (or, on PSP, the directory EBOOT.PBP itself lives in, since dusk.dsk is embedded inside it). Wii SD and PSP now write their single combined save file directly into that directory instead of a separate hardcoded path, and no longer need to create it (it's already known to exist). Linux now uses a `saves/` subdirectory of that same directory instead of ~/.dusk/saves, since it keeps multiple files (settings + one per slot). Wii NAND and GameCube/Wii memory-card storage are unaffected - neither has a real filesystem-path concept this applies to. Built and verified on Linux, PSP (Docker), GameCube (Docker) and Wii (Docker) - confirmed via the compiler invocation that Wii now compiles with DUSK_SAVE_WII_METHOD_SD. Co-Authored-By: Claude Sonnet 5 --- cmake/targets/wii.cmake | 2 +- src/dusk/asset/asset.h | 13 ++++++++++ src/duskdolphin/asset/assetdolphinfat.c | 5 ++++ src/duskdolphin/save/savedevicedolphinsd.c | 28 +++++++--------------- src/duskdolphin/save/savedevicedolphinsd.h | 14 +++++------ src/dusklinux/asset/assetlinux.c | 7 ++++++ src/dusklinux/save/savedevicelinux.c | 22 ++++++++--------- src/dusklinux/save/savedevicelinux.h | 6 ++++- src/duskpsp/asset/assetpbp.c | 9 +++++++ src/duskpsp/save/savedeviceplatform.h | 7 +++--- src/duskpsp/save/savedevicepsp.c | 6 ++--- 11 files changed, 70 insertions(+), 49 deletions(-) diff --git a/cmake/targets/wii.cmake b/cmake/targets/wii.cmake index 5b2ae662..fa6be9c4 100644 --- a/cmake/targets/wii.cmake +++ b/cmake/targets/wii.cmake @@ -5,7 +5,7 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ) # Wii save storage method - see src/duskdolphin/save/savedeviceplatform.h. -set(DUSK_SAVE_WII_METHOD "NAND" CACHE STRING +set(DUSK_SAVE_WII_METHOD "SD" CACHE STRING "Wii save storage: NAND (internal storage via ISFS), CARD (GameCube-\ compatible memory card emulation), or SD (SD card via libfat)" ) diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h index 15f3dfd0..b84d9830 100644 --- a/src/dusk/asset/asset.h +++ b/src/dusk/asset/asset.h @@ -26,6 +26,8 @@ #define ASSET_LOADING_COUNT_MAX 10 #define ASSET_ENTRY_COUNT_MAX 64 +#define ASSET_BASE_DIRECTORY_MAX 256 + typedef struct asset_s { // Compressed (DEFLATE) archive - expected to hold the bulk of a game's // binary assets. Looked up first by assetFileInit(). @@ -37,6 +39,17 @@ typedef struct asset_s { // assetFileInit() only if the name isn't found in `zip`. zip_t *zipStored; + // Directory dusk.dsk was actually found/opened in (on PSP, the directory + // EBOOT.PBP lives in, since dusk.dsk is embedded inside it rather than a + // loose file) - populated by each platform's own assetInitXxx() once it + // locates the file. Save devices that persist to a real filesystem path + // (Linux, PSP, Wii SD) use this as their base directory instead of a + // separate hardcoded location, so saves live next to wherever the game + // itself was actually launched from rather than some unrelated fixed + // path. Not meaningful for devices with no filesystem-path concept at + // all (GameCube/Wii memory card, Wii NAND's ISFS namespace). + char_t baseDirectory[ASSET_BASE_DIRECTORY_MAX]; + assetplatform_t platform; // Guards every libzip call against ASSET.zip/ASSET.zipStored (zip_fopen, diff --git a/src/duskdolphin/asset/assetdolphinfat.c b/src/duskdolphin/asset/assetdolphinfat.c index 577597dc..88ea98ac 100644 --- a/src/duskdolphin/asset/assetdolphinfat.c +++ b/src/duskdolphin/asset/assetdolphinfat.c @@ -41,6 +41,11 @@ errorret_t assetInitDolphinFAT(void) { *dolphinSearchPath, ASSET_FILE_NAME ); + // Save devices with a real filesystem path (Wii SD) reuse this as + // their base directory - see ASSET.baseDirectory's own comment. + stringCopy( + ASSET.baseDirectory, *dolphinSearchPath, ASSET_BASE_DIRECTORY_MAX + ); break; } diff --git a/src/duskdolphin/save/savedevicedolphinsd.c b/src/duskdolphin/save/savedevicedolphinsd.c index a8febbd2..32074889 100644 --- a/src/duskdolphin/save/savedevicedolphinsd.c +++ b/src/duskdolphin/save/savedevicedolphinsd.c @@ -7,12 +7,12 @@ #include "save/savedevice.h" #include "save/savedevicedolphinsd.h" +#include "asset/asset.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" #include #include -#include #include #define SAVE_DEVICE_DOLPHIN_SD_PATH_MAX 128 @@ -39,29 +39,15 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) { "Device state incorrect?" ); - // Safe to call more than once - returns true if already mounted. + // Safe to call more than once - returns true if already mounted. No + // directory to create here - ASSET.baseDirectory is already known to + // exist, since dusk.dsk was just successfully opened from it. 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); @@ -75,7 +61,7 @@ errorret_t saveDeviceDolphinSDGetDataPath( stringFormat( dest, destSize, "%s/%s", - SAVE_DEVICE_DOLPHIN_SD_DIRECTORY, SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME + ASSET.baseDirectory, SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME ); errorOk(); @@ -96,7 +82,9 @@ errorret_t saveDeviceDolphinSDDataWrite( stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath); FILE *file = fopen(tempPath, "wb"); - if(file == NULL) errorThrow("Failed to open save data for writing: %s", tempPath); + if(file == NULL) { + errorThrow("Failed to open save data for writing: %s", tempPath); + } size_t written = fwrite(buffer, 1, size, file); fclose(file); diff --git a/src/duskdolphin/save/savedevicedolphinsd.h b/src/duskdolphin/save/savedevicedolphinsd.h index a684be8b..bc6622eb 100644 --- a/src/duskdolphin/save/savedevicedolphinsd.h +++ b/src/duskdolphin/save/savedevicedolphinsd.h @@ -8,12 +8,10 @@ #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 +// Data file lives directly in ASSET.baseDirectory (the same directory +// dusk.dsk was found in - see its own comment) rather than a separate +// fixed directory, so it doesn't need creating: that directory is already +// known to exist, since dusk.dsk was just successfully opened from it. #ifndef SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME #define SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME "save.dat" #endif @@ -42,8 +40,8 @@ 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). + * whence completed. Initializes libfat - the save directory itself is + * ASSET.baseDirectory, already known to exist. * * @param device The save device platform to check availability. */ diff --git a/src/dusklinux/asset/assetlinux.c b/src/dusklinux/asset/assetlinux.c index 1f761d14..2e6a5a11 100644 --- a/src/dusklinux/asset/assetlinux.c +++ b/src/dusklinux/asset/assetlinux.c @@ -83,6 +83,13 @@ errorret_t assetInitLinux(void) { continue; } opened = true; + + // Save devices with a real filesystem path (Linux) reuse this as + // their base directory - see ASSET.baseDirectory's own comment. + stringCopy(ASSET.baseDirectory, searchPath, ASSET_BASE_DIRECTORY_MAX); + char_t *lastSlash = stringFindLastChar(ASSET.baseDirectory, '/'); + if(lastSlash != NULL) *lastSlash = '\0'; + break;// Found! } while(*(++path) != NULL); diff --git a/src/dusklinux/save/savedevicelinux.c b/src/dusklinux/save/savedevicelinux.c index 453e299e..1aafd1d0 100644 --- a/src/dusklinux/save/savedevicelinux.c +++ b/src/dusklinux/save/savedevicelinux.c @@ -10,10 +10,10 @@ #include "save/saveslot.h" #include "save/savesettings.h" #include "save/savejson.h" +#include "asset/asset.h" #include "assert/assert.h" #include "util/mkdirp.h" #include "util/string.h" -#include errorret_t saveDeviceLinuxInit(savedevice_t *device) { assertNotNull(device, "device cannot be null"); @@ -37,17 +37,15 @@ void saveDeviceLinuxCheckAvailability(savedevice_t *device) { "Device state incorrect?" ); - // Resolve the save directory. + // Resolve the save directory - can't actually fail (just a + // stringFormat), unlike when this derived from $HOME, but this function + // returns void (via saveDeviceFireCallback below) rather than + // errorret_t, so there's no return-early errorChain to use here. char_t directory[SAVE_DEVICE_LINUX_PATH_MAX]; errorret_t directoryResult = saveDeviceLinuxGetDirectory( directory, sizeof(directory) ); - if(errorIsNotOk(directoryResult)) { - errorCatch(errorPrint(directoryResult)); - device->state = SAVE_DEVICE_STATE_UNAVAILABLE; - device->reasonKey = "save.linux.no_home"; - return saveDeviceFireCallback(device); - } + if(errorIsNotOk(directoryResult)) errorCatch(errorPrint(directoryResult)); // Mkdirp the save directory. int mkdirpResult = mkdirp(directory, 0700); @@ -66,10 +64,10 @@ void saveDeviceLinuxCheckAvailability(savedevice_t *device) { errorret_t saveDeviceLinuxGetDirectory(char_t *dest, const size_t destSize) { assertNotNull(dest, "dest cannot be null"); - const char_t *home = getenv("HOME"); - if(home == NULL) errorThrow("HOME environment variable is not set"); - - stringFormat(dest, destSize, "%s/%s", home, SAVE_DEVICE_LINUX_DIRECTORY_NAME); + stringFormat( + dest, destSize, "%s/%s", + ASSET.baseDirectory, SAVE_DEVICE_LINUX_SAVES_SUBDIR_NAME + ); errorOk(); } diff --git a/src/dusklinux/save/savedevicelinux.h b/src/dusklinux/save/savedevicelinux.h index c1920ec3..99ccbf89 100644 --- a/src/dusklinux/save/savedevicelinux.h +++ b/src/dusklinux/save/savedevicelinux.h @@ -8,7 +8,11 @@ #pragma once #include "error/error.h" -#define SAVE_DEVICE_LINUX_DIRECTORY_NAME ".dusk/saves" +// Subdirectory of ASSET.baseDirectory (the directory dusk.dsk was found +// in - see its own comment) saves are kept in - a subdirectory, unlike +// the other platforms' single combined-blob file, since Linux keeps +// settings and each save slot as their own separate file. +#define SAVE_DEVICE_LINUX_SAVES_SUBDIR_NAME "saves" #define SAVE_DEVICE_LINUX_SETTINGS_FILENAME "settings.json" #define SAVE_DEVICE_LINUX_SLOT_FILENAME_FORMAT "slot%u.json" #define SAVE_DEVICE_LINUX_PATH_MAX FILENAME_MAX diff --git a/src/duskpsp/asset/assetpbp.c b/src/duskpsp/asset/assetpbp.c index b55c5ee9..dc7dd424 100644 --- a/src/duskpsp/asset/assetpbp.c +++ b/src/duskpsp/asset/assetpbp.c @@ -9,12 +9,21 @@ #include "asset/assetdsk.h" #include "assert/assert.h" #include "util/memory.h" +#include "util/string.h" errorret_t assetInitPBP(const char_t *pbpPath) { assertNotNull(pbpPath, "PBP path cannot be null."); assertStrLenMin(pbpPath, 1, "PBP path cannot be empty."); assertStrLenMax(pbpPath, ASSET_PBP_PATH_MAX, "PBP path is too long."); + // Save devices with a real filesystem path (PSP) reuse this as their + // base directory - see ASSET.baseDirectory's own comment. dusk.dsk + // itself is embedded inside this PBP rather than a loose file, so this + // is the directory the PBP (and thus the game) actually lives in. + stringCopy(ASSET.baseDirectory, pbpPath, ASSET_BASE_DIRECTORY_MAX); + char_t *lastSlash = stringFindLastChar(ASSET.baseDirectory, '/'); + if(lastSlash != NULL) *lastSlash = '\0'; + FILE *pbpFile = fopen(pbpPath, "rb"); if(pbpFile == NULL) { errorThrow("Failed to open PBP file: %s", pbpPath); diff --git a/src/duskpsp/save/savedeviceplatform.h b/src/duskpsp/save/savedeviceplatform.h index 4942b780..a992c259 100644 --- a/src/duskpsp/save/savedeviceplatform.h +++ b/src/duskpsp/save/savedeviceplatform.h @@ -23,9 +23,10 @@ #define saveDeviceDataWritePlatform saveDevicePSPDataWrite #define saveDeviceDataReadPlatform saveDevicePSPDataRead -#ifndef SAVE_DEVICE_PSP_DIRECTORY_NAME - #define SAVE_DEVICE_PSP_DIRECTORY_NAME "ms0:/DUSK" -#endif +// Data file lives directly in ASSET.baseDirectory (the directory EBOOT.PBP +// was launched from - see its own comment) rather than a separate fixed +// directory, so it doesn't need creating: that directory is already known +// to exist, since the running EBOOT.PBP lives there. #ifndef SAVE_DEVICE_PSP_DATA_FILENAME #define SAVE_DEVICE_PSP_DATA_FILENAME "save.dat" #endif diff --git a/src/duskpsp/save/savedevicepsp.c b/src/duskpsp/save/savedevicepsp.c index 19f94d38..8eadee92 100644 --- a/src/duskpsp/save/savedevicepsp.c +++ b/src/duskpsp/save/savedevicepsp.c @@ -7,6 +7,7 @@ #include "save/savedevice.h" #include "save/savedevicepsp.h" +#include "asset/asset.h" #include "assert/assert.h" #include "util/memory.h" #include "util/string.h" @@ -64,7 +65,7 @@ errorret_t saveDevicePSPGetDataPath( stringFormat( dest, destSize, "%s/%s", - SAVE_DEVICE_PSP_DIRECTORY_NAME, SAVE_DEVICE_PSP_DATA_FILENAME + ASSET.baseDirectory, SAVE_DEVICE_PSP_DATA_FILENAME ); errorOk(); @@ -78,9 +79,6 @@ errorret_t saveDevicePSPDataWrite( assertNotNull(device, "device cannot be null"); assertNotNull(buffer, "buffer cannot be null"); - // Best-effort create - a fresh memory stick won't have this directory yet. - sceIoMkdir(SAVE_DEVICE_PSP_DIRECTORY_NAME, 0777); - char_t finalPath[SAVE_DEVICE_PSP_PATH_MAX]; errorChain(saveDevicePSPGetDataPath(finalPath, sizeof(finalPath)));