/* PCSX2 - PS2 Emulator for PCs * Copyright (C) 2002-2010 PCSX2 Dev Team * * PCSX2 is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCSX2. * If not, see . */ #include "PrecompiledHeader.h" #include "common/FileSystem.h" #include "common/SafeArray.inl" #include "common/Path.h" #include "common/StringUtil.h" #include #include #include "MemoryCardFile.h" #include "MemoryCardFolder.h" #include "System.h" #include "Config.h" #include "Host.h" #include "IconsFontAwesome5.h" #include "svnrev.h" #include "fmt/core.h" #include static const int MCD_SIZE = 1024 * 8 * 16; // Legacy PSX card default size static const int MC2_MBSIZE = 1024 * 528 * 2; // Size of a single megabyte of card data static const char* s_folder_mem_card_id_file = "_pcsx2_superblock"; bool FileMcd_Open = false; // ECC code ported from mymc // https://sourceforge.net/p/mymc-opl/code/ci/master/tree/ps2mc_ecc.py // Public domain license static u32 CalculateECC(u8* buf) { const u8 parity_table[256] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1, 0,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0, 1,1,0,1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,0,1,1, 0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,0, 1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1, 0,1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,1,0,0,1,0, 1,1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1,0,1,1,0,1,0,0,1,1,0,0, 1,0,1,1,0}; const u8 column_parity_mask[256] = {0,7,22,17,37,34,51,52,52,51,34,37,17,22, 7,0,67,68,85,82,102,97,112,119,119,112,97,102,82,85,68,67,82,85,68,67,119,112, 97,102,102,97,112,119,67,68,85,82,17,22,7,0,52,51,34,37,37,34,51,52,0,7,22,17, 97,102,119,112,68,67,82,85,85,82,67,68,112,119,102,97,34,37,52,51,7,0,17,22, 22,17,0,7,51,52,37,34,51,52,37,34,22,17,0,7,7,0,17,22,34,37,52,51,112,119,102, 97,85,82,67,68,68,67,82,85,97,102,119,112,112,119,102,97,85,82,67,68,68,67,82, 85,97,102,119,112,51,52,37,34,22,17,0,7,7,0,17,22,34,37,52,51,34,37,52,51,7,0, 17,22,22,17,0,7,51,52,37,34,97,102,119,112,68,67,82,85,85,82,67,68,112,119,102, 97,17,22,7,0,52,51,34,37,37,34,51,52,0,7,22,17,82,85,68,67,119,112,97,102,102, 97,112,119,67,68,85,82,67,68,85,82,102,97,112,119,119,112,97,102,82,85,68,67, 0,7,22,17,37,34,51,52,52,51,34,37,17,22,7,0}; u8 column_parity = 0x77; u8 line_parity_0 = 0x7F; u8 line_parity_1 = 0x7F; for (int i = 0; i < 128; i++) { u8 b = buf[i]; column_parity ^= column_parity_mask[b]; if (parity_table[b]) { line_parity_0 ^= ~i; line_parity_1 ^= i; } } return column_parity | (line_parity_0 << 8) | (line_parity_1 << 16); } static bool ConvertNoECCtoRAW(const char* file_in, const char* file_out) { auto fin = FileSystem::OpenManagedCFile(file_in, "rb"); if (!fin) return false; auto fout = FileSystem::OpenManagedCFile(file_out, "wb"); if (!fout) return false; const s64 size = FileSystem::FSize64(fin.get()); u8 buffer[512]; for (s64 i = 0; i < (size / 512); i++) { if (std::fread(buffer, sizeof(buffer), 1, fin.get()) != 1 || std::fwrite(buffer, sizeof(buffer), 1, fout.get()) != 1) { return false; } for (int j = 0; j < 4; j++) { u32 checksum = CalculateECC(&buffer[j * 128]); if (std::fwrite(&checksum, 3, 1, fout.get()) != 1) return false; } u32 nullbytes = 0; if (std::fwrite(&nullbytes, sizeof(nullbytes), 1, fout.get()) != 1) return false; } if (std::fflush(fout.get()) != 0) return false; return true; } static bool ConvertRAWtoNoECC(const char* file_in, const char* file_out) { auto fin = FileSystem::OpenManagedCFile(file_in, "rb"); if (!fin) return false; auto fout = FileSystem::OpenManagedCFile(file_out, "wb"); if (!fout) return false; const s64 size = FileSystem::FSize64(fin.get()); u8 buffer[512]; u8 checksum[16]; for (s64 i = 0; i < (size / 528); i++) { if (std::fread(buffer, sizeof(buffer), 1, fin.get()) != 1 || std::fwrite(buffer, sizeof(buffer), 1, fout.get()) != 1 || std::fread(checksum, sizeof(checksum), 1, fin.get()) != 1) { return false; } } if (std::fflush(fout.get()) != 0) return false; return true; } // -------------------------------------------------------------------------------------- // FileMemoryCard // -------------------------------------------------------------------------------------- // Provides thread-safe direct file IO mapping. // class FileMemoryCard { protected: std::FILE* m_file[8]; std::string m_filenames[8]; u8 m_effeffs[528 * 16]; SafeArray m_currentdata; u64 m_chksum[8]; bool m_ispsx[8]; u32 m_chkaddr; public: FileMemoryCard(); virtual ~FileMemoryCard() = default; void Lock(); void Unlock(); void Open(); void Close(); s32 IsPresent(uint slot); void GetSizeInfo(uint slot, McdSizeInfo& outways); bool IsPSX(uint slot); s32 Read(uint slot, u8* dest, u32 adr, int size); s32 Save(uint slot, const u8* src, u32 adr, int size); s32 EraseBlock(uint slot, u32 adr); u64 GetCRC(uint slot); protected: bool Seek(std::FILE* f, u32 adr); bool Create(const char* mcdFile, uint sizeInMB); }; uint FileMcd_GetMtapPort(uint slot) { switch (slot) { case 0: case 2: case 3: case 4: return 0; case 1: case 5: case 6: case 7: return 1; jNO_DEFAULT } return 0; // technically unreachable. } // Returns the multitap slot number, range 1 to 3 (slot 0 refers to the standard // 1st and 2nd player slots). uint FileMcd_GetMtapSlot(uint slot) { switch (slot) { case 0: case 1: pxFailDev("Invalid parameter in call to GetMtapSlot -- specified slot is one of the base slots, not a Multitap slot."); break; case 2: case 3: case 4: return slot - 1; case 5: case 6: case 7: return slot - 4; jNO_DEFAULT } return 0; // technically unreachable. } bool FileMcd_IsMultitapSlot(uint slot) { return (slot > 1); } std::string FileMcd_GetDefaultName(uint slot) { if (FileMcd_IsMultitapSlot(slot)) return StringUtil::StdStringFromFormat("Mcd-Multitap%u-Slot%02u.ps2", FileMcd_GetMtapPort(slot) + 1, FileMcd_GetMtapSlot(slot) + 1); else return StringUtil::StdStringFromFormat("Mcd%03u.ps2", slot + 1); } FileMemoryCard::FileMemoryCard() : m_chkaddr(0) { memset8<0xff>(m_effeffs); } void FileMemoryCard::Open() { for (int slot = 0; slot < 8; ++slot) { m_filenames[slot] = {}; if (FileMcd_IsMultitapSlot(slot)) { if (!EmuConfig.MultitapPort0_Enabled && (FileMcd_GetMtapPort(slot) == 0)) continue; if (!EmuConfig.MultitapPort1_Enabled && (FileMcd_GetMtapPort(slot) == 1)) continue; } std::string fname(EmuConfig.FullpathToMcd(slot)); std::string_view str(fname); bool cont = false; if (fname.empty()) { str = "[empty filename]"; cont = true; } if (!EmuConfig.Mcd[slot].Enabled) { str = "[disabled]"; cont = true; } if (EmuConfig.Mcd[slot].Type != MemoryCardType::File) { str = "[is not memcard file]"; cont = true; } Console.WriteLn(cont ? Color_Gray : Color_Green, "McdSlot %u [File]: %.*s", slot, static_cast(str.size()), str.data()); if (cont) continue; if (FileSystem::GetPathFileSize(fname.c_str()) <= 0) { // FIXME : Ideally this should prompt the user for the size of the // memory card file they would like to create, instead of trying to // create one automatically. if (!Create(fname.c_str(), 8)) { Host::ReportFormattedErrorAsync("Memory Card", "Could not create a memory card: \n\n%s\n\n", fname.c_str()); } } // [TODO] : Add memcard size detection and report it to the console log. // (8MB, 256Mb, formatted, unformatted, etc ...) #ifdef _WIN32 FileSystem::SetPathCompression(fname.c_str(), EmuConfig.McdCompressNTFS); #endif if (StringUtil::EndsWith(fname, ".bin")) { std::string newname(fname + "x"); if (!ConvertNoECCtoRAW(fname.c_str(), newname.c_str())) { Console.Error("Could convert memory card: %s", fname.c_str()); FileSystem::DeleteFilePath(newname.c_str()); continue; } // store the original filename m_file[slot] = FileSystem::OpenSharedCFile(newname.c_str(), "r+b", FileSystem::FileShareMode::DenyWrite); } else { m_file[slot] = FileSystem::OpenSharedCFile(fname.c_str(), "r+b", FileSystem::FileShareMode::DenyWrite); } if (!m_file[slot]) { // Translation note: detailed description should mention that the memory card will be disabled // for the duration of this session. Host::ReportFormattedErrorAsync("Memory Card", "Access denied to memory card: \n\n%s\n\n" "Another instance of PCSX2 may be using this memory card. Close any other instances of PCSX2, or restart your computer.%s", fname.c_str(), #ifdef WIN32 "\n\nIf your memory card is in a write-protected folder such as \"Program Files\" or \"Program Files (x86)\", move it to another folder, such as \"Documents\" or \"Desktop\"." #else "" #endif ); } else // Load checksum { m_filenames[slot] = std::move(fname); m_ispsx[slot] = FileSystem::FSize64(m_file[slot]) == 0x20000; m_chkaddr = 0x210; if (!m_ispsx[slot] && FileSystem::FSeek64(m_file[slot], m_chkaddr, SEEK_SET) == 0) { const size_t read_result = std::fread(&m_chksum[slot], sizeof(m_chksum[slot]), 1, m_file[slot]); if (read_result == 0) Host::ReportFormattedErrorAsync("Memory Card", "Error reading memcard.\n"); } } } } void FileMemoryCard::Close() { for (int slot = 0; slot < 8; ++slot) { if (!m_file[slot]) continue; // Store checksum if (!m_ispsx[slot] && FileSystem::FSeek64(m_file[slot], m_chkaddr, SEEK_SET) == 0) std::fwrite(&m_chksum[slot], sizeof(m_chksum[slot]), 1, m_file[slot]); std::fclose(m_file[slot]); m_file[slot] = nullptr; if (StringUtil::EndsWith(m_filenames[slot], ".bin")) { const std::string name_in(m_filenames[slot] + 'x'); if (ConvertRAWtoNoECC(name_in.c_str(), m_filenames[slot].c_str())) FileSystem::DeleteFilePath(name_in.c_str()); } m_filenames[slot] = {}; } } // Returns FALSE if the seek failed (is outside the bounds of the file). bool FileMemoryCard::Seek(std::FILE* f, u32 adr) { const s64 size = FileSystem::FSize64(f); // If anyone knows why this filesize logic is here (it appears to be related to legacy PSX // cards, perhaps hacked support for some special emulator-specific memcard formats that // had header info?), then please replace this comment with something useful. Thanks! -- air u32 offset = 0; if (size == MCD_SIZE + 64) offset = 64; else if (size == MCD_SIZE + 3904) offset = 3904; else { // perform sanity checks here? } return (FileSystem::FSeek64(f, adr + offset, SEEK_SET) == 0); } // returns FALSE if an error occurred (either permission denied or disk full) bool FileMemoryCard::Create(const char* mcdFile, uint sizeInMB) { //int enc[16] = {0x77,0x7f,0x7f,0x77,0x7f,0x7f,0x77,0x7f,0x7f,0x77,0x7f,0x7f,0,0,0,0}; Console.WriteLn("(FileMcd) Creating new %uMB memory card: %s", sizeInMB, mcdFile); auto fp = FileSystem::OpenManagedCFile(mcdFile, "wb"); if (!fp) return false; for (uint i = 0; i < (MC2_MBSIZE * sizeInMB) / sizeof(m_effeffs); i++) { if (std::fwrite(m_effeffs, sizeof(m_effeffs), 1, fp.get()) != 1) return false; } return true; } s32 FileMemoryCard::IsPresent(uint slot) { return m_file[slot] != nullptr; } void FileMemoryCard::GetSizeInfo(uint slot, McdSizeInfo& outways) { outways.SectorSize = 512; // 0x0200 outways.EraseBlockSizeInSectors = 16; // 0x0010 outways.Xor = 18; // 0x12, XOR 02 00 00 10 if (pxAssert(m_file[slot])) outways.McdSizeInSectors = static_cast(FileSystem::FSize64(m_file[slot])) / (outways.SectorSize + outways.EraseBlockSizeInSectors); else outways.McdSizeInSectors = 0x4000; u8* pdata = (u8*)&outways.McdSizeInSectors; outways.Xor ^= pdata[0] ^ pdata[1] ^ pdata[2] ^ pdata[3]; } bool FileMemoryCard::IsPSX(uint slot) { return m_ispsx[slot]; } s32 FileMemoryCard::Read(uint slot, u8* dest, u32 adr, int size) { std::FILE* mcfp = m_file[slot]; if (!mcfp) { DevCon.Error("(FileMcd) Ignoring attempted read from disabled slot."); memset(dest, 0, size); return 1; } if (!Seek(mcfp, adr)) return 0; return std::fread(dest, size, 1, mcfp) == 1; } s32 FileMemoryCard::Save(uint slot, const u8* src, u32 adr, int size) { std::FILE* mcfp = m_file[slot]; if (!mcfp) { DevCon.Error("(FileMcd) Ignoring attempted save/write to disabled slot."); return 1; } if (m_ispsx[slot]) { m_currentdata.MakeRoomFor(size); for (int i = 0; i < size; i++) m_currentdata[i] = src[i]; } else { if (!Seek(mcfp, adr)) return 0; m_currentdata.MakeRoomFor(size); const size_t read_result = std::fread(m_currentdata.GetPtr(), size, 1, mcfp); if (read_result == 0) Host::ReportFormattedErrorAsync("Memory Card", "Error reading memcard.\n"); for (int i = 0; i < size; i++) { if ((m_currentdata[i] & src[i]) != src[i]) Console.Warning("(FileMcd) Warning: writing to uncleared data. (%d) [%08X]", slot, adr); m_currentdata[i] &= src[i]; } // Checksumness { if (adr == m_chkaddr) Console.Warning("(FileMcd) Warning: checksum sector overwritten. (%d)", slot); u64* pdata = (u64*)&m_currentdata[0]; u32 loops = size / 8; for (u32 i = 0; i < loops; i++) m_chksum[slot] ^= pdata[i]; } } if (!Seek(mcfp, adr)) return 0; if (std::fwrite(m_currentdata.GetPtr(), size, 1, mcfp) == 1) { static auto last = std::chrono::time_point(); std::chrono::duration elapsed = std::chrono::system_clock::now() - last; if (elapsed > std::chrono::seconds(5)) { Host::AddIconOSDMessage(fmt::format("MemoryCardSave{}", slot), ICON_FA_SD_CARD, fmt::format("Memory card '{}' was saved to storage.", Path::GetFileName(m_filenames[slot])), Host::OSD_INFO_DURATION); last = std::chrono::system_clock::now(); } return 1; } return 0; } s32 FileMemoryCard::EraseBlock(uint slot, u32 adr) { std::FILE* mcfp = m_file[slot]; if (!mcfp) { DevCon.Error("MemoryCard: Ignoring erase for disabled slot."); return 1; } if (!Seek(mcfp, adr)) return 0; return std::fwrite(m_effeffs, sizeof(m_effeffs), 1, mcfp) == 1; } u64 FileMemoryCard::GetCRC(uint slot) { std::FILE* mcfp = m_file[slot]; if (!mcfp) return 0; u64 retval = 0; if (m_ispsx[slot]) { if (!Seek(mcfp, 0)) return 0; const s64 mcfpsize = FileSystem::FSize64(mcfp); if (mcfpsize < 0) return 0; // Process the file in 4k chunks. Speeds things up significantly. u64 buffer[528 * 8]; // use 528 (sector size), ensures even divisibility const uint filesize = static_cast(mcfpsize) / sizeof(buffer); for (uint i = filesize; i; --i) { if (std::fread(buffer, sizeof(buffer), 1, mcfp) != 1) return 0; for (uint t = 0; t < std::size(buffer); ++t) retval ^= buffer[t]; } } else { retval = m_chksum[slot]; } return retval; } // -------------------------------------------------------------------------------------- // MemoryCard Component API Bindings // -------------------------------------------------------------------------------------- namespace Mcd { FileMemoryCard impl; // class-based implementations we refer to when API is invoked FolderMemoryCardAggregator implFolder; }; // namespace Mcd uint FileMcd_ConvertToSlot(uint port, uint slot) { if (slot == 0) return port; if (port == 0) return slot + 1; // multitap 1 return slot + 4; // multitap 2 } void FileMcd_EmuOpen() { if(FileMcd_Open) return; FileMcd_Open = true; // detect inserted memory card types for (uint slot = 0; slot < 8; ++slot) { if (EmuConfig.Mcd[slot].Filename.empty()) { EmuConfig.Mcd[slot].Type = MemoryCardType::Empty; } else if (EmuConfig.Mcd[slot].Enabled) { MemoryCardType type = MemoryCardType::File; // default to file if we can't find anything at the path so it gets auto-generated const std::string path(EmuConfig.FullpathToMcd(slot)); if (FileSystem::DirectoryExists(path.c_str())) type = MemoryCardType::Folder; EmuConfig.Mcd[slot].Type = type; } } Mcd::impl.Open(); Mcd::implFolder.SetFiltering(EmuConfig.McdFolderAutoManage); Mcd::implFolder.Open(); } void FileMcd_EmuClose() { if(!FileMcd_Open) return; FileMcd_Open = false; Mcd::implFolder.Close(); Mcd::impl.Close(); } s32 FileMcd_IsPresent(uint port, uint slot) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { case MemoryCardType::File: return Mcd::impl.IsPresent(combinedSlot); case MemoryCardType::Folder: return Mcd::implFolder.IsPresent(combinedSlot); default: return false; } } void FileMcd_GetSizeInfo(uint port, uint slot, McdSizeInfo* outways) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { case MemoryCardType::File: Mcd::impl.GetSizeInfo(combinedSlot, *outways); break; case MemoryCardType::Folder: Mcd::implFolder.GetSizeInfo(combinedSlot, *outways); break; default: return; } } bool FileMcd_IsPSX(uint port, uint slot) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { case MemoryCardType::File: return Mcd::impl.IsPSX(combinedSlot); case MemoryCardType::Folder: return Mcd::implFolder.IsPSX(combinedSlot); default: return false; } } s32 FileMcd_Read(uint port, uint slot, u8* dest, u32 adr, int size) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { case MemoryCardType::File: return Mcd::impl.Read(combinedSlot, dest, adr, size); case MemoryCardType::Folder: return Mcd::implFolder.Read(combinedSlot, dest, adr, size); default: return 0; } } s32 FileMcd_Save(uint port, uint slot, const u8* src, u32 adr, int size) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { case MemoryCardType::File: return Mcd::impl.Save(combinedSlot, src, adr, size); case MemoryCardType::Folder: return Mcd::implFolder.Save(combinedSlot, src, adr, size); default: return 0; } } s32 FileMcd_EraseBlock(uint port, uint slot, u32 adr) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { case MemoryCardType::File: return Mcd::impl.EraseBlock(combinedSlot, adr); case MemoryCardType::Folder: return Mcd::implFolder.EraseBlock(combinedSlot, adr); default: return 0; } } u64 FileMcd_GetCRC(uint port, uint slot) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { case MemoryCardType::File: return Mcd::impl.GetCRC(combinedSlot); case MemoryCardType::Folder: return Mcd::implFolder.GetCRC(combinedSlot); default: return 0; } } void FileMcd_NextFrame(uint port, uint slot) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { //case MemoryCardType::MemoryCard_File: // Mcd::impl.NextFrame( combinedSlot ); // break; case MemoryCardType::Folder: Mcd::implFolder.NextFrame(combinedSlot); break; default: return; } } bool FileMcd_ReIndex(uint port, uint slot, const std::string& filter) { const uint combinedSlot = FileMcd_ConvertToSlot(port, slot); switch (EmuConfig.Mcd[combinedSlot].Type) { //case MemoryCardType::File: // return Mcd::impl.ReIndex( combinedSlot, filter ); // break; case MemoryCardType::Folder: return Mcd::implFolder.ReIndex(combinedSlot, EmuConfig.McdFolderAutoManage, filter); break; default: return false; } } // -------------------------------------------------------------------------------------- // Library API Implementations // -------------------------------------------------------------------------------------- static MemoryCardFileType GetMemoryCardFileTypeFromSize(s64 size) { if (size == (8 * MC2_MBSIZE)) return MemoryCardFileType::PS2_8MB; else if (size == (16 * MC2_MBSIZE)) return MemoryCardFileType::PS2_16MB; else if (size == (32 * MC2_MBSIZE)) return MemoryCardFileType::PS2_32MB; else if (size == (64 * MC2_MBSIZE)) return MemoryCardFileType::PS2_64MB; else if (size == MCD_SIZE) return MemoryCardFileType::PS1; else return MemoryCardFileType::Unknown; } static bool IsMemoryCardFolder(const std::string& path) { const std::string superblock_path(Path::Combine(path, s_folder_mem_card_id_file)); return FileSystem::FileExists(superblock_path.c_str()); } static bool IsMemoryCardFormatted(const std::string& path) { auto fp = FileSystem::OpenManagedSharedCFile(path.c_str(), "rb", FileSystem::FileShareMode::DenyNone); if (!fp) return false; static const char formatted_psx[] = "MC"; static const char formatted_string[] = "Sony PS2 Memory Card Format"; static constexpr size_t read_length = sizeof(formatted_string) - 1; u8 data[read_length]; if (std::fread(data, read_length, 1, fp.get()) != 1) return false; return (std::memcmp(data, formatted_string, sizeof(formatted_string) - 1) == 0 || std::memcmp(data, formatted_psx, sizeof(formatted_psx) - 1) == 0); } std::vector FileMcd_GetAvailableCards(bool include_in_use_cards) { std::vector files; FileSystem::FindFiles(EmuFolders::MemoryCards.c_str(), "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES, &files); std::vector mcds; mcds.reserve(files.size()); for (FILESYSTEM_FIND_DATA& fd : files) { std::string basename(Path::GetFileName(fd.FileName)); if (!include_in_use_cards) { bool in_use = false; for (size_t i = 0; i < std::size(EmuConfig.Mcd); i++) { if (EmuConfig.Mcd[i].Filename == basename) { in_use = true; break; } } if (in_use) continue; } if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) { if (!IsMemoryCardFolder(fd.FileName)) continue; mcds.push_back({std::move(basename), std::move(fd.FileName), fd.ModificationTime, MemoryCardType::Folder, MemoryCardFileType::Unknown, 0u, true}); } else { if (fd.Size < MCD_SIZE) continue; const bool formatted = IsMemoryCardFormatted(fd.FileName); mcds.push_back({std::move(basename), std::move(fd.FileName), fd.ModificationTime, MemoryCardType::File, GetMemoryCardFileTypeFromSize(fd.Size), static_cast(fd.Size), formatted}); } } return mcds; } std::optional FileMcd_GetCardInfo(const std::string_view& name) { std::optional ret; std::string basename(name); std::string path(Path::Combine(EmuFolders::MemoryCards, basename)); FILESYSTEM_STAT_DATA sd; if (!FileSystem::StatFile(path.c_str(), &sd)) return ret; if (sd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) { if (IsMemoryCardFolder(path)) { ret = {std::move(basename), std::move(path), sd.ModificationTime, MemoryCardType::Folder, MemoryCardFileType::Unknown, 0u, true}; } } else { if (sd.Size >= MCD_SIZE) { const bool formatted = IsMemoryCardFormatted(path); ret = {std::move(basename), std::move(path), sd.ModificationTime, MemoryCardType::File, GetMemoryCardFileTypeFromSize(sd.Size), static_cast(sd.Size), formatted}; } } return ret; } bool FileMcd_CreateNewCard(const std::string_view& name, MemoryCardType type, MemoryCardFileType file_type) { const std::string full_path(Path::Combine(EmuFolders::MemoryCards, name)); if (type == MemoryCardType::Folder) { Console.WriteLn("(FileMcd) Creating new PS2 folder memory card: '%.*s'", static_cast(name.size()), name.data()); if (!FileSystem::CreateDirectoryPath(full_path.c_str(), false)) { Host::ReportFormattedErrorAsync("Memory Card Creation Failed", "Failed to create directory '%s'.", full_path.c_str()); return false; } // write the superblock auto fp = FileSystem::OpenManagedCFile(Path::Combine(full_path, s_folder_mem_card_id_file).c_str(), "wb"); if (!fp) { Host::ReportFormattedErrorAsync("Memory Card Creation Failed", "Failed to write memory card folder superblock '%s'.", full_path.c_str()); return false; } return true; } if (type == MemoryCardType::File) { if (file_type <= MemoryCardFileType::Unknown || file_type >= MemoryCardFileType::MaxCount) return false; static constexpr std::array(MemoryCardFileType::MaxCount)> sizes = {{0, 8 * MC2_MBSIZE, 16 * MC2_MBSIZE, 32 * MC2_MBSIZE, 64 * MC2_MBSIZE, MCD_SIZE}}; const bool isPSX = (type == MemoryCardType::File && file_type == MemoryCardFileType::PS1); const u32 size = sizes[static_cast(file_type)]; if (!isPSX && size == 0) return false; auto fp = FileSystem::OpenManagedCFile(full_path.c_str(), "wb"); if (!fp) { Host::ReportFormattedErrorAsync("Memory Card Creation Failed", "Failed to open file '%s'.", full_path.c_str()); return false; } if (!isPSX) { Console.WriteLn("(FileMcd) Creating new PS2 %uMB memory card: '%s'", size / MC2_MBSIZE, full_path.c_str()); // PS2 Memory Card u8 m_effeffs[528 * 16]; memset8<0xff>(m_effeffs); const u32 count = size / sizeof(m_effeffs); for (uint i = 0; i < count; i++) { if (std::fwrite(m_effeffs, sizeof(m_effeffs), 1, fp.get()) != 1) { Host::ReportFormattedErrorAsync("Memory Card Creation Failed", "Failed to write file '%s'.", full_path.c_str()); return false; } } return true; } else { Console.WriteLn("(FileMcd) Creating new PSX 128 KiB memory card: '%s'", full_path.c_str()); // PSX Memory Card; 8192 is the size in bytes of a single block of a PSX memory card (8 KiB). u8 m_effeffs_psx[8192]; memset8<0xff>(m_effeffs_psx); // PSX cards consist of 16 blocks, each 8 KiB in size. for (uint i = 0; i < 16; i++) { if (std::fwrite(m_effeffs_psx, sizeof(m_effeffs_psx), 1, fp.get()) != 1) { Host::ReportFormattedErrorAsync("Memory Card Creation Failed", "Failed to write file '%s'.", full_path.c_str()); return false; } } return true; } } return false; } bool FileMcd_RenameCard(const std::string_view& name, const std::string_view& new_name) { const std::string name_path(Path::Combine(EmuFolders::MemoryCards, name)); const std::string new_name_path(Path::Combine(EmuFolders::MemoryCards, new_name)); FILESYSTEM_STAT_DATA sd, new_sd; if (!FileSystem::StatFile(name_path.c_str(), &sd) || FileSystem::StatFile(new_name_path.c_str(), &new_sd)) { Console.Error("(FileMcd) New name already exists, or old name does not"); return false; } Console.WriteLn("(FileMcd) Renaming memory card '%.*s' to '%.*s'", static_cast(name.size()), name.data(), static_cast(new_name.size()), new_name.data()); if (!FileSystem::RenamePath(name_path.c_str(), new_name_path.c_str())) { Console.Error("(FileMcd) Failed to rename '%s' to '%s'", name_path.c_str(), new_name_path.c_str()); return false; } return true; } bool FileMcd_DeleteCard(const std::string_view& name) { const std::string name_path(Path::Combine(EmuFolders::MemoryCards, name)); FILESYSTEM_STAT_DATA sd; if (!FileSystem::StatFile(name_path.c_str(), &sd)) { Console.Error("(FileMcd) Can't stat '%s' for deletion", name_path.c_str()); return false; } Console.WriteLn("(FileMcd) Deleting memory card '%.*s'", static_cast(name.size()), name.data()); if (sd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) { // must be a folder memcard, so do a recursive delete (scary) if (!FileSystem::RecursiveDeleteDirectory(name_path.c_str())) { Console.Error("(FileMcd) Failed to recursively delete '%s'", name_path.c_str()); return false; } } else { if (!FileSystem::DeleteFilePath(name_path.c_str())) { Console.Error("(FileMcd) Failed to delete file '%s'", name_path.c_str()); return false; } } return true; }