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:
+435
-8
@@ -9,6 +9,16 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
#include "yyjson.h"
|
||||
#include "save/savejson.h"
|
||||
#include "save/saveslot.h"
|
||||
#include "save/savesettings.h"
|
||||
#include "util/crypt.h"
|
||||
#include "util/endian.h"
|
||||
#include <zlib.h>
|
||||
#endif
|
||||
|
||||
errorret_t saveDeviceInit(savedevice_t *device) {
|
||||
assertNotNull(device, "device cannot be null");
|
||||
|
||||
@@ -79,6 +89,415 @@ void saveDeviceCheckAvailability(
|
||||
saveDeviceCheckAvailabilityPlatform(device);
|
||||
}
|
||||
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
|
||||
// Bumped to 2 for the addition of `generation` below.
|
||||
#define SAVE_DEVICE_RAW_VERSION ((uint32_t)2)
|
||||
// Sanity cap against a corrupted/hostile header driving a bogus allocation.
|
||||
#define SAVE_DEVICE_RAW_MAX_SIZE ((uint32_t)(256 * 1024))
|
||||
|
||||
static const char_t SAVE_DEVICE_RAW_MAGIC[4] = {'D', 'S', 'A', 'V'};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
char_t magic[4];
|
||||
uint32_t version;
|
||||
uint32_t uncompressedSize;// size of the logical blob, pre-compression
|
||||
uint32_t compressedSize;// size of the payload following this header
|
||||
uint32_t checksum;// cryptCRC32() over the compressed payload
|
||||
// Monotonically increasing with every store. Unused by single-file
|
||||
// platforms (rename already guarantees the current file is the latest),
|
||||
// but load-bearing for platforms with no rename primitive and multiple
|
||||
// physical copies - e.g. GameCube memory cards ping-ponging between two
|
||||
// fixed files, where this is how saveDeviceRawIsValid() picks the newest
|
||||
// valid copy.
|
||||
uint32_t generation;
|
||||
} savedevicerawheader_t;
|
||||
#pragma pack(pop)
|
||||
|
||||
typedef struct {
|
||||
const uint8_t *ptr;
|
||||
uint32_t len;
|
||||
} savedevicerawspan_t;
|
||||
|
||||
typedef struct {
|
||||
savedevicerawspan_t settings;
|
||||
savedevicerawspan_t slots[SAVE_SLOT_COUNT];
|
||||
} savedevicerawspans_t;
|
||||
|
||||
// A single settings/slot item's JSON bytes, either borrowed from an existing
|
||||
// decompressed blob (owned == NULL) or freshly serialized just now via
|
||||
// yyjson_mut_write (owned != NULL, must be freed with plain free()).
|
||||
typedef struct {
|
||||
const uint8_t *ptr;
|
||||
uint32_t len;
|
||||
char_t *owned;
|
||||
} savedevicerawitem_t;
|
||||
|
||||
// Parses the length-prefixed span table at the front of a decompressed
|
||||
// logical blob. Spans point directly into `logical`, nothing is copied.
|
||||
static void saveDeviceRawParseSpans(
|
||||
const uint8_t *logical,
|
||||
savedevicerawspans_t *spans
|
||||
) {
|
||||
size_t offset = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT);
|
||||
|
||||
spans->settings.len = endianLittleToHost32(*(const uint32_t *)(logical + 0));
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
spans->slots[i].len = endianLittleToHost32(
|
||||
*(const uint32_t *)(logical + sizeof(uint32_t) * (1 + i))
|
||||
);
|
||||
}
|
||||
|
||||
spans->settings.ptr = logical + offset;
|
||||
offset += spans->settings.len;
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
spans->slots[i].ptr = logical + offset;
|
||||
offset += spans->slots[i].len;
|
||||
}
|
||||
}
|
||||
|
||||
// Cheaply checks whether `raw` is a well-formed, checksum-valid raw save
|
||||
// blob, without decompressing it - just header + checksum validation. Used
|
||||
// both by saveDeviceRawInflate below and, publicly (see savedevice.h), by
|
||||
// platforms that keep more than one physical copy and need to pick the
|
||||
// newest valid one (e.g. GameCube memory cards - see `generation` above).
|
||||
bool_t saveDeviceRawIsValid(
|
||||
const uint8_t *raw,
|
||||
const size_t rawSize,
|
||||
uint32_t *outGeneration
|
||||
) {
|
||||
if(raw == NULL || rawSize < sizeof(savedevicerawheader_t)) return false;
|
||||
|
||||
const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw;
|
||||
if(memoryCompare(header->magic, SAVE_DEVICE_RAW_MAGIC, 4) != 0) {
|
||||
return false;
|
||||
}
|
||||
if(endianLittleToHost32(header->version) != SAVE_DEVICE_RAW_VERSION) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize);
|
||||
uint32_t compressedSize = endianLittleToHost32(header->compressedSize);
|
||||
if(
|
||||
uncompressedSize > SAVE_DEVICE_RAW_MAX_SIZE ||
|
||||
compressedSize > SAVE_DEVICE_RAW_MAX_SIZE ||
|
||||
// Trailing bytes beyond the declared payload are tolerated as padding -
|
||||
// e.g. a GameCube memory card file rounded up to a whole sector.
|
||||
rawSize < sizeof(savedevicerawheader_t) + compressedSize
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t checksum = cryptCRC32(raw + sizeof(savedevicerawheader_t), compressedSize);
|
||||
if(checksum != endianLittleToHost32(header->checksum)) return false;
|
||||
|
||||
if(outGeneration != NULL) {
|
||||
*outGeneration = endianLittleToHost32(header->generation);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validates the header and inflates the compressed payload that follows it.
|
||||
// On success, *outLogical is a memoryAllocate'd buffer the caller must free.
|
||||
static errorret_t saveDeviceRawInflate(
|
||||
const uint8_t *raw,
|
||||
const size_t rawSize,
|
||||
uint8_t **outLogical,
|
||||
size_t *outLogicalSize,
|
||||
uint32_t *outGeneration
|
||||
) {
|
||||
if(!saveDeviceRawIsValid(raw, rawSize, outGeneration)) {
|
||||
errorThrow("Save data failed validation (magic/version/size/checksum)");
|
||||
}
|
||||
|
||||
const savedevicerawheader_t *header = (const savedevicerawheader_t *)raw;
|
||||
uint32_t uncompressedSize = endianLittleToHost32(header->uncompressedSize);
|
||||
uint32_t compressedSize = endianLittleToHost32(header->compressedSize);
|
||||
const uint8_t *compressed = raw + sizeof(savedevicerawheader_t);
|
||||
|
||||
uint8_t *logical = memoryAllocate(uncompressedSize);
|
||||
uLongf destLen = (uLongf)uncompressedSize;
|
||||
int result = uncompress(
|
||||
logical, &destLen, compressed, (uLong)compressedSize
|
||||
);
|
||||
if(result != Z_OK || destLen != uncompressedSize) {
|
||||
memoryFree(logical);
|
||||
errorThrow("Failed to decompress save data (zlib error %d)", result);
|
||||
}
|
||||
|
||||
*outLogical = logical;
|
||||
*outLogicalSize = uncompressedSize;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Serializes `settings` to a freshly malloc'd JSON string via yyjson - the
|
||||
// result must be freed with plain free(), not memoryFree().
|
||||
static errorret_t saveDeviceRawSerializeSettings(
|
||||
savesettings_t *settings,
|
||||
char_t **outJson,
|
||||
size_t *outLen
|
||||
) {
|
||||
writeInit();
|
||||
|
||||
errorret_t writeResult = saveSettingsWriteJSON(settings, doc, object);
|
||||
if(errorIsNotOk(writeResult)) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
errorChain(writeResult);
|
||||
}
|
||||
|
||||
*outJson = yyjson_mut_write(doc, 0, outLen);
|
||||
yyjson_mut_doc_free(doc);
|
||||
if(*outJson == NULL) errorThrow("Failed to write settings JSON");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Serializes `slot` to a freshly malloc'd JSON string via yyjson - the
|
||||
// result must be freed with plain free(), not memoryFree().
|
||||
static errorret_t saveDeviceRawSerializeSlot(
|
||||
saveslot_t *slot,
|
||||
char_t **outJson,
|
||||
size_t *outLen
|
||||
) {
|
||||
writeInit();
|
||||
|
||||
errorret_t writeResult = saveSlotWriteJSON(slot, doc, object);
|
||||
if(errorIsNotOk(writeResult)) {
|
||||
yyjson_mut_doc_free(doc);
|
||||
errorChain(writeResult);
|
||||
}
|
||||
|
||||
*outJson = yyjson_mut_write(doc, 0, outLen);
|
||||
yyjson_mut_doc_free(doc);
|
||||
if(*outJson == NULL) errorThrow("Failed to write slot JSON");
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Frees whatever's been collected so far - safe to call at any point since
|
||||
// unset items are zero-initialized (owned == NULL is a no-op skip).
|
||||
static void saveDeviceRawCleanupStore(
|
||||
uint8_t *oldRaw,
|
||||
uint8_t *oldLogical,
|
||||
savedevicerawitem_t *settingsItem,
|
||||
savedevicerawitem_t *slotItems
|
||||
) {
|
||||
if(oldRaw != NULL) memoryFree(oldRaw);
|
||||
if(oldLogical != NULL) memoryFree(oldLogical);
|
||||
if(settingsItem->owned != NULL) free(settingsItem->owned);
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
if(slotItems[i].owned != NULL) free(slotItems[i].owned);
|
||||
}
|
||||
}
|
||||
|
||||
// Reads the existing combined blob (if any), replaces exactly one item -
|
||||
// `settings`, or the slot at `slotIndex` when `slot` is given, exactly one
|
||||
// of the two must be non-NULL - and rewrites the whole blob. Every other
|
||||
// item's JSON is carried through byte-for-byte from what was already
|
||||
// stored (or freshly defaulted if nothing was stored yet), so a save never
|
||||
// reconstructs data it wasn't given.
|
||||
static errorret_t saveDeviceRawStoreItem(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
) {
|
||||
uint8_t *oldRaw = NULL;
|
||||
size_t oldRawSize = 0;
|
||||
errorChain(saveDeviceDataReadPlatform(device, &oldRaw, &oldRawSize));
|
||||
|
||||
uint8_t *oldLogical = NULL;
|
||||
size_t oldLogicalSize = 0;
|
||||
uint32_t oldGeneration = 0;
|
||||
savedevicerawspans_t oldSpans;
|
||||
bool_t haveOld = oldRaw != NULL;
|
||||
if(haveOld) {
|
||||
errorret_t inflateResult = saveDeviceRawInflate(
|
||||
oldRaw, oldRawSize, &oldLogical, &oldLogicalSize, &oldGeneration
|
||||
);
|
||||
if(errorIsNotOk(inflateResult)) {
|
||||
memoryFree(oldRaw);
|
||||
errorChain(inflateResult);
|
||||
}
|
||||
saveDeviceRawParseSpans(oldLogical, &oldSpans);
|
||||
}
|
||||
|
||||
savedevicerawitem_t settingsItem = {0};
|
||||
savedevicerawitem_t slotItems[SAVE_SLOT_COUNT] = {0};
|
||||
|
||||
if(settings != NULL) {
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSettings(settings, &json, &len);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
settingsItem = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
} else if(haveOld) {
|
||||
settingsItem.ptr = oldSpans.settings.ptr;
|
||||
settingsItem.len = oldSpans.settings.len;
|
||||
} else {
|
||||
savesettings_t defaultSettings;
|
||||
saveSettingsInit(&defaultSettings);
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSettings(
|
||||
&defaultSettings, &json, &len
|
||||
);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
settingsItem = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
}
|
||||
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
if(slot != NULL && i == slotIndex) {
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSlot(slot, &json, &len);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
slotItems[i] = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
} else if(haveOld) {
|
||||
slotItems[i].ptr = oldSpans.slots[i].ptr;
|
||||
slotItems[i].len = oldSpans.slots[i].len;
|
||||
} else {
|
||||
saveslot_t defaultSlot;
|
||||
saveSlotInit(&defaultSlot);
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSlot(&defaultSlot, &json, &len);
|
||||
if(errorIsNotOk(result)) {
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
errorChain(result);
|
||||
}
|
||||
slotItems[i] = (savedevicerawitem_t){
|
||||
.ptr = (const uint8_t *)json, .len = (uint32_t)len, .owned = json
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble the new logical blob: length table, then each item's bytes.
|
||||
size_t tableSize = sizeof(uint32_t) * (1 + SAVE_SLOT_COUNT);
|
||||
size_t logicalSize = tableSize + settingsItem.len;
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) logicalSize += slotItems[i].len;
|
||||
|
||||
uint8_t *newLogical = memoryAllocate(logicalSize);
|
||||
*(uint32_t *)(newLogical + 0) = endianLittleToHost32(settingsItem.len);
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
*(uint32_t *)(newLogical + sizeof(uint32_t) * (1 + i)) =
|
||||
endianLittleToHost32(slotItems[i].len);
|
||||
}
|
||||
|
||||
size_t offset = tableSize;
|
||||
memoryCopy(newLogical + offset, settingsItem.ptr, settingsItem.len);
|
||||
offset += settingsItem.len;
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
memoryCopy(newLogical + offset, slotItems[i].ptr, slotItems[i].len);
|
||||
offset += slotItems[i].len;
|
||||
}
|
||||
|
||||
saveDeviceRawCleanupStore(oldRaw, oldLogical, &settingsItem, slotItems);
|
||||
|
||||
// Compress and frame with the header.
|
||||
uLongf compressedCap = compressBound((uLong)logicalSize);
|
||||
uint8_t *compressedBuf = memoryAllocate(compressedCap);
|
||||
uLongf compressedLen = compressedCap;
|
||||
int compressResult = compress2(
|
||||
compressedBuf, &compressedLen, newLogical, (uLong)logicalSize,
|
||||
Z_DEFAULT_COMPRESSION
|
||||
);
|
||||
memoryFree(newLogical);
|
||||
if(compressResult != Z_OK) {
|
||||
memoryFree(compressedBuf);
|
||||
errorThrow("Failed to compress save data (zlib error %d)", compressResult);
|
||||
}
|
||||
|
||||
savedevicerawheader_t header;
|
||||
memoryCopy(header.magic, SAVE_DEVICE_RAW_MAGIC, 4);
|
||||
header.version = endianLittleToHost32(SAVE_DEVICE_RAW_VERSION);
|
||||
header.uncompressedSize = endianLittleToHost32((uint32_t)logicalSize);
|
||||
header.compressedSize = endianLittleToHost32((uint32_t)compressedLen);
|
||||
header.checksum = endianLittleToHost32(cryptCRC32(compressedBuf, compressedLen));
|
||||
header.generation = endianLittleToHost32(haveOld ? oldGeneration + 1 : 1);
|
||||
|
||||
size_t finalSize = sizeof(header) + compressedLen;
|
||||
uint8_t *finalBuf = memoryAllocate(finalSize);
|
||||
memoryCopy(finalBuf, &header, sizeof(header));
|
||||
memoryCopy(finalBuf + sizeof(header), compressedBuf, compressedLen);
|
||||
memoryFree(compressedBuf);
|
||||
|
||||
errorret_t writeResult = saveDeviceDataWritePlatform(
|
||||
device, finalBuf, finalSize
|
||||
);
|
||||
memoryFree(finalBuf);
|
||||
errorChain(writeResult);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
// Reads the combined blob and populates exactly one item - `settings`, or
|
||||
// the slot at `slotIndex` when `slot` is given, exactly one of the two must
|
||||
// be non-NULL. Leaves the destination untouched if nothing's been saved yet.
|
||||
static errorret_t saveDeviceRawFetchItem(
|
||||
savedevice_t *device,
|
||||
savesettings_t *settings,
|
||||
saveslot_t *slot,
|
||||
const uint8_t slotIndex
|
||||
) {
|
||||
uint8_t *raw = NULL;
|
||||
size_t rawSize = 0;
|
||||
errorChain(saveDeviceDataReadPlatform(device, &raw, &rawSize));
|
||||
if(raw == NULL) errorOk();
|
||||
|
||||
uint8_t *logical = NULL;
|
||||
size_t logicalSize = 0;
|
||||
errorret_t inflateResult = saveDeviceRawInflate(
|
||||
raw, rawSize, &logical, &logicalSize, NULL
|
||||
);
|
||||
memoryFree(raw);
|
||||
errorChain(inflateResult);
|
||||
|
||||
savedevicerawspans_t spans;
|
||||
saveDeviceRawParseSpans(logical, &spans);
|
||||
const savedevicerawspan_t *span = settings != NULL ?
|
||||
&spans.settings : &spans.slots[slotIndex];
|
||||
|
||||
yyjson_doc *jsonDoc = yyjson_read((const char_t *)span->ptr, span->len, 0);
|
||||
if(jsonDoc == NULL) {
|
||||
memoryFree(logical);
|
||||
errorThrow("Failed to parse save data item JSON");
|
||||
}
|
||||
|
||||
yyjson_val *object = yyjson_doc_get_root(jsonDoc);
|
||||
if(object == NULL) {
|
||||
yyjson_doc_free(jsonDoc);
|
||||
memoryFree(logical);
|
||||
errorThrow("Save data item JSON missing root object");
|
||||
}
|
||||
|
||||
errorret_t readResult = settings != NULL ?
|
||||
saveSettingsReadJSON(settings, object) : saveSlotReadJSON(slot, object);
|
||||
yyjson_doc_free(jsonDoc);
|
||||
memoryFree(logical);
|
||||
errorChain(readResult);
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
#endif// defined(SAVE_DEVICE_DATA_RAW)
|
||||
|
||||
errorret_t saveDeviceSlotWrite(
|
||||
savedevice_t *device,
|
||||
saveslot_t *slot,
|
||||
@@ -87,9 +506,11 @@ errorret_t saveDeviceSlotWrite(
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(slot, "slot cannot be null");
|
||||
|
||||
#ifdef saveDeviceSlotWritePlatform
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawStoreItem(device, NULL, slot, slotIndex));
|
||||
#elif defined(saveDeviceSlotWritePlatform)
|
||||
errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -102,9 +523,11 @@ errorret_t saveDeviceSlotRead(
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(slot, "slot cannot be null");
|
||||
|
||||
#ifdef saveDeviceSlotReadPlatform
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawFetchItem(device, NULL, slot, slotIndex));
|
||||
#elif defined(saveDeviceSlotReadPlatform)
|
||||
errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -116,9 +539,11 @@ errorret_t saveDeviceSettingsWrite(
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(settings, "settings cannot be null");
|
||||
|
||||
#ifdef saveDeviceSettingsWritePlatform
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawStoreItem(device, settings, NULL, 0));
|
||||
#elif defined(saveDeviceSettingsWritePlatform)
|
||||
errorChain(saveDeviceSettingsWritePlatform(device, settings));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
@@ -130,9 +555,11 @@ errorret_t saveDeviceSettingsRead(
|
||||
assertNotNull(device, "device cannot be null");
|
||||
assertNotNull(settings, "settings cannot be null");
|
||||
|
||||
#ifdef saveDeviceSettingsReadPlatform
|
||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||
errorChain(saveDeviceRawFetchItem(device, settings, NULL, 0));
|
||||
#elif defined(saveDeviceSettingsReadPlatform)
|
||||
errorChain(saveDeviceSettingsReadPlatform(device, settings));
|
||||
#endif
|
||||
#endif
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,34 @@
|
||||
#error "SAVE_DEVICE_COUNT must be defined"
|
||||
#endif
|
||||
|
||||
// Platform opt-in: define this in a platform's savedeviceplatform.h (instead
|
||||
// of the four saveDevice{Slot,Settings}{Write,Read}Platform macros) for
|
||||
// devices without free-form filesystem access - e.g. a memory card/stick
|
||||
// that suits one combined save blob better than N+1 separate files. When
|
||||
// defined, the platform must instead provide these two:
|
||||
//
|
||||
// errorret_t saveDeviceDataWritePlatform(
|
||||
// savedevice_t *device, const uint8_t *buffer, size_t size
|
||||
// );
|
||||
// errorret_t saveDeviceDataReadPlatform(
|
||||
// savedevice_t *device, uint8_t **outBuffer, size_t *outSize
|
||||
// );
|
||||
//
|
||||
// savedevice.c compresses settings + all save slots' JSON into one
|
||||
// magic/version/checksum-framed blob and read/writes it as a single raw
|
||||
// buffer through these two hooks instead. saveDeviceDataReadPlatform must
|
||||
// allocate its output with memoryAllocate (ownership passes to the caller,
|
||||
// who frees it with memoryFree), and set *outBuffer = NULL/*outSize = 0
|
||||
// (not an error) when nothing has been saved yet - same convention as the
|
||||
// "file not found" case in the non-raw four-hook mode.
|
||||
// #define SAVE_DEVICE_DATA_RAW
|
||||
//
|
||||
// saveDeviceDataReadPlatform must always resolve to "the one current blob"
|
||||
// even if the platform keeps more than one physical copy under the hood -
|
||||
// e.g. a platform with no rename primitive (GameCube memory cards) that
|
||||
// ping-pongs between two fixed files instead. saveDeviceRawIsValid() below
|
||||
// is what such a platform uses to tell which of its copies is newest.
|
||||
|
||||
typedef struct savedevice_s savedevice_t;
|
||||
typedef struct savesettings_s savesettings_t;
|
||||
typedef struct saveslot_s saveslot_t;
|
||||
@@ -133,3 +161,22 @@ errorret_t saveDeviceSettingsRead(
|
||||
* @return Error state if any.
|
||||
*/
|
||||
errorret_t saveDeviceDispose(savedevice_t *device);
|
||||
|
||||
/**
|
||||
* Only relevant to SAVE_DEVICE_DATA_RAW platforms that keep more than one
|
||||
* physical copy of the save blob (see the note above) - cheaply checks
|
||||
* whether `raw` is a well-formed, checksum-valid raw save blob without
|
||||
* decompressing it, and if so reports its generation counter so the caller
|
||||
* can tell which of several copies is newest.
|
||||
*
|
||||
* @param raw The raw bytes to validate.
|
||||
* @param rawSize The number of bytes in raw.
|
||||
* @param outGeneration Receives the blob's generation counter if valid, may
|
||||
* be NULL if the caller doesn't need it.
|
||||
* @return true if raw is a valid, checksum-passing save blob.
|
||||
*/
|
||||
bool_t saveDeviceRawIsValid(
|
||||
const uint8_t *raw,
|
||||
const size_t rawSize,
|
||||
uint32_t *outGeneration
|
||||
);
|
||||
@@ -10,11 +10,9 @@
|
||||
#include "savedevice.h"
|
||||
#include "yyjson.h"
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct savesettings_s {
|
||||
int32_t someSetting;
|
||||
} savesettings_t;
|
||||
#pragma pack(pop)
|
||||
|
||||
/**
|
||||
* Inits the save settings with the default state, this is functionally "new
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#define SAVE_SLOT_NAME_LENGTH 8
|
||||
#define SAVE_SLOT_COUNT 3
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
||||
dusktimeepoch_t time;
|
||||
@@ -26,7 +25,6 @@ typedef struct saveslot_s {
|
||||
|
||||
saveslotcache_t cachedData;
|
||||
} saveslot_t;
|
||||
#pragma pack(pop)
|
||||
|
||||
/**
|
||||
* Inits the save slot with the default state, this is functionally "new game"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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