Browse Source

add all versions to config

fs_external_dlcupdates
unknown 2 months ago
committed by crueter
parent
commit
809a1b4d06
  1. 138
      src/core/file_sys/external_content_index.cpp
  2. 2
      src/core/file_sys/external_content_index.h
  3. 210
      src/core/file_sys/patch_manager.cpp
  4. 107
      src/yuzu/configuration/configure_per_game_addons.cpp
  5. 8
      src/yuzu/configuration/configure_per_game_addons.h

138
src/core/file_sys/external_content_index.cpp

@ -4,20 +4,20 @@
#include "core/file_sys/external_content_index.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <string>
#include <cctype>
#include "common/fs/fs_util.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/fs/fs_util.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/romfs.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/content_archive.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"
#include "core/file_sys/common_funcs.h"
namespace fs = std::filesystem;
@ -30,7 +30,7 @@ ExternalContentIndexer::ExternalContentIndexer(VirtualFilesystem vfs,
void ExternalContentIndexer::Rebuild() {
m_provider.ClearAllEntries();
m_best_update_by_title.clear();
m_updates_by_title.clear();
m_all_dlc.clear();
for (const auto& dir : m_paths.update_dirs) {
@ -46,7 +46,8 @@ void ExternalContentIndexer::Rebuild() {
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)); });
std::transform(s.begin(), s.end(), out.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
return out;
}
@ -58,8 +59,10 @@ void ExternalContentIndexer::IndexUpdatesDir(const std::string& dir) {
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;
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);
@ -79,8 +82,10 @@ void ExternalContentIndexer::IndexDlcDir(const std::string& dir) {
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;
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);
@ -94,7 +99,7 @@ void ExternalContentIndexer::IndexDlcDir(const std::string& dir) {
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 (lower.size() >= 4 && lower.rfind(".nsp") == lower.size() - 4) {
if (auto vf = m_vfs->OpenFile(path, OpenMode::Read)) {
ParseContainerNSP(vf, is_update);
}
@ -104,23 +109,29 @@ void ExternalContentIndexer::TryIndexFileAsContainer(const std::string& path, bo
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;
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;
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);
ParseLooseCnmtNca(
vf, Common::FS::ToUTF8String(entry.path().parent_path().u8string()), is_update);
}
}
}
}
void ExternalContentIndexer::ParseContainerNSP(VirtualFile file, bool is_update) {
if (file == nullptr) return;
if (file == nullptr)
return;
NSP nsp(file);
if (nsp.GetStatus() != Loader::ResultStatus::Success) {
LOG_WARNING(Loader, "ExternalContent: NSP parse failed");
@ -128,7 +139,8 @@ void ExternalContentIndexer::ParseContainerNSP(VirtualFile file, bool is_update)
}
const auto title_map = nsp.GetNCAs();
if (title_map.empty()) return;
if (title_map.empty())
return;
for (const auto& [title_id, nca_map] : title_map) {
std::shared_ptr<NCA> meta_nca;
@ -159,24 +171,23 @@ void ExternalContentIndexer::ParseContainerNSP(VirtualFile file, bool is_update)
candidate.ncas[rec.type] = it->second->GetBaseFile();
}
}
auto& best = m_best_update_by_title[base_id];
if (best.title_id ==0 || candidate.version > best.version) {
best = std::move(candidate);
}
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()});
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) {
void ExternalContentIndexer::ParseLooseCnmtNca(VirtualFile meta_nca_file, const std::string& folder,
bool is_update) {
if (meta_nca_file == nullptr)
return;
@ -186,7 +197,8 @@ void ExternalContentIndexer::ParseLooseCnmtNca(VirtualFile meta_nca_file, const
return;
auto cnmt_opt = ExtractCnmtFromMetaNca(meta);
if (!cnmt_opt) return;
if (!cnmt_opt)
return;
const auto& cnmt = *cnmt_opt;
const auto base_id = BaseTitleId(cnmt.GetTitleID());
@ -199,21 +211,21 @@ void ExternalContentIndexer::ParseLooseCnmtNca(VirtualFile meta_nca_file, const
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());
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& best = m_best_update_by_title[base_id];
if (best.title_id ==0 || candidate.version > best.version) {
best = std::move(candidate);
}
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());
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));
@ -247,25 +259,55 @@ bool ExternalContentIndexer::IsMeta(const NCA& nca) {
}
void ExternalContentIndexer::Commit() {
// Updates: for now just register the highest version per DLC
for (auto& [base_title, upd] : m_best_update_by_title) {
for (const auto& kv : upd.ncas) {
const auto rec_type = kv.first;
const auto& file = kv.second;
if (!file) continue;
// Use the Update TitleID for provider registration so core queries by update_tid succeed
const auto update_tid = FileSys::GetUpdateTitleID(base_title);
m_provider.AddEntry(TitleType::Update, rec_type, update_tid, file);
// 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: additiv
// DLC: additive
for (const auto& dlc : m_all_dlc) {
if (!dlc.file) continue;
if (!dlc.file)
continue;
m_provider.AddEntry(TitleType::AOC, ContentRecordType::Data, dlc.title_id, dlc.file);
}
LOG_INFO(Loader, "ExternalContent: registered {} titles with updates, {} DLC records",
m_best_update_by_title.size(), m_all_dlc.size());
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} // namespace FileSys
} // namespace FileSys

2
src/core/file_sys/external_content_index.h

@ -66,7 +66,7 @@ private:
ManualContentProvider& m_provider;
ExternalContentPaths m_paths;
std::unordered_map<TitleID, ParsedUpdate> m_best_update_by_title;
std::unordered_map<TitleID, std::vector<ParsedUpdate>> m_updates_by_title;
std::vector<ParsedDlcRecord> m_all_dlc;
};

