Add compressed combined save format for PSP and Dolphin
Memory sticks/cards don't suit N+1 separate save files the way Linux's filesystem does, so add an opt-in SAVE_DEVICE_DATA_RAW mode: settings and all save slots get serialized to JSON, concatenated, zlib-compressed, and framed with a magic/version/checksum/generation header, then read/written as a single blob through one combined platform hook instead of four. - PSP and Dolphin's SD/NAND backends write via a temp file + atomic rename, so a crash mid-write can never leave a half-written save behind. - GameCube memory cards have no rename or resize primitive, so they ping-pong between two fixed files instead, picking whichever is valid and has the higher generation counter on load. - Linux is untouched (still four separate JSON files); saveslot.h/ savesettings.h drop their now-unnecessary pack(1) now that nothing persists them as raw bytes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
#include "save/savedevicedolphincard.h"
|
||||
#include "save/save.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t saveDeviceDolphinCardInit(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
@@ -128,6 +129,159 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) {
|
||||
return usedBlocks < (uint32_t)blockCount;
|
||||
}
|
||||
|
||||
// One probe result for one of the two ping-pong slots.
|
||||
typedef struct {
|
||||
bool_t valid;
|
||||
uint32_t generation;
|
||||
uint8_t *buffer;// only set if keepBuffer was requested, else NULL
|
||||
uint32_t len;
|
||||
} savedevicedolphincardslot_t;
|
||||
|
||||
// Opens the given card file (if it exists) and validates it as a raw save
|
||||
// blob via saveDeviceRawIsValid(). CARD_Read requires a 32-byte aligned
|
||||
// buffer, so the read (and any buffer kept for the caller) uses memoryAlign
|
||||
// rather than memoryAllocate. If keepBuffer is false, or the file doesn't
|
||||
// exist/validate, no buffer is left allocated.
|
||||
static savedevicedolphincardslot_t saveDeviceDolphinCardProbeSlot(
|
||||
const int32_t channel,
|
||||
const char_t *filename,
|
||||
const bool_t keepBuffer
|
||||
) {
|
||||
savedevicedolphincardslot_t result = {0};
|
||||
|
||||
card_file file;
|
||||
if(CARD_Open(channel, filename, &file) < 0) return result;
|
||||
|
||||
uint32_t len = (uint32_t)file.len;
|
||||
uint8_t *buffer = memoryAlign(32, len);
|
||||
int32_t readResult = CARD_Read(&file, buffer, len, 0);
|
||||
CARD_Close(&file);
|
||||
|
||||
if(readResult < 0 || !saveDeviceRawIsValid(buffer, len, &result.generation)) {
|
||||
memoryFree(buffer);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.valid = true;
|
||||
result.len = len;
|
||||
if(keepBuffer) {
|
||||
result.buffer = buffer;
|
||||
} else {
|
||||
memoryFree(buffer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinCardDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(buffer, "buffer cannot be null");
|
||||
|
||||
int32_t channel = device->platform.channel;
|
||||
|
||||
savedevicedolphincardslot_t slot0 = saveDeviceDolphinCardProbeSlot(
|
||||
channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0, false
|
||||
);
|
||||
savedevicedolphincardslot_t slot1 = saveDeviceDolphinCardProbeSlot(
|
||||
channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1, false
|
||||
);
|
||||
|
||||
// Target whichever slot is NOT the currently valid+newest one, so a crash
|
||||
// partway through this write can never touch the copy a load would use.
|
||||
const char_t *targetFilename;
|
||||
if(slot0.valid && slot1.valid) {
|
||||
targetFilename = slot0.generation <= slot1.generation ?
|
||||
SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 : SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1;
|
||||
} else if(slot0.valid) {
|
||||
targetFilename = SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1;
|
||||
} else {
|
||||
targetFilename = SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0;
|
||||
}
|
||||
|
||||
uint32_t sectorSize = 0;
|
||||
if(CARD_GetSectorSize(channel, §orSize) < 0 || sectorSize == 0) {
|
||||
errorThrow("Failed to query memory card sector size");
|
||||
}
|
||||
|
||||
// CARD_Create's size must be a sector-size multiple, and CARD_Read always
|
||||
// reports that full (possibly padded) size back via file.len - harmless,
|
||||
// since saveDeviceRawIsValid() tolerates trailing padding beyond the
|
||||
// header's declared payload length.
|
||||
uint32_t paddedSize =
|
||||
(uint32_t)(((size + sectorSize - 1) / sectorSize) * sectorSize);
|
||||
uint8_t *paddedBuffer = memoryAlign(32, paddedSize);
|
||||
memorySet(paddedBuffer, 0, paddedSize);
|
||||
memoryCopy(paddedBuffer, buffer, size);
|
||||
|
||||
// The target file may already exist from an earlier generation at a
|
||||
// different size - memory card files can't be resized, so it has to be
|
||||
// deleted and recreated. This only ever touches the stale/inactive slot,
|
||||
// never the one saveDeviceDolphinCardDataRead would currently pick.
|
||||
CARD_Delete(channel, targetFilename);// Not an error if it doesn't exist.
|
||||
|
||||
card_file file;
|
||||
int32_t createResult = CARD_Create(
|
||||
channel, targetFilename, paddedSize, &file
|
||||
);
|
||||
if(createResult < 0) {
|
||||
memoryFree(paddedBuffer);
|
||||
errorThrow("Failed to create save data file: %s", targetFilename);
|
||||
}
|
||||
|
||||
int32_t writeResult = CARD_Write(&file, paddedBuffer, paddedSize, 0);
|
||||
CARD_Close(&file);
|
||||
memoryFree(paddedBuffer);
|
||||
if(writeResult < 0) errorThrow("Failed to write save data: %s", targetFilename);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinCardDataRead(
|
||||
savedevice_t *device,
|
||||
uint8_t **outBuffer,
|
||||
size_t *outSize
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(outBuffer, "outBuffer cannot be null");
|
||||
assertNotNull(outSize, "outSize cannot be null");
|
||||
|
||||
int32_t channel = device->platform.channel;
|
||||
|
||||
savedevicedolphincardslot_t slot0 = saveDeviceDolphinCardProbeSlot(
|
||||
channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0, true
|
||||
);
|
||||
savedevicedolphincardslot_t slot1 = saveDeviceDolphinCardProbeSlot(
|
||||
channel, SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1, true
|
||||
);
|
||||
|
||||
savedevicedolphincardslot_t *chosen = NULL;
|
||||
if(slot0.valid && slot1.valid) {
|
||||
chosen = slot0.generation >= slot1.generation ? &slot0 : &slot1;
|
||||
} else if(slot0.valid) {
|
||||
chosen = &slot0;
|
||||
} else if(slot1.valid) {
|
||||
chosen = &slot1;
|
||||
}
|
||||
|
||||
if(chosen == NULL) {
|
||||
// Neither slot has ever been saved to (or both failed validation) -
|
||||
// not an error, leave the destination as-is.
|
||||
*outBuffer = NULL;
|
||||
*outSize = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(chosen != &slot0 && slot0.buffer != NULL) memoryFree(slot0.buffer);
|
||||
if(chosen != &slot1 && slot1.buffer != NULL) memoryFree(slot1.buffer);
|
||||
|
||||
*outBuffer = chosen->buffer;
|
||||
*outSize = chosen->len;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinCardDispose(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
if(device->platform.mounted) {
|
||||
|
||||
@@ -17,6 +17,20 @@
|
||||
#define SAVE_DEVICE_DOLPHIN_GAME_CODE "DUSK"
|
||||
#endif
|
||||
|
||||
// Memory cards have no rename primitive and files can't be resized once
|
||||
// created, so a single "current" file can't be replaced atomically the way
|
||||
// the other backends do. Instead saveDeviceDolphinCardDataWrite() ping-pongs
|
||||
// between these two fixed files - every store targets whichever one is NOT
|
||||
// the currently valid+newest copy (by saveDeviceRawIsValid()'s generation
|
||||
// counter), so a crash mid-write can only ever corrupt the copy nobody
|
||||
// would load from; the other one stays intact as the fallback.
|
||||
#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0
|
||||
#define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0 "DUSKSAVE0"
|
||||
#endif
|
||||
#ifndef SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1
|
||||
#define SAVE_DEVICE_DOLPHIN_CARD_FILENAME_1 "DUSKSAVE1"
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
// Set from this device's index into SAVE.devices - device 0 is
|
||||
// CARD_SLOTA, device 1 is CARD_SLOTB.
|
||||
@@ -82,3 +96,38 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel);
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinCardDispose(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Writes the combined save data blob out to whichever of the two fixed
|
||||
* card files (see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0/1 above) is not
|
||||
* currently the valid+newest copy - ping-pong in place of the atomic
|
||||
* rename the other backends use, since the CARD API has neither a rename
|
||||
* nor a way to resize an existing file.
|
||||
*
|
||||
* @param device The save device to write to.
|
||||
* @param buffer The raw bytes to write.
|
||||
* @param size The number of bytes to write.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinCardDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads whichever of the two fixed card files is valid and has the higher
|
||||
* generation counter. If neither file exists or validates, this is not an
|
||||
* error - *outBuffer is set to NULL and *outSize to 0.
|
||||
*
|
||||
* @param device The save device to read from.
|
||||
* @param outBuffer Receives a memoryAlign'd (32-byte) buffer the caller
|
||||
* must free with memoryFree, or NULL if nothing has been saved yet.
|
||||
* @param outSize Receives the number of bytes in *outBuffer.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinCardDataRead(
|
||||
savedevice_t *device,
|
||||
uint8_t **outBuffer,
|
||||
size_t *outSize
|
||||
);
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
#include "save/savedevice.h"
|
||||
#include "save/savedevicedolphinnand.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
#define SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX 64
|
||||
|
||||
errorret_t saveDeviceDolphinNandInit(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
@@ -62,6 +66,107 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device) {
|
||||
saveDeviceFireCallback(device);
|
||||
}
|
||||
|
||||
static errorret_t saveDeviceDolphinNandGetDataPath(
|
||||
char_t *dest,
|
||||
const size_t destSize
|
||||
) {
|
||||
assertNotNull(dest, "dest cannot be null");
|
||||
|
||||
stringFormat(
|
||||
dest, destSize, "%s/%s",
|
||||
SAVE_DEVICE_DOLPHIN_NAND_DIR, SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinNandDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(buffer, "buffer cannot be null");
|
||||
|
||||
char_t finalPath[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX];
|
||||
errorChain(saveDeviceDolphinNandGetDataPath(finalPath, sizeof(finalPath)));
|
||||
|
||||
char_t tempPath[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX];
|
||||
stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath);
|
||||
|
||||
// Best-effort create - unlike POSIX's O_CREAT, ISFS_Open can't create a
|
||||
// file that doesn't exist yet, so ISFS_CreateFile has to run first. Best
|
||||
// effort since a leftover temp file from a previous crash is fine to
|
||||
// clobber.
|
||||
ISFS_Delete(tempPath);
|
||||
int32_t createResult = ISFS_CreateFile(
|
||||
tempPath, 0, ISFS_OPEN_RW, ISFS_OPEN_RW, ISFS_OPEN_RW
|
||||
);
|
||||
if(createResult != ISFS_OK) {
|
||||
errorThrow("Failed to create save data file: %s", tempPath);
|
||||
}
|
||||
|
||||
int32_t fd = ISFS_Open(tempPath, ISFS_OPEN_WRITE);
|
||||
if(fd < 0) errorThrow("Failed to open save data for writing: %s", tempPath);
|
||||
|
||||
int32_t written = ISFS_Write(fd, buffer, (u32)size);
|
||||
ISFS_Close(fd);
|
||||
if(written < 0 || (size_t)written != size) {
|
||||
errorThrow("Failed to write save data: %s", tempPath);
|
||||
}
|
||||
|
||||
// Atomic replace: the temp file is fully written before it ever takes the
|
||||
// final name, so a crash/power-loss here can never leave a half-written
|
||||
// save behind - at worst the previous save survives untouched.
|
||||
ISFS_Delete(finalPath);// Not an error if it doesn't exist yet.
|
||||
if(ISFS_Rename(tempPath, finalPath) != ISFS_OK) {
|
||||
errorThrow("Failed to finalize save data: %s", finalPath);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinNandDataRead(
|
||||
savedevice_t *device,
|
||||
uint8_t **outBuffer,
|
||||
size_t *outSize
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(outBuffer, "outBuffer cannot be null");
|
||||
assertNotNull(outSize, "outSize cannot be null");
|
||||
|
||||
char_t path[SAVE_DEVICE_DOLPHIN_NAND_PATH_MAX];
|
||||
errorChain(saveDeviceDolphinNandGetDataPath(path, sizeof(path)));
|
||||
|
||||
int32_t fd = ISFS_Open(path, ISFS_OPEN_READ);
|
||||
if(fd < 0) {
|
||||
// No save yet - not an error.
|
||||
*outBuffer = NULL;
|
||||
*outSize = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
fstats stats;
|
||||
if(ISFS_GetFileStats(fd, &stats) != ISFS_OK) {
|
||||
ISFS_Close(fd);
|
||||
errorThrow("Failed to stat save data: %s", path);
|
||||
}
|
||||
|
||||
size_t size = (size_t)stats.file_length;
|
||||
uint8_t *buffer = memoryAllocate(size);
|
||||
|
||||
int32_t readBytes = ISFS_Read(fd, buffer, (u32)size);
|
||||
ISFS_Close(fd);
|
||||
if(readBytes < 0 || (size_t)readBytes != size) {
|
||||
memoryFree(buffer);
|
||||
errorThrow("Failed to read save data: %s", path);
|
||||
}
|
||||
|
||||
*outBuffer = buffer;
|
||||
*outSize = size;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinNandDispose(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
if(device->platform.initialized) {
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#ifndef SAVE_DEVICE_DOLPHIN_NAND_DIR
|
||||
#define SAVE_DEVICE_DOLPHIN_NAND_DIR "/title/00010001/44555348/data"
|
||||
#endif
|
||||
#ifndef SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME
|
||||
#define SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME "save.dat"
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
bool_t initialized;
|
||||
@@ -58,3 +61,36 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device);
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinNandDispose(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Writes the combined save data blob out to the NAND, replacing it
|
||||
* atomically (write to a temp file, then swap it in via ISFS_Rename) so a
|
||||
* crash mid-write can never leave a half-written file behind.
|
||||
*
|
||||
* @param device The save device to write to.
|
||||
* @param buffer The raw bytes to write.
|
||||
* @param size The number of bytes to write.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinNandDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the combined save data blob in from the NAND. If no save data
|
||||
* exists yet, this is not an error - *outBuffer is set to NULL and
|
||||
* *outSize to 0.
|
||||
*
|
||||
* @param device The save device to read from.
|
||||
* @param outBuffer Receives a memoryAllocate'd buffer the caller must free
|
||||
* with memoryFree, or NULL if nothing has been saved yet.
|
||||
* @param outSize Receives the number of bytes in *outBuffer.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinNandDataRead(
|
||||
savedevice_t *device,
|
||||
uint8_t **outBuffer,
|
||||
size_t *outSize
|
||||
);
|
||||
|
||||
@@ -8,9 +8,14 @@
|
||||
#include "save/savedevice.h"
|
||||
#include "save/savedevicedolphinsd.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <fat.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define SAVE_DEVICE_DOLPHIN_SD_PATH_MAX 128
|
||||
|
||||
errorret_t saveDeviceDolphinSDInit(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
@@ -62,6 +67,93 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) {
|
||||
saveDeviceFireCallback(device);
|
||||
}
|
||||
|
||||
static errorret_t saveDeviceDolphinSDGetDataPath(
|
||||
char_t *dest,
|
||||
const size_t destSize
|
||||
) {
|
||||
assertNotNull(dest, "dest cannot be null");
|
||||
|
||||
stringFormat(
|
||||
dest, destSize, "%s/%s",
|
||||
SAVE_DEVICE_DOLPHIN_SD_DIRECTORY, SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinSDDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(buffer, "buffer cannot be null");
|
||||
|
||||
char_t finalPath[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX];
|
||||
errorChain(saveDeviceDolphinSDGetDataPath(finalPath, sizeof(finalPath)));
|
||||
|
||||
char_t tempPath[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX];
|
||||
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);
|
||||
|
||||
size_t written = fwrite(buffer, 1, size, file);
|
||||
fclose(file);
|
||||
if(written != size) errorThrow("Failed to write save data: %s", tempPath);
|
||||
|
||||
// Atomic replace: the temp file is fully written before it ever takes the
|
||||
// final name, so a crash/power-loss here can never leave a half-written
|
||||
// save behind - at worst the previous save survives untouched.
|
||||
remove(finalPath);// Not an error if it doesn't exist yet.
|
||||
if(rename(tempPath, finalPath) != 0) {
|
||||
errorThrow("Failed to finalize save data: %s", finalPath);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinSDDataRead(
|
||||
savedevice_t *device,
|
||||
uint8_t **outBuffer,
|
||||
size_t *outSize
|
||||
) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(outBuffer, "outBuffer cannot be null");
|
||||
assertNotNull(outSize, "outSize cannot be null");
|
||||
|
||||
char_t path[SAVE_DEVICE_DOLPHIN_SD_PATH_MAX];
|
||||
errorChain(saveDeviceDolphinSDGetDataPath(path, sizeof(path)));
|
||||
|
||||
struct stat st;
|
||||
if(stat(path, &st) != 0) {
|
||||
// No save yet - not an error.
|
||||
*outBuffer = NULL;
|
||||
*outSize = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
size_t size = (size_t)st.st_size;
|
||||
uint8_t *buffer = memoryAllocate(size);
|
||||
|
||||
FILE *file = fopen(path, "rb");
|
||||
if(file == NULL) {
|
||||
memoryFree(buffer);
|
||||
errorThrow("Failed to open save data for reading: %s", path);
|
||||
}
|
||||
|
||||
size_t readBytes = fread(buffer, 1, size, file);
|
||||
fclose(file);
|
||||
if(readBytes != size) {
|
||||
memoryFree(buffer);
|
||||
errorThrow("Failed to read save data: %s", path);
|
||||
}
|
||||
|
||||
*outBuffer = buffer;
|
||||
*outSize = size;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeviceDolphinSDDispose(savedevice_t *device) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#ifndef SAVE_DEVICE_DOLPHIN_SD_DIRECTORY
|
||||
#define SAVE_DEVICE_DOLPHIN_SD_DIRECTORY "/dusk/save"
|
||||
#endif
|
||||
#ifndef SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME
|
||||
#define SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME "save.dat"
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
void *nothing;
|
||||
@@ -53,3 +56,36 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device);
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinSDDispose(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Writes the combined save data blob out to the SD card, replacing it
|
||||
* atomically (write to a temp file, then swap it in) so a crash mid-write
|
||||
* can never leave a half-written file behind.
|
||||
*
|
||||
* @param device The save device to write to.
|
||||
* @param buffer The raw bytes to write.
|
||||
* @param size The number of bytes to write.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinSDDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the combined save data blob in from the SD card. If no save data
|
||||
* exists yet, this is not an error - *outBuffer is set to NULL and
|
||||
* *outSize to 0.
|
||||
*
|
||||
* @param device The save device to read from.
|
||||
* @param outBuffer Receives a memoryAllocate'd buffer the caller must free
|
||||
* with memoryFree, or NULL if nothing has been saved yet.
|
||||
* @param outSize Receives the number of bytes in *outBuffer.
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDolphinSDDataRead(
|
||||
savedevice_t *device,
|
||||
uint8_t **outBuffer,
|
||||
size_t *outSize
|
||||
);
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
#define saveDeviceCheckAvailabilityPlatform \
|
||||
saveDeviceDolphinNandCheckAvailability
|
||||
#define saveDevicePlatformDispose saveDeviceDolphinNandDispose
|
||||
|
||||
// ISFS_Rename exists, so NAND uses the same write-temp-then-rename
|
||||
// pattern as PSP/Linux - see SAVE_DEVICE_DATA_RAW in save/savedevice.h.
|
||||
#define SAVE_DEVICE_DATA_RAW
|
||||
#define saveDeviceDataWritePlatform saveDeviceDolphinNandDataWrite
|
||||
#define saveDeviceDataReadPlatform saveDeviceDolphinNandDataRead
|
||||
#elif defined(DUSK_WII) && defined(DUSK_SAVE_WII_METHOD_SD)
|
||||
#include "savedevicedolphinsd.h"
|
||||
#define SAVE_DEVICE_COUNT 1
|
||||
@@ -33,6 +39,13 @@
|
||||
#define saveDeviceCheckAvailabilityPlatform \
|
||||
saveDeviceDolphinSDCheckAvailability
|
||||
#define saveDevicePlatformDispose saveDeviceDolphinSDDispose
|
||||
|
||||
// libfat gives standard POSIX rename(), so SD uses the same
|
||||
// write-temp-then-rename pattern as PSP/Linux - see SAVE_DEVICE_DATA_RAW
|
||||
// in save/savedevice.h.
|
||||
#define SAVE_DEVICE_DATA_RAW
|
||||
#define saveDeviceDataWritePlatform saveDeviceDolphinSDDataWrite
|
||||
#define saveDeviceDataReadPlatform saveDeviceDolphinSDDataRead
|
||||
#else
|
||||
#include "savedevicedolphincard.h"
|
||||
#define SAVE_DEVICE_COUNT 2
|
||||
@@ -41,4 +54,11 @@
|
||||
#define saveDeviceCheckAvailabilityPlatform \
|
||||
saveDeviceDolphinCardCheckAvailability
|
||||
#define saveDevicePlatformDispose saveDeviceDolphinCardDispose
|
||||
|
||||
// No rename or resize primitive on the CARD API, so this ping-pongs
|
||||
// between two fixed files instead - see SAVE_DEVICE_DOLPHIN_CARD_FILENAME_0
|
||||
// in savedevicedolphincard.h and SAVE_DEVICE_DATA_RAW in save/savedevice.h.
|
||||
#define SAVE_DEVICE_DATA_RAW
|
||||
#define saveDeviceDataWritePlatform saveDeviceDolphinCardDataWrite
|
||||
#define saveDeviceDataReadPlatform saveDeviceDolphinCardDataRead
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user