2017-12-31 19:33:36 +00:00
|
|
|
// Copyright 2017 Dolphin Emulator Project
|
|
|
|
// Licensed under GPLv2+
|
|
|
|
// Refer to the license.txt file included.
|
|
|
|
|
|
|
|
#include "UICommon/GameFileCache.h"
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
#include <cstddef>
|
|
|
|
#include <functional>
|
|
|
|
#include <list>
|
|
|
|
#include <memory>
|
|
|
|
#include <mutex>
|
|
|
|
#include <string>
|
|
|
|
#include <unordered_set>
|
|
|
|
#include <vector>
|
|
|
|
|
|
|
|
#include "Common/ChunkFile.h"
|
|
|
|
#include "Common/CommonTypes.h"
|
|
|
|
#include "Common/File.h"
|
|
|
|
#include "Common/FileSearch.h"
|
|
|
|
#include "Common/FileUtil.h"
|
|
|
|
|
|
|
|
#include "Core/TitleDatabase.h"
|
|
|
|
|
|
|
|
#include "DiscIO/DirectoryBlob.h"
|
|
|
|
|
|
|
|
#include "UICommon/GameFile.h"
|
|
|
|
|
|
|
|
namespace UICommon
|
|
|
|
{
|
2018-03-10 21:41:49 +00:00
|
|
|
static constexpr u32 CACHE_REVISION = 10; // Last changed in PR 6429
|
2017-12-31 19:33:36 +00:00
|
|
|
|
|
|
|
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", ".wad", ".dol", ".elf"};
|
|
|
|
|
|
|
|
// TODO: We could process paths iteratively as they are found
|
|
|
|
return Common::DoFileSearch(directories_to_scan, search_extensions, recursive_scan);
|
|
|
|
}
|
|
|
|
|
|
|
|
void GameFileCache::ForEach(std::function<void(const std::shared_ptr<const GameFile>&)> f) const
|
|
|
|
{
|
|
|
|
for (const std::shared_ptr<const GameFile>& item : m_cached_files)
|
|
|
|
f(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
void GameFileCache::Clear()
|
|
|
|
{
|
|
|
|
m_cached_files.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::shared_ptr<const GameFile> GameFileCache::AddOrGet(const std::string& path,
|
|
|
|
bool* cache_changed,
|
|
|
|
const Core::TitleDatabase& title_database)
|
|
|
|
{
|
|
|
|
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)
|
2018-03-29 19:52:21 +00:00
|
|
|
{
|
|
|
|
std::shared_ptr<UICommon::GameFile> game = std::make_shared<GameFile>(path);
|
|
|
|
if (!game->IsValid())
|
|
|
|
return nullptr;
|
|
|
|
m_cached_files.emplace_back(std::move(game));
|
|
|
|
}
|
2017-12-31 19:33:36 +00:00
|
|
|
std::shared_ptr<GameFile>& result = found ? *it : m_cached_files.back();
|
|
|
|
if (UpdateAdditionalMetadata(&result, title_database) || !found)
|
|
|
|
*cache_changed = true;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-05-26 16:22:14 +00:00
|
|
|
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)
|
2017-12-31 19:33:36 +00:00
|
|
|
{
|
|
|
|
// 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,
|
2018-05-26 17:09:33 +00:00
|
|
|
// while simultaneously deleting paths that are in m_cached_files from game_paths.
|
2017-12-31 19:33:36 +00:00
|
|
|
// 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 (game_paths.erase((*it)->GetFilePath()))
|
|
|
|
{
|
|
|
|
++it;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-05-26 16:22:14 +00:00
|
|
|
if (game_removed_from_cache)
|
|
|
|
game_removed_from_cache((*it)->GetFilePath());
|
|
|
|
|
2017-12-31 19:33:36 +00:00
|
|
|
cache_changed = true;
|
|
|
|
--end;
|
|
|
|
*it = std::move(*end);
|
|
|
|
}
|
|
|
|
}
|
2018-05-27 20:08:12 +00:00
|
|
|
m_cached_files.erase(it, m_cached_files.end());
|
2017-12-31 19:33:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2018-05-26 16:22:14 +00:00
|
|
|
for (const std::string& path : game_paths)
|
2017-12-31 19:33:36 +00:00
|
|
|
{
|
|
|
|
auto file = std::make_shared<GameFile>(path);
|
|
|
|
if (file->IsValid())
|
|
|
|
{
|
2018-05-26 16:22:14 +00:00
|
|
|
if (game_added_to_cache)
|
|
|
|
game_added_to_cache(file);
|
|
|
|
|
2017-12-31 19:33:36 +00:00
|
|
|
cache_changed = true;
|
|
|
|
m_cached_files.push_back(std::move(file));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cache_changed;
|
|
|
|
}
|
|
|
|
|
2018-05-26 16:22:14 +00:00
|
|
|
bool GameFileCache::UpdateAdditionalMetadata(
|
|
|
|
const Core::TitleDatabase& title_database,
|
|
|
|
std::function<void(const std::shared_ptr<const GameFile>&)> game_updated)
|
2017-12-31 19:33:36 +00:00
|
|
|
{
|
|
|
|
bool cache_changed = false;
|
|
|
|
|
2018-05-26 16:22:14 +00:00
|
|
|
for (std::shared_ptr<GameFile>& file : m_cached_files)
|
|
|
|
{
|
|
|
|
const bool updated = UpdateAdditionalMetadata(&file, title_database);
|
|
|
|
cache_changed |= updated;
|
|
|
|
if (game_updated && updated)
|
|
|
|
game_updated(file);
|
|
|
|
}
|
2017-12-31 19:33:36 +00:00
|
|
|
|
|
|
|
return cache_changed;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GameFileCache::UpdateAdditionalMetadata(std::shared_ptr<GameFile>* game_file,
|
|
|
|
const Core::TitleDatabase& title_database)
|
|
|
|
{
|
2018-03-10 21:41:49 +00:00
|
|
|
const bool wii_banner_changed = (*game_file)->WiiBannerChanged();
|
|
|
|
const bool custom_banner_changed = (*game_file)->CustomBannerChanged();
|
2017-12-31 19:33:36 +00:00
|
|
|
const bool custom_title_changed = (*game_file)->CustomNameChanged(title_database);
|
2018-03-10 21:41:49 +00:00
|
|
|
if (!wii_banner_changed && !custom_banner_changed && !custom_title_changed)
|
2017-12-31 19:33:36 +00:00
|
|
|
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);
|
2018-03-10 21:41:49 +00:00
|
|
|
if (wii_banner_changed)
|
|
|
|
copy->WiiBannerCommit();
|
|
|
|
if (custom_banner_changed)
|
|
|
|
copy->CustomBannerCommit();
|
2017-12-31 19:33:36 +00:00
|
|
|
if (custom_title_changed)
|
|
|
|
copy->CustomNameCommit();
|
|
|
|
*game_file = std::move(copy);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GameFileCache::Load()
|
|
|
|
{
|
|
|
|
return SyncCacheFile(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GameFileCache::Save()
|
|
|
|
{
|
|
|
|
return SyncCacheFile(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool GameFileCache::SyncCacheFile(bool save)
|
|
|
|
{
|
|
|
|
std::string filename(File::GetUserPath(D_CACHE_IDX) + "gamelist.cache");
|
|
|
|
const char* open_mode = save ? "wb" : "rb";
|
|
|
|
File::IOFile f(filename, open_mode);
|
|
|
|
if (!f)
|
|
|
|
return false;
|
|
|
|
bool success = false;
|
|
|
|
if (save)
|
|
|
|
{
|
|
|
|
// Measure the size of the buffer.
|
|
|
|
u8* ptr = nullptr;
|
|
|
|
PointerWrap p(&ptr, PointerWrap::MODE_MEASURE);
|
|
|
|
DoState(&p);
|
|
|
|
const size_t buffer_size = reinterpret_cast<size_t>(ptr);
|
|
|
|
|
|
|
|
// Then actually do the write.
|
|
|
|
std::vector<u8> buffer(buffer_size);
|
|
|
|
ptr = &buffer[0];
|
|
|
|
p.SetMode(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.size() && f.ReadBytes(buffer.data(), buffer.size()))
|
|
|
|
{
|
|
|
|
u8* ptr = buffer.data();
|
|
|
|
PointerWrap p(&ptr, 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(filename);
|
|
|
|
}
|
|
|
|
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 DiscIO
|