Compare commits
14 Commits
vk-fix-oom
...
fs_externa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a89cef721 | ||
|
|
7b37ac2992 | ||
|
|
aa8b4ccdbe | ||
|
|
365f4dfee8 | ||
|
|
a065adbf94 | ||
|
|
6413de66ac | ||
|
|
336b9a806f | ||
|
|
78b9b7808e | ||
|
|
b311c9c3f4 | ||
|
|
695ae6a71f | ||
|
|
809a1b4d06 | ||
|
|
cd92452202 | ||
|
|
6974dc2247 | ||
|
|
5ba9d3fb9f |
@@ -759,6 +759,7 @@ struct Values {
|
||||
|
||||
// Add-Ons
|
||||
std::map<u64, std::vector<std::string>> disabled_addons;
|
||||
std::vector<std::string> external_dirs;
|
||||
};
|
||||
|
||||
extern Values values;
|
||||
|
||||
@@ -54,6 +54,8 @@ add_library(core STATIC
|
||||
file_sys/control_metadata.cpp
|
||||
file_sys/control_metadata.h
|
||||
file_sys/errors.h
|
||||
file_sys/external_content_index.cpp
|
||||
file_sys/external_content_index.h
|
||||
file_sys/fs_directory.h
|
||||
file_sys/fs_file.h
|
||||
file_sys/fs_filesystem.h
|
||||
|
||||
313
src/core/file_sys/external_content_index.cpp
Normal file
313
src/core/file_sys/external_content_index.cpp
Normal file
@@ -0,0 +1,313 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "core/file_sys/external_content_index.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include "common/fs/fs_util.h"
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/file_sys/common_funcs.h"
|
||||
#include "core/file_sys/content_archive.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/romfs.h"
|
||||
#include "core/file_sys/submission_package.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/loader/loader.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
ExternalContentIndexer::ExternalContentIndexer(VirtualFilesystem vfs,
|
||||
ManualContentProvider& provider,
|
||||
ExternalContentPaths paths)
|
||||
: m_vfs(std::move(vfs)), m_provider(provider), m_paths(std::move(paths)) {}
|
||||
|
||||
void ExternalContentIndexer::Rebuild() {
|
||||
m_provider.ClearAllEntries();
|
||||
m_updates_by_title.clear();
|
||||
m_all_dlc.clear();
|
||||
|
||||
for (const auto& dir : m_paths.update_dirs) {
|
||||
IndexUpdatesDir(dir);
|
||||
}
|
||||
for (const auto& dir : m_paths.dlc_dirs) {
|
||||
IndexDlcDir(dir);
|
||||
}
|
||||
|
||||
Commit();
|
||||
}
|
||||
|
||||
static std::string ToLowerCopy(const std::string& s) {
|
||||
std::string out;
|
||||
out.resize(s.size());
|
||||
std::transform(s.begin(), s.end(), out.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
return out;
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::IndexUpdatesDir(const std::string& dir) {
|
||||
try {
|
||||
const fs::path p = Common::FS::ToU8String(dir);
|
||||
std::error_code ec;
|
||||
if (!fs::exists(p, ec) || ec)
|
||||
return;
|
||||
|
||||
if (fs::is_directory(p, ec) && !ec) {
|
||||
for (const auto& entry : fs::recursive_directory_iterator(
|
||||
p, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (entry.is_directory(ec))
|
||||
continue;
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(entry.path().u8string()), true);
|
||||
}
|
||||
TryIndexLooseDir(Common::FS::ToUTF8String(p.u8string()), true);
|
||||
} else {
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(p.u8string()), true);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Loader, "Error accessing update directory '{}': {}", dir, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::IndexDlcDir(const std::string& dir) {
|
||||
try {
|
||||
const fs::path p = Common::FS::ToU8String(dir);
|
||||
std::error_code ec;
|
||||
if (!fs::exists(p, ec) || ec)
|
||||
return;
|
||||
|
||||
if (fs::is_directory(p, ec) && !ec) {
|
||||
for (const auto& entry : fs::recursive_directory_iterator(
|
||||
p, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (entry.is_directory(ec))
|
||||
continue;
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(entry.path().u8string()), false);
|
||||
}
|
||||
TryIndexLooseDir(Common::FS::ToUTF8String(p.u8string()), false);
|
||||
} else {
|
||||
TryIndexFileAsContainer(Common::FS::ToUTF8String(p.u8string()), false);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR(Loader, "Error accessing DLC directory '{}': {}", dir, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::TryIndexFileAsContainer(const std::string& path, bool is_update) {
|
||||
const auto lower = ToLowerCopy(path);
|
||||
if (lower.size() >= 4 && lower.rfind(".nsp") == lower.size() - 4) {
|
||||
if (auto vf = m_vfs->OpenFile(path, OpenMode::Read)) {
|
||||
ParseContainerNSP(vf, is_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::TryIndexLooseDir(const std::string& dir, bool is_update) {
|
||||
fs::path p = Common::FS::ToU8String(dir);
|
||||
std::error_code ec;
|
||||
if (!fs::is_directory(p, ec) || ec)
|
||||
return;
|
||||
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator(p, fs::directory_options::skip_permission_denied, ec)) {
|
||||
if (ec)
|
||||
break;
|
||||
if (!entry.is_regular_file(ec))
|
||||
continue;
|
||||
const auto path = Common::FS::ToUTF8String(entry.path().u8string());
|
||||
const auto lower = ToLowerCopy(path);
|
||||
if (lower.size() >= 9 && lower.rfind(".cnmt.nca") == lower.size() - 9) {
|
||||
if (auto vf = m_vfs->OpenFile(path, OpenMode::Read)) {
|
||||
ParseLooseCnmtNca(
|
||||
vf, Common::FS::ToUTF8String(entry.path().parent_path().u8string()), is_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::ParseContainerNSP(VirtualFile file, bool is_update) {
|
||||
if (file == nullptr)
|
||||
return;
|
||||
NSP nsp(file);
|
||||
if (nsp.GetStatus() != Loader::ResultStatus::Success) {
|
||||
LOG_WARNING(Loader, "ExternalContent: NSP parse failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto title_map = nsp.GetNCAs();
|
||||
if (title_map.empty())
|
||||
return;
|
||||
|
||||
for (const auto& [title_id, nca_map] : title_map) {
|
||||
std::shared_ptr<NCA> meta_nca;
|
||||
for (const auto& [key, nca_ptr] : nca_map) {
|
||||
if (nca_ptr && nca_ptr->GetType() == NCAContentType::Meta) {
|
||||
meta_nca = nca_ptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!meta_nca)
|
||||
continue;
|
||||
|
||||
auto cnmt_opt = ExtractCnmtFromMetaNca(*meta_nca);
|
||||
if (!cnmt_opt)
|
||||
continue;
|
||||
const auto& cnmt = *cnmt_opt;
|
||||
|
||||
const auto base_id = BaseTitleId(title_id);
|
||||
|
||||
if (is_update && cnmt.GetType() == TitleType::Update) {
|
||||
ParsedUpdate candidate{};
|
||||
// Register updates under their Update TID so PatchManager can find/apply them
|
||||
candidate.title_id = FileSys::GetUpdateTitleID(base_id);
|
||||
candidate.version = cnmt.GetTitleVersion();
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto it = nca_map.find({cnmt.GetType(), rec.type});
|
||||
if (it != nca_map.end() && it->second) {
|
||||
candidate.ncas[rec.type] = it->second->GetBaseFile();
|
||||
}
|
||||
}
|
||||
auto& vec = m_updates_by_title[base_id];
|
||||
vec.emplace_back(std::move(candidate));
|
||||
} else if (cnmt.GetType() == TitleType::AOC) {
|
||||
const auto dlc_title_id = cnmt.GetTitleID();
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto it = nca_map.find({cnmt.GetType(), rec.type});
|
||||
if (it != nca_map.end() && it->second) {
|
||||
m_all_dlc.push_back(
|
||||
ParsedDlcRecord{dlc_title_id, {}, it->second->GetBaseFile()});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::ParseLooseCnmtNca(VirtualFile meta_nca_file, const std::string& folder,
|
||||
bool is_update) {
|
||||
if (meta_nca_file == nullptr)
|
||||
return;
|
||||
|
||||
NCA meta(meta_nca_file);
|
||||
|
||||
if (!IsMeta(meta))
|
||||
return;
|
||||
|
||||
auto cnmt_opt = ExtractCnmtFromMetaNca(meta);
|
||||
if (!cnmt_opt)
|
||||
return;
|
||||
const auto& cnmt = *cnmt_opt;
|
||||
|
||||
const auto base_id = BaseTitleId(cnmt.GetTitleID());
|
||||
|
||||
if (is_update && cnmt.GetType() == TitleType::Update) {
|
||||
ParsedUpdate candidate{};
|
||||
// Register updates under their Update TID so PatchManager can find/apply them
|
||||
candidate.title_id = FileSys::GetUpdateTitleID(base_id);
|
||||
candidate.version = cnmt.GetTitleVersion();
|
||||
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto file_name = Common::HexToString(rec.nca_id) + ".nca";
|
||||
const auto full = Common::FS::ToUTF8String(
|
||||
(fs::path(Common::FS::ToU8String(folder)) / fs::path(file_name)).u8string());
|
||||
if (auto vf = m_vfs->OpenFile(full, OpenMode::Read)) {
|
||||
candidate.ncas[rec.type] = vf;
|
||||
}
|
||||
}
|
||||
|
||||
auto& vec = m_updates_by_title[base_id];
|
||||
vec.emplace_back(std::move(candidate));
|
||||
} else if (cnmt.GetType() == TitleType::AOC) {
|
||||
const auto dlc_title_id = cnmt.GetTitleID();
|
||||
for (const auto& rec : cnmt.GetContentRecords()) {
|
||||
const auto file_name = Common::HexToString(rec.nca_id) + ".nca";
|
||||
const auto full = Common::FS::ToUTF8String(
|
||||
(fs::path(Common::FS::ToU8String(folder)) / fs::path(file_name)).u8string());
|
||||
if (auto vf = m_vfs->OpenFile(full, OpenMode::Read)) {
|
||||
ParsedDlcRecord dl{dlc_title_id, {}, vf};
|
||||
m_all_dlc.push_back(std::move(dl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<CNMT> ExternalContentIndexer::ExtractCnmtFromMetaNca(const NCA& meta_nca) {
|
||||
if (meta_nca.GetStatus() != Loader::ResultStatus::Success)
|
||||
return std::nullopt;
|
||||
|
||||
const auto subs = meta_nca.GetSubdirectories();
|
||||
if (subs.empty() || !subs[0])
|
||||
return std::nullopt;
|
||||
|
||||
const auto files = subs[0]->GetFiles();
|
||||
if (files.empty() || !files[0])
|
||||
return std::nullopt;
|
||||
|
||||
CNMT cnmt(files[0]);
|
||||
return cnmt;
|
||||
}
|
||||
|
||||
ExternalContentIndexer::TitleID ExternalContentIndexer::BaseTitleId(TitleID id) {
|
||||
return FileSys::GetBaseTitleID(id);
|
||||
}
|
||||
|
||||
bool ExternalContentIndexer::IsMeta(const NCA& nca) {
|
||||
return nca.GetType() == NCAContentType::Meta;
|
||||
}
|
||||
|
||||
void ExternalContentIndexer::Commit() {
|
||||
// Updates: register all discovered versions per base title under unique variant TIDs,
|
||||
// and additionally register the highest version under the canonical update TID for default
|
||||
// usage.
|
||||
size_t update_variants_count = 0;
|
||||
for (auto& [base_title, vec] : m_updates_by_title) {
|
||||
if (vec.empty())
|
||||
continue;
|
||||
// sort ascending by version, dedupe identical versions (for NAND overlap, for example)
|
||||
std::stable_sort(vec.begin(), vec.end(), [](const ParsedUpdate& a, const ParsedUpdate& b) {
|
||||
return a.version < b.version;
|
||||
});
|
||||
vec.erase(std::unique(vec.begin(), vec.end(),
|
||||
[](const ParsedUpdate& a, const ParsedUpdate& b) {
|
||||
return a.version == b.version;
|
||||
}),
|
||||
vec.end());
|
||||
|
||||
// highest version for canonical TID
|
||||
const auto& latest = vec.back();
|
||||
for (const auto& [rtype, file] : latest.ncas) {
|
||||
if (!file)
|
||||
continue;
|
||||
const auto canonical_tid = FileSys::GetUpdateTitleID(base_title);
|
||||
m_provider.AddEntry(TitleType::Update, rtype, canonical_tid, file);
|
||||
}
|
||||
|
||||
// variants under update_tid + i (i starts at1 to avoid colliding with canonical)
|
||||
for (size_t i = 0; i < vec.size(); ++i) {
|
||||
const auto& upd = vec[i];
|
||||
const u64 variant_tid = FileSys::GetUpdateTitleID(base_title) + static_cast<u64>(i + 1);
|
||||
for (const auto& [rtype, file] : upd.ncas) {
|
||||
if (!file)
|
||||
continue;
|
||||
m_provider.AddEntry(TitleType::Update, rtype, variant_tid, file);
|
||||
}
|
||||
}
|
||||
update_variants_count += vec.size();
|
||||
}
|
||||
|
||||
// DLC: additive
|
||||
for (const auto& dlc : m_all_dlc) {
|
||||
if (!dlc.file)
|
||||
continue;
|
||||
m_provider.AddEntry(TitleType::AOC, ContentRecordType::Data, dlc.title_id, dlc.file);
|
||||
}
|
||||
|
||||
LOG_INFO(Loader,
|
||||
"ExternalContent: registered updates for {} titles ({} variants), {} DLC records",
|
||||
m_updates_by_title.size(), update_variants_count, m_all_dlc.size());
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
||||
74
src/core/file_sys/external_content_index.h
Normal file
74
src/core/file_sys/external_content_index.h
Normal file
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
#include "core/file_sys/nca_metadata.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
class ManualContentProvider;
|
||||
class NCA;
|
||||
|
||||
struct ExternalContentPaths {
|
||||
std::vector<std::string> update_dirs;
|
||||
std::vector<std::string> dlc_dirs;
|
||||
};
|
||||
|
||||
class ExternalContentIndexer {
|
||||
public:
|
||||
ExternalContentIndexer(VirtualFilesystem vfs,
|
||||
ManualContentProvider& provider,
|
||||
ExternalContentPaths paths);
|
||||
|
||||
void Rebuild();
|
||||
|
||||
private:
|
||||
using TitleID = u64;
|
||||
|
||||
struct ParsedUpdate {
|
||||
TitleID title_id{};
|
||||
u32 version{};
|
||||
std::unordered_map<ContentRecordType, VirtualFile> ncas;
|
||||
};
|
||||
|
||||
struct ParsedDlcRecord {
|
||||
TitleID title_id{};
|
||||
NcaID nca_id{};
|
||||
VirtualFile file{};
|
||||
};
|
||||
|
||||
void IndexUpdatesDir(const std::string& dir);
|
||||
void IndexDlcDir(const std::string& dir);
|
||||
|
||||
void TryIndexFileAsContainer(const std::string& path, bool is_update);
|
||||
void TryIndexLooseDir(const std::string& dir, bool is_update);
|
||||
|
||||
void ParseContainerNSP(VirtualFile file, bool is_update);
|
||||
void ParseLooseCnmtNca(VirtualFile meta_nca_file, const std::string& folder, bool is_update);
|
||||
|
||||
static std::optional<CNMT> ExtractCnmtFromMetaNca(const NCA& meta_nca);
|
||||
static TitleID BaseTitleId(TitleID id);
|
||||
static bool IsMeta(const NCA& nca);
|
||||
|
||||
void Commit();
|
||||
|
||||
private:
|
||||
VirtualFilesystem m_vfs;
|
||||
ManualContentProvider& m_provider;
|
||||
ExternalContentPaths m_paths;
|
||||
|
||||
std::unordered_map<TitleID, std::vector<ParsedUpdate>> m_updates_by_title;
|
||||
|
||||
std::vector<ParsedDlcRecord> m_all_dlc;
|
||||
};
|
||||
|
||||
} // namespace FileSys
|
||||
@@ -8,6 +8,9 @@
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -64,6 +67,30 @@ std::string FormatTitleVersion(u32 version,
|
||||
return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
|
||||
}
|
||||
|
||||
static std::array<int, 4> ParseVersionComponents(std::string_view label) {
|
||||
std::array<int, 4> out{0, 0, 0, 0};
|
||||
if (!label.empty() && (label.front() == 'v' || label.front() == 'V')) {
|
||||
label.remove_prefix(1);
|
||||
}
|
||||
size_t part = 0;
|
||||
size_t start = 0;
|
||||
std::string s(label);
|
||||
while (part < out.size() && start < s.size()) {
|
||||
size_t dot = s.find('.', start);
|
||||
auto token = s.substr(start, dot == std::string::npos ? std::string::npos : dot - start);
|
||||
try {
|
||||
out[part] = std::stoi(token);
|
||||
} catch (...) {
|
||||
out[part] = 0;
|
||||
}
|
||||
++part;
|
||||
if (dot == std::string::npos)
|
||||
break;
|
||||
start = dot + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Returns a directory with name matching name case-insensitive. Returns nullptr if directory
|
||||
// doesn't have a directory with name.
|
||||
VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) {
|
||||
@@ -117,6 +144,83 @@ void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
|
||||
bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
|
||||
return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
|
||||
}
|
||||
|
||||
static std::vector<u64> EnumerateUpdateVariants(const ContentProvider& provider, u64 base_title_id,
|
||||
ContentRecordType type) {
|
||||
std::vector<u64> tids;
|
||||
const auto entries = provider.ListEntriesFilter(TitleType::Update, type);
|
||||
for (const auto& e : entries) {
|
||||
if (GetBaseTitleID(e.title_id) == base_title_id) {
|
||||
tids.push_back(e.title_id);
|
||||
}
|
||||
}
|
||||
std::sort(tids.begin(), tids.end());
|
||||
tids.erase(std::unique(tids.begin(), tids.end()), tids.end());
|
||||
return tids;
|
||||
}
|
||||
|
||||
static std::string GetUpdateVersionLabel(u64 update_tid,
|
||||
const Service::FileSystem::FileSystemController& fs,
|
||||
const ContentProvider& provider) {
|
||||
PatchManager pm{update_tid, fs, provider};
|
||||
const auto meta = pm.GetControlMetadata();
|
||||
if (meta.first != nullptr) {
|
||||
auto str = meta.first->GetVersionString();
|
||||
if (!str.empty()) {
|
||||
if (str.front() != 'v' && str.front() != 'V') {
|
||||
str.insert(str.begin(), 'v');
|
||||
}
|
||||
return str;
|
||||
}
|
||||
}
|
||||
const auto ver = provider.GetEntryVersion(update_tid).value_or(0);
|
||||
return FormatTitleVersion(ver);
|
||||
}
|
||||
|
||||
static std::optional<u64> ChooseUpdateVariant(const ContentProvider& provider, u64 base_title_id,
|
||||
ContentRecordType type,
|
||||
const Service::FileSystem::FileSystemController& fs) {
|
||||
const auto& disabled = Settings::values.disabled_addons[base_title_id];
|
||||
|
||||
if (std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto candidates = EnumerateUpdateVariants(provider, base_title_id, type);
|
||||
if (candidates.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Sort candidates by numeric version descending, using meta version; fallback to0
|
||||
std::vector<std::pair<u32, u64>> ordered; // (version,uTid)
|
||||
ordered.reserve(candidates.size());
|
||||
for (const auto tid : candidates) {
|
||||
const u32 ver = provider.GetEntryVersion(tid).value_or(0);
|
||||
ordered.emplace_back(ver, tid);
|
||||
}
|
||||
std::sort(ordered.begin(), ordered.end(), [](auto const& a, auto const& b) {
|
||||
return a.first > b.first; // highest version first
|
||||
});
|
||||
|
||||
// Pick the first candidate that is not specifically disabled via "Update vX.Y.Z"
|
||||
for (const auto& [ver, tid] : ordered) {
|
||||
const auto label = GetUpdateVersionLabel(tid, fs, provider);
|
||||
const auto toggle_name = fmt::format("Update {}", label);
|
||||
if (std::find(disabled.cbegin(), disabled.cend(), toggle_name) == disabled.cend()) {
|
||||
return tid;
|
||||
}
|
||||
}
|
||||
|
||||
// All variants disabled, do not apply any update
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static bool HasVariantPreference(const std::vector<std::string>& disabled) {
|
||||
return std::any_of(disabled.begin(), disabled.end(), [](const std::string& s) {
|
||||
return s.rfind("Update v", 0) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
PatchManager::PatchManager(u64 title_id_,
|
||||
@@ -141,13 +245,22 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
||||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||
|
||||
// Game Updates
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
|
||||
std::optional<u64> selected_update_tid;
|
||||
if (!update_disabled) {
|
||||
selected_update_tid = ChooseUpdateVariant(content_provider, title_id,
|
||||
ContentRecordType::Program, fs_controller);
|
||||
if (!selected_update_tid.has_value()) {
|
||||
selected_update_tid = GetUpdateTitleID(title_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
|
||||
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
|
||||
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
|
||||
exefs = update->GetExeFS();
|
||||
if (selected_update_tid.has_value()) {
|
||||
const auto update =
|
||||
content_provider.GetEntry(*selected_update_tid, ContentRecordType::Program);
|
||||
if (update != nullptr && update->GetExeFS() != nullptr) {
|
||||
LOG_INFO(Loader, " ExeFS: Update applied successfully");
|
||||
exefs = update->GetExeFS();
|
||||
}
|
||||
}
|
||||
|
||||
// LayeredExeFS
|
||||
@@ -316,7 +429,8 @@ bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name)
|
||||
return !CollectPatches(patch_dirs, build_id).empty();
|
||||
}
|
||||
|
||||
std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildID& build_id_) const {
|
||||
std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(
|
||||
const BuildID& build_id_) const {
|
||||
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
|
||||
if (load_dir == nullptr) {
|
||||
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
||||
@@ -325,16 +439,19 @@ std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildI
|
||||
|
||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||
auto patch_dirs = load_dir->GetSubdirectories();
|
||||
std::sort(patch_dirs.begin(), patch_dirs.end(), [](auto const& l, auto const& r) { return l->GetName() < r->GetName(); });
|
||||
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
||||
[](auto const& l, auto const& r) { return l->GetName() < r->GetName(); });
|
||||
|
||||
// <mod dir> / <folder> / cheats / <build id>.txt
|
||||
std::vector<Core::Memory::CheatEntry> out;
|
||||
for (const auto& subdir : patch_dirs) {
|
||||
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) == disabled.cend()) {
|
||||
if (auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats"); cheats_dir != nullptr) {
|
||||
if (auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats");
|
||||
cheats_dir != nullptr) {
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true))
|
||||
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||
if (auto const res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false))
|
||||
if (auto const res =
|
||||
ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false))
|
||||
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
||||
}
|
||||
}
|
||||
@@ -344,14 +461,17 @@ std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildI
|
||||
auto const patch_files = load_dir->GetFiles();
|
||||
for (auto const& f : patch_files) {
|
||||
auto const name = f->GetName();
|
||||
if (name.starts_with("cheat_") && std::find(disabled.cbegin(), disabled.cend(), name) == disabled.cend()) {
|
||||
if (name.starts_with("cheat_") &&
|
||||
std::find(disabled.cbegin(), disabled.cend(), name) == disabled.cend()) {
|
||||
std::vector<u8> data(f->GetSize());
|
||||
if (f->Read(data.data(), data.size()) == data.size()) {
|
||||
const Core::Memory::TextCheatParser parser;
|
||||
auto const res = parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
|
||||
auto const res = parser.Parse(
|
||||
std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
|
||||
std::copy(res.begin(), res.end(), std::back_inserter(out));
|
||||
} else {
|
||||
LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}", title_id);
|
||||
LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}",
|
||||
title_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,8 +567,12 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
|
||||
auto romfs = base_romfs;
|
||||
|
||||
// Game Updates
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
const auto update_raw = content_provider.GetEntryRaw(update_tid, type);
|
||||
std::optional<u64> selected_update_tid =
|
||||
ChooseUpdateVariant(content_provider, title_id, type, fs_controller);
|
||||
if (!selected_update_tid.has_value()) {
|
||||
selected_update_tid = GetUpdateTitleID(title_id);
|
||||
}
|
||||
const auto update_raw = content_provider.GetEntryRaw(*selected_update_tid, type);
|
||||
|
||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||
const auto update_disabled =
|
||||
@@ -458,18 +582,45 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
|
||||
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
|
||||
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
||||
new_nca->GetRomFS() != nullptr) {
|
||||
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
|
||||
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
|
||||
const auto ver_num = content_provider.GetEntryVersion(*selected_update_tid).value_or(0);
|
||||
LOG_DEBUG(Loader, " RomFS: Update ({}) applied successfully",
|
||||
FormatTitleVersion(ver_num));
|
||||
romfs = new_nca->GetRomFS();
|
||||
const auto version =
|
||||
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0));
|
||||
} else {
|
||||
LOG_WARNING(Loader, " RomFS: Update NCA is not valid");
|
||||
}
|
||||
} else if (!update_disabled && base_nca != nullptr) {
|
||||
ContentRecordType alt_type = type;
|
||||
|
||||
if (type == ContentRecordType::Program) {
|
||||
alt_type = ContentRecordType::Data;
|
||||
} else if (type == ContentRecordType::Data) {
|
||||
alt_type = ContentRecordType::Program;
|
||||
}
|
||||
|
||||
if (alt_type != type) {
|
||||
const auto alt_update_raw =
|
||||
content_provider.GetEntryRaw(*selected_update_tid, alt_type);
|
||||
if (alt_update_raw != nullptr) {
|
||||
const auto new_nca = std::make_shared<NCA>(alt_update_raw, base_nca);
|
||||
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
||||
new_nca->GetRomFS() != nullptr) {
|
||||
LOG_DEBUG(Loader, " RomFS: Update (fallback {}) applied successfully",
|
||||
alt_type == ContentRecordType::Data ? "DATA" : "PROGRAM");
|
||||
romfs = new_nca->GetRomFS();
|
||||
} else {
|
||||
LOG_WARNING(Loader, " RomFS: Update (fallback) NCA is not valid");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
|
||||
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
|
||||
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
||||
new_nca->GetRomFS() != nullptr) {
|
||||
LOG_INFO(Loader, " RomFS: Update (PACKED) applied successfully");
|
||||
LOG_DEBUG(Loader, " RomFS: Update (PACKED) applied successfully");
|
||||
romfs = new_nca->GetRomFS();
|
||||
} else {
|
||||
LOG_WARNING(Loader, " RomFS: Update (PACKED) NCA is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,36 +640,86 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
||||
std::vector<Patch> out;
|
||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||
|
||||
// Game Updates
|
||||
const auto update_tid = GetUpdateTitleID(title_id);
|
||||
PatchManager update{update_tid, fs_controller, content_provider};
|
||||
const auto metadata = update.GetControlMetadata();
|
||||
const auto& nacp = metadata.first;
|
||||
auto variant_tids =
|
||||
EnumerateUpdateVariants(content_provider, title_id, ContentRecordType::Program);
|
||||
{
|
||||
auto data_tids =
|
||||
EnumerateUpdateVariants(content_provider, title_id, ContentRecordType::Data);
|
||||
variant_tids.insert(variant_tids.end(), data_tids.begin(), data_tids.end());
|
||||
auto control_tids =
|
||||
EnumerateUpdateVariants(content_provider, title_id, ContentRecordType::Control);
|
||||
variant_tids.insert(variant_tids.end(), control_tids.begin(), control_tids.end());
|
||||
std::sort(variant_tids.begin(), variant_tids.end());
|
||||
variant_tids.erase(std::unique(variant_tids.begin(), variant_tids.end()),
|
||||
variant_tids.end());
|
||||
}
|
||||
|
||||
const auto update_disabled =
|
||||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||
Patch update_patch = {.enabled = !update_disabled,
|
||||
.name = "Update",
|
||||
.version = "",
|
||||
.type = PatchType::Update,
|
||||
.program_id = title_id,
|
||||
.title_id = title_id};
|
||||
if (!variant_tids.empty()) {
|
||||
const auto update_disabled =
|
||||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||
|
||||
if (nacp != nullptr) {
|
||||
update_patch.version = nacp->GetVersionString();
|
||||
out.push_back(update_patch);
|
||||
} else {
|
||||
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
||||
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
|
||||
if (meta_ver.value_or(0) == 0) {
|
||||
out.push_back(update_patch);
|
||||
out.push_back({.enabled = !update_disabled,
|
||||
.name = "Update",
|
||||
.version = "",
|
||||
.type = PatchType::Update,
|
||||
.program_id = title_id,
|
||||
.title_id = title_id});
|
||||
|
||||
std::optional<u64> selected_variant_tid;
|
||||
if (!update_disabled) {
|
||||
const bool has_pref = HasVariantPreference(Settings::values.disabled_addons[title_id]);
|
||||
if (has_pref) {
|
||||
selected_variant_tid = ChooseUpdateVariant(
|
||||
content_provider, title_id, ContentRecordType::Program, fs_controller);
|
||||
} else {
|
||||
update_patch.version = FormatTitleVersion(*meta_ver);
|
||||
out.push_back(update_patch);
|
||||
selected_variant_tid = GetUpdateTitleID(title_id);
|
||||
}
|
||||
} else if (update_raw != nullptr) {
|
||||
update_patch.version = "PACKED";
|
||||
out.push_back(update_patch);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, u64>> variant_labels;
|
||||
variant_labels.reserve(variant_tids.size());
|
||||
|
||||
for (const auto tid : variant_tids) {
|
||||
variant_labels.emplace_back(GetUpdateVersionLabel(tid, fs_controller, content_provider),
|
||||
tid);
|
||||
}
|
||||
|
||||
std::sort(variant_labels.begin(), variant_labels.end(),
|
||||
[this](auto const& a, auto const& b) {
|
||||
const auto va = content_provider.GetEntryVersion(a.second).value_or(0);
|
||||
const auto vb = content_provider.GetEntryVersion(b.second).value_or(0);
|
||||
|
||||
if (va != vb)
|
||||
return va > vb;
|
||||
|
||||
const auto ca = ParseVersionComponents(a.first);
|
||||
const auto cb = ParseVersionComponents(b.first);
|
||||
|
||||
if (ca != cb)
|
||||
return ca > cb;
|
||||
|
||||
return a.first > b.first;
|
||||
});
|
||||
|
||||
std::set<std::string> seen_versions;
|
||||
for (const auto& [label, tid] : variant_labels) {
|
||||
std::string version = label;
|
||||
if (!version.empty() && (version.front() == 'v' || version.front() == 'V')) {
|
||||
version.erase(version.begin());
|
||||
}
|
||||
if (seen_versions.find(version) != seen_versions.end()) {
|
||||
continue;
|
||||
}
|
||||
const bool is_selected =
|
||||
selected_variant_tid.has_value() && tid == *selected_variant_tid;
|
||||
const bool variant_disabled = update_disabled || !is_selected;
|
||||
out.push_back({.enabled = !variant_disabled,
|
||||
.name = "Update",
|
||||
.version = version,
|
||||
.type = PatchType::Update,
|
||||
.program_id = title_id,
|
||||
.title_id = tid});
|
||||
seen_versions.insert(version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -208,6 +211,7 @@ enum class ContentProviderUnionSlot {
|
||||
UserNAND, ///< User NAND
|
||||
SDMC, ///< SD Card
|
||||
FrontendManual, ///< Frontend-defined game list or similar
|
||||
External ///< External Updates/DLCs (not installed to NAND)
|
||||
};
|
||||
|
||||
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
|
||||
|
||||
@@ -203,7 +203,7 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
|
||||
FileReference& reference) {
|
||||
std::unique_lock lk{list_lock};
|
||||
|
||||
// Temporarily remove from list.
|
||||
// Temporarily remove from list (regardless of the current list).
|
||||
this->RemoveReferenceFromListLocked(reference);
|
||||
|
||||
// Restore file if needed.
|
||||
@@ -211,7 +211,8 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
|
||||
this->EvictSingleReferenceLocked();
|
||||
|
||||
reference.file =
|
||||
FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile);
|
||||
FS::FileOpen(path, ModeFlagsToFileAccessMode(perms), FS::FileType::BinaryFile,
|
||||
FS::FileShareFlag::ShareReadWrite);
|
||||
if (reference.file) {
|
||||
num_open_files++;
|
||||
}
|
||||
@@ -226,7 +227,7 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
|
||||
void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference) {
|
||||
std::scoped_lock lk{list_lock};
|
||||
|
||||
// Remove from list.
|
||||
// Remove from list if present.
|
||||
this->RemoveReferenceFromListLocked(*reference);
|
||||
|
||||
// Close the file.
|
||||
@@ -236,6 +237,19 @@ void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference
|
||||
}
|
||||
}
|
||||
|
||||
void RealVfsFilesystem::CloseReference(FileReference& reference) {
|
||||
std::scoped_lock lk{list_lock};
|
||||
if (!reference.file) {
|
||||
return;
|
||||
}
|
||||
this->RemoveReferenceFromListLocked(reference);
|
||||
reference.file.reset();
|
||||
if (num_open_files > 0) {
|
||||
num_open_files--;
|
||||
}
|
||||
this->InsertReferenceIntoListLocked(reference);
|
||||
}
|
||||
|
||||
void RealVfsFilesystem::EvictSingleReferenceLocked() {
|
||||
if (num_open_files < MaxOpenFiles || open_references.empty()) {
|
||||
return;
|
||||
@@ -256,6 +270,18 @@ void RealVfsFilesystem::EvictSingleReferenceLocked() {
|
||||
}
|
||||
|
||||
void RealVfsFilesystem::InsertReferenceIntoListLocked(FileReference& reference) {
|
||||
// Ensure the node is not already linked to any list before inserting.
|
||||
if (reference.IsLinked()) {
|
||||
// Unlink from the list it currently belongs to.
|
||||
if (reference.file) {
|
||||
open_references.erase(open_references.iterator_to(reference));
|
||||
}
|
||||
|
||||
if (reference.IsLinked()) {
|
||||
closed_references.erase(closed_references.iterator_to(reference));
|
||||
}
|
||||
}
|
||||
|
||||
if (reference.file) {
|
||||
open_references.push_front(reference);
|
||||
} else {
|
||||
@@ -264,9 +290,17 @@ void RealVfsFilesystem::InsertReferenceIntoListLocked(FileReference& reference)
|
||||
}
|
||||
|
||||
void RealVfsFilesystem::RemoveReferenceFromListLocked(FileReference& reference) {
|
||||
// Unlink from whichever list the node currently belongs to, if any.
|
||||
if (!reference.IsLinked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Erase from the correct list to avoid cross-list corruption.
|
||||
if (reference.file) {
|
||||
open_references.erase(open_references.iterator_to(reference));
|
||||
} else {
|
||||
}
|
||||
|
||||
if(reference.IsLinked()) {
|
||||
closed_references.erase(closed_references.iterator_to(reference));
|
||||
}
|
||||
}
|
||||
@@ -296,13 +330,19 @@ std::size_t RealVfsFile::GetSize() const {
|
||||
return *size;
|
||||
}
|
||||
auto lk = base.RefreshReference(path, perms, *reference);
|
||||
return reference->file ? reference->file->GetSize() : 0;
|
||||
const auto result = reference->file ? reference->file->GetSize() : 0;
|
||||
lk.unlock();
|
||||
base.CloseReference(*reference);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RealVfsFile::Resize(std::size_t new_size) {
|
||||
size.reset();
|
||||
auto lk = base.RefreshReference(path, perms, *reference);
|
||||
return reference->file ? reference->file->SetSize(new_size) : false;
|
||||
const bool ok = reference->file ? reference->file->SetSize(new_size) : false;
|
||||
lk.unlock();
|
||||
base.CloseReference(*reference);
|
||||
return ok;
|
||||
}
|
||||
|
||||
VirtualDir RealVfsFile::GetContainingDirectory() const {
|
||||
@@ -318,20 +358,37 @@ bool RealVfsFile::IsReadable() const {
|
||||
}
|
||||
|
||||
std::size_t RealVfsFile::Read(u8* data, std::size_t length, std::size_t offset) const {
|
||||
auto lk = base.RefreshReference(path, perms, *reference);
|
||||
if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
|
||||
if (length != 0 && data == nullptr) {
|
||||
LOG_ERROR(Common_Filesystem,
|
||||
"RealVfsFile::Read called with null buffer (len={}, off={}, path={})",
|
||||
length, offset, path);
|
||||
return 0;
|
||||
}
|
||||
return reference->file->ReadSpan(std::span{data, length});
|
||||
|
||||
auto lk = base.RefreshReference(path, perms, *reference);
|
||||
if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
|
||||
lk.unlock();
|
||||
base.CloseReference(*reference);
|
||||
return 0;
|
||||
}
|
||||
const auto read = reference->file->ReadSpan(std::span{data, length});
|
||||
lk.unlock();
|
||||
base.CloseReference(*reference);
|
||||
return read;
|
||||
}
|
||||
|
||||
std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t offset) {
|
||||
size.reset();
|
||||
auto lk = base.RefreshReference(path, perms, *reference);
|
||||
if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
|
||||
lk.unlock();
|
||||
base.CloseReference(*reference);
|
||||
return 0;
|
||||
}
|
||||
return reference->file->WriteSpan(std::span{data, length});
|
||||
const auto written = reference->file->WriteSpan(std::span{data, length});
|
||||
lk.unlock();
|
||||
base.CloseReference(*reference);
|
||||
return written;
|
||||
}
|
||||
|
||||
bool RealVfsFile::Rename(std::string_view name) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -58,6 +61,7 @@ private:
|
||||
std::unique_lock<std::mutex> RefreshReference(const std::string& path, OpenMode perms,
|
||||
FileReference& reference);
|
||||
void DropReference(std::unique_ptr<FileReference>&& reference);
|
||||
void CloseReference(FileReference& reference);
|
||||
|
||||
private:
|
||||
friend class RealVfsDirectory;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <utility>
|
||||
#include <filesystem>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/fs/fs.h"
|
||||
@@ -12,6 +16,7 @@
|
||||
#include "core/file_sys/card_image.h"
|
||||
#include "core/file_sys/control_metadata.h"
|
||||
#include "core/file_sys/errors.h"
|
||||
#include "core/file_sys/external_content_index.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "core/file_sys/romfs_factory.h"
|
||||
@@ -713,6 +718,36 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
|
||||
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
|
||||
sdmc_factory->GetSDMCContents());
|
||||
}
|
||||
|
||||
if (external_provider == nullptr) {
|
||||
external_provider = std::make_unique<FileSys::ManualContentProvider>();
|
||||
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External,
|
||||
external_provider.get());
|
||||
}
|
||||
|
||||
RebuildExternalContentIndex();
|
||||
}
|
||||
|
||||
void FileSystemController::RebuildExternalContentIndex() {
|
||||
if (external_provider == nullptr) {
|
||||
LOG_WARNING(Service_FS, "External provider not initialized, skipping re-index.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Settings::values.external_dirs.empty()) {
|
||||
FileSys::ExternalContentPaths paths{};
|
||||
for (const auto& dir : Settings::values.external_dirs) {
|
||||
if (dir.empty())
|
||||
continue;
|
||||
paths.update_dirs.push_back(dir);
|
||||
paths.dlc_dirs.push_back(dir);
|
||||
}
|
||||
FileSys::ExternalContentIndexer indexer{system.GetFilesystem(), *this->external_provider,
|
||||
std::move(paths)};
|
||||
indexer.Rebuild();
|
||||
} else {
|
||||
external_provider->ClearAllEntries();
|
||||
}
|
||||
}
|
||||
|
||||
void FileSystemController::Reset() {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -5,6 +8,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <core/file_sys/registered_cache.h>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/fs_directory.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
@@ -121,6 +125,8 @@ public:
|
||||
// above is called.
|
||||
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
|
||||
|
||||
void RebuildExternalContentIndex();
|
||||
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
@@ -141,6 +147,7 @@ private:
|
||||
std::unique_ptr<FileSys::XCI> gamecard;
|
||||
std::unique_ptr<FileSys::RegisteredCache> gamecard_registered;
|
||||
std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder;
|
||||
std::unique_ptr<FileSys::ManualContentProvider> external_provider;
|
||||
|
||||
Core::System& system;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -283,6 +286,19 @@ void Config::ReadDataStorageValues() {
|
||||
|
||||
ReadCategory(Settings::Category::DataStorage);
|
||||
|
||||
Settings::values.external_dirs.clear();
|
||||
const int num_dirs = BeginArray(std::string("external_dirs"));
|
||||
Settings::values.external_dirs.reserve(num_dirs);
|
||||
for (int i = 0; i < num_dirs; ++i) {
|
||||
SetArrayIndex(i);
|
||||
std::string dir = ReadStringSetting(std::string("path"), std::string(""));
|
||||
if (!dir.empty()) {
|
||||
Settings::values.external_dirs.emplace_back(std::move(dir));
|
||||
}
|
||||
}
|
||||
|
||||
EndArray();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
@@ -591,6 +607,14 @@ void Config::SaveDataStorageValues() {
|
||||
|
||||
WriteCategory(Settings::Category::DataStorage);
|
||||
|
||||
BeginArray(std::string("external_dirs"));
|
||||
for (std::size_t i = 0; i < Settings::values.external_dirs.size(); ++i) {
|
||||
SetArrayIndex(static_cast<int>(i));
|
||||
WriteStringSetting(std::string("path"), Settings::values.external_dirs[i],
|
||||
std::make_optional(std::string("")));
|
||||
}
|
||||
EndArray();
|
||||
|
||||
EndGroup();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <QFileDialog>
|
||||
#include <QListWidgetItem>
|
||||
#include <QMessageBox>
|
||||
#include "common/settings.h"
|
||||
#include "core/core.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "ui_configure_general.h"
|
||||
#include "yuzu/configuration/configuration_shared.h"
|
||||
#include "yuzu/configuration/configure_general.h"
|
||||
#include "yuzu/configuration/shared_widget.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "qt_common/util/game.h"
|
||||
|
||||
ConfigureGeneral::ConfigureGeneral(const Core::System& system_,
|
||||
std::shared_ptr<std::vector<ConfigurationShared::Tab*>> group_,
|
||||
@@ -22,21 +26,45 @@ ConfigureGeneral::ConfigureGeneral(const Core::System& system_,
|
||||
: Tab(group_, parent), ui{std::make_unique<Ui::ConfigureGeneral>()}, system{system_} {
|
||||
ui->setupUi(this);
|
||||
|
||||
apply_funcs.push_back([this](bool) {
|
||||
Settings::values.external_dirs.clear();
|
||||
for (int i = 0; i < ui->external_dirs_list->count(); ++i) {
|
||||
QListWidgetItem* item = ui->external_dirs_list->item(i);
|
||||
if (item) {
|
||||
Settings::values.external_dirs.push_back(item->text().toStdString());
|
||||
}
|
||||
}
|
||||
auto& fs_controller = const_cast<Core::System&>(system).GetFileSystemController();
|
||||
fs_controller.RebuildExternalContentIndex();
|
||||
QtCommon::Game::ResetMetadata(false);
|
||||
UISettings::values.is_game_list_reload_pending.exchange(true);
|
||||
});
|
||||
|
||||
Setup(builder);
|
||||
|
||||
SetConfiguration();
|
||||
|
||||
connect(ui->button_reset_defaults, &QPushButton::clicked, this,
|
||||
&ConfigureGeneral::ResetDefaults);
|
||||
connect(ui->add_dir_button, &QPushButton::clicked, this, &ConfigureGeneral::OnAddDirClicked);
|
||||
connect(ui->remove_dir_button, &QPushButton::clicked, this,
|
||||
&ConfigureGeneral::OnRemoveDirClicked);
|
||||
connect(ui->external_dirs_list, &QListWidget::itemSelectionChanged, this,
|
||||
&ConfigureGeneral::OnDirSelectionChanged);
|
||||
|
||||
ui->remove_dir_button->setEnabled(false);
|
||||
|
||||
if (!Settings::IsConfiguringGlobal()) {
|
||||
ui->button_reset_defaults->setVisible(false);
|
||||
ui->DataDirsGroupBox->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureGeneral::~ConfigureGeneral() = default;
|
||||
|
||||
void ConfigureGeneral::SetConfiguration() {}
|
||||
void ConfigureGeneral::SetConfiguration() {
|
||||
LoadExternalDirs();
|
||||
}
|
||||
|
||||
void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) {
|
||||
QLayout& general_layout = *ui->general_widget->layout();
|
||||
@@ -109,6 +137,7 @@ void ConfigureGeneral::ResetDefaults() {
|
||||
UISettings::values.reset_to_defaults = true;
|
||||
UISettings::values.is_game_list_reload_pending.exchange(true);
|
||||
reset_callback();
|
||||
SetConfiguration();
|
||||
}
|
||||
|
||||
void ConfigureGeneral::ApplyConfiguration() {
|
||||
@@ -129,3 +158,37 @@ void ConfigureGeneral::changeEvent(QEvent* event) {
|
||||
void ConfigureGeneral::RetranslateUI() {
|
||||
ui->retranslateUi(this);
|
||||
}
|
||||
|
||||
void ConfigureGeneral::LoadExternalDirs() {
|
||||
ui->external_dirs_list->clear();
|
||||
for (const auto& dir : Settings::values.external_dirs) {
|
||||
ui->external_dirs_list->addItem(QString::fromStdString(dir));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureGeneral::OnAddDirClicked() {
|
||||
QString default_path = QDir::homePath();
|
||||
if (ui->external_dirs_list->count() > 0) {
|
||||
default_path = ui->external_dirs_list->item(ui->external_dirs_list->count() - 1)->text();
|
||||
}
|
||||
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Select Directory"), default_path);
|
||||
if (!dir.isEmpty()) {
|
||||
if (ui->external_dirs_list->findItems(dir, Qt::MatchExactly).isEmpty()) {
|
||||
ui->external_dirs_list->addItem(dir);
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Directory already added"),
|
||||
tr("The directory \"%1\" is already in the list.").arg(dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureGeneral::OnRemoveDirClicked() {
|
||||
for (auto* item : ui->external_dirs_list->selectedItems()) {
|
||||
delete ui->external_dirs_list->takeItem(ui->external_dirs_list->row(item));
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigureGeneral::OnDirSelectionChanged() {
|
||||
ui->remove_dir_button->setEnabled(!ui->external_dirs_list->selectedItems().isEmpty());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -45,6 +48,11 @@ private:
|
||||
void changeEvent(QEvent* event) override;
|
||||
void RetranslateUI();
|
||||
|
||||
void LoadExternalDirs();
|
||||
void OnAddDirClicked();
|
||||
void OnRemoveDirClicked();
|
||||
void OnDirSelectionChanged();
|
||||
|
||||
std::function<void()> reset_callback;
|
||||
|
||||
std::unique_ptr<Ui::ConfigureGeneral> ui;
|
||||
|
||||
@@ -73,6 +73,59 @@
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="DataDirsGroupBox">
|
||||
<property name="title">
|
||||
<string>Additional Directories (Updates, DLC, etc.)</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="DataDirsVerticalLayout">
|
||||
<item>
|
||||
<widget class="QListWidget" name="external_dirs_list">
|
||||
<property name="toolTip">
|
||||
<string>This list contains directories that will be searched for game updates and DLC.</string>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="DataDirsHorizontalLayout">
|
||||
<item>
|
||||
<spacer name="spacer_dirs_left">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="add_dir_button">
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="remove_dir_button">
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/xts_archive.h"
|
||||
#include "core/loader/loader.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "ui_configure_per_game_addons.h"
|
||||
#include "yuzu/configuration/configure_input.h"
|
||||
#include "yuzu/configuration/configure_per_game_addons.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
|
||||
ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* parent)
|
||||
: QWidget(parent), ui{std::make_unique<Ui::ConfigurePerGameAddons>()}, system{system_} {
|
||||
@@ -64,6 +64,8 @@ ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* p
|
||||
|
||||
ui->scrollArea->setEnabled(!system.IsPoweredOn());
|
||||
|
||||
connect(item_model, &QStandardItemModel::itemChanged, this,
|
||||
&ConfigurePerGameAddons::OnItemChanged);
|
||||
connect(item_model, &QStandardItemModel::itemChanged,
|
||||
[] { UISettings::values.is_game_list_reload_pending.exchange(true); });
|
||||
}
|
||||
@@ -71,12 +73,26 @@ ConfigurePerGameAddons::ConfigurePerGameAddons(Core::System& system_, QWidget* p
|
||||
ConfigurePerGameAddons::~ConfigurePerGameAddons() = default;
|
||||
|
||||
void ConfigurePerGameAddons::ApplyConfiguration() {
|
||||
bool any_variant_checked = false;
|
||||
for (auto* v : update_variant_items) {
|
||||
if (v && v->checkState() == Qt::Checked) {
|
||||
any_variant_checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (any_variant_checked && default_update_item &&
|
||||
default_update_item->checkState() == Qt::Unchecked) {
|
||||
default_update_item->setCheckState(Qt::Checked);
|
||||
}
|
||||
|
||||
std::vector<std::string> disabled_addons;
|
||||
|
||||
for (const auto& item : list_items) {
|
||||
const auto disabled = item.front()->checkState() == Qt::Unchecked;
|
||||
if (disabled)
|
||||
disabled_addons.push_back(item.front()->text().toStdString());
|
||||
if (disabled) {
|
||||
const auto key = item.front()->data(Qt::UserRole).toString();
|
||||
disabled_addons.push_back(key.toStdString());
|
||||
}
|
||||
}
|
||||
|
||||
auto current = Settings::values.disabled_addons[title_id];
|
||||
@@ -116,6 +132,12 @@ void ConfigurePerGameAddons::LoadConfiguration() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset model and caches to avoid duplicates
|
||||
item_model->removeRows(0, item_model->rowCount());
|
||||
list_items.clear();
|
||||
default_update_item = nullptr;
|
||||
update_variant_items.clear();
|
||||
|
||||
const FileSys::PatchManager pm{title_id, system.GetFileSystemController(),
|
||||
system.GetContentProvider()};
|
||||
const auto loader = Loader::GetLoader(system, file);
|
||||
@@ -126,21 +148,86 @@ void ConfigurePerGameAddons::LoadConfiguration() {
|
||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||
|
||||
for (const auto& patch : pm.GetPatches(update_raw)) {
|
||||
const auto name = QString::fromStdString(patch.name);
|
||||
const auto display_name = QString::fromStdString(patch.name);
|
||||
const auto version_q = QString::fromStdString(patch.version);
|
||||
|
||||
QString toggle_key = display_name;
|
||||
const bool is_update = (patch.type == FileSys::PatchType::Update);
|
||||
const bool is_default_update_row = is_update && patch.version.empty();
|
||||
if (is_update) {
|
||||
if (is_default_update_row) {
|
||||
toggle_key = QStringLiteral("Update");
|
||||
} else if (!patch.version.empty() && patch.version != "PACKED") {
|
||||
toggle_key = QStringLiteral("Update v%1").arg(version_q);
|
||||
} else {
|
||||
toggle_key = QStringLiteral("Update");
|
||||
}
|
||||
}
|
||||
|
||||
auto* const first_item = new QStandardItem;
|
||||
first_item->setText(name);
|
||||
first_item->setText(display_name);
|
||||
first_item->setCheckable(true);
|
||||
first_item->setData(toggle_key, Qt::UserRole);
|
||||
|
||||
const auto patch_disabled =
|
||||
std::find(disabled.begin(), disabled.end(), name.toStdString()) != disabled.end();
|
||||
const bool disabled_match_key =
|
||||
std::find(disabled.begin(), disabled.end(), toggle_key.toStdString()) != disabled.end();
|
||||
const bool disabled_all_updates =
|
||||
is_update &&
|
||||
std::find(disabled.begin(), disabled.end(), std::string("Update")) != disabled.end();
|
||||
|
||||
const bool patch_disabled =
|
||||
disabled_match_key || (is_update && !is_default_update_row && disabled_all_updates);
|
||||
first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked);
|
||||
|
||||
list_items.push_back(QList<QStandardItem*>{
|
||||
first_item, new QStandardItem{QString::fromStdString(patch.version)}});
|
||||
item_model->appendRow(list_items.back());
|
||||
auto* const second_item = new QStandardItem{version_q};
|
||||
|
||||
if (is_default_update_row) {
|
||||
QList<QStandardItem*> row{first_item, second_item};
|
||||
item_model->appendRow(row);
|
||||
list_items.push_back(row);
|
||||
default_update_item = first_item;
|
||||
tree_view->expand(first_item->index());
|
||||
} else if (is_update && default_update_item != nullptr) {
|
||||
QList<QStandardItem*> row{first_item, second_item};
|
||||
default_update_item->appendRow(row);
|
||||
list_items.push_back(row);
|
||||
update_variant_items.push_back(first_item);
|
||||
} else {
|
||||
QList<QStandardItem*> row{first_item, second_item};
|
||||
item_model->appendRow(row);
|
||||
list_items.push_back(row);
|
||||
}
|
||||
}
|
||||
|
||||
tree_view->expandAll();
|
||||
tree_view->resizeColumnToContents(1);
|
||||
}
|
||||
|
||||
void ConfigurePerGameAddons::OnItemChanged(QStandardItem* item) {
|
||||
if (!item)
|
||||
return;
|
||||
const auto key = item->data(Qt::UserRole).toString();
|
||||
const bool is_update_row = key.startsWith(QStringLiteral("Update"));
|
||||
if (!is_update_row)
|
||||
return;
|
||||
|
||||
if (item == default_update_item) {
|
||||
if (default_update_item->checkState() == Qt::Unchecked) {
|
||||
for (auto* v : update_variant_items) {
|
||||
if (v && v->checkState() != Qt::Unchecked)
|
||||
v->setCheckState(Qt::Unchecked);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (item->checkState() == Qt::Checked) {
|
||||
for (auto* v : update_variant_items) {
|
||||
if (v && v != item && v->checkState() != Qt::Unchecked)
|
||||
v->setCheckState(Qt::Unchecked);
|
||||
}
|
||||
if (default_update_item && default_update_item->checkState() == Qt::Unchecked) {
|
||||
default_update_item->setCheckState(Qt::Checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
@@ -44,6 +47,8 @@ private:
|
||||
|
||||
void LoadConfiguration();
|
||||
|
||||
void OnItemChanged(QStandardItem* item);
|
||||
|
||||
std::unique_ptr<Ui::ConfigurePerGameAddons> ui;
|
||||
FileSys::VirtualFile file;
|
||||
u64 title_id;
|
||||
@@ -54,5 +59,8 @@ private:
|
||||
|
||||
std::vector<QList<QStandardItem*>> list_items;
|
||||
|
||||
QStandardItem* default_update_item = nullptr;
|
||||
std::vector<QStandardItem*> update_variant_items;
|
||||
|
||||
Core::System& system;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "yuzu/game_list.h"
|
||||
#include <regex>
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
@@ -13,20 +13,21 @@
|
||||
#include <QMenu>
|
||||
#include <QThreadPool>
|
||||
#include <QToolButton>
|
||||
#include <fmt/ranges.h>
|
||||
#include "common/common_types.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/patch_manager.h"
|
||||
#include "core/file_sys/registered_cache.h"
|
||||
#include "qt_common/util/game.h"
|
||||
#include "core/hle/service/filesystem/filesystem.h"
|
||||
#include "qt_common/config/uisettings.h"
|
||||
#include "qt_common/util/game.h"
|
||||
#include "yuzu/compatibility_list.h"
|
||||
#include "yuzu/game_list.h"
|
||||
#include "yuzu/game_list_p.h"
|
||||
#include "yuzu/game_list_worker.h"
|
||||
#include "yuzu/main.h"
|
||||
#include "yuzu/util/controller_navigation.h"
|
||||
#include <fmt/ranges.h>
|
||||
#include <regex>
|
||||
|
||||
GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist_, QObject* parent)
|
||||
: QObject(parent), gamelist{gamelist_} {}
|
||||
@@ -318,7 +319,8 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
|
||||
: QWidget{parent}, vfs{std::move(vfs_)}, provider{provider_},
|
||||
play_time_manager{play_time_manager_}, system{system_} {
|
||||
watcher = new QFileSystemWatcher(this);
|
||||
connect(watcher, &QFileSystemWatcher::directoryChanged, this, &GameList::RefreshGameDirectory);
|
||||
connect(watcher, &QFileSystemWatcher::directoryChanged, this,
|
||||
&GameList::OnWatchedDirectoryChanged);
|
||||
|
||||
this->main_window = parent;
|
||||
layout = new QVBoxLayout;
|
||||
@@ -486,14 +488,21 @@ void GameList::DonePopulating(const QStringList& watch_list) {
|
||||
if (!watch_dirs.isEmpty()) {
|
||||
watcher->removePaths(watch_dirs);
|
||||
}
|
||||
|
||||
QStringList all_watch_paths = watch_list;
|
||||
for (const auto& dir : Settings::values.external_dirs) {
|
||||
all_watch_paths.append(QString::fromStdString(dir));
|
||||
}
|
||||
all_watch_paths.removeDuplicates();
|
||||
|
||||
// Workaround: Add the watch paths in chunks to allow the gui to refresh
|
||||
// This prevents the UI from stalling when a large number of watch paths are added
|
||||
// Also artificially caps the watcher to a certain number of directories
|
||||
constexpr int LIMIT_WATCH_DIRECTORIES = 5000;
|
||||
constexpr int SLICE_SIZE = 25;
|
||||
int len = (std::min)(static_cast<int>(watch_list.size()), LIMIT_WATCH_DIRECTORIES);
|
||||
int len = (std::min)(static_cast<int>(all_watch_paths.size()), LIMIT_WATCH_DIRECTORIES);
|
||||
for (int i = 0; i < len; i += SLICE_SIZE) {
|
||||
watcher->addPaths(watch_list.mid(i, i + SLICE_SIZE));
|
||||
watcher->addPaths(all_watch_paths.mid(i, i + SLICE_SIZE));
|
||||
QCoreApplication::processEvents();
|
||||
}
|
||||
tree_view->setEnabled(true);
|
||||
@@ -619,26 +628,32 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
|
||||
emit RemoveInstalledEntryRequested(program_id, QtCommon::Game::InstalledEntryType::Update);
|
||||
});
|
||||
connect(remove_dlc, &QAction::triggered, [this, program_id]() {
|
||||
emit RemoveInstalledEntryRequested(program_id, QtCommon::Game::InstalledEntryType::AddOnContent);
|
||||
emit RemoveInstalledEntryRequested(program_id,
|
||||
QtCommon::Game::InstalledEntryType::AddOnContent);
|
||||
});
|
||||
connect(remove_gl_shader_cache, &QAction::triggered, [this, program_id, path]() {
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::GlShaderCache, path);
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::GlShaderCache,
|
||||
path);
|
||||
});
|
||||
connect(remove_vk_shader_cache, &QAction::triggered, [this, program_id, path]() {
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::VkShaderCache, path);
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::VkShaderCache,
|
||||
path);
|
||||
});
|
||||
connect(remove_shader_cache, &QAction::triggered, [this, program_id, path]() {
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::AllShaderCache, path);
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::AllShaderCache,
|
||||
path);
|
||||
});
|
||||
connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() {
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path);
|
||||
emit RemoveFileRequested(program_id,
|
||||
QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path);
|
||||
});
|
||||
connect(set_play_time, &QAction::triggered,
|
||||
[this, program_id]() { emit SetPlayTimeRequested(program_id); });
|
||||
connect(remove_play_time_data, &QAction::triggered,
|
||||
[this, program_id]() { emit RemovePlayTimeRequested(program_id); });
|
||||
connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] {
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CacheStorage, path);
|
||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CacheStorage,
|
||||
path);
|
||||
});
|
||||
connect(dump_romfs, &QAction::triggered, [this, program_id, path]() {
|
||||
emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::Normal);
|
||||
@@ -665,8 +680,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
|
||||
connect(properties, &QAction::triggered,
|
||||
[this, path]() { emit OpenPerGameGeneralRequested(path); });
|
||||
|
||||
connect(ryujinx, &QAction::triggered, [this, program_id]() { emit LinkToRyujinxRequested(program_id);
|
||||
});
|
||||
connect(ryujinx, &QAction::triggered,
|
||||
[this, program_id]() { emit LinkToRyujinxRequested(program_id); });
|
||||
};
|
||||
|
||||
void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) {
|
||||
@@ -830,8 +845,7 @@ QStandardItemModel* GameList::GetModel() const {
|
||||
return item_model;
|
||||
}
|
||||
|
||||
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs)
|
||||
{
|
||||
void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
|
||||
tree_view->setEnabled(false);
|
||||
|
||||
// Update the columns in case UISettings has changed
|
||||
@@ -848,12 +862,8 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs)
|
||||
item_model->removeRows(0, item_model->rowCount());
|
||||
search_field->clear();
|
||||
|
||||
current_worker = std::make_unique<GameListWorker>(vfs,
|
||||
provider,
|
||||
game_dirs,
|
||||
compatibility_list,
|
||||
play_time_manager,
|
||||
system);
|
||||
current_worker = std::make_unique<GameListWorker>(vfs, provider, game_dirs, compatibility_list,
|
||||
play_time_manager, system);
|
||||
|
||||
// Get events from the worker as data becomes available
|
||||
connect(current_worker.get(), &GameListWorker::DataAvailable, this, &GameList::WorkerEvent,
|
||||
@@ -882,8 +892,7 @@ const QStringList GameList::supported_file_extensions = {
|
||||
QStringLiteral("nso"), QStringLiteral("nro"), QStringLiteral("nca"),
|
||||
QStringLiteral("xci"), QStringLiteral("nsp"), QStringLiteral("kip")};
|
||||
|
||||
void GameList::RefreshGameDirectory()
|
||||
{
|
||||
void GameList::RefreshGameDirectory() {
|
||||
if (!UISettings::values.game_dirs.empty() && current_worker != nullptr) {
|
||||
LOG_INFO(Frontend, "Change detected in the games directory. Reloading game list.");
|
||||
PopulateAsync(UISettings::values.game_dirs);
|
||||
@@ -967,6 +976,17 @@ GameListPlaceholder::GameListPlaceholder(GMainWindow* parent) : QWidget{parent}
|
||||
|
||||
GameListPlaceholder::~GameListPlaceholder() = default;
|
||||
|
||||
void GameList::OnWatchedDirectoryChanged(const QString& path) {
|
||||
LOG_INFO(Frontend, "Change detected in watched directory {}. Reloading content.",
|
||||
path.toStdString());
|
||||
|
||||
system.GetFileSystemController().RebuildExternalContentIndex();
|
||||
|
||||
QtCommon::Game::ResetMetadata(false);
|
||||
|
||||
RefreshGameDirectory();
|
||||
}
|
||||
|
||||
void GameListPlaceholder::onUpdateThemedIcons() {
|
||||
image->setPixmap(QIcon::fromTheme(QStringLiteral("plus_folder")).pixmap(200));
|
||||
}
|
||||
|
||||
@@ -81,6 +81,8 @@ public:
|
||||
void LoadCompatibilityList();
|
||||
void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);
|
||||
|
||||
void OnWatchedDirectoryChanged(const QString& path);
|
||||
|
||||
void SaveInterfaceLayout();
|
||||
void LoadInterfaceLayout();
|
||||
|
||||
|
||||
@@ -173,6 +173,10 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_update && patch.version.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString type =
|
||||
QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user