diff --git a/cmake/aurora_card.cmake b/cmake/aurora_card.cmake index 07f2d78..99eb066 100644 --- a/cmake/aurora_card.cmake +++ b/cmake/aurora_card.cmake @@ -2,6 +2,7 @@ add_library(aurora_card STATIC lib/card/BlockAllocationTable.cpp lib/card/Card.cpp lib/card/Directory.cpp + lib/card/DolphinCardPath.cpp lib/card/File.cpp lib/card/FileIO.cpp lib/card/SRAM.cpp diff --git a/include/dolphin/card.h b/include/dolphin/card.h index 9a0785d..016ada4 100644 --- a/include/dolphin/card.h +++ b/include/dolphin/card.h @@ -1,6 +1,7 @@ #ifndef _DOLPHIN_CARD_H_ #define _DOLPHIN_CARD_H_ +#include #include #include #include @@ -234,6 +235,8 @@ s32 CARDRenameAsync(s32 chan, const char* oldName, const char* newName, CARDCall #if TARGET_PC void CARDInit(const char* game, const char* maker); void CARDSetGameAndMaker(const s32 chan, const char* game, const char* maker); +void CARDDetectDolphin(u32 slot = -1); +void CARDSetBasePath(const std::string_view& path, u32 slot = -1); #else void CARDInit(void); #endif diff --git a/lib/card/DolphinCardPath.cpp b/lib/card/DolphinCardPath.cpp new file mode 100644 index 0000000..2c2c9df --- /dev/null +++ b/lib/card/DolphinCardPath.cpp @@ -0,0 +1,144 @@ +#include + +#include "DolphinCardPath.hpp" + +#if WIN32 +#include "windows.h" +#include "winreg.h" +#include "shlobj_core.h" +#endif + +#include + +#include "../internal.hpp" + +#if WIN32 +namespace { + std::string ConvertWideToANSI(const std::wstring& wstr) { + int count = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), wstr.length(), NULL, 0, NULL, NULL); + std::string str(count, 0); + WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &str[0], count, NULL, NULL); + return str; + } +} +#endif + +namespace aurora::card { +aurora::Module Log("aurora::card"); + +#if WIN32 + +#if WINDOWS_STORE +using namespace Windows::Storage; +#endif + +std::string ResolveDolphinCardPath(ECardSlot slot, const char* regionCode, bool isGciFolder) { +#if !WINDOWS_STORE + /* Detect where the User directory is. There are two different cases + * 1. HKCU\Software\Dolphin Emulator\UserConfigPath exists + * -> Use this as the user directory path + * 2. My Documents exists + * -> Use My Documents\Dolphin Emulator as the User directory path + */ + + /* Check our registry keys */ + HKEY hkey; + wchar_t configPath[MAX_PATH] = {0}; + if (RegOpenKeyEx(HKEY_CURRENT_USER, TEXT("Software\\Dolphin Emulator"), 0, KEY_QUERY_VALUE, &hkey) == ERROR_SUCCESS) { + DWORD size = MAX_PATH; + if (RegQueryValueEx(hkey, TEXT("UserConfigPath"), nullptr, nullptr, (LPBYTE)configPath, &size) != ERROR_SUCCESS) + configPath[0] = 0; + RegCloseKey(hkey); + } + + /* Get My Documents path in case we need it. */ + PWSTR my_documents; + bool my_documents_found = + SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &my_documents)); + + std::wstring w_path; + if (configPath[0]) /* Case 1 */ + w_path = configPath; + else if (my_documents_found) /* Case 2 */ { + w_path = my_documents; + w_path += L"\\Dolphin Emulator"; + } + else /* Unable to find */ { + Log.error("Unable to find Dolphin Emulator user directory!"); + return {}; + } +#else + StorageFolder ^ localFolder = ApplicationData::Current->LocalFolder; + std::string path(localFolder->Path->Data()); +#endif + + std::string path; + if (isGciFolder) { + path = fmt::format("{}/GC/{}/Card {}", ConvertWideToANSI(w_path), regionCode, slot == ECardSlot::SlotA ? 'A' : 'B'); + }else { + path = fmt::format("{}/GC/MemoryCard{}.{}.raw", ConvertWideToANSI(w_path), slot == ECardSlot::SlotA ? 'A' : 'B', regionCode); + } + + if (!std::filesystem::exists(path)) { + Log.error("Unable to find Dolphin Card file! Search Path: {}", path); + return {}; + } + + return path; +} +#else +static std::optional GetPrefPath(const char* app) { + char* path = SDL_GetPrefPath(nullptr, app); + if (path == nullptr) { + return {}; + } + std::string str{path}; + SDL_free(path); + return str; +} + +std::string ResolveDolphinCardPath(ECardSlot slot, const char* regionCode, bool isGciFolder) { + const auto dolphinPath = GetPrefPath("dolphin-emu"); + if (!dolphinPath) { + return {}; + } + auto path = *dolphinPath; + if (isGciFolder) { + path += fmt::format("GC/{}/Card {}", regionCode, slot == ECardSlot::SlotA ? 'A' : 'B'); + }else { + path += fmt::format("GC/MemoryCard{}.{}.raw", slot == ECardSlot::SlotA ? 'A' : 'B', regionCode); + } + + if (!std::filesystem::exists(path)) { + /* legacy case for older dolphin versions */ + const char* home = getenv("HOME"); + if (home == nullptr || home[0] != '/') { + return {}; + } + + path = home; +#ifndef __APPLE__ + if (isGciFolder) { + path += fmt::format("/.dolphin-emu/GC/{}/Card {}", + regionCode, slot == ECardSlot::SlotA ? 'A' : 'B'); + }else { + path += fmt::format("/.dolphin-emu/GC/MemoryCard{}.{}.raw", + slot == ECardSlot::SlotA ? 'A' : 'B', regionCode); + } +#else + if (isGciFolder) { + path += fmt::format("/Library/Application Support/Dolphin/GC/{}/Card {}", home, slot == ECardSlot::SlotA ? 'A' : 'B', regionCode); + }else { + path += fmt::format("/Library/Application Support/Dolphin/GC/MemoryCard{}.{}.raw", home, slot == ECardSlot::SlotA ? 'A' : 'B', regionCode); + } +#endif + if (!std::filesystem::exists(path)) { + return {}; + } + } + + return path; +} +#endif + +} \ No newline at end of file diff --git a/lib/card/DolphinCardPath.hpp b/lib/card/DolphinCardPath.hpp new file mode 100644 index 0000000..2a45182 --- /dev/null +++ b/lib/card/DolphinCardPath.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +#include "Constants.hpp" + +namespace aurora::card { +std::string ResolveDolphinCardPath(ECardSlot slot, const char* regionCode, bool isGciFolder); +} \ No newline at end of file diff --git a/lib/dolphin/card.cpp b/lib/dolphin/card.cpp index 9ac1263..bb035fe 100644 --- a/lib/dolphin/card.cpp +++ b/lib/dolphin/card.cpp @@ -5,6 +5,7 @@ #include "dolphin/types.h" #include "../card/Card.hpp" +#include "../card/DolphinCardPath.hpp" #include "../logging.hpp" namespace { @@ -13,13 +14,16 @@ std::array CardChannels = {{ aurora::card::Card{nullptr, nullptr}, aurora::card::Card{nullptr, nullptr}, }}; -std::string cardPath; +std::array cardPaths; #define GET_CARD(slot) &CardChannels[slot] -#define CARD_READY(slot) std::filesystem::exists(cardPath) +#define CARD_READY(slot) std::filesystem::exists(CardChannels[slot].cardFilename()) #define CARD_STUB Log.debug("{} is stubbed.", __FUNCTION__); +#define CARD_REGION "USA" + bool Initialized = false; +bool UseGciFolder = false; } // namespace extern "C" { @@ -72,6 +76,63 @@ void CopyDolphinStatsToKabu(aurora::card::CardStat& kabuStats, CARDStat* stats) kabuStats.x68_offsetData = stats->offsetData; } +std::string GetCardFullPath(const std::string& path, aurora::card::ECardSlot slot) { + if (path.empty()) + return ""; + + if (UseGciFolder) { + // TODO: probably not this + return fmt::format("{}/{}/Card {}", path, CARD_REGION, slot == aurora::card::ECardSlot::SlotA ? "A" : "B"); + }else { + return fmt::format("{}/MemoryCard{}.{}.raw", path, slot == aurora::card::ECardSlot::SlotA ? "A" : "B", CARD_REGION); + } +} + +void CARDDetectDolphin(u32 slot) { + if (Initialized) { + Log.fatal("CARDDetectDolphin() called after CARDInit()!"); + } + + if (slot == 0 || slot == 1) { + cardPaths[slot] = aurora::card::ResolveDolphinCardPath((aurora::card::ECardSlot)slot, CARD_REGION, UseGciFolder); + if (cardPaths[slot].empty()) { + Log.error("Failed to detect Dolphin Card!"); + return; + } + Log.info("Detected Dolphin Card at: {}", cardPaths[slot]); + }else { + cardPaths[0] = aurora::card::ResolveDolphinCardPath(aurora::card::ECardSlot::SlotA, CARD_REGION, UseGciFolder); + cardPaths[1] = aurora::card::ResolveDolphinCardPath(aurora::card::ECardSlot::SlotB, CARD_REGION, UseGciFolder); + + if (cardPaths[0].empty() && cardPaths[1].empty()) { + Log.error("Failed to detect Dolphin Card!"); + return; + } + + Log.info("Detected Dolphin Card at: {} and {}", cardPaths[0], cardPaths[1]); + } +} + +void CARDSetBasePath(const std::string_view& path, u32 slot) { + 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: {}", filePath.string()); + } + + if (slot == 0 || slot == 1) { + cardPaths[slot] = GetCardFullPath(filePath.string(), (aurora::card::ECardSlot)slot); + }else { + cardPaths[0] = GetCardFullPath(filePath.string(), aurora::card::ECardSlot::SlotA); + cardPaths[1] = GetCardFullPath(filePath.string(), aurora::card::ECardSlot::SlotB); + } +} + void CARDInit(const char* game, const char* maker) { if (Initialized) { return; @@ -82,18 +143,35 @@ void CARDInit(const char* game, const char* maker) { CardChannels[0] = aurora::card::Card{game, maker}; CardChannels[1] = aurora::card::Card{game, maker}; - cardPath = fmt::format("{}{}.raw", game, maker); + std::string cardWorkingDir; + if (aurora::g_config.configPath != nullptr) + cardWorkingDir = aurora::g_config.configPath; + else + cardWorkingDir = std::filesystem::current_path().string(); - // TODO: SlotB support - if (!std::filesystem::exists(cardPath)) { - CardChannels[0].open(cardPath); - CardChannels[0].format(aurora::card::ECardSlot::SlotA); - CardChannels[0].commit(); - } else { - CardChannels[0].open(cardPath); + 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, (aurora::card::ECardSlot)i); + } + + const auto& curPath = cardPaths[i]; + + if (std::filesystem::exists(curPath)) { + CardChannels[i].open(curPath); + loadedCard = true; + Log.info("Loaded GC Card Image: {}", curPath); + } } - Log.info("Loaded GC Card Disk: {}", cardPath); + // 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].commit(); + } } void CARDSetGameAndMaker(const s32 chan, const char* game, const char* maker) { @@ -425,7 +503,7 @@ BOOL CARDProbe(s32 chan) { if (chan < 0 || chan >= 2) { return CARD_RESULT_FATAL_ERROR; } - aurora::card::ProbeResults probeData = aurora::card::Card::probeCardFile(cardPath); + aurora::card::ProbeResults probeData = aurora::card::Card::probeCardFile(cardPaths[chan]); return (s32)probeData.x0_error; } @@ -434,7 +512,7 @@ s32 CARDProbeEx(s32 chan, s32* memSize, s32* sectorSize) { return CARD_RESULT_FATAL_ERROR; } - aurora::card::ProbeResults probeData = aurora::card::Card::probeCardFile(cardPath); + aurora::card::ProbeResults probeData = aurora::card::Card::probeCardFile(cardPaths[chan]); *memSize = probeData.x4_cardSize; *sectorSize = probeData.x8_sectorSize;