memory_card: added support for .vgs, .gme, .vmp memory card formats

Load memory cards by drag&drop
Better parsing of memory card contents
Card formatting
This commit is contained in:
Jakub Czekański 2021-11-16 01:49:26 +01:00
parent 0c457b2665
commit b51c2b8a64
14 changed files with 468 additions and 67 deletions

View file

@ -247,6 +247,7 @@ add_library(core STATIC
src/disc/position.cpp
src/disc/subchannel_q.cpp
src/input/input_manager.cpp
src/memory_card/card_formats.cpp
src/sound/adpcm.cpp
src/sound/tables.cpp
src/sound/wave.cpp

View file

@ -16,6 +16,8 @@ See [Avocado compatibility list](https://avocado-db.czekanski.info)
## Changelog
*16.11.2021* - .vgs, .gme, .vmp memory card format support, load memory cards by drag&drop
*28.06.2020* - .ecm format support
*16.09.2019* - Save states

View file

@ -149,6 +149,9 @@ uint8_t MemoryCard::handleWrite(uint8_t byte) {
flag.fresh = 0;
state = 0;
command = Command::None;
bus.notify(Event::Controller::MemoryCardContentsChanged{port});
return static_cast<uint8_t>(writeStatus);
default:

View file

@ -38,5 +38,7 @@ struct MemoryCard : public AbstractDevice {
MemoryCard(int port);
uint8_t handle(uint8_t byte) override;
void setFresh() { flag.fresh = true; }
};
} // namespace peripherals

View file

@ -0,0 +1,262 @@
#include "card_formats.h"
#include "utils/file.h"
#include "fmt/core.h"
#include <algorithm>
namespace memory_card {
// Thanks ShendoXT - https://github.com/ShendoXT/memcardrex
const std::array<std::string, 9> rawFormats = {
// Raw 128kB images
"psm", // SmartLink
"ps", // WinPSM
"ddf", // DataDeck
"mcr", // FPSX
"mcd", // ePSXe
"mc", // PSXGame Edit
"mci", // MCExplorer
"srm", // PCSX ReARMed/RetroArch
"vm1", // PS3 virtual card
};
const std::array<std::string, 4> complexFormats = {
"mem",
"vgs", // Connectix Virtual Game Station
"gme", // DexDrive
"vmp", // PSP/Vita
};
const std::array<std::string, 8> saveFileFormats = {
"mcs", // PSXGameEdit single save
"psv", // PS3 signed save
"psx", // XP, AR, GS, Caetla single save
"ps1", // Memory Juggler
"mcb", // Smart Link
"mcx",
"pda", // Datel
"B???????????*", // RAW single save
};
bool isMemoryCardImage(const std::string& path) {
std::string ext = getExtension(path);
transform(ext.begin(), ext.end(), ext.begin(), tolower);
if (std::find(rawFormats.begin(), rawFormats.end(), ext) != rawFormats.end()) {
return true;
}
if (std::find(complexFormats.begin(), complexFormats.end(), ext) != complexFormats.end()) {
return true;
}
return false;
}
constexpr int VGS_HEADER_SIZE = 64;
constexpr int GME_HEADER_SIZE = 3904;
constexpr int VMP_HEADER_SIZE = 128;
std::optional<std::vector<uint8_t>> load(const std::string& path) {
std::string ext = getExtension(path);
transform(ext.begin(), ext.end(), ext.begin(), tolower);
auto raw = getFileContents(path);
int offset = 0;
if (ext == "raw" || ext == "ps" || ext == "ddf" || ext == "mcr" || ext == "mcd") {
if (memcmp("MC", raw.data(), 2) != 0) {
fmt::print("[memory_card] Raw memory card image - Invalid header (expected MC).\n");
return {};
}
} else if (ext == "mem" || ext == "vgs") {
if (memcmp("VgsM", raw.data(), 4) != 0) {
fmt::print("[memory_card] VGS image - Invalid header (expected VgsM).\n");
return {};
}
offset = VGS_HEADER_SIZE;
} else if (ext == "gme") {
if (memcmp("123-456-STD", raw.data(), 11) != 0) {
fmt::print("[memory_card] DexDrive image - Invalid header (expected 123-456-STD).\n");
return {};
}
offset = GME_HEADER_SIZE;
} else if (ext == "vmp") {
if (memcmp("\0PMV", raw.data(), 4) != 0) {
fmt::print("[memory_card] PSP/Vita image - Invalid header (expected \\0PMV).\n");
return {};
}
offset = VMP_HEADER_SIZE;
} else {
fmt::print("Unsupported memory card image format {}, please report it here https://github.com/JaCzekanski/Avocado/issues/new\n",
ext);
return {};
}
std::vector<uint8_t> memory(MEMCARD_SIZE, 0);
int size = std::min(MEMCARD_SIZE, (int)raw.size() - offset);
std::copy(raw.begin() + offset, raw.begin() + offset + size, memory.begin());
return memory;
}
bool saveRaw(const std::array<uint8_t, MEMCARD_SIZE>& data, const std::string& path) {
auto output = std::vector<uint8_t>(data.begin(), data.end());
if (!putFileContents(path, output)) {
return false;
}
return true;
}
bool saveVgs(const std::array<uint8_t, MEMCARD_SIZE>& data, const std::string& path) {
auto output = std::vector<uint8_t>(VGS_HEADER_SIZE + MEMCARD_SIZE);
output[0] = 'V';
output[1] = 'g';
output[2] = 's';
output[3] = 'M';
output[4] = 1;
output[8] = 1;
output[12] = 1;
output[17] = 2;
std::copy(data.begin(), data.end(), output.begin() + VGS_HEADER_SIZE);
if (!putFileContents(path, output)) {
return false;
}
return true;
}
bool saveGme(const std::array<uint8_t, MEMCARD_SIZE>& data, const std::string& path) {
auto output = std::vector<uint8_t>(GME_HEADER_SIZE + MEMCARD_SIZE);
output[0] = '1';
output[1] = '2';
output[2] = '3';
output[3] = '-';
output[4] = '4';
output[5] = '5';
output[6] = '6';
output[7] = '-';
output[8] = 'S';
output[9] = 'T';
output[10] = 'D';
output[18] = 0x1;
output[20] = 0x1;
output[21] = 'M';
for (int slot = 0; slot < 15; slot++) {
int ptr = (slot + 1) * 0x80;
output[22 + slot] = data[ptr + 0];
output[38 + slot] = data[ptr + 8];
}
// Comments are skipped
std::copy(data.begin(), data.end(), output.begin() + GME_HEADER_SIZE);
if (!putFileContents(path, output)) {
return false;
}
return true;
}
bool saveVmp(const std::array<uint8_t, MEMCARD_SIZE>& data, const std::string& path) {
auto output = std::vector<uint8_t>(VMP_HEADER_SIZE + MEMCARD_SIZE);
output[0] = 0;
output[1] = 'P';
output[2] = 'M';
output[3] = 'V';
output[4] = 0x80;
// offset: 0x0c, length: 0x14 - key seed, aes 128 cbc
// offset: 0x20, length: 0x14 - sha1 hmac
std::copy(data.begin(), data.end(), output.begin() + VMP_HEADER_SIZE);
if (!putFileContents(path, output)) {
return false;
}
return true;
}
bool save(const std::array<uint8_t, MEMCARD_SIZE>& data, const std::string& path) {
std::string ext = getExtension(path);
transform(ext.begin(), ext.end(), ext.begin(), tolower);
if (ext == "raw" || ext == "ps" || ext == "ddf" || ext == "mcr" || ext == "mcd") {
return saveRaw(data, path);
} else if (ext == "mem" || ext == "vgs") {
return saveVgs(data, path);
} else if (ext == "gme") {
return saveGme(data, path);
} else if (ext == "vmp") {
return saveVmp(data, path);
} else {
fmt::print("Unsupported memory card image format {}, please report it here https://github.com/JaCzekanski/Avocado/issues/new\n",
ext);
return false;
}
}
void format(std::array<uint8_t, MEMCARD_SIZE>& data) {
std::array<uint8_t, 0x80> frame;
auto writeFrame = [&](int frameNum, bool withChecksum = false) {
if (withChecksum) {
uint8_t checksum = 0;
for (int i = 0; i < 0x7f; i++) {
checksum ^= frame[i];
}
frame[0x7f] = checksum;
}
int offset = frameNum * 128;
for (int i = 0; i < 0x80; i++) {
data[offset + i] = frame[i];
}
};
data.fill(0);
frame.fill(0);
frame[0] = 'M';
frame[1] = 'C';
writeFrame(0, true);
writeFrame(63, true);
// Directory frames
for (int i = 0; i < 15; i++) {
frame.fill(0);
// 0x00-0x03 - Block allocation state
frame[0] = 0xa0; // Free, freshly formatted
frame[1] = 0x00;
frame[2] = 0x00;
frame[3] = 0x00;
// Pointer to next block
frame[8] = 0xff;
frame[9] = 0xff;
writeFrame(1 + i, true);
}
// Broken sector list
for (int i = 0; i < 20; i++) {
frame.fill(0);
// Sector number
frame[0] = 0xff;
frame[1] = 0xff;
frame[2] = 0xff;
frame[3] = 0xff;
// Garbage
frame[8] = 0xff;
frame[9] = 0xff;
writeFrame(16 + i);
}
}
} // namespace memory_card

View file

@ -0,0 +1,13 @@
#pragma once
#include <array>
#include <string>
#include <optional>
#include <vector>
namespace memory_card {
constexpr int MEMCARD_SIZE = 128 * 1024;
bool isMemoryCardImage(const std::string& path);
std::optional<std::vector<uint8_t>> load(const std::string& path);
bool save(const std::array<uint8_t, MEMCARD_SIZE>& data, const std::string& path);
void format(std::array<uint8_t, MEMCARD_SIZE>& data);
}; // namespace memory_card

View file

@ -12,6 +12,8 @@
#include "utils/file.h"
#include "utils/string.h"
#include "images.h"
#include "disc/load.h"
#include "memory_card/card_formats.h"
float GUI::scale = 1.f;
@ -321,17 +323,21 @@ void GUI::render(std::unique_ptr<System>& sys) {
notInitializedWindowShown = true;
ImGui::OpenPopup("Avocado");
} else if (droppedItem) {
if (!droppedItemDialogShown) {
droppedItemDialogShown = true;
ImGui::OpenPopup("Disc");
if (droppedItemDialog == DroppedItemDialog::None) {
if (memory_card::isMemoryCardImage(*droppedItem)) {
droppedItemDialog = DroppedItemDialog::MemoryCard;
ImGui::OpenPopup("Memory card##select_file");
} else if (disc::isDiscImage(*droppedItem)) {
droppedItemDialog = DroppedItemDialog::Disc;
ImGui::OpenPopup("Disc##select_file");
}
}
} else {
drawControls(sys);
}
if (droppedItem) {
discDialog();
}
memoryCardDialog();
discDialog();
// Work in progress
// renderController();
@ -340,8 +346,33 @@ void GUI::render(std::unique_ptr<System>& sys) {
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
void GUI::memoryCardDialog() {
if (!ImGui::BeginPopupModal("Memory card##select_file", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize)) {
return;
}
ImGui::Text("Select card slot for %s", getFilenameExt(*droppedItem).c_str());
for (int slot = 0; slot < 2; slot++) {
if (ImGui::Button(fmt::format("Slot {}", slot + 1).c_str())) {
config.memoryCard[slot].path = *droppedItem;
bus.notify(Event::Controller::MemoryCardSwapped{slot});
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
}
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
if (!ImGui::IsPopupOpen("Memory card##select_file")) {
droppedItem = {};
droppedItemDialog = DroppedItemDialog::None;
}
}
void GUI::discDialog() {
if (!ImGui::BeginPopupModal("Disc", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize)) {
if (!ImGui::BeginPopupModal("Disc##select_file", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize)) {
return;
}
@ -372,9 +403,9 @@ void GUI::discDialog() {
}
ImGui::EndPopup();
if (!ImGui::IsPopupOpen("Disc")) {
if (!ImGui::IsPopupOpen("Disc##select_file")) {
droppedItem = {};
droppedItemDialogShown = false;
droppedItemDialog = DroppedItemDialog::None;
}
}

View file

@ -44,6 +44,7 @@ class GUI {
gui::Toasts toasts;
void mainMenu(std::unique_ptr<System>& sys);
void memoryCardDialog();
void discDialog();
void drawControls(std::unique_ptr<System>& sys);
void renderController();
@ -62,7 +63,9 @@ class GUI {
// Drag&drop
std::optional<std::string> droppedItem;
bool droppedItemDialogShown = false;
enum class DroppedItemDialog { None, Disc, MemoryCard };
DroppedItemDialog droppedItemDialog = DroppedItemDialog::None;
GUI(SDL_Window* window, void* glContext);
~GUI();

View file

@ -4,6 +4,8 @@
#include <misc/cpp/imgui_stdlib.h>
#include "config.h"
#include "../../../../system.h"
#include "utils/string.h"
#include "memory_card/card_formats.h"
namespace gui::options {
@ -11,28 +13,34 @@ char sjisToAscii(uint16_t sjis) {
uint8_t l = sjis & 0xff;
uint8_t h = (sjis >> 8) & 0xff;
if (sjis == 0) return 0;
if (l == 0x00) {
if (h == 0x40) return 0;
}
if (l == 0x81) {
// if (h == 0x14) return '-';
if (h == 0x5b) return '-';
// clang-format off
if (h == 0x40) return ' ';
if (h == 0x46) return ':';
if (h == 0x49) return '!';
if (h == 0x5e) return '/';
if (h == 0x6d) return '[';
if (h == 0x6e) return ']';
if (h == 0x69) return '(';
if (h == 0x6a) return ')';
if (h == 0x7b) return '+';
if (h == 0x7c) return ',';
if (h == 0x93) return '%';
// if (h >= 0x43 && h <= 0x97) return ' ' + (h-0x43);
else if (h == 0x44) return '.';
else if (h == 0x46) return ':';
else if (h == 0x49) return '!';
else if (h == 0x51) return '_';
else if (h == 0x5b) return '-';
else if (h == 0x5e) return '/';
else if (h == 0x68) return '"';
else if (h == 0x69) return '(';
else if (h == 0x6a) return ')';
else if (h == 0x6d) return '[';
else if (h == 0x6e) return ']';
else if (h == 0x7b) return '+';
else if (h == 0x7c) return ',';
else if (h == 0x93) return '%';
// clang-format on
}
if (l == 0x82) {
if (h >= 0x4f && h <= 0x58) return '0' + (h - 0x4f);
if (h >= 0x60 && h <= 0x79) return 'A' + (h - 0x60);
if (h >= 0x81 && h <= 0x9a) return 'a' + (h - 0x81);
}
// fmt::print("Unknown S-JIS: 0x{:02x} 0x{:02x}\n", l, h);
// fmt::print("Unknown S-JIS: 0x{:02x} 0x{:02x}\n", l, h);
return '?';
}
@ -77,7 +85,14 @@ void MemoryCard::parseAndDisplayCard(peripherals::MemoryCard* card) {
uint32_t size = read32(offset + 4) / 1024;
std::string filename;
for (int i = 0; i < 20; i++) {
filename += data[offset + 0x0a + i];
char c = data[offset + 0x0a + i];
if (c == 0) break;
filename += c;
}
Region region = parseRegion(filename);
std::string gameId;
if (filename.length() >= 12) {
gameId = filename.substr(2, 10); // Strip region
}
// Parse block
@ -91,14 +106,31 @@ void MemoryCard::parseAndDisplayCard(peripherals::MemoryCard* card) {
if (c == 0) break;
title += c;
}
title = trim(title);
ImGui::Selectable(fmt::format("{:2d}. {} ({})", i, title, filename).c_str());
ImGui::Selectable(fmt::format("{:2d}. {} ({})", i, title, gameId).c_str());
continue;
}
ImGui::Selectable(fmt::format("{:2d}. USED", i).c_str());
}
}
Region MemoryCard::parseRegion(const std::string& filename) const {
if (filename.length() < 2) return Region::Unknown;
auto region = filename.substr(0, 2);
if (region == "BI") {
return Region::Japan;
} else if (region == "BE") {
return Region::Europe;
} else if (region == "BA") {
return Region::America;
} else {
return Region::Unknown;
}
}
void MemoryCard::memoryCardWindow(System* sys) {
if (loadPaths) {
for (size_t i = 0; i < cardPaths.size(); i++) {
@ -111,8 +143,9 @@ void MemoryCard::memoryCardWindow(System* sys) {
for (size_t i = 0; i < sys->controller->card.size(); i++) {
if (ImGui::BeginTabItem(fmt::format("Slot {}", i + 1).c_str())) {
if (ImGui::InputText("Path", &cardPaths[i])) {
if (ImGui::InputText("Path (enter to load)", &cardPaths[i], ImGuiInputTextFlags_EnterReturnsTrue)) {
config.memoryCard[i].path = cardPaths[i];
bus.notify(Event::Controller::MemoryCardSwapped{(int)i});
}
// TODO: Reload contents on change?
// TODO: Check if card exists, override or reload?
@ -126,12 +159,35 @@ void MemoryCard::memoryCardWindow(System* sys) {
parseAndDisplayCard(sys->controller->card[i].get());
// ImGui::EndChild();
if (showConfirmFormatButton) {
if (ImGui::Button("Cancel")) {
showConfirmFormatButton = false;
}
ImGui::SameLine();
}
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.8, 0., 0., 1.));
if (ImGui::Button(showConfirmFormatButton ? "Confirm" : "Format")) {
if (!showConfirmFormatButton) {
showConfirmFormatButton = true;
} else {
memory_card::format(sys->controller->card[i]->data);
bus.notify(Event::Controller::MemoryCardContentsChanged{(int)i});
showConfirmFormatButton = false;
}
}
ImGui::PopStyleColor();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
ImGui::End();
if (!memoryCardWindowOpen) {
showConfirmFormatButton = false;
}
}
void MemoryCard::displayWindows(System* sys) {

View file

@ -8,12 +8,17 @@ struct MemoryCard;
};
namespace gui::options {
enum class Region { Unknown, Japan, Europe, America };
class MemoryCard {
bool loadPaths = true;
bool showConfirmFormatButton = false;
std::array<std::string, 2> cardPaths;
void parseAndDisplayCard(peripherals::MemoryCard* card);
void memoryCardWindow(System* sys);
Region parseRegion(const std::string& filename) const;
public:
bool memoryCardWindowOpen = false;

View file

@ -20,6 +20,7 @@
#include "utils/string.h"
#include "utils/platform_tools.h"
#include "version.h"
#include "memory_card/card_formats.h"
#ifdef _WIN32
#include <direct.h>
@ -199,13 +200,13 @@ int main(int argc, char** argv) {
std::unique_ptr<System> sys = system_tools::hardReset();
int busToken = bus.listen<Event::File::Load>([&](auto e) {
if (disc::isDiscImage(e.file)) {
if (e.action == Event::File::Load::Action::ask) {
// Show dialog and decide what to do
gui->droppedItem = e.file;
return;
}
if (e.action == Event::File::Load::Action::ask && (disc::isDiscImage(e.file) || memory_card::isMemoryCardImage(e.file))) {
// Show dialog and decide what to do
gui->droppedItem = e.file;
return;
}
if (disc::isDiscImage(e.file)) {
std::unique_ptr<disc::Disc> disc = disc::load(e.file);
if (!disc) {
toast(fmt::format("Cannot load {}", getFilenameExt(e.file)));
@ -257,6 +258,18 @@ int main(int argc, char** argv) {
bus.listen<Event::System::SaveState>(busToken, [&](auto e) { state::quickSave(sys.get(), e.slot); });
bus.listen<Event::System::LoadState>(busToken, [&](auto e) { state::quickLoad(sys.get(), e.slot); });
bus.listen<Event::Controller::MemoryCardContentsChanged>(busToken, [&](auto e) {
// TODO: throttleLast save event
system_tools::saveMemoryCard(sys, e.slot);
});
bus.listen<Event::Controller::MemoryCardSwapped>(busToken, [&](auto e) {
sys->controller->card[e.slot]->inserted = false;
for (int i = 0; i < 60; i++) sys->emulateFrame();
system_tools::loadMemoryCard(sys, e.slot);
});
bus.listen<Event::Gui::ToggleFullscreen>(busToken, [&](auto) {
if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) {
SDL_SetWindowFullscreen(window, 0);
@ -433,7 +446,8 @@ int main(int argc, char** argv) {
if (config.options.emulator.preserveState && sys->state != System::State::halted) {
state::saveLastState(sys.get());
}
system_tools::saveMemoryCards(sys, true);
system_tools::saveMemoryCard(sys, 0, true);
system_tools::saveMemoryCard(sys, 1, true);
saveConfigFile();
bus.unlistenAll(busToken);

View file

@ -5,6 +5,7 @@
#include "sound/sound.h"
#include "state/state.h"
#include "system.h"
#include "memory_card/card_formats.h"
#include "utils/file.h"
#include "utils/gpu_draw_list.h"
#include "utils/psf.h"
@ -83,32 +84,50 @@ void loadFile(std::unique_ptr<System>& sys, const std::string& path) {
}
}
void saveMemoryCards(std::unique_ptr<System>& sys, bool force) {
auto saveMemoryCard = [&](int slot) {
if (!force && !sys->controller->card[slot]->dirty) return;
bool saveMemoryCard(std::unique_ptr<System>& sys, int slot, bool force) {
if (!force && !sys->controller->card[slot]->dirty) return true;
std::string pathCard = config.memoryCard[slot].path;
if (pathCard.empty()) {
fmt::print("[INFO] No memory card {} path in config, skipping save\n", slot + 1);
return;
return false;
}
auto& data = sys->controller->card[slot]->data;
auto output = std::vector<uint8_t>(data.begin(), data.end());
if (!putFileContents(pathCard, output)) {
fmt::print("[INFO] Unable to save memory card {} to {}\n", slot + 1, getFilenameExt(pathCard));
return;
if (!memory_card::save(sys->controller->card[slot]->data, pathCard)) {
fmt::print("[ERROR] Unable to save memory card {} to {}\n", slot + 1, getFilenameExt(pathCard));
return false;
}
sys->controller->card[slot]->dirty = false;
fmt::print("[INFO] Saved memory card {} to {}\n", slot + 1, getFilenameExt(pathCard));
return true;
};
saveMemoryCard(0);
saveMemoryCard(1);
bool loadMemoryCard(std::unique_ptr<System>& sys, int slot) {
assert(slot == 0 || slot == 1);
auto card = sys->controller->card[slot].get();
card->inserted = false;
auto path = config.memoryCard[slot].path;
if (path.empty()) {
return false;
}
auto cardData = memory_card::load(path);
if (!cardData) {
fmt::print("[ERROR] Cannot load memory card {} ({})\n", slot, getFilenameExt(path));
return false;
}
std::copy(cardData->begin(), cardData->end(), sys->controller->card[slot]->data.begin());
card->inserted = true;
card->setFresh();
fmt::print("[INFO] Loaded memory card {} from {}\n", slot + 1, getFilenameExt(path));
return true;
};
std::unique_ptr<System> hardReset() {
auto sys = std::make_unique<System>();
@ -128,26 +147,8 @@ std::unique_ptr<System> hardReset() {
fmt::print("[INFO] Using iso {}\n", iso);
}
auto loadMemoryCard = [&](int slot) {
assert(slot == 0 || slot == 1);
auto card = sys->controller->card[slot].get();
std::string pathCard = config.memoryCard[slot].path;
card->inserted = false;
if (!pathCard.empty()) {
auto cardData = getFileContents(pathCard);
if (!cardData.empty()) {
std::copy_n(std::make_move_iterator(cardData.begin()), cardData.size(), sys->controller->card[slot]->data.begin());
card->inserted = true;
fmt::print("[INFO] Loaded memory card {} from {}\n", slot + 1, getFilenameExt(pathCard));
}
}
};
loadMemoryCard(0);
loadMemoryCard(1);
loadMemoryCard(sys, 0);
loadMemoryCard(sys, 1);
return sys;
}

View file

@ -8,7 +8,8 @@ namespace system_tools {
void bootstrap(std::unique_ptr<System>& sys);
void loadFile(std::unique_ptr<System>& sys, const std::string& path);
void saveMemoryCards(std::unique_ptr<System>& sys, bool force = false);
bool loadMemoryCard(std::unique_ptr<System>& sys, int slot);
bool saveMemoryCard(std::unique_ptr<System>& sys, int slot, bool force = false);
std::unique_ptr<System> hardReset();
}; // namespace system_tools

View file

@ -53,6 +53,13 @@ struct Vibration {
bool small;
uint8_t big;
};
struct MemoryCardContentsChanged {
int slot = 0;
};
struct MemoryCardSwapped {
int slot = 0;
};
} // namespace Controller
}; // namespace Event