/** * Copyright (c) 2026 Dominic Masters * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ #include "save/savedevice.h" #include "save/savedevicedolphincard.h" #include "save/savemanager.h" #include "assert/assert.h" errorret_t saveDeviceDolphinCardInit(savedevice_t *device) { assertNotNull(device, "device cannot be null"); device->state = SAVE_DEVICE_STATE_UNKNOWN; // Each savedevice_t in SAVE_MANAGER.devices maps to one physical memory // card slot - device 0 is slot A, device 1 is slot B, determined by this // device's position in that array (the same index math savemanager.c // itself uses to identify a device). uint8_t index = (uint8_t)(device - &SAVE_MANAGER.devices[0]); device->platform.channel = index == 0 ? CARD_SLOTA : CARD_SLOTB; errorOk(); } errorret_t saveDeviceDolphinCardUpdate(savedevice_t *device) { assertNotNull(device, "device cannot be null"); assertIsMainThread("Invalid thread"); errorOk(); } void saveDeviceDolphinCardCheckAvailability(savedevice_t *device) { assertNotNull(device, "device cannot be null"); assertIsMainThread("Invalid thread"); assertTrue( device->state == SAVE_DEVICE_STATE_CHECKING_AVAILABILITY, "Device state incorrect?" ); savedeviceplatform_t *platform = &device->platform; // CARD_Init 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) - safe to call more than once, and cheap // enough to just always call before every mount attempt. int32_t result = CARD_Init(SAVE_DEVICE_DOLPHIN_GAME_CODE, NULL); if(result < 0) { device->state = SAVE_DEVICE_STATE_UNAVAILABLE; device->reasonKey = "save.dolphin.card_init_failed"; return saveDeviceFireCallback(device); } do { result = CARD_Mount(platform->channel, platform->cardBuffer, NULL); } while(result == CARD_ERROR_BUSY); if(result < 0) { platform->mounted = false; device->state = SAVE_DEVICE_STATE_UNAVAILABLE; switch(result) { case CARD_ERROR_NOCARD: device->reasonKey = "save.dolphin.no_card"; break; case CARD_ERROR_WRONGDEVICE: device->reasonKey = "save.dolphin.wrong_device"; break; case CARD_ERROR_BROKEN: device->reasonKey = "save.dolphin.broken"; break; default: device->reasonKey = "save.dolphin.mount_failed"; break; } return saveDeviceFireCallback(device); } platform->mounted = true; if(!saveDeviceDolphinCardHasFreeSpace(platform->channel)) { device->state = SAVE_DEVICE_STATE_UNAVAILABLE; device->reasonKey = "save.dolphin.card_full"; return saveDeviceFireCallback(device); } device->state = SAVE_DEVICE_STATE_AVAILABLE; device->reasonKey = NULL; saveDeviceFireCallback(device); } bool_t saveDeviceDolphinCardHasFreeSpace(const int32_t channel) { uint16_t blockCount = 0; if(CARD_GetBlockCount(channel, &blockCount) < 0) return false; uint32_t sectorSize = 0; if(CARD_GetSectorSize(channel, §orSize) < 0 || sectorSize == 0) { return false; } // showall=true - free space is a card-wide resource, not just what this // game itself has already written. card_dir entries[CARD_MAXFILES]; int32_t count = 0; if(CARD_GetDirectory(channel, entries, &count, true) < 0) return false; if(count >= CARD_MAXFILES) return false;// No free file entry left either. uint32_t usedBlocks = 0; for(int32_t i = 0; i < count; i++) { usedBlocks += (entries[i].filelen + sectorSize - 1) / sectorSize; } return usedBlocks < (uint32_t)blockCount; } errorret_t saveDeviceDolphinCardDispose(savedevice_t *device) { assertNotNull(device, "device cannot be null"); if(device->platform.mounted) { CARD_Unmount(device->platform.channel); device->platform.mounted = false; } errorOk(); }