diff --git a/Source/Core/Core/CMakeLists.txt b/Source/Core/Core/CMakeLists.txt index f72a43861d..498b430d71 100644 --- a/Source/Core/Core/CMakeLists.txt +++ b/Source/Core/Core/CMakeLists.txt @@ -107,6 +107,7 @@ add_library(core HW/EXI/EXI_DeviceMic.cpp HW/GCKeyboard.cpp HW/GCKeyboardEmu.cpp + HW/GCMemcard/GCIFile.cpp HW/GCMemcard/GCMemcard.cpp HW/GCMemcard/GCMemcardDirectory.cpp HW/GCMemcard/GCMemcardRaw.cpp diff --git a/Source/Core/Core/Core.vcxproj b/Source/Core/Core/Core.vcxproj index 82709080f4..62b00d0f70 100644 --- a/Source/Core/Core/Core.vcxproj +++ b/Source/Core/Core/Core.vcxproj @@ -146,6 +146,7 @@ + @@ -414,6 +415,7 @@ + diff --git a/Source/Core/Core/Core.vcxproj.filters b/Source/Core/Core/Core.vcxproj.filters index 62091ccacd..68a8141f87 100644 --- a/Source/Core/Core/Core.vcxproj.filters +++ b/Source/Core/Core/Core.vcxproj.filters @@ -462,6 +462,9 @@ HW %28Flipper/Hollywood%29\EXI - Expansion Interface + + HW %28Flipper/Hollywood%29\GCMemcard + HW %28Flipper/Hollywood%29\GCMemcard @@ -1173,6 +1176,9 @@ HW %28Flipper/Hollywood%29\EXI - Expansion Interface + + HW %28Flipper/Hollywood%29\GCMemcard + HW %28Flipper/Hollywood%29\GCMemcard diff --git a/Source/Core/Core/HW/GCMemcard/GCIFile.cpp b/Source/Core/Core/HW/GCMemcard/GCIFile.cpp new file mode 100644 index 0000000000..b173417b8a --- /dev/null +++ b/Source/Core/Core/HW/GCMemcard/GCIFile.cpp @@ -0,0 +1,102 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "Core/HW/GCMemcard/GCIFile.h" + +#include + +#include "Common/ChunkFile.h" +#include "Common/CommonTypes.h" +#include "Common/File.h" +#include "Common/Logging/Log.h" + +bool GCIFile::LoadHeader() +{ + if (m_filename.empty()) + return false; + + File::IOFile save_file(m_filename, "rb"); + if (!save_file) + return false; + + INFO_LOG(EXPANSIONINTERFACE, "Reading header from disk for %s", m_filename.c_str()); + if (!save_file.ReadBytes(&m_gci_header, sizeof(m_gci_header))) + { + ERROR_LOG(EXPANSIONINTERFACE, "Failed to read header for %s", m_filename.c_str()); + return false; + } + + return true; +} + +bool GCIFile::LoadSaveBlocks() +{ + if (m_save_data.empty()) + { + if (m_filename.empty()) + return false; + + File::IOFile save_file(m_filename, "rb"); + if (!save_file) + return false; + + INFO_LOG(EXPANSIONINTERFACE, "Reading savedata from disk for %s", m_filename.c_str()); + u16 num_blocks = m_gci_header.m_block_count; + + const u32 size = num_blocks * BLOCK_SIZE; + u64 file_size = save_file.GetSize(); + if (file_size != size + DENTRY_SIZE) + { + ERROR_LOG(EXPANSIONINTERFACE, + "%s\nwas not loaded because it is an invalid GCI.\n File size (0x%" PRIx64 + ") does not match the size recorded in the header (0x%x)", + m_filename.c_str(), file_size, size + DENTRY_SIZE); + return false; + } + + m_save_data.resize(num_blocks); + save_file.Seek(DENTRY_SIZE, SEEK_SET); + if (!save_file.ReadBytes(m_save_data.data(), size)) + { + ERROR_LOG(EXPANSIONINTERFACE, "Failed to read data from GCI file %s", m_filename.c_str()); + m_save_data.clear(); + return false; + } + } + return true; +} + +bool GCIFile::HasCopyProtection() const +{ + if ((strcmp(reinterpret_cast(m_gci_header.m_filename.data()), "PSO_SYSTEM") == 0) || + (strcmp(reinterpret_cast(m_gci_header.m_filename.data()), "PSO3_SYSTEM") == 0) || + (strcmp(reinterpret_cast(m_gci_header.m_filename.data()), "f_zero.dat") == 0)) + return true; + return false; +} + +int GCIFile::UsesBlock(u16 block_num) +{ + for (u16 i = 0; i < m_used_blocks.size(); ++i) + { + if (m_used_blocks[i] == block_num) + return i; + } + return -1; +} + +void GCIFile::DoState(PointerWrap& p) +{ + p.DoPOD(m_gci_header); + p.Do(m_dirty); + p.Do(m_filename); + int num_blocks = (int)m_save_data.size(); + p.Do(num_blocks); + m_save_data.resize(num_blocks); + for (auto itr = m_save_data.begin(); itr != m_save_data.end(); ++itr) + { + p.DoPOD(*itr); + } + p.Do(m_used_blocks); +} diff --git a/Source/Core/Core/HW/GCMemcard/GCIFile.h b/Source/Core/Core/HW/GCMemcard/GCIFile.h new file mode 100644 index 0000000000..330c519612 --- /dev/null +++ b/Source/Core/Core/HW/GCMemcard/GCIFile.h @@ -0,0 +1,29 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" +#include "Core/HW/GCMemcard/GCMemcard.h" + +class PointerWrap; + +class GCIFile +{ +public: + bool LoadHeader(); + bool LoadSaveBlocks(); + bool HasCopyProtection() const; + void DoState(PointerWrap& p); + int UsesBlock(u16 blocknum); + + DEntry m_gci_header; + std::vector m_save_data; + std::vector m_used_blocks; + bool m_dirty; + std::string m_filename; +}; diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcard.h b/Source/Core/Core/HW/GCMemcard/GCMemcard.h index 75017fe28b..8d735ced1f 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcard.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcard.h @@ -337,30 +337,6 @@ struct BlockAlloc static_assert(sizeof(BlockAlloc) == BLOCK_SIZE); #pragma pack(pop) -class GCIFile -{ -public: - bool LoadSaveBlocks(); - bool HasCopyProtection() const - { - if ((strcmp(reinterpret_cast(m_gci_header.m_filename.data()), "PSO_SYSTEM") == - 0) || - (strcmp(reinterpret_cast(m_gci_header.m_filename.data()), "PSO3_SYSTEM") == - 0) || - (strcmp(reinterpret_cast(m_gci_header.m_filename.data()), "f_zero.dat") == 0)) - return true; - return false; - } - - void DoState(PointerWrap& p); - DEntry m_gci_header; - std::vector m_save_data; - std::vector m_used_blocks; - int UsesBlock(u16 blocknum); - bool m_dirty; - std::string m_filename; -}; - class GCMemcard { private: diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp index 0353e9e4b8..54fae94d24 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.cpp @@ -32,96 +32,64 @@ const int NO_INDEX = -1; static const char* MC_HDR = "MC_SYSTEM_AREA"; -int GCMemcardDirectory::LoadGCI(const std::string& file_name, bool current_game_only) +bool GCMemcardDirectory::LoadGCI(GCIFile gci) { - File::IOFile gci_file(file_name, "rb"); - if (gci_file) + // check if any already loaded file has the same internal name as the new file + for (const GCIFile& already_loaded_gci : m_saves) { - GCIFile gci; - gci.m_filename = file_name; - gci.m_dirty = false; - if (!gci_file.ReadBytes(&(gci.m_gci_header), DENTRY_SIZE)) + if (gci.m_gci_header.GCI_FileName() == already_loaded_gci.m_gci_header.GCI_FileName()) { - ERROR_LOG(EXPANSIONINTERFACE, "%s failed to read header", file_name.c_str()); - return NO_INDEX; + ERROR_LOG(EXPANSIONINTERFACE, + "%s\nwas not loaded because it has the same internal filename as previously " + "loaded save\n%s", + gci.m_filename.c_str(), already_loaded_gci.m_filename.c_str()); + return false; } - - std::string gci_filename = gci.m_gci_header.GCI_FileName(); - for (u16 i = 0; i < m_loaded_saves.size(); ++i) - { - if (m_loaded_saves[i] == gci_filename) - { - PanicAlertT("%s\nwas not loaded because it has the same internal filename as previously " - "loaded save\n%s", - gci.m_filename.c_str(), m_saves[i].m_filename.c_str()); - return NO_INDEX; - } - } - - u16 num_blocks = gci.m_gci_header.m_block_count; - // largest number of free blocks on a memory card - // in reality, there are not likely any valid gci files > 251 blocks - if (num_blocks > 2043) - { - PanicAlertT( - "%s\nwas not loaded because it is an invalid GCI.\n Number of blocks claimed to be %u", - gci.m_filename.c_str(), num_blocks); - return NO_INDEX; - } - - u32 size = num_blocks * BLOCK_SIZE; - u64 file_size = gci_file.GetSize(); - if (file_size != size + DENTRY_SIZE) - { - PanicAlertT("%s\nwas not loaded because it is an invalid GCI.\n File size (0x%" PRIx64 - ") does not match the size recorded in the header (0x%x)", - gci.m_filename.c_str(), file_size, size + DENTRY_SIZE); - return NO_INDEX; - } - - if (m_game_id == BE32(gci.m_gci_header.m_gamecode.data())) - { - gci.LoadSaveBlocks(); - } - else - { - if (current_game_only) - { - return NO_INDEX; - } - int total_blocks = m_hdr.m_size_mb * MBIT_TO_BLOCKS - MC_FST_BLOCKS; - int free_blocks = m_bat1.m_free_blocks; - if (total_blocks > free_blocks * 10) - { - PanicAlertT("%s\nwas not loaded because there is less than 10%% free blocks available on " - "the memory card\n" - "Total Blocks: %d; Free Blocks: %d", - gci.m_filename.c_str(), total_blocks, free_blocks); - return NO_INDEX; - } - } - u16 first_block = m_bat1.AssignBlocksContiguous(num_blocks); - if (first_block == 0xFFFF) - { - PanicAlertT( - "%s\nwas not loaded because there are not enough free blocks on the virtual memory card", - file_name.c_str()); - return NO_INDEX; - } - gci.m_gci_header.m_first_block = first_block; - if (gci.HasCopyProtection() && gci.LoadSaveBlocks()) - { - GCMemcard::PSO_MakeSaveGameValid(m_hdr, gci.m_gci_header, gci.m_save_data); - GCMemcard::FZEROGX_MakeSaveGameValid(m_hdr, gci.m_gci_header, gci.m_save_data); - } - int idx = (int)m_saves.size(); - m_dir1.Replace(gci.m_gci_header, idx); - m_saves.push_back(std::move(gci)); - SetUsedBlocks(idx); - - return idx; } - return NO_INDEX; + + // check if this file has a valid block size + // 2043 is the largest number of free blocks on a memory card + // in reality, there are not likely any valid gci files > 251 blocks + const u16 num_blocks = gci.m_gci_header.m_block_count; + if (num_blocks > 2043) + { + ERROR_LOG(EXPANSIONINTERFACE, + "%s\nwas not loaded because it is an invalid GCI.\nNumber of blocks claimed to be %u", + gci.m_filename.c_str(), num_blocks); + return false; + } + + if (!gci.LoadSaveBlocks()) + { + ERROR_LOG(EXPANSIONINTERFACE, "Failed to load data of %s", gci.m_filename.c_str()); + return false; + } + + // reserve storage for the save file in the BAT + u16 first_block = m_bat1.AssignBlocksContiguous(num_blocks); + if (first_block == 0xFFFF) + { + ERROR_LOG( + EXPANSIONINTERFACE, + "%s\nwas not loaded because there are not enough free blocks on the virtual memory card", + gci.m_filename.c_str()); + return false; + } + gci.m_gci_header.m_first_block = first_block; + + if (gci.HasCopyProtection()) + { + GCMemcard::PSO_MakeSaveGameValid(m_hdr, gci.m_gci_header, gci.m_save_data); + GCMemcard::FZEROGX_MakeSaveGameValid(m_hdr, gci.m_gci_header, gci.m_save_data); + } + + // actually load save file into memory card + int idx = (int)m_saves.size(); + m_dir1.Replace(gci.m_gci_header, idx); + m_saves.push_back(std::move(gci)); + SetUsedBlocks(idx); + + return true; } // This is only used by NetPlay but it made sense to put it here to keep the relevant code together @@ -187,34 +155,69 @@ GCMemcardDirectory::GCMemcardDirectory(const std::string& directory, int slot, u File::IOFile((m_save_directory + MC_HDR), "rb").ReadBytes(&m_hdr, BLOCK_SIZE); } + const bool current_game_only = Config::Get(Config::MAIN_GCI_FOLDER_CURRENT_GAME_ONLY); std::vector filenames = Common::DoFileSearch({m_save_directory}, {".gci"}); - if (filenames.size() > 112) + // split up into files for current games we should definitely load, + // and files for other games that we don't care too much about + std::vector gci_current_game; + std::vector gci_other_games; + for (const std::string& filename : filenames) { - Core::DisplayMessage("Warning: There are more than 112 save files on this memory card.\n" - " Only loading the first 112 in the folder, unless the game ID is the " - "same as the current game's ID", - 4000); + GCIFile gci; + gci.m_filename = filename; + gci.m_dirty = false; + if (!gci.LoadHeader()) + { + ERROR_LOG(EXPANSIONINTERFACE, "Failed to load header of %s", filename.c_str()); + continue; + } + + if (m_game_id == BE32(gci.m_gci_header.m_gamecode.data())) + gci_current_game.emplace_back(std::move(gci)); + else if (!current_game_only) + gci_other_games.emplace_back(std::move(gci)); } - for (const std::string& gci_file : filenames) + m_saves.reserve(DIRLEN); + + // load files for current game + size_t failed_loads_current_game = 0; + for (GCIFile& gci : gci_current_game) { - if (m_saves.size() == DIRLEN) + if (!LoadGCI(std::move(gci))) { - PanicAlertT( - "There are too many GCI files in the folder\n%s.\nOnly the first 127 will be available", - m_save_directory.c_str()); + // keep track of how many files failed to load for the current game so we can display a + // message to the user informing them why some of their saves may not be loaded + ++failed_loads_current_game; + } + } + + // leave about 10% of free space on the card if possible + const int total_blocks = m_hdr.m_size_mb * MBIT_TO_BLOCKS - MC_FST_BLOCKS; + const int reserved_blocks = total_blocks / 10; + + // load files for other games + for (GCIFile& gci : gci_other_games) + { + // leave some free file entries for new saves that might be created + if (m_saves.size() > 112) break; - } - int index = LoadGCI(gci_file, m_saves.size() > 112 || - Config::Get(Config::MAIN_GCI_FOLDER_CURRENT_GAME_ONLY)); - if (index != NO_INDEX) - { - m_loaded_saves.push_back(m_saves.at(index).m_gci_header.GCI_FileName()); - } + + // leave some free blocks for new saves that might be created + const int free_blocks = m_bat1.m_free_blocks; + const int gci_blocks = gci.m_gci_header.m_block_count; + if (free_blocks - gci_blocks < reserved_blocks) + continue; + + LoadGCI(std::move(gci)); + } + + if (failed_loads_current_game > 0) + { + Core::DisplayMessage("Warning: Save file(s) of the current game failed to load.", 10000); } - m_loaded_saves.clear(); m_dir1.FixChecksums(); m_dir2 = m_dir1; m_bat2 = m_bat1; @@ -693,56 +696,6 @@ void GCMemcardDirectory::DoState(PointerWrap& p) } } -bool GCIFile::LoadSaveBlocks() -{ - if (m_save_data.empty()) - { - if (m_filename.empty()) - return false; - - File::IOFile save_file(m_filename, "rb"); - if (!save_file) - return false; - - INFO_LOG(EXPANSIONINTERFACE, "Reading savedata from disk for %s", m_filename.c_str()); - save_file.Seek(DENTRY_SIZE, SEEK_SET); - u16 num_blocks = m_gci_header.m_block_count; - m_save_data.resize(num_blocks); - if (!save_file.ReadBytes(m_save_data.data(), num_blocks * BLOCK_SIZE)) - { - PanicAlertT("Failed to read data from GCI file %s", m_filename.c_str()); - m_save_data.clear(); - return false; - } - } - return true; -} - -int GCIFile::UsesBlock(u16 block_num) -{ - for (u16 i = 0; i < m_used_blocks.size(); ++i) - { - if (m_used_blocks[i] == block_num) - return i; - } - return -1; -} - -void GCIFile::DoState(PointerWrap& p) -{ - p.DoPOD(m_gci_header); - p.Do(m_dirty); - p.Do(m_filename); - int num_blocks = (int)m_save_data.size(); - p.Do(num_blocks); - m_save_data.resize(num_blocks); - for (auto itr = m_save_data.begin(); itr != m_save_data.end(); ++itr) - { - p.DoPOD(*itr); - } - p.Do(m_used_blocks); -} - void MigrateFromMemcardFile(const std::string& directory_name, int card_index) { File::CreateFullPath(directory_name); diff --git a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h index ce784b8101..473c1b713c 100644 --- a/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h +++ b/Source/Core/Core/HW/GCMemcard/GCMemcardDirectory.h @@ -10,6 +10,7 @@ #include #include "Common/Event.h" +#include "Core/HW/GCMemcard/GCIFile.h" #include "Core/HW/GCMemcard/GCMemcard.h" // Uncomment this to write the system data of the memorycard from directory to disc @@ -39,7 +40,7 @@ public: void DoState(PointerWrap& p) override; private: - int LoadGCI(const std::string& file_name, bool current_game_only); + bool LoadGCI(GCIFile gci); inline s32 SaveAreaRW(u32 block, bool writing = false); // s32 DirectoryRead(u32 offset, u32 length, u8* dest_address); s32 DirectoryWrite(u32 dest_address, u32 length, const u8* src_address); @@ -55,7 +56,6 @@ private: BlockAlloc m_bat1, m_bat2; std::vector m_saves; - std::vector m_loaded_saves; std::string m_save_directory; Common::Event m_flush_trigger; std::mutex m_write_mutex;