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:
@@ -14,3 +14,18 @@
|
||||
#define saveDevicePlatformUpdate saveDevicePSPUpdate
|
||||
#define saveDeviceCheckAvailabilityPlatform saveDevicePSPCheckAvailability
|
||||
#define saveDevicePlatformDispose saveDevicePSPDispose
|
||||
|
||||
// The memory stick has no directory-listing UI convention to preserve like
|
||||
// GameCube/Wii memory cards do, so settings + all save slots are combined
|
||||
// into one compressed blob rather than kept as separate files - see
|
||||
// SAVE_DEVICE_DATA_RAW in save/savedevice.h.
|
||||
#define SAVE_DEVICE_DATA_RAW
|
||||
#define saveDeviceDataWritePlatform saveDevicePSPDataWrite
|
||||
#define saveDeviceDataReadPlatform saveDevicePSPDataRead
|
||||
|
||||
#ifndef SAVE_DEVICE_PSP_DIRECTORY_NAME
|
||||
#define SAVE_DEVICE_PSP_DIRECTORY_NAME "ms0:/DUSK"
|
||||
#endif
|
||||
#ifndef SAVE_DEVICE_PSP_DATA_FILENAME
|
||||
#define SAVE_DEVICE_PSP_DATA_FILENAME "save.dat"
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,12 @@
|
||||
#include "save/savedevice.h"
|
||||
#include "save/savedevicepsp.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <pspiofilemgr.h>
|
||||
|
||||
#define SAVE_DEVICE_PSP_PATH_MAX 64
|
||||
|
||||
errorret_t saveDevicePSPInit(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
|
||||
@@ -47,6 +51,100 @@ void saveDevicePSPCheckAvailability(savedevice_t *device) {
|
||||
saveDeviceFireCallback(device);
|
||||
}
|
||||
|
||||
static errorret_t saveDevicePSPGetDataPath(
|
||||
char_t *dest,
|
||||
const size_t destSize
|
||||
) {
|
||||
assertNotNull(dest, "dest cannot be null");
|
||||
|
||||
stringFormat(
|
||||
dest, destSize, "%s/%s",
|
||||
SAVE_DEVICE_PSP_DIRECTORY_NAME, SAVE_DEVICE_PSP_DATA_FILENAME
|
||||
);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDevicePSPDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
) {
|
||||
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)));
|
||||
|
||||
char_t tempPath[SAVE_DEVICE_PSP_PATH_MAX];
|
||||
stringFormat(tempPath, sizeof(tempPath), "%s.tmp", finalPath);
|
||||
|
||||
SceUID fd = sceIoOpen(
|
||||
tempPath, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777
|
||||
);
|
||||
if(fd < 0) errorThrow("Failed to open save data for writing: %s", tempPath);
|
||||
|
||||
int written = sceIoWrite(fd, buffer, (SceSize)size);
|
||||
sceIoClose(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.
|
||||
sceIoRemove(finalPath);// Not an error if it doesn't exist yet.
|
||||
if(sceIoRename(tempPath, finalPath) < 0) {
|
||||
errorThrow("Failed to finalize save data: %s", finalPath);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDevicePSPDataRead(
|
||||
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_PSP_PATH_MAX];
|
||||
errorChain(saveDevicePSPGetDataPath(path, sizeof(path)));
|
||||
|
||||
SceIoStat stat;
|
||||
if(sceIoGetstat(path, &stat) < 0) {
|
||||
// No save yet - not an error.
|
||||
*outBuffer = NULL;
|
||||
*outSize = 0;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
size_t size = (size_t)stat.st_size;
|
||||
uint8_t *buffer = memoryAllocate(size);
|
||||
|
||||
SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0);
|
||||
if(fd < 0) {
|
||||
memoryFree(buffer);
|
||||
errorThrow("Failed to open save data for reading: %s", path);
|
||||
}
|
||||
|
||||
int readBytes = sceIoRead(fd, buffer, (SceSize)size);
|
||||
sceIoClose(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 saveDevicePSPDispose(savedevice_t *device) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,39 @@ typedef struct savedevice_s savedevice_t;
|
||||
*/
|
||||
errorret_t saveDevicePSPInit(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Writes the combined save data blob out to the memory stick, 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 saveDevicePSPDataWrite(
|
||||
savedevice_t *device,
|
||||
const uint8_t *buffer,
|
||||
size_t size
|
||||
);
|
||||
|
||||
/**
|
||||
* Reads the combined save data blob in from the memory stick. 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 saveDevicePSPDataRead(
|
||||
savedevice_t *device,
|
||||
uint8_t **outBuffer,
|
||||
size_t *outSize
|
||||
);
|
||||
|
||||
/**
|
||||
* Updates the save device platform.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user