Skip to content

Commit

Permalink
memory_card: added support for .vgs, .gme, .vmp memory card formats
Browse files Browse the repository at this point in the history
Load memory cards by drag&drop
Better parsing of memory card contents
Card formatting
  • Loading branch information
JaCzekanski committed Nov 16, 2021
1 parent c371e08 commit f016004
Show file tree
Hide file tree
Showing 14 changed files with 468 additions and 67 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/device/controller/peripherals/memory_card.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions src/device/controller/peripherals/memory_card.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
262 changes: 262 additions & 0 deletions src/memory_card/card_formats.cpp
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions src/memory_card/card_formats.h
Original file line number Diff line number Diff line change
@@ -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
49 changes: 40 additions & 9 deletions src/platform/windows/gui/gui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
}

Expand Down
Loading

0 comments on commit f016004

Please sign in to comment.