Set path of card image before CARDInit (#87)

* add ability to set path of card image before CARDInit

* add missing func to osx/linux variant of ResolveDolphinCardPath

* remove format specifiers in formatting funcs

* missed some oops

* refactor card loading to allow loading slot A and B if present

if neither slots have card images, a slot A image will be created at the save directory
this commit also changes card image names to "MemoryCardA/B.USA.raw" to fit with dolphins naming conventions (region code will always be USA unless changed in the macro, which could eventually be changed to be something in aurora config)

* only remove file name from CARDSetBasePath if not a directory
This commit is contained in:
CraftyBoss
2026-04-05 23:14:15 -07:00
committed by GitHub
parent 50163edfe7
commit 5a23bcbd5e
5 changed files with 248 additions and 13 deletions
+1
View File
@@ -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
+3
View File
@@ -1,6 +1,7 @@
#ifndef _DOLPHIN_CARD_H_
#define _DOLPHIN_CARD_H_
#include <string_view>
#include <dolphin/os.h>
#include <dolphin/dsp.h>
#include <dolphin/dvd.h>
@@ -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
+144
View File
@@ -0,0 +1,144 @@
#include <filesystem>
#include "DolphinCardPath.hpp"
#if WIN32
#include "windows.h"
#include "winreg.h"
#include "shlobj_core.h"
#endif
#include <SDL3/SDL_filesystem.h>
#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<std::string> 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
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include <optional>
#include <string>
#include "Constants.hpp"
namespace aurora::card {
std::string ResolveDolphinCardPath(ECardSlot slot, const char* regionCode, bool isGciFolder);
}
+91 -13
View File
@@ -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<aurora::card::Card, 2> CardChannels = {{
aurora::card::Card{nullptr, nullptr},
aurora::card::Card{nullptr, nullptr},
}};
std::string cardPath;
std::array<std::string, 2> 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;