#include "dolphin/card.h" #include #include "../internal.hpp" #include "dolphin/types.h" #include "../card/CardRawFile.hpp" #include "../card/DolphinCardPath.hpp" #include "../logging.hpp" #include "../card/CardGciFolder.hpp" #include "../fs_helper.hpp" namespace { aurora::Module Log("aurora::card"); std::array, 2> CardChannels = {{}}; std::array cardPaths; constexpr uint16_t CARD_SECTOR_SIZE = 8192; const char* GetCardRegion() { switch (aurora::g_gameName[3]) { case 'E': default: return "USA"; case 'P': return "EUR"; case 'J': return "JAP"; } } #define GET_CARD(slot) CardChannels[slot] #define CARD_USE_GCI_FOLDER SelectedFileType == CARD_GCIFOLDER #define CARD_READY(slot) (CARD_USE_GCI_FOLDER ? true : std::filesystem::exists(CardChannels[slot]->cardFilename())) #define CARD_STUB Log.debug("{} is stubbed.", __FUNCTION__); bool Initialized = false; CARDFileType SelectedFileType = CARD_GCIFOLDER; bool UseFastMode = false; void CopyKabuFileHandleToDolphin(const s32 chan, const aurora::card::FileHandle& handle, CARDFileInfo* fileInfo) { fileInfo->chan = chan; fileInfo->fileNo = handle.getFileNo(); fileInfo->offset = handle.getOffset(); } aurora::card::FileHandle CreateKabuFileHandleFromDolphin(const CARDFileInfo* fileInfo) { return aurora::card::FileHandle{static_cast(fileInfo->fileNo), fileInfo->offset}; } std::filesystem::path GetCardFullPath(const std::filesystem::path& path, const aurora::card::ECardSlot slot) { if (path.empty()) return ""; if (CARD_USE_GCI_FOLDER) { return path / GetCardRegion() / (slot == aurora::card::ECardSlot::SlotA ? "Card A" : "Card B"); } else { return path / fmt::format("MemoryCard{}.{}.raw", slot == aurora::card::ECardSlot::SlotA ? "A" : "B", GetCardRegion()); } } } // namespace extern "C" { void CopyKabuStatsToDolphin(const aurora::card::CardStat& kabuStats, CARDStat* stats) { memcpy(stats->fileName, kabuStats.x0_fileName, std::size(kabuStats.x0_fileName)); memcpy(stats->gameName, kabuStats.x28_gameName.data(), kabuStats.x28_gameName.size()); memcpy(stats->company, kabuStats.x2c_company.data(), kabuStats.x2c_company.size()); memcpy(stats->offsetIcon, kabuStats.x44_offsetIcon.data(), kabuStats.x44_offsetIcon.size()); stats->length = kabuStats.x20_length; stats->time = kabuStats.x24_time; stats->bannerFormat = kabuStats.x2e_bannerFormat; stats->iconAddr = kabuStats.x30_iconAddr; stats->iconFormat = kabuStats.x34_iconFormat; stats->iconSpeed = kabuStats.x36_iconSpeed; stats->commentAddr = kabuStats.x38_commentAddr; stats->offsetBanner = kabuStats.x3c_offsetBanner; stats->offsetBannerTlut = kabuStats.x40_offsetBannerTlut; stats->offsetIconTlut = kabuStats.x64_offsetIconTlut; stats->offsetData = kabuStats.x68_offsetData; } void CopyDolphinStatsToKabu(aurora::card::CardStat& kabuStats, const CARDStat* stats) { memcpy(kabuStats.x0_fileName, stats->fileName, std::size(kabuStats.x0_fileName)); memcpy(kabuStats.x28_gameName.data(), stats->gameName, kabuStats.x28_gameName.size()); memcpy(kabuStats.x2c_company.data(), stats->company, kabuStats.x2c_company.size()); memcpy(kabuStats.x44_offsetIcon.data(), stats->offsetIcon, kabuStats.x44_offsetIcon.size()); kabuStats.x20_length = stats->length; kabuStats.x24_time = stats->time; kabuStats.x2e_bannerFormat = stats->bannerFormat; kabuStats.x30_iconAddr = stats->iconAddr; kabuStats.x34_iconFormat = stats->iconFormat; kabuStats.x36_iconSpeed = stats->iconSpeed; kabuStats.x38_commentAddr = stats->commentAddr; kabuStats.x3c_offsetBanner = stats->offsetBanner; kabuStats.x40_offsetBannerTlut = stats->offsetBannerTlut; kabuStats.x64_offsetIconTlut = stats->offsetIconTlut; kabuStats.x68_offsetData = stats->offsetData; } void CARDDetectDolphin(const s32 chan) { if (Initialized) { Log.fatal("CARDDetectDolphin() called after CARDInit()!"); } if (chan == 0 || chan == 1) { cardPaths[chan] = aurora::card::ResolveDolphinCardPath(static_cast(chan), GetCardRegion(), CARD_USE_GCI_FOLDER); if (cardPaths[chan].empty()) { Log.error("Failed to detect Dolphin Card!"); return; } Log.info("Detected Dolphin Card at: {}", fs_path_to_string(cardPaths[chan])); } else { cardPaths[0] = aurora::card::ResolveDolphinCardPath(aurora::card::ECardSlot::SlotA, GetCardRegion(), CARD_USE_GCI_FOLDER); cardPaths[1] = aurora::card::ResolveDolphinCardPath(aurora::card::ECardSlot::SlotB, GetCardRegion(), CARD_USE_GCI_FOLDER); if (cardPaths[0].empty() && cardPaths[1].empty()) { Log.error("Failed to detect Dolphin Card!"); return; } Log.info( "Detected Dolphin Card at: {} and {}", fs_path_to_string(cardPaths[0]), fs_path_to_string(cardPaths[1])); } } void CARDSetBasePath(const char* path, const s32 chan) { if (Initialized) { Log.fatal("CARDSetBasePath() called after CARDInit()!"); } std::filesystem::path filePath(path); if (filePath.has_filename() && !std::filesystem::is_directory(filePath)) { filePath = filePath.remove_filename(); Log.warn("Path supplied a filename, discarding. New Path: {}", fs_path_to_string(filePath)); } if (chan == 0 || chan == 1) { cardPaths[chan] = GetCardFullPath(filePath, static_cast(chan)); } else { cardPaths[0] = GetCardFullPath(filePath, aurora::card::ECardSlot::SlotA); cardPaths[1] = GetCardFullPath(filePath, aurora::card::ECardSlot::SlotB); } } void CARDSetLoadType(CARDFileType type) { SelectedFileType = type; } void CARDInit(const char* game, const char* maker) { if (Initialized) { return; } Initialized = true; Log.info("CARD API Initialized BUILT <{} {}>", __DATE__, __TIME__); for (int i = 0; i < CardChannels.size(); ++i) { switch (SelectedFileType) { case CARD_RAWIMAGE: CardChannels[i] = std::make_unique(); break; case CARD_GCIFOLDER: CardChannels[i] = std::make_unique(); break; } CardChannels[i]->InitCard(game, maker); } std::filesystem::path cardWorkingDir; if (aurora::g_config.configPath != nullptr) cardWorkingDir = reinterpret_cast(aurora::g_config.configPath); else cardWorkingDir = std::filesystem::current_path(); bool loadedCard = false; for (int i = 0; i < 2; ++i) { // use default working directory if no path was supplied for card if (cardPaths[i].empty()) { cardPaths[i] = GetCardFullPath(cardWorkingDir, static_cast(i)); } const auto& curPath = cardPaths[i]; if (std::filesystem::exists(curPath)) { CardChannels[i]->open(curPath); loadedCard = true; Log.info("Loaded GC Card Image: {}", fs_path_to_string(curPath)); } } // create a SlotA card if no cards were loaded if (!loadedCard) { CardChannels[0]->open(cardPaths[0]); CardChannels[0]->format(aurora::card::ECardSlot::SlotA); CardChannels[0]->close(); CardChannels[0]->open(cardPaths[0]); } } void CARDSetGameAndMaker(const s32 chan, const char* game, const char* maker) { if (chan < 0 || chan >= 2) { return; } CardChannels[chan]->setCurrentGame(game); CardChannels[chan]->setCurrentMaker(maker); } // TODO: Investigate if this is necessary. Do some games throw an error if CARD's fast mode can't be use? BOOL CARDGetFastMode() { return UseFastMode; } BOOL CARDSetFastMode(const BOOL enable) { const bool oldFast = UseFastMode; UseFastMode = enable; return oldFast; } s32 CARDCheck(const s32 chan) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); return static_cast(card->getError()); } s32 CARDCheckAsync(const s32 chan, const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); const auto res = static_cast(card->getError()); callback(chan, res); return static_cast(card->getError()); } s32 CARDCheckEx(const s32 chan, s32* xferBytes [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); return static_cast(card->getError()); } s32 CARDCheckExAsync(const s32 chan, s32* xferBytes [[maybe_unused]], const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto& card = GET_CARD(chan); const auto res = static_cast(card->getError()); callback(chan, res); return static_cast(card->getError()); } s32 CARDCreate(const s32 chan, const char* fileName, const u32 size, CARDFileInfo* fileInfo) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); aurora::card::FileHandle handle; auto res = card->createFile(fileName, size, handle); if (res == aurora::card::ECardResult::READY) { CopyKabuFileHandleToDolphin(chan, handle, fileInfo); card->commit(); } else Log.error("Failed to create file: {}", fileName); return static_cast(res); } s32 CARDCreateAsync(const s32 chan, const char* fileName, const u32 size, CARDFileInfo* fileInfo, const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto res = CARDCreate(chan, fileName, size, fileInfo); callback(chan, res); return res; } s32 CARDDelete(const s32 chan, const char* fileName) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); const auto res = card->deleteFile(fileName); if (res != aurora::card::ECardResult::READY) { Log.error("Failed to delete file: {}", fileName); } else { card->commit(); } return static_cast(res); } s32 CARDDeleteAsync(const s32 chan, const char* fileName, const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto res = CARDDelete(chan, fileName); callback(chan, res); return res; } s32 CARDFastDelete(const s32 chan, const s32 fileNo) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); const auto res = card->deleteFile(fileNo); if (res != aurora::card::ECardResult::READY) { Log.error("Failed to delete file at idx: {}", fileNo); } else { card->commit(); } return static_cast(res); } s32 CARDFastDeleteAsync(const s32 chan, const s32 fileNo, const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto res = CARDFastDelete(chan, fileNo); callback(chan, res); return res; } s32 CARDFastOpen(const s32 chan, const s32 fileNo, CARDFileInfo* fileInfo) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); aurora::card::FileHandle handle; const auto res = card->openFile(fileNo, handle); if (res == aurora::card::ECardResult::READY) CopyKabuFileHandleToDolphin(chan, handle, fileInfo); else Log.error("Failed to open file at idx: {}", fileNo); return static_cast(res); } s32 CARDFormat(s32 chan) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); card->format(static_cast(chan)); card->commit(); return CARD_RESULT_READY; } s32 CARDFormatAsync(const s32 chan, const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto res = CARDFormat(chan); callback(chan, res); return res; } s32 CARDFreeBlocks(const s32 chan, s32* byteNotUsed, s32* filesNotUsed) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); card->getFreeBlocks(*byteNotUsed, *filesNotUsed); return CARD_RESULT_READY; } s32 CARDGetAttributes(const s32 chan, const s32 fileNo [[maybe_unused]], u8* attr [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: CARD_STUB return CARD_RESULT_READY; } s32 CARDGetEncoding(const s32 chan, u16* encode) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); card->getEncoding(*encode); return CARD_RESULT_READY; } s32 CARDGetMemSize(const s32 chan, u16* size [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: CARD_STUB return CARD_RESULT_READY; } s32 CARDGetResultCode(const s32 chan) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); return static_cast(card->getError()); } s32 CARDGetSectorSize(const s32 chan, u32* size) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; *size = CARD_SECTOR_SIZE; return CARD_RESULT_READY; } s32 CARDGetSerialNo(const s32 chan, u64* serialNo) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); card->getSerial(*serialNo); return CARD_RESULT_READY; } s32 __CARDGetStatusEx(const s32 chan, const s32 fileNo [[maybe_unused]], CARDDir* dirent [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: CARD_STUB return CARD_RESULT_READY; } s32 CARDGetStatus(const s32 chan, s32 fileNo, CARDStat* stat) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); aurora::card::CardStat kabuStat; const auto res = card->getStatus(fileNo, kabuStat); if (res == aurora::card::ECardResult::READY) CopyKabuStatsToDolphin(kabuStat, stat); else Log.error("Failed to get status of file at idx: {}", fileNo); return static_cast(res); } s32 CARDGetXferredBytes(const s32 chan) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; // TODO: CARD_STUB return CARD_RESULT_READY; } // these two funcs are out of scope for aurora::card. stubbed for now s32 CARDMount(const s32 chan, void* workArea [[maybe_unused]], CARDCallback detachCallback [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } return CARD_RESULT_READY; } s32 CARDMountAsync(const s32 chan, void* workArea [[maybe_unused]], const CARDCallback detachCallback [[maybe_unused]], const CARDCallback attachCallback [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } return CARD_RESULT_READY; } s32 CARDOpen(const s32 chan, const char* fileName, CARDFileInfo* fileInfo) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); aurora::card::FileHandle handle; const auto res = card->openFile(fileName, handle); if (res == aurora::card::ECardResult::READY) CopyKabuFileHandleToDolphin(chan, handle, fileInfo); else Log.error("Failed to open file: {}", fileName); return static_cast(res); } BOOL CARDProbe(const s32 chan) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto& card = GET_CARD(chan); aurora::card::ProbeResults probeData = card->probeCardFile(cardPaths[chan]); return probeData.x0_error != aurora::card::ECardResult::NOCARD ? TRUE : FALSE; } s32 CARDProbeEx(const s32 chan, s32* memSize, s32* sectorSize) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto& card = GET_CARD(chan); // ReSharper disable once CppUseStructuredBinding const aurora::card::ProbeResults probeData = card->probeCardFile(cardPaths[chan]); *memSize = probeData.x4_cardSize; *sectorSize = probeData.x8_sectorSize; return static_cast(probeData.x0_error); } s32 CARDRename(const s32 chan, const char* oldName, const char* newName) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); return static_cast(card->renameFile(oldName, newName)); } s32 CARDRenameAsync(const s32 chan, const char* oldName, const char* newName, const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto res = CARDRename(chan, oldName, newName); callback(chan, res); return res; } s32 CARDSetAttributesAsync(const s32 chan, s32 fileNo [[maybe_unused]], u8 attr [[maybe_unused]], CARDCallback callback [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: CARD_STUB return CARD_RESULT_READY; } s32 CARDSetAttributes(const s32 chan, s32 fileNo [[maybe_unused]], u8 attr [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: CARD_STUB return CARD_RESULT_READY; } s32 CARDSetStatus(const s32 chan, s32 fileNo, const CARDStat* stat) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(chan); aurora::card::CardStat kabuStat; CopyDolphinStatsToKabu(kabuStat, stat); auto res = card->setStatus(fileNo, kabuStat); if (res != aurora::card::ECardResult::READY) { Log.error("Failed to set status of file at idx: {}", fileNo); } else { card->commit(); } return static_cast(res); } s32 __CARDSetStatusEx(const s32 chan, s32 fileNo [[maybe_unused]], CARDDir* dirent [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: CARD_STUB return CARD_RESULT_READY; } s32 CARDSetStatusAsync(const s32 chan, const s32 fileNo, const CARDStat* stat, const CARDCallback callback) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } const auto res = CARDSetStatus(chan, fileNo, stat); callback(chan, res); return res; } s32 CARDUnmount(const s32 chan) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: return CARD_RESULT_NOCARD; } s32 CARDGetCurrentMode(const s32 chan, u32* mode [[maybe_unused]]) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } // TODO: return CARD_RESULT_NOCARD; } s32 CARDCancel(CARDFileInfo* fileInfo [[maybe_unused]]) { // TODO:ge return CARD_RESULT_NOCARD; } s32 CARDClose(CARDFileInfo* fileInfo) { if (fileInfo->chan < 0 || fileInfo->chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(fileInfo->chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(fileInfo->chan); auto handle = CreateKabuFileHandleFromDolphin(fileInfo); const auto res = card->closeFile(handle); if (res != aurora::card::ECardResult::READY) Log.error("Failed to close file at idx: {}", fileInfo->fileNo); return static_cast(res); } s32 CARDRead(const CARDFileInfo* fileInfo, void* addr, s32 length, const s32 offset) { if (fileInfo->chan < 0 || fileInfo->chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(fileInfo->chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(fileInfo->chan); aurora::card::FileHandle handle = CreateKabuFileHandleFromDolphin(fileInfo); card->seek(handle, offset, aurora::card::SeekOrigin::Begin); auto res = card->fileRead(handle, addr, length); if (res != aurora::card::ECardResult::READY) Log.error("Failed to read {} bytes from card", length); return static_cast(res); } s32 CARDReadAsync(const CARDFileInfo* fileInfo, void* addr, const s32 length, const s32 offset, const CARDCallback callback) { const auto res = CARDRead(fileInfo, addr, length, offset); callback(fileInfo->chan, res); return res; } s32 CARDWrite(const CARDFileInfo* fileInfo, const void* addr, const s32 length, const s32 offset) { if (fileInfo->chan < 0 || fileInfo->chan >= 2) { return CARD_RESULT_FATAL_ERROR; } if (!CARD_READY(fileInfo->chan)) return CARD_RESULT_NOCARD; const auto& card = GET_CARD(fileInfo->chan); aurora::card::FileHandle handle = CreateKabuFileHandleFromDolphin(fileInfo); card->seek(handle, offset, aurora::card::SeekOrigin::Begin); auto res = card->fileWrite(handle, addr, length); if (res != aurora::card::ECardResult::READY) { Log.error("Failed to write {} bytes to card", length); } else { card->commit(); } return static_cast(res); } s32 CARDWriteAsync(const CARDFileInfo* fileInfo, const void* addr, const s32 length, const s32 offset, const CARDCallback callback) { const auto res = CARDWrite(fileInfo, addr, length, offset); callback(fileInfo->chan, res); return res; } }