Add PSP/GameCube/Wii save-device availability checks
PSP checks memory-stick reachability via sceIoGetstat. GameCube gets two devices (CARD_SLOTA/CARD_SLOTB), each mounted via CARD_Init/CARD_Mount and checked for free space via CARD_GetDirectory/CARD_GetBlockCount. Wii picks between three storage methods at compile time (DUSK_SAVE_WII_METHOD = NAND/CARD/SD in wii.cmake, default NAND via ISFS) since real hardware behavior for the default is unverified. Also fixes two pre-existing bugs found while build/runtime-testing this across PPSSPP and Dolphin: wrong libogc language macros in systemGetLocaleDolphin, and a missing SYS_STDIO_Report(true) call that silently swallowed all guest console output in Dolphin. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,17 @@ target_compile_definitions(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
DUSK_WII
|
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
|
# Generate Homebrew Channel meta.xml from project identity variables
|
||||||
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
string(TIMESTAMP DUSK_BUILD_DATE "%Y%m%d000000" UTC)
|
||||||
configure_file(
|
configure_file(
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ add_subdirectory(input)
|
|||||||
if(DUSK_NETWORK)
|
if(DUSK_NETWORK)
|
||||||
add_subdirectory(network)
|
add_subdirectory(network)
|
||||||
endif()
|
endif()
|
||||||
|
add_subdirectory(save)
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
@@ -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()
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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 <gccore.h>
|
||||||
|
|
||||||
|
// 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);
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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 <ogc/isfs.h>
|
||||||
|
|
||||||
|
// 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 <ahb_access/> 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);
|
||||||
@@ -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 <fat.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
@@ -12,6 +12,11 @@
|
|||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
|
|
||||||
errorret_t systemInitDolphin(void) {
|
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[] = {
|
char_t *names[] = {
|
||||||
"4:3",
|
"4:3",
|
||||||
"16:9",
|
"16:9",
|
||||||
@@ -43,8 +48,8 @@ int32_t systemGetLanguageDolphin(void) {
|
|||||||
|
|
||||||
const localeinfo_t * systemGetLocaleDolphin(void) {
|
const localeinfo_t * systemGetLocaleDolphin(void) {
|
||||||
switch(systemGetLanguageDolphin()) {
|
switch(systemGetLanguageDolphin()) {
|
||||||
case CONF_LANG_JAPANESE: return &LOCALE_INFO_JP_JP;
|
case SYS_LANG_JAPANESE: return &LOCALE_INFO_JP_JP;
|
||||||
case CONF_LANG_SPANISH: return &LOCALE_INFO_ES_MX;
|
case SYS_LANG_SPANISH: return &LOCALE_INFO_ES_MX;
|
||||||
default: return &LOCALE_DEFAULT;
|
default: return &LOCALE_DEFAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,5 +21,6 @@ add_subdirectory(log)
|
|||||||
if(DUSK_NETWORK)
|
if(DUSK_NETWORK)
|
||||||
add_subdirectory(network)
|
add_subdirectory(network)
|
||||||
endif()
|
endif()
|
||||||
|
add_subdirectory(save)
|
||||||
add_subdirectory(system)
|
add_subdirectory(system)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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 <pspiofilemgr.h>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user