dolphin/Source/Core/UICommon/GameFileCache.cpp

289 lines
8 KiB
C++

// Copyright 2017 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "UICommon/GameFileCache.h"
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/FileSearch.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "DiscIO/DirectoryBlob.h"
#include "UICommon/GameFile.h"
namespace UICommon
{
static constexpr u32 CACHE_REVISION = 21; // Last changed in PR 10187
std::vector<std::string> FindAllGamePaths(const std::vector<std::string>& directories_to_scan,
bool recursive_scan)
{
static const std::vector<std::string> search_extensions = {".gcm", ".tgc", ".iso", ".ciso",
".gcz", ".wbfs", ".wia", ".rvz",
".wad", ".dol", ".elf", ".json"};
// TODO: We could process paths iteratively as they are found
return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);
}
GameFileCache::GameFileCache() : m_path(File::GetUserPath(D_CACHE_IDX) + "gamelist.cache")
{
}
void GameFileCache::ForEach(std::function<void(const std::shared_ptr<const GameFile>&)> f) const
{
for (const std::shared_ptr<GameFile>& item : m_cached_files)
f(item);
}
size_t GameFileCache::GetSize() const
{
return m_cached_files.size();
}
void GameFileCache::Clear(DeleteOnDisk delete_on_disk)
{
if (delete_on_disk != DeleteOnDisk::No)
File::Delete(m_path);
m_cached_files.clear();
}
std::shared_ptr<const GameFile> GameFileCache::AddOrGet(const std::string& path,
bool* cache_changed)
{
auto it = std::find_if(
m_cached_files.begin(), m_cached_files.end(),
[&path](const std::shared_ptr<GameFile>& file) { return file->GetFilePath() == path; });
const bool found = it != m_cached_files.cend();
if (!found)
{
std::shared_ptr<UICommon::GameFile> game = std::make_shared<GameFile>(path);
if (!game->IsValid())
return nullptr;
m_cached_files.emplace_back(std::move(game));
}
std::shared_ptr<GameFile>& result = found ? *it : m_cached_files.back();
if (UpdateAdditionalMetadata(&result) || !found)
*cache_changed = true;
return result;
}
bool GameFileCache::Update(
const std::vector<std::string>& all_game_paths,
std::function<void(const std::shared_ptr<const GameFile>&)> game_added_to_cache,
std::function<void(const std::string&)> game_removed_from_cache,
const std::atomic_bool& processing_halted)
{
// Copy game paths into a set, except ones that match DiscIO::ShouldHideFromGameList.
// TODO: Prevent DoFileSearch from looking inside /files/ directories of DirectoryBlobs at all?
// TODO: Make DoFileSearch support filter predicates so we don't have remove things afterwards?
std::unordered_set<std::string> game_paths;
game_paths.reserve(all_game_paths.size());
for (const std::string& path : all_game_paths)
{
if (!DiscIO::ShouldHideFromGameList(path))
game_paths.insert(path);
}
bool cache_changed = false;
// Delete paths that aren't in game_paths from m_cached_files,
// while simultaneously deleting paths that are in m_cached_files from game_paths.
// For the sake of speed, we don't care about maintaining the order of m_cached_files.
{
auto it = m_cached_files.begin();
auto end = m_cached_files.end();
while (it != end)
{
if (processing_halted)
break;
if (game_paths.erase((*it)->GetFilePath()))
{
++it;
}
else
{
if (game_removed_from_cache)
game_removed_from_cache((*it)->GetFilePath());
cache_changed = true;
--end;
*it = std::move(*end);
}
}
m_cached_files.erase(it, m_cached_files.end());
}
// Now that the previous loop has run, game_paths only contains paths that
// aren't in m_cached_files, so we simply add all of them to m_cached_files.
for (const std::string& path : game_paths)
{
if (processing_halted)
break;
auto file = std::make_shared<GameFile>(path);
if (file->IsValid())
{
if (game_added_to_cache)
game_added_to_cache(file);
cache_changed = true;
m_cached_files.push_back(std::move(file));
}
}
return cache_changed;
}
bool GameFileCache::UpdateAdditionalMetadata(
std::function<void(const std::shared_ptr<const GameFile>&)> game_updated,
const std::atomic_bool& processing_halted)
{
bool cache_changed = false;
for (std::shared_ptr<GameFile>& file : m_cached_files)
{
if (processing_halted)
break;
const bool updated = UpdateAdditionalMetadata(&file);
cache_changed |= updated;
if (game_updated && updated)
game_updated(file);
}
return cache_changed;
}
bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr<GameFile>* game_file)
{
const bool xml_metadata_changed = (*game_file)->XMLMetadataChanged();
const bool wii_banner_changed = (*game_file)->WiiBannerChanged();
const bool custom_banner_changed = (*game_file)->CustomBannerChanged();
(*game_file)->DownloadDefaultCover();
const bool default_cover_changed = (*game_file)->DefaultCoverChanged();
const bool custom_cover_changed = (*game_file)->CustomCoverChanged();
if (!xml_metadata_changed && !wii_banner_changed && !custom_banner_changed &&
!default_cover_changed && !custom_cover_changed)
{
return false;
}
// If a cached file needs an update, apply the updates to a copy and delete the original.
// This makes the usage of cached files in other threads safe.
std::shared_ptr<GameFile> copy = std::make_shared<GameFile>(**game_file);
if (xml_metadata_changed)
copy->XMLMetadataCommit();
if (wii_banner_changed)
copy->WiiBannerCommit();
if (custom_banner_changed)
copy->CustomBannerCommit();
if (default_cover_changed)
copy->DefaultCoverCommit();
if (custom_cover_changed)
copy->CustomCoverCommit();
std::atomic_store(game_file, std::move(copy));
return true;
}
bool GameFileCache::Load()
{
return SyncCacheFile(false);
}
bool GameFileCache::Save()
{
return SyncCacheFile(true);
}
bool GameFileCache::SyncCacheFile(bool save)
{
const char* open_mode = save ? "wb" : "rb";
File::IOFile f(m_path, open_mode);
if (!f)
return false;
bool success = false;
if (save)
{
// Measure the size of the buffer.
u8* ptr = nullptr;
PointerWrap p_measure(&ptr, 0, PointerWrap::MODE_MEASURE);
DoState(&p_measure);
const size_t buffer_size = reinterpret_cast<size_t>(ptr);
// Then actually do the write.
std::vector<u8> buffer(buffer_size);
ptr = buffer.data();
PointerWrap p(&ptr, buffer_size, PointerWrap::MODE_WRITE);
DoState(&p, buffer_size);
if (f.WriteBytes(buffer.data(), buffer.size()))
success = true;
}
else
{
std::vector<u8> buffer(f.GetSize());
if (!buffer.empty() && f.ReadBytes(buffer.data(), buffer.size()))
{
u8* ptr = buffer.data();
PointerWrap p(&ptr, buffer.size(), PointerWrap::MODE_READ);
DoState(&p, buffer.size());
if (p.GetMode() == PointerWrap::MODE_READ)
success = true;
}
}
if (!success)
{
// If some file operation failed, try to delete the probably-corrupted cache
f.Close();
File::Delete(m_path);
}
return success;
}
void GameFileCache::DoState(PointerWrap* p, u64 size)
{
struct
{
u32 revision;
u64 expected_size;
} header = {CACHE_REVISION, size};
p->Do(header);
if (p->GetMode() == PointerWrap::MODE_READ)
{
if (header.revision != CACHE_REVISION || header.expected_size != size)
{
p->SetMode(PointerWrap::MODE_MEASURE);
return;
}
}
p->DoEachElement(m_cached_files, [](PointerWrap& state, std::shared_ptr<GameFile>& elem) {
if (state.GetMode() == PointerWrap::MODE_READ)
elem = std::make_shared<GameFile>();
elem->DoState(state);
});
}
} // namespace UICommon