210
src/core/file_sys/patch_manager.cpp

@ -8,6 +8,7 @@
#include <array>
#include <cstddef>
#include <cstring>
#include <set>
#include "common/hex_util.h"
#include "common/logging/log.h"
@ -117,6 +118,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 +219,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 +403,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 +413,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 +435,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 +541,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,11 +556,12 @@ 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_INFO(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 && packed_update_raw != nullptr && base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
@ -470,6 +569,8 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
new_nca->GetRomFS() != nullptr) {
LOG_INFO(Loader, " RomFS: Update (PACKED) applied successfully");
romfs = new_nca->GetRomFS();
} else {
LOG_WARNING(Loader, " RomFS: Update (PACKED) NCA is not valid");
}
}
@ -489,11 +590,10 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::vector<Patch> out;
const auto& disabled = Settings::values.disabled_addons[title_id];
// Game Updates
// Game Updates (default latest)
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;
const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
@ -504,22 +604,60 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.program_id = title_id,
.title_id = title_id};
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);
} else {
update_patch.version = FormatTitleVersion(*meta_ver);
out.push_back(update_patch);
}
} else if (update_raw != nullptr) {
update_patch.version = "PACKED";
out.push_back(update_patch);
out.push_back(update_patch);
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 {
selected_variant_tid = GetUpdateTitleID(title_id);
}
}
const auto variant_tids =
EnumerateUpdateVariants(content_provider, title_id, ContentRecordType::Program);
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;
return a.first < b.first;
});
std::set<std::string> seen_versions;
if (!out.empty() && !out.back().version.empty()) {
auto ver = out.back().version;
if (!ver.empty() && (ver.front() == 'v' || ver.front() == 'V')) {
ver.erase(ver.begin());
}
seen_versions.insert(std::move(ver));
}
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 auto toggle_name = fmt::format("Update {}", label);
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);
}
// General Mods (LayeredFS and IPS)

107
src/yuzu/configuration/configure_per_game_addons.cpp

@ -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);
}
}
}

8
src/yuzu/configuration/configure_per_game_addons.h

@ -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;
};
Loading…
Cancel
Save