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:
+439
-12
@@ -9,6 +9,16 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.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) {
|
errorret_t saveDeviceInit(savedevice_t *device) {
|
||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
|
|
||||||
@@ -79,6 +89,415 @@ void saveDeviceCheckAvailability(
|
|||||||
saveDeviceCheckAvailabilityPlatform(device);
|
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(
|
errorret_t saveDeviceSlotWrite(
|
||||||
savedevice_t *device,
|
savedevice_t *device,
|
||||||
saveslot_t *slot,
|
saveslot_t *slot,
|
||||||
@@ -87,9 +506,11 @@ errorret_t saveDeviceSlotWrite(
|
|||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
assertNotNull(slot, "slot cannot be null");
|
assertNotNull(slot, "slot cannot be null");
|
||||||
|
|
||||||
#ifdef saveDeviceSlotWritePlatform
|
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||||
errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex));
|
errorChain(saveDeviceRawStoreItem(device, NULL, slot, slotIndex));
|
||||||
#endif
|
#elif defined(saveDeviceSlotWritePlatform)
|
||||||
|
errorChain(saveDeviceSlotWritePlatform(device, slot, slotIndex));
|
||||||
|
#endif
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -102,9 +523,11 @@ errorret_t saveDeviceSlotRead(
|
|||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
assertNotNull(slot, "slot cannot be null");
|
assertNotNull(slot, "slot cannot be null");
|
||||||
|
|
||||||
#ifdef saveDeviceSlotReadPlatform
|
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||||
errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex));
|
errorChain(saveDeviceRawFetchItem(device, NULL, slot, slotIndex));
|
||||||
#endif
|
#elif defined(saveDeviceSlotReadPlatform)
|
||||||
|
errorChain(saveDeviceSlotReadPlatform(device, slot, slotIndex));
|
||||||
|
#endif
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -116,9 +539,11 @@ errorret_t saveDeviceSettingsWrite(
|
|||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
assertNotNull(settings, "settings cannot be null");
|
assertNotNull(settings, "settings cannot be null");
|
||||||
|
|
||||||
#ifdef saveDeviceSettingsWritePlatform
|
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||||
errorChain(saveDeviceSettingsWritePlatform(device, settings));
|
errorChain(saveDeviceRawStoreItem(device, settings, NULL, 0));
|
||||||
#endif
|
#elif defined(saveDeviceSettingsWritePlatform)
|
||||||
|
errorChain(saveDeviceSettingsWritePlatform(device, settings));
|
||||||
|
#endif
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -130,9 +555,11 @@ errorret_t saveDeviceSettingsRead(
|
|||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
assertNotNull(settings, "settings cannot be null");
|
assertNotNull(settings, "settings cannot be null");
|
||||||
|
|
||||||
#ifdef saveDeviceSettingsReadPlatform
|
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||||
errorChain(saveDeviceSettingsReadPlatform(device, settings));
|
errorChain(saveDeviceRawFetchItem(device, settings, NULL, 0));
|
||||||
#endif
|
#elif defined(saveDeviceSettingsReadPlatform)
|
||||||
|
errorChain(saveDeviceSettingsReadPlatform(device, settings));
|
||||||
|
#endif
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,34 @@
|
|||||||
#error "SAVE_DEVICE_COUNT must be defined"
|
#error "SAVE_DEVICE_COUNT must be defined"
|
||||||
#endif
|
#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 savedevice_s savedevice_t;
|
||||||
typedef struct savesettings_s savesettings_t;
|
typedef struct savesettings_s savesettings_t;
|
||||||
typedef struct saveslot_s saveslot_t;
|
typedef struct saveslot_s saveslot_t;
|
||||||
@@ -133,3 +161,22 @@ errorret_t saveDeviceSettingsRead(
|
|||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDispose(savedevice_t *device);
|
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 "savedevice.h"
|
||||||
#include "yyjson.h"
|
#include "yyjson.h"
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
|
||||||
typedef struct savesettings_s {
|
typedef struct savesettings_s {
|
||||||
int32_t someSetting;
|
int32_t someSetting;
|
||||||
} savesettings_t;
|
} savesettings_t;
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inits the save settings with the default state, this is functionally "new
|
* 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_NAME_LENGTH 8
|
||||||
#define SAVE_SLOT_COUNT 3
|
#define SAVE_SLOT_COUNT 3
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
||||||
dusktimeepoch_t time;
|
dusktimeepoch_t time;
|
||||||
@@ -26,7 +25,6 @@ typedef struct saveslot_s {
|
|||||||
|
|
||||||
saveslotcache_t cachedData;
|
saveslotcache_t cachedData;
|
||||||
} saveslot_t;
|
} saveslot_t;
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inits the save slot with the default state, this is functionally "new game"
|
* Inits the save slot with the default state, this is functionally "new game"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "save/savedevicedolphincard.h"
|
#include "save/savedevicedolphincard.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
|
||||||
errorret_t saveDeviceDolphinCardInit(savedevice_t *device) {
|
errorret_t saveDeviceDolphinCardInit(savedevice_t *device) {
|
||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
@@ -128,6 +129,159 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) {
|
|||||||
return usedBlocks < (uint32_t)blockCount;
|
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) {
|
errorret_t saveDeviceDolphinCardDispose(savedevice_t *device) {
|
||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
if(device->platform.mounted) {
|
if(device->platform.mounted) {
|
||||||
|
|||||||
@@ -17,6 +17,20 @@
|
|||||||
#define SAVE_DEVICE_DOLPHIN_GAME_CODE "DUSK"
|
#define SAVE_DEVICE_DOLPHIN_GAME_CODE "DUSK"
|
||||||
#endif
|
#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 {
|
typedef struct {
|
||||||
// Set from this device's index into SAVE.devices - device 0 is
|
// Set from this device's index into SAVE.devices - device 0 is
|
||||||
// CARD_SLOTA, device 1 is CARD_SLOTB.
|
// CARD_SLOTA, device 1 is CARD_SLOTB.
|
||||||
@@ -82,3 +96,38 @@ bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel);
|
|||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDolphinCardDispose(savedevice_t *device);
|
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/savedevice.h"
|
||||||
#include "save/savedevicedolphinnand.h"
|
#include "save/savedevicedolphinnand.h"
|
||||||
#include "assert/assert.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) {
|
errorret_t saveDeviceDolphinNandInit(savedevice_t *device) {
|
||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
@@ -62,6 +66,107 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device) {
|
|||||||
saveDeviceFireCallback(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) {
|
errorret_t saveDeviceDolphinNandDispose(savedevice_t *device) {
|
||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
if(device->platform.initialized) {
|
if(device->platform.initialized) {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
#ifndef SAVE_DEVICE_DOLPHIN_NAND_DIR
|
#ifndef SAVE_DEVICE_DOLPHIN_NAND_DIR
|
||||||
#define SAVE_DEVICE_DOLPHIN_NAND_DIR "/title/00010001/44555348/data"
|
#define SAVE_DEVICE_DOLPHIN_NAND_DIR "/title/00010001/44555348/data"
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME
|
||||||
|
#define SAVE_DEVICE_DOLPHIN_NAND_DATA_FILENAME "save.dat"
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool_t initialized;
|
bool_t initialized;
|
||||||
@@ -58,3 +61,36 @@ void saveDeviceDolphinNandCheckAvailability(savedevice_t *device);
|
|||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDolphinNandDispose(savedevice_t *device);
|
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/savedevice.h"
|
||||||
#include "save/savedevicedolphinsd.h"
|
#include "save/savedevicedolphinsd.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
#include <fat.h>
|
#include <fat.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define SAVE_DEVICE_DOLPHIN_SD_PATH_MAX 128
|
||||||
|
|
||||||
errorret_t saveDeviceDolphinSDInit(savedevice_t *device) {
|
errorret_t saveDeviceDolphinSDInit(savedevice_t *device) {
|
||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
@@ -62,6 +67,93 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device) {
|
|||||||
saveDeviceFireCallback(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) {
|
errorret_t saveDeviceDolphinSDDispose(savedevice_t *device) {
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
#ifndef SAVE_DEVICE_DOLPHIN_SD_DIRECTORY
|
#ifndef SAVE_DEVICE_DOLPHIN_SD_DIRECTORY
|
||||||
#define SAVE_DEVICE_DOLPHIN_SD_DIRECTORY "/dusk/save"
|
#define SAVE_DEVICE_DOLPHIN_SD_DIRECTORY "/dusk/save"
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME
|
||||||
|
#define SAVE_DEVICE_DOLPHIN_SD_DATA_FILENAME "save.dat"
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
void *nothing;
|
void *nothing;
|
||||||
@@ -53,3 +56,36 @@ void saveDeviceDolphinSDCheckAvailability(savedevice_t *device);
|
|||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
*/
|
*/
|
||||||
errorret_t saveDeviceDolphinSDDispose(savedevice_t *device);
|
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 \
|
#define saveDeviceCheckAvailabilityPlatform \
|
||||||
saveDeviceDolphinNandCheckAvailability
|
saveDeviceDolphinNandCheckAvailability
|
||||||
#define saveDevicePlatformDispose saveDeviceDolphinNandDispose
|
#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)
|
#elif defined(DUSK_WII) && defined(DUSK_SAVE_WII_METHOD_SD)
|
||||||
#include "savedevicedolphinsd.h"
|
#include "savedevicedolphinsd.h"
|
||||||
#define SAVE_DEVICE_COUNT 1
|
#define SAVE_DEVICE_COUNT 1
|
||||||
@@ -33,6 +39,13 @@
|
|||||||
#define saveDeviceCheckAvailabilityPlatform \
|
#define saveDeviceCheckAvailabilityPlatform \
|
||||||
saveDeviceDolphinSDCheckAvailability
|
saveDeviceDolphinSDCheckAvailability
|
||||||
#define saveDevicePlatformDispose saveDeviceDolphinSDDispose
|
#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
|
#else
|
||||||
#include "savedevicedolphincard.h"
|
#include "savedevicedolphincard.h"
|
||||||
#define SAVE_DEVICE_COUNT 2
|
#define SAVE_DEVICE_COUNT 2
|
||||||
@@ -41,4 +54,11 @@
|
|||||||
#define saveDeviceCheckAvailabilityPlatform \
|
#define saveDeviceCheckAvailabilityPlatform \
|
||||||
saveDeviceDolphinCardCheckAvailability
|
saveDeviceDolphinCardCheckAvailability
|
||||||
#define saveDevicePlatformDispose saveDeviceDolphinCardDispose
|
#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
|
#endif
|
||||||
|
|||||||
@@ -14,3 +14,18 @@
|
|||||||
#define saveDevicePlatformUpdate saveDevicePSPUpdate
|
#define saveDevicePlatformUpdate saveDevicePSPUpdate
|
||||||
#define saveDeviceCheckAvailabilityPlatform saveDevicePSPCheckAvailability
|
#define saveDeviceCheckAvailabilityPlatform saveDevicePSPCheckAvailability
|
||||||
#define saveDevicePlatformDispose saveDevicePSPDispose
|
#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/savedevice.h"
|
||||||
#include "save/savedevicepsp.h"
|
#include "save/savedevicepsp.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
#include <pspiofilemgr.h>
|
#include <pspiofilemgr.h>
|
||||||
|
|
||||||
|
#define SAVE_DEVICE_PSP_PATH_MAX 64
|
||||||
|
|
||||||
errorret_t saveDevicePSPInit(savedevice_t *device) {
|
errorret_t saveDevicePSPInit(savedevice_t *device) {
|
||||||
assertNotNull(device, "device cannot be null");
|
assertNotNull(device, "device cannot be null");
|
||||||
|
|
||||||
@@ -47,6 +51,100 @@ void saveDevicePSPCheckAvailability(savedevice_t *device) {
|
|||||||
saveDeviceFireCallback(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) {
|
errorret_t saveDevicePSPDispose(savedevice_t *device) {
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,39 @@ typedef struct savedevice_s savedevice_t;
|
|||||||
*/
|
*/
|
||||||
errorret_t saveDevicePSPInit(savedevice_t *device);
|
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.
|
* Updates the save device platform.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user