Files
dusk/src/duskdolphin/save/savedolphin.c
T
YourWishes aa0180571e Unify save system into one save.h/.c; diverge storage format per platform
Renames savefile_t to saveslot_t and folds last session's standalone
settings.h/.c module back in as savemeta_t, so there's one save system
(SAVE.slots[] + SAVE.meta) instead of two parallel ones - while letting
each platform pick its own physical format for the two concepts:

- Linux now writes human-editable JSON (slot0.json, settings.json, ...)
  via yyjson's mutable writer API, so players can hand-fix a bad setting.
- PSP folds meta into the same sceUtilitySavedata binary payload as its
  one save slot (SAVE_SLOT_COUNT_MAX=1 there - a future save picker will
  let players manage multiple named saves via the OS's own browser).
- GameCube consolidates the 3 per-slot memory card files and the separate
  settings file into one combined card file.

Also fixes two bugs surfaced while building this: the CRC finalize step
seeked to a hardcoded offset (only safe for one section per file, breaks
once meta+slots share a buffer), and save.c's async/sync dispatch left an
unconditional fallback call that doesn't exist on PSP-only platforms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 18:35:06 -05:00

169 lines
5.1 KiB
C

/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "save/savestream.h"
#include "util/memory.h"
#include "util/string.h"
errorret_t saveInitDolphin(void) {
SAVE.platform.mounted = false;
// Must run once before any other CARD_* call: sets up card_inited,
// the per-channel control blocks (wait queues, alarms) CARD_Mount reads,
// and initializes the DSP (needed for the card unlock sequence).
// Skipping this leaves those structures unset, so CARD_Mount ends up
// touching hardware state that was never brought up -- e.g. Dolphin's
// "Trying to read 32 bits from an invalid MMIO" error -- rather than
// failing cleanly with a CARD_ERROR_* code.
int32_t result = CARD_Init(SAVE_DOLPHIN_GAME_CODE, NULL);
if(result < 0) {
errorThrow("Failed to initialize memory card subsystem: %s (%d)",
saveCardErrorStringDolphin(result), result
);
}
do {
result = CARD_Mount(
SAVE_DOLPHIN_CHANNEL,
SAVE.platform.cardBuffer,
NULL
);
} while(result == CARD_ERROR_BUSY);
// Special-case the failures a player can actually act on; everything
// else falls through to the generic, fully-enumerated message below.
switch(result) {
case CARD_ERROR_NOCARD:
errorThrow("No memory card inserted in the slot");
case CARD_ERROR_WRONGDEVICE:
errorThrow("Unsupported device inserted in the memory card slot");
case CARD_ERROR_BROKEN:
errorThrow("Memory card is damaged or unformatted");
default:
break;
}
if(result < 0) {
errorThrow("Failed to mount memory card: %s (%d)",
saveCardErrorStringDolphin(result), result
);
}
SAVE.platform.mounted = true;
errorOk();
}
errorret_t saveDisposeDolphin(void) {
if(SAVE.platform.mounted) {
CARD_Unmount(SAVE_DOLPHIN_CHANNEL);
SAVE.platform.mounted = false;
}
errorOk();
}
errorret_t saveCombinedLoadDolphin(void) {
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
errorret_t openRet = saveStreamOpenReadPlatform(&stream);
SAVE.available = errorIsOk(openRet);
errorChain(openRet);
if(!stream.found) errorOk();
errorret_t ret = saveMetaSerializeRead(&stream, &SAVE.meta);
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
ret = saveSlotSerializeRead(&stream, &SAVE.slots[i]);
}
#ifdef saveStreamClosePlatform
saveStreamClosePlatform(&stream);
#endif
errorChain(ret);
errorOk();
}
errorret_t saveCombinedWriteDolphin(void) {
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
errorret_t openRet = saveStreamOpenWritePlatform(&stream);
SAVE.available = errorIsOk(openRet);
errorChain(openRet);
errorret_t ret = saveMetaSerializeWrite(&stream, &SAVE.meta);
for(uint8_t i = 0; errorIsOk(ret) && i < SAVE_SLOT_COUNT_MAX; i++) {
ret = saveSlotSerializeWrite(&stream, &SAVE.slots[i]);
}
#ifdef saveStreamClosePlatform
saveStreamClosePlatform(&stream);
#endif
errorChain(ret);
errorOk();
}
errorret_t saveSlotLoadDolphin(const uint8_t slot, saveslot_t *out) {
(void)slot;
(void)out;
return saveCombinedLoadDolphin();
}
errorret_t saveSlotWriteDolphin(const uint8_t slot, saveslot_t *slotData) {
(void)slot;
(void)slotData;
return saveCombinedWriteDolphin();
}
errorret_t saveSlotDeleteDolphin(const uint8_t slot) {
memoryZero(&SAVE.slots[slot], sizeof(saveslot_t));
SAVE.slots[slot].exists = false;
return saveCombinedWriteDolphin();
}
errorret_t saveMetaLoadDolphin(savemeta_t *out) {
(void)out;
return saveCombinedLoadDolphin();
}
errorret_t saveMetaWriteDolphin(savemeta_t *meta) {
(void)meta;
return saveCombinedWriteDolphin();
}
const char_t *saveCardErrorStringDolphin(const int32_t result) {
switch(result) {
case CARD_ERROR_READY: return "card is ready";
case CARD_ERROR_UNLOCKED:
return "card is being unlocked or already unlocked";
case CARD_ERROR_BUSY: return "card is busy";
case CARD_ERROR_WRONGDEVICE: return "wrong device connected in slot";
case CARD_ERROR_NOCARD: return "no memory card in slot";
case CARD_ERROR_NOFILE: return "specified file not found";
case CARD_ERROR_IOERROR: return "internal EXI I/O error";
case CARD_ERROR_BROKEN:
return "directory structure or file entry broken";
case CARD_ERROR_EXIST:
return "file already exists with the specified parameters";
case CARD_ERROR_NOENT:
return "no empty block available to create the file";
case CARD_ERROR_INSSPACE:
return "not enough space to write file to memory card";
case CARD_ERROR_NOPERM:
return "not enough permissions to operate on the file";
case CARD_ERROR_LIMIT: return "card size limit reached";
case CARD_ERROR_NAMETOOLONG: return "filename too long";
case CARD_ERROR_ENCODING: return "font encoding PAL/SJIS mismatch";
case CARD_ERROR_CANCELED: return "card operation canceled";
case CARD_ERROR_FATAL_ERROR: return "fatal error, non-recoverable";
default: return "unknown card error";
}
}