mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-20 17:36:10 +00:00
3c38afad74
- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions). - [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md) - [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability. ------------------- Normal games are Applications; Bundled games are Application + Programs; We've been treating both as the same thing, calling both programs, and making programs unable to see settings and saves from application. References: https://switchbrew.org/wiki/Filesystem_services https://switchbrew.org/wiki/NCM_services#ApplicationId https://switchbrew.org/wiki/Super_Mario_3D_All-Stars Ryujinx.HLE/HOS/ApplicationLoader.cs:GetMultiProgramInfo UPDATE: The first version was a big rework to make AppId/ProgId concepts proper, and it was successful in most tests, but it caused a regression with applets, which are programs that breed straight from no application. In order to simplify things i've reverted all the rework, added a helper to identify whether a program has a parent Application and bounded it to it's proper stores. Although this is no big deal coz it doesn't have impact on emulation, it permeates to several routines, so it needs to be tested broadly. Applets, Normal games, Bundled games, Blobbed NSPs/XCIs, Mods, Updates, Saves, DLCs,. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4319 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: lizzie <lizzie@eden-emu.dev>
1195 lines
48 KiB
C++
1195 lines
48 KiB
C++
// SPDX-FileCopyrightText: Copyright 2026 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 <algorithm>
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstring>
|
|
|
|
#include "common/assert.h"
|
|
#include "common/hex_util.h"
|
|
#include "common/logging.h"
|
|
#include "common/settings.h"
|
|
#ifndef _WIN32
|
|
#include "common/string_util.h"
|
|
#endif
|
|
|
|
#include "core/core.h"
|
|
#include "core/file_sys/common_funcs.h"
|
|
#include "core/file_sys/content_archive.h"
|
|
#include "core/file_sys/control_metadata.h"
|
|
#include "core/file_sys/ips_layer.h"
|
|
#include "core/file_sys/patch_manager.h"
|
|
#include "core/file_sys/registered_cache.h"
|
|
#include "core/file_sys/romfs.h"
|
|
#include "core/file_sys/vfs/vfs_cached.h"
|
|
#include "core/file_sys/vfs/vfs_layered.h"
|
|
#include "core/file_sys/vfs/vfs_vector.h"
|
|
#include "core/hle/service/filesystem/filesystem.h"
|
|
#include "core/hle/service/ns/language.h"
|
|
#include "core/hle/service/set/settings_server.h"
|
|
#include "core/loader/loader.h"
|
|
#include "core/loader/nso.h"
|
|
#include "core/memory/cheat_engine.h"
|
|
|
|
namespace FileSys {
|
|
namespace {
|
|
|
|
constexpr u32 SINGLE_BYTE_MODULUS = 0x100;
|
|
|
|
constexpr std::array<const char*, 14> EXEFS_FILE_NAMES{
|
|
"main", "main.npdm", "rtld", "sdk", "subsdk0", "subsdk1", "subsdk2",
|
|
"subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "subsdk8", "subsdk9",
|
|
};
|
|
|
|
enum class TitleVersionFormat : u8 {
|
|
ThreeElements, ///< vX.Y.Z
|
|
FourElements, ///< vX.Y.Z.W
|
|
};
|
|
|
|
std::string FormatTitleVersion(u32 version,
|
|
TitleVersionFormat format = TitleVersionFormat::ThreeElements) {
|
|
std::array<u8, sizeof(u32)> bytes{};
|
|
bytes[0] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
|
|
for (std::size_t i = 1; i < bytes.size(); ++i) {
|
|
version /= SINGLE_BYTE_MODULUS;
|
|
bytes[i] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
|
|
}
|
|
|
|
if (format == TitleVersionFormat::FourElements) {
|
|
return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
|
|
}
|
|
return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
|
|
}
|
|
|
|
// 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) {
|
|
#ifdef _WIN32
|
|
return dir->GetSubdirectory(name);
|
|
#else
|
|
const auto subdirs = dir->GetSubdirectories();
|
|
for (const auto& subdir : subdirs) {
|
|
std::string dir_name = Common::ToLower(subdir->GetName());
|
|
if (dir_name == name) {
|
|
return subdir;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
#endif
|
|
}
|
|
|
|
std::optional<std::vector<Core::Memory::CheatEntry>> ReadCheatFileFromFolder(
|
|
u64 title_id, const PatchManager::BuildID& build_id_, const VirtualDir& base_path, bool upper) {
|
|
const auto build_id_raw = Common::HexToString(build_id_, upper);
|
|
const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2);
|
|
const auto file = base_path->GetFile(fmt::format("{}.txt", build_id));
|
|
|
|
if (file == nullptr) {
|
|
LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}",
|
|
title_id, build_id);
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::vector<u8> data(file->GetSize());
|
|
if (file->Read(data.data(), data.size()) != data.size()) {
|
|
LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}",
|
|
title_id, build_id);
|
|
return std::nullopt;
|
|
}
|
|
|
|
const Core::Memory::TextCheatParser parser;
|
|
return parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
|
|
}
|
|
|
|
void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
|
|
if (to.empty()) {
|
|
to += with;
|
|
} else {
|
|
to += ", ";
|
|
to += with;
|
|
}
|
|
}
|
|
|
|
bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
|
|
return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
|
|
}
|
|
|
|
bool IsVersionedExternalUpdateDisabled(const std::vector<std::string>& disabled, u32 version) {
|
|
const std::string disabled_key = fmt::format("Update@{}", version);
|
|
return std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend() ||
|
|
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
|
}
|
|
|
|
std::string GetUpdateVersionStringFromSlot(const ContentProvider* provider, u64 update_tid) {
|
|
if (provider == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
auto control_nca = provider->GetEntry(update_tid, ContentRecordType::Control);
|
|
if (control_nca == nullptr ||
|
|
control_nca->GetStatus() != Loader::ResultStatus::Success) {
|
|
return {};
|
|
}
|
|
|
|
const auto romfs = control_nca->GetRomFS();
|
|
if (romfs == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
const auto extracted = ExtractRomFS(romfs);
|
|
if (extracted == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
auto nacp_file = extracted->GetFile("control.nacp");
|
|
if (nacp_file == nullptr) {
|
|
nacp_file = extracted->GetFile("Control.nacp");
|
|
}
|
|
if (nacp_file == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
NACP nacp{nacp_file};
|
|
return nacp.GetVersionString();
|
|
}
|
|
YUZU_NO_INLINE std::optional<u64> GetParentApplicationId(const ContentProvider* provider, u64 title_id) {
|
|
return provider == nullptr ? std::nullopt : provider->GetParentApplicationId(title_id);
|
|
}
|
|
} // Anonymous namespace
|
|
|
|
PatchManager::PatchManager(u64 title_id_,
|
|
const Service::FileSystem::FileSystemController& fs_controller_,
|
|
const ContentProvider& content_provider_)
|
|
: title_id{title_id_}, parent_title_id{GetParentApplicationId(std::addressof(content_provider_), title_id_)}, fs_controller{fs_controller_}, content_provider{content_provider_} {}
|
|
|
|
PatchManager::~PatchManager() = default;
|
|
|
|
u64 PatchManager::GetTitleID() const {
|
|
return title_id;
|
|
}
|
|
|
|
VirtualDir PatchManager::GetModificationLoadRoot(bool sdmc) const {
|
|
const auto get_root = [&](u64 id) {
|
|
return sdmc ? fs_controller.GetSDMCModificationLoadRoot(id) : fs_controller.GetModificationLoadRoot(id);
|
|
};
|
|
auto root = get_root(title_id);
|
|
if (!parent_title_id) {
|
|
return root;
|
|
}
|
|
std::vector<VirtualDir> roots{std::move(root), get_root(*parent_title_id)};
|
|
std::erase(roots, nullptr);
|
|
return LayeredVfsDirectory::MakeLayeredDirectory(std::move(roots));
|
|
}
|
|
|
|
std::vector<std::string> PatchManager::GetDisabledAddons() const {
|
|
auto disabled = Settings::values.disabled_addons[title_id];
|
|
if (parent_title_id) {
|
|
const auto& shared = Settings::values.disabled_addons[*parent_title_id];
|
|
disabled.insert(disabled.end(), shared.begin(), shared.end());
|
|
}
|
|
return disabled;
|
|
}
|
|
|
|
VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
|
LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
|
|
|
|
if (exefs == nullptr)
|
|
return exefs;
|
|
|
|
const auto disabled = GetDisabledAddons();
|
|
|
|
bool update_disabled = true;
|
|
std::optional<u32> enabled_version;
|
|
bool checked_external = false;
|
|
bool checked_manual = false;
|
|
|
|
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
|
|
const auto update_tid = GetUpdateTitleID(title_id);
|
|
|
|
if (content_union) {
|
|
// First, check ExternalContentProvider
|
|
const auto* external_provider = content_union->GetExternalProvider();
|
|
if (external_provider) {
|
|
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
|
|
|
|
if (!update_versions.empty()) {
|
|
checked_external = true;
|
|
for (const auto& update_entry : update_versions) {
|
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
|
update_disabled = false;
|
|
enabled_version = update_entry.version;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check ManualContentProvider (for Android)
|
|
if (!checked_external) {
|
|
const auto* manual_provider = static_cast<const ManualContentProvider*>(
|
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
|
if (manual_provider) {
|
|
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
|
|
|
|
if (!manual_update_versions.empty()) {
|
|
checked_manual = true;
|
|
for (const auto& update_entry : manual_update_versions) {
|
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
|
update_disabled = false;
|
|
enabled_version = update_entry.version;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// check for original NAND style
|
|
// Check NAND if: no external updates exist, OR all external updates are disabled
|
|
if (!checked_external && !checked_manual) {
|
|
// Only enable NAND update if it exists AND is not disabled
|
|
// We need to check if an update actually exists in the content provider
|
|
const bool has_nand_update = content_provider.HasEntry(update_tid, ContentRecordType::Program);
|
|
|
|
if (has_nand_update) {
|
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
|
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
|
|
|
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
|
|
update_disabled = false;
|
|
}
|
|
}
|
|
} else if (update_disabled && content_union) {
|
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
|
|
|
if (!nand_disabled || !sdmc_disabled) {
|
|
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
|
|
std::nullopt, TitleType::Update, ContentRecordType::Program, update_tid);
|
|
|
|
for (const auto& [slot, entry] : nand_sdmc_entries) {
|
|
if (slot == ContentProviderUnionSlot::UserNAND ||
|
|
slot == ContentProviderUnionSlot::SysNAND) {
|
|
if (!nand_disabled) {
|
|
update_disabled = false;
|
|
break;
|
|
}
|
|
} else if (slot == ContentProviderUnionSlot::SDMC) {
|
|
if (!sdmc_disabled) {
|
|
update_disabled = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Game Updates
|
|
std::unique_ptr<NCA> update = nullptr;
|
|
|
|
// If we have a specific enabled version from external provider, use it
|
|
if (enabled_version.has_value() && content_union) {
|
|
const auto* external_provider = content_union->GetExternalProvider();
|
|
if (external_provider) {
|
|
auto file = external_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
|
|
if (file != nullptr) {
|
|
update = std::make_unique<NCA>(file);
|
|
}
|
|
}
|
|
|
|
// Also try ManualContentProvider
|
|
if (update == nullptr) {
|
|
const auto* manual_provider = static_cast<const ManualContentProvider*>(
|
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
|
if (manual_provider) {
|
|
auto file = manual_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
|
|
if (file != nullptr) {
|
|
update = std::make_unique<NCA>(file);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to regular content provider if no external update was loaded
|
|
if (update == nullptr && !update_disabled) {
|
|
update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// LayeredExeFS
|
|
const auto load_dir = GetModificationLoadRoot();
|
|
const auto sdmc_load_dir = GetModificationLoadRoot(true);
|
|
|
|
std::vector<VirtualDir> patch_dirs = {sdmc_load_dir};
|
|
if (load_dir != nullptr) {
|
|
const auto load_patch_dirs = load_dir->GetSubdirectories();
|
|
patch_dirs.insert(patch_dirs.end(), load_patch_dirs.begin(), load_patch_dirs.end());
|
|
}
|
|
|
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
|
|
|
std::vector<VirtualDir> layers;
|
|
layers.reserve(patch_dirs.size() + 1);
|
|
for (const auto& subdir : patch_dirs) {
|
|
if (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end())
|
|
continue;
|
|
|
|
auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
|
|
if (exefs_dir != nullptr)
|
|
layers.push_back(std::move(exefs_dir));
|
|
}
|
|
layers.push_back(exefs);
|
|
|
|
auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
|
|
if (layered != nullptr) {
|
|
LOG_INFO(Loader, " ExeFS: LayeredExeFS patches applied successfully");
|
|
exefs = std::move(layered);
|
|
}
|
|
|
|
if (Settings::values.dump_exefs) {
|
|
LOG_INFO(Loader, "Dumping ExeFS for title_id={:016X}", title_id);
|
|
const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
|
|
if (dump_dir != nullptr) {
|
|
const auto exefs_dir = GetOrCreateDirectoryRelative(dump_dir, "/exefs");
|
|
VfsRawCopyD(exefs, exefs_dir);
|
|
}
|
|
}
|
|
|
|
return exefs;
|
|
}
|
|
|
|
std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs, const std::string& build_id) const {
|
|
const auto disabled = GetDisabledAddons();
|
|
const auto nso_build_id = fmt::format("{:0<64}", build_id);
|
|
|
|
std::vector<VirtualFile> out;
|
|
out.reserve(patch_dirs.size());
|
|
for (const auto& subdir : patch_dirs) {
|
|
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend())
|
|
continue;
|
|
|
|
auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
|
|
if (exefs_dir != nullptr) {
|
|
for (const auto& file : exefs_dir->GetFiles()) {
|
|
if (file->GetExtension() == "ips") {
|
|
auto name = file->GetName();
|
|
const auto this_build_id = fmt::format("{:0<64}", name.substr(0, name.find('.')));
|
|
if (nso_build_id == this_build_id)
|
|
out.push_back(file);
|
|
} else if (file->GetExtension() == "pchtxt") {
|
|
IPSwitchCompiler compiler{file};
|
|
const auto this_build_id = Common::HexToString(compiler.GetBuildID());
|
|
if (nso_build_id == this_build_id)
|
|
out.push_back(file);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso, const std::string& name) const {
|
|
if (nso.size() < sizeof(Loader::NSOHeader)) {
|
|
return nso;
|
|
}
|
|
|
|
Loader::NSOHeader header;
|
|
std::memcpy(&header, nso.data(), sizeof(header));
|
|
|
|
if (header.magic != Common::MakeMagic('N', 'S', 'O', '0')) {
|
|
return nso;
|
|
}
|
|
|
|
const auto build_id_raw = Common::HexToString(header.build_id);
|
|
const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
|
|
|
|
if (Settings::values.dump_nso) {
|
|
LOG_INFO(Loader, "Dumping NSO for name={}, build_id={}, title_id={:016X}", name, build_id,
|
|
title_id);
|
|
const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
|
|
if (dump_dir != nullptr) {
|
|
const auto nso_dir = GetOrCreateDirectoryRelative(dump_dir, "/nso");
|
|
const auto file = nso_dir->CreateFile(fmt::format("{}-{}.nso", name, build_id));
|
|
|
|
file->Resize(nso.size());
|
|
file->WriteBytes(nso);
|
|
}
|
|
}
|
|
|
|
LOG_INFO(Loader, "Patching NSO for name={}, build_id={}", name, build_id);
|
|
|
|
const auto load_dir = GetModificationLoadRoot();
|
|
if (load_dir == nullptr) {
|
|
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
|
return nso;
|
|
}
|
|
|
|
auto patch_dirs = load_dir->GetSubdirectories();
|
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
|
const auto patches = CollectPatches(patch_dirs, build_id);
|
|
|
|
auto out = nso;
|
|
for (const auto& patch_file : patches) {
|
|
if (patch_file->GetExtension() == "ips") {
|
|
LOG_INFO(Loader, " - Applying IPS patch from mod \"{}\"",
|
|
patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
|
|
const auto patched = PatchIPS(std::make_shared<VectorVfsFile>(out), patch_file);
|
|
if (patched != nullptr)
|
|
out = patched->ReadAllBytes();
|
|
} else if (patch_file->GetExtension() == "pchtxt") {
|
|
LOG_INFO(Loader, " - Applying IPSwitch patch from mod \"{}\"",
|
|
patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
|
|
const IPSwitchCompiler compiler{patch_file};
|
|
const auto patched = compiler.Apply(std::make_shared<VectorVfsFile>(out));
|
|
if (patched != nullptr)
|
|
out = patched->ReadAllBytes();
|
|
}
|
|
}
|
|
|
|
if (out.size() < sizeof(Loader::NSOHeader)) {
|
|
return nso;
|
|
}
|
|
|
|
std::memcpy(out.data(), &header, sizeof(header));
|
|
return out;
|
|
}
|
|
|
|
bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name) const {
|
|
const auto build_id_raw = Common::HexToString(build_id_);
|
|
const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
|
|
|
|
LOG_INFO(Loader, "Querying NSO patch existence for build_id={}, name={}", build_id, name);
|
|
|
|
const auto load_dir = GetModificationLoadRoot();
|
|
if (load_dir == nullptr) {
|
|
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
|
return false;
|
|
}
|
|
|
|
auto patch_dirs = load_dir->GetSubdirectories();
|
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
|
|
|
return !CollectPatches(patch_dirs, build_id).empty();
|
|
}
|
|
|
|
std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(const BuildID& build_id_) const {
|
|
const auto load_dir = GetModificationLoadRoot();
|
|
if (load_dir == nullptr) {
|
|
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
|
return {};
|
|
}
|
|
|
|
const auto disabled = GetDisabledAddons();
|
|
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(); });
|
|
|
|
// <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 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))
|
|
std::copy(res->begin(), res->end(), std::back_inserter(out));
|
|
}
|
|
}
|
|
}
|
|
// Uncareless user-friendly loading of patches (must start with 'cheat_')
|
|
// <mod dir> / <cheat file>.txt
|
|
for (auto const& f : load_dir->GetFiles()) {
|
|
auto const name = f->GetName();
|
|
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()));
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void PatchManager::ApplyLayeredFS(VirtualFile& romfs, ContentRecordType type) const {
|
|
const auto load_dir = GetModificationLoadRoot();
|
|
const auto sdmc_load_dir = GetModificationLoadRoot(true);
|
|
if ((type != ContentRecordType::Program && type != ContentRecordType::Data &&
|
|
type != ContentRecordType::HtmlDocument) ||
|
|
(load_dir == nullptr && sdmc_load_dir == nullptr)) {
|
|
return;
|
|
}
|
|
|
|
const auto disabled = GetDisabledAddons();
|
|
std::vector<VirtualDir> patch_dirs = load_dir->GetSubdirectories();
|
|
if (std::find(disabled.cbegin(), disabled.cend(), "SDMC") == disabled.cend()) {
|
|
patch_dirs.push_back(sdmc_load_dir);
|
|
}
|
|
std::sort(patch_dirs.begin(), patch_dirs.end(),
|
|
[](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
|
|
|
|
std::vector<VirtualDir> layers;
|
|
std::vector<VirtualDir> layers_ext;
|
|
layers.reserve(patch_dirs.size() + 1);
|
|
layers_ext.reserve(patch_dirs.size() + 1);
|
|
for (const auto& subdir : patch_dirs) {
|
|
if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) {
|
|
continue;
|
|
}
|
|
|
|
auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs");
|
|
if (romfs_dir != nullptr)
|
|
layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(romfs_dir)));
|
|
|
|
// Support for romfslite introduced in Atmosphere 1.9.5
|
|
auto romfslite_dir = FindSubdirectoryCaseless(subdir, "romfslite");
|
|
if (romfslite_dir != nullptr)
|
|
layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(romfslite_dir)));
|
|
|
|
auto ext_dir = FindSubdirectoryCaseless(subdir, "romfs_ext");
|
|
if (ext_dir != nullptr)
|
|
layers_ext.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(ext_dir)));
|
|
|
|
if (type == ContentRecordType::HtmlDocument) {
|
|
auto manual_dir = FindSubdirectoryCaseless(subdir, "manual_html");
|
|
if (manual_dir != nullptr)
|
|
layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(manual_dir)));
|
|
}
|
|
}
|
|
|
|
// When there are no layers to apply, return early as there is no need to rebuild the RomFS
|
|
if (layers.empty() && layers_ext.empty()) {
|
|
return;
|
|
}
|
|
|
|
auto extracted = ExtractRomFS(romfs);
|
|
if (extracted == nullptr) {
|
|
return;
|
|
}
|
|
|
|
layers.emplace_back(std::move(extracted));
|
|
|
|
auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
|
|
auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext));
|
|
if (layered == nullptr && layered_ext == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto packed = CreateRomFS(std::move(layered), std::move(layered_ext));
|
|
if (packed == nullptr) {
|
|
return;
|
|
}
|
|
|
|
LOG_INFO(Loader, " RomFS: LayeredFS patches applied successfully");
|
|
romfs = std::move(packed);
|
|
}
|
|
|
|
VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs,
|
|
ContentRecordType type, VirtualFile packed_update_raw,
|
|
bool apply_layeredfs) const {
|
|
const auto log_string = fmt::format("Patching RomFS for title_id={:016X}, type={:02X}",
|
|
title_id, static_cast<u8>(type));
|
|
if (type == ContentRecordType::Program || type == ContentRecordType::Data) {
|
|
LOG_INFO(Loader, "{}", log_string);
|
|
} else {
|
|
LOG_DEBUG(Loader, "{}", log_string);
|
|
}
|
|
|
|
auto romfs = base_romfs;
|
|
|
|
// Game Updates
|
|
const auto update_tid = GetUpdateTitleID(title_id);
|
|
const auto disabled = GetDisabledAddons();
|
|
|
|
bool update_disabled = true;
|
|
std::optional<u32> enabled_version;
|
|
VirtualFile update_raw = nullptr;
|
|
bool checked_external = false;
|
|
bool checked_manual = false;
|
|
|
|
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
|
|
if (content_union) {
|
|
// First, check ExternalContentProvider
|
|
const auto* external_provider = content_union->GetExternalProvider();
|
|
if (external_provider) {
|
|
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
|
|
|
|
if (!update_versions.empty()) {
|
|
checked_external = true;
|
|
for (const auto& update_entry : update_versions) {
|
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
|
update_disabled = false;
|
|
enabled_version = update_entry.version;
|
|
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!checked_external) {
|
|
const auto* manual_provider = static_cast<const ManualContentProvider*>(
|
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
|
if (manual_provider) {
|
|
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
|
|
|
|
if (!manual_update_versions.empty()) {
|
|
checked_manual = true;
|
|
for (const auto& update_entry : manual_update_versions) {
|
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
|
update_disabled = false;
|
|
enabled_version = update_entry.version;
|
|
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!checked_external && !checked_manual) {
|
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
|
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
|
|
|
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
|
|
update_disabled = false;
|
|
}
|
|
if (!update_disabled) {
|
|
update_raw = content_provider.GetEntryRaw(update_tid, type);
|
|
}
|
|
} else if (update_disabled && content_union) {
|
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
|
|
|
if (!nand_disabled || !sdmc_disabled) {
|
|
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
|
|
std::nullopt, TitleType::Update, type, update_tid);
|
|
|
|
for (const auto& [slot, entry] : nand_sdmc_entries) {
|
|
if (slot == ContentProviderUnionSlot::UserNAND ||
|
|
slot == ContentProviderUnionSlot::SysNAND) {
|
|
if (!nand_disabled) {
|
|
update_disabled = false;
|
|
update_raw = content_provider.GetEntryRaw(update_tid, type);
|
|
break;
|
|
}
|
|
} else if (slot == ContentProviderUnionSlot::SDMC) {
|
|
if (!sdmc_disabled) {
|
|
update_disabled = false;
|
|
update_raw = content_provider.GetEntryRaw(update_tid, type);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
|
|
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",
|
|
enabled_version.has_value() ? FormatTitleVersion(*enabled_version) :
|
|
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
|
|
romfs = new_nca->GetRomFS();
|
|
}
|
|
} 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");
|
|
romfs = new_nca->GetRomFS();
|
|
}
|
|
}
|
|
|
|
// LayeredFS
|
|
if (apply_layeredfs) {
|
|
ApplyLayeredFS(romfs, type);
|
|
}
|
|
|
|
return romfs;
|
|
}
|
|
|
|
std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|
if (title_id == 0) {
|
|
return {};
|
|
}
|
|
|
|
std::vector<Patch> out;
|
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
|
|
|
// Game Updates
|
|
const auto update_tid = GetUpdateTitleID(title_id);
|
|
|
|
std::vector<Patch> external_update_patches;
|
|
|
|
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
|
|
|
|
if (content_union) {
|
|
// First, check ExternalContentProvider for updates
|
|
const auto* external_provider = content_union->GetExternalProvider();
|
|
if (external_provider) {
|
|
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
|
|
|
|
for (const auto& update_entry : update_versions) {
|
|
std::string version_str = update_entry.version_string;
|
|
if (version_str.empty()) {
|
|
version_str = FormatTitleVersion(update_entry.version);
|
|
}
|
|
|
|
const auto update_disabled =
|
|
IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
|
|
|
|
Patch update_patch = {.enabled = !update_disabled,
|
|
.name = "Update",
|
|
.version = version_str,
|
|
.type = PatchType::Update,
|
|
.program_id = title_id,
|
|
.title_id = update_tid,
|
|
.source = PatchSource::External,
|
|
.numeric_version = update_entry.version};
|
|
|
|
external_update_patches.push_back(update_patch);
|
|
}
|
|
}
|
|
|
|
const auto* manual_provider = static_cast<const ManualContentProvider*>(
|
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
|
if (manual_provider && external_update_patches.empty()) {
|
|
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
|
|
|
|
for (const auto& update_entry : manual_update_versions) {
|
|
std::string version_str = update_entry.version_string;
|
|
if (version_str.empty()) {
|
|
version_str = FormatTitleVersion(update_entry.version);
|
|
}
|
|
|
|
const auto update_disabled =
|
|
IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
|
|
|
|
|
|
Patch update_patch = {.enabled = !update_disabled,
|
|
.name = "Update",
|
|
.version = version_str,
|
|
.type = PatchType::Update,
|
|
.program_id = title_id,
|
|
.title_id = update_tid,
|
|
.source = PatchSource::External,
|
|
.numeric_version = update_entry.version};
|
|
|
|
external_update_patches.push_back(update_patch);
|
|
}
|
|
}
|
|
|
|
if (external_update_patches.size() > 1) {
|
|
bool found_enabled = false;
|
|
for (auto& patch : external_update_patches) {
|
|
if (patch.enabled) {
|
|
if (found_enabled) {
|
|
patch.enabled = false;
|
|
} else {
|
|
found_enabled = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& patch : external_update_patches) {
|
|
out.push_back(std::move(patch));
|
|
}
|
|
|
|
const auto all_updates = content_union->ListEntriesFilterOrigin(
|
|
std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
|
|
|
|
for (const auto& [slot, entry] : all_updates) {
|
|
(void)entry;
|
|
if (slot == ContentProviderUnionSlot::External ||
|
|
slot == ContentProviderUnionSlot::FrontendManual) {
|
|
continue;
|
|
}
|
|
|
|
PatchSource source_type = PatchSource::Unknown;
|
|
std::string source_suffix;
|
|
|
|
switch (slot) {
|
|
case ContentProviderUnionSlot::UserNAND:
|
|
case ContentProviderUnionSlot::SysNAND:
|
|
source_type = PatchSource::NAND;
|
|
source_suffix = " (NAND)";
|
|
break;
|
|
case ContentProviderUnionSlot::SDMC:
|
|
source_type = PatchSource::SDMC;
|
|
source_suffix = " (SDMC)";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
std::string version_str;
|
|
u32 numeric_ver = 0;
|
|
const auto* slot_provider = content_union->GetSlotProvider(slot);
|
|
version_str = GetUpdateVersionStringFromSlot(slot_provider, update_tid);
|
|
|
|
if (slot_provider != nullptr) {
|
|
const auto slot_ver = slot_provider->GetEntryVersion(update_tid);
|
|
if (slot_ver.has_value()) {
|
|
numeric_ver = *slot_ver;
|
|
if (version_str.empty() && numeric_ver != 0) {
|
|
version_str = FormatTitleVersion(numeric_ver);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string patch_name = "Update" + source_suffix;
|
|
|
|
bool update_disabled =
|
|
std::find(disabled.cbegin(), disabled.cend(), patch_name) != disabled.cend();
|
|
|
|
Patch update_patch = {.enabled = !update_disabled,
|
|
.name = patch_name,
|
|
.version = version_str,
|
|
.type = PatchType::Update,
|
|
.program_id = title_id,
|
|
.title_id = update_tid,
|
|
.source = source_type,
|
|
.numeric_version = numeric_ver};
|
|
|
|
out.push_back(update_patch);
|
|
}
|
|
} else {
|
|
PatchManager update{update_tid, fs_controller, content_provider};
|
|
const auto metadata = update.GetControlMetadata();
|
|
const auto& nacp = metadata.first;
|
|
|
|
bool 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,
|
|
.source = PatchSource::Unknown,
|
|
.numeric_version = 0};
|
|
|
|
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);
|
|
update_patch.numeric_version = *meta_ver;
|
|
out.push_back(update_patch);
|
|
}
|
|
} else if (update_raw != nullptr) {
|
|
update_patch.version = "PACKED";
|
|
update_patch.source = PatchSource::Packed;
|
|
out.push_back(update_patch);
|
|
}
|
|
}
|
|
}
|
|
|
|
// General Mods (LayeredFS and IPS)
|
|
const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id);
|
|
if (mod_dir != nullptr) {
|
|
for (auto const& f : mod_dir->GetFiles())
|
|
if (auto const name = f->GetName(); name.starts_with("cheat_")) {
|
|
auto const mod_disabled = std::find(disabled.begin(), disabled.end(), name) != disabled.end();
|
|
out.push_back({
|
|
.enabled = !mod_disabled,
|
|
.name = name,
|
|
.version = "Cheats",
|
|
.type = PatchType::Mod,
|
|
.program_id = title_id,
|
|
.title_id = title_id,
|
|
.source = PatchSource::Unknown,
|
|
.location = f->GetFullPath(),
|
|
});
|
|
}
|
|
|
|
for (const auto& mod : mod_dir->GetSubdirectories()) {
|
|
std::string types;
|
|
|
|
const auto exefs_dir = FindSubdirectoryCaseless(mod, "exefs");
|
|
if (IsDirValidAndNonEmpty(exefs_dir)) {
|
|
bool ips = false;
|
|
bool ipswitch = false;
|
|
bool layeredfs = false;
|
|
|
|
for (const auto& file : exefs_dir->GetFiles()) {
|
|
if (file->GetExtension() == "ips") {
|
|
ips = true;
|
|
} else if (file->GetExtension() == "pchtxt") {
|
|
ipswitch = true;
|
|
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
|
|
file->GetName()) != EXEFS_FILE_NAMES.end()) {
|
|
layeredfs = true;
|
|
}
|
|
}
|
|
|
|
if (ips)
|
|
AppendCommaIfNotEmpty(types, "IPS");
|
|
if (ipswitch)
|
|
AppendCommaIfNotEmpty(types, "IPSwitch");
|
|
if (layeredfs)
|
|
AppendCommaIfNotEmpty(types, "LayeredExeFS");
|
|
}
|
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")) ||
|
|
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfslite")))
|
|
AppendCommaIfNotEmpty(types, "LayeredFS");
|
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs_ext")))
|
|
AppendCommaIfNotEmpty(types, "ExtLayeredFS");
|
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats")))
|
|
AppendCommaIfNotEmpty(types, "Cheats");
|
|
|
|
if (types.empty())
|
|
continue;
|
|
|
|
const auto mod_disabled = std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
|
|
out.push_back({.enabled = !mod_disabled,
|
|
.name = mod->GetName(),
|
|
.version = types,
|
|
.type = PatchType::Mod,
|
|
.program_id = title_id,
|
|
.title_id = title_id,
|
|
.source = PatchSource::Unknown,
|
|
.location = mod->GetFullPath()});
|
|
}
|
|
}
|
|
|
|
// SDMC mod directory (RomFS LayeredFS)
|
|
const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
|
|
if (sdmc_mod_dir != nullptr) {
|
|
std::string types;
|
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "exefs")))
|
|
AppendCommaIfNotEmpty(types, "LayeredExeFS");
|
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) ||
|
|
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite")))
|
|
AppendCommaIfNotEmpty(types, "LayeredFS");
|
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs_ext")))
|
|
AppendCommaIfNotEmpty(types, "ExtLayeredFS");
|
|
|
|
if (!types.empty()) {
|
|
const auto mod_disabled =
|
|
std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end();
|
|
out.push_back({.enabled = !mod_disabled,
|
|
.name = "SDMC",
|
|
.version = types,
|
|
.type = PatchType::Mod,
|
|
.program_id = title_id,
|
|
.title_id = title_id,
|
|
.source = PatchSource::Unknown});
|
|
}
|
|
}
|
|
|
|
// DLC
|
|
std::vector<ContentProviderEntry> dlc_match;
|
|
bool has_external_dlc = false;
|
|
bool has_nand_dlc = false;
|
|
bool has_sdmc_dlc = false;
|
|
bool has_other_dlc = false;
|
|
const auto dlc_entries_with_origin =
|
|
content_union->ListEntriesFilterOrigin(std::nullopt, TitleType::AOC, ContentRecordType::Data);
|
|
|
|
dlc_match.reserve(dlc_entries_with_origin.size());
|
|
for (const auto& [slot, entry] : dlc_entries_with_origin) {
|
|
const auto base_tid = GetBaseTitleID(entry.title_id);
|
|
const bool matches_base = base_tid == title_id;
|
|
if (!matches_base) {
|
|
LOG_DEBUG(Loader, "DLC {:016X} base {:016X} doesn't match title {:016X}",
|
|
entry.title_id, base_tid, title_id);
|
|
continue;
|
|
}
|
|
|
|
const auto* slot_provider = content_union->GetSlotProvider(slot);
|
|
if (slot_provider == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
auto nca = slot_provider->GetEntry(entry);
|
|
if (!nca) {
|
|
LOG_DEBUG(Loader, "Failed to get NCA for DLC {:016X}", entry.title_id);
|
|
continue;
|
|
}
|
|
|
|
const auto status = nca->GetStatus();
|
|
if (status != Loader::ResultStatus::Success) {
|
|
LOG_DEBUG(Loader, "DLC {:016X} NCA has status {}", entry.title_id,
|
|
static_cast<int>(status));
|
|
continue;
|
|
}
|
|
|
|
switch (slot) {
|
|
case ContentProviderUnionSlot::External:
|
|
case ContentProviderUnionSlot::FrontendManual:
|
|
has_external_dlc = true;
|
|
break;
|
|
case ContentProviderUnionSlot::UserNAND:
|
|
case ContentProviderUnionSlot::SysNAND:
|
|
has_nand_dlc = true;
|
|
break;
|
|
case ContentProviderUnionSlot::SDMC:
|
|
has_sdmc_dlc = true;
|
|
break;
|
|
default:
|
|
has_other_dlc = true;
|
|
break;
|
|
}
|
|
dlc_match.push_back(entry);
|
|
}
|
|
|
|
if (!dlc_match.empty()) {
|
|
// Ensure sorted so DLC IDs show in order.
|
|
std::sort(dlc_match.begin(), dlc_match.end());
|
|
|
|
std::string list;
|
|
for (size_t i = 0; i < dlc_match.size() - 1; ++i)
|
|
list += fmt::format("{}, ", dlc_match[i].title_id & 0x7FF);
|
|
|
|
list += fmt::format("{}", dlc_match.back().title_id & 0x7FF);
|
|
|
|
const auto dlc_disabled =
|
|
std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
|
|
PatchSource dlc_source = PatchSource::Unknown;
|
|
if (has_external_dlc && !has_nand_dlc && !has_sdmc_dlc && !has_other_dlc) {
|
|
dlc_source = PatchSource::External;
|
|
} else if (has_nand_dlc && !has_external_dlc && !has_sdmc_dlc && !has_other_dlc) {
|
|
dlc_source = PatchSource::NAND;
|
|
} else if (has_sdmc_dlc && !has_external_dlc && !has_nand_dlc && !has_other_dlc) {
|
|
dlc_source = PatchSource::SDMC;
|
|
}
|
|
|
|
out.push_back({.enabled = !dlc_disabled,
|
|
.name = "DLC",
|
|
.version = std::move(list),
|
|
.type = PatchType::DLC,
|
|
.program_id = title_id,
|
|
.title_id = dlc_match.back().title_id,
|
|
.source = dlc_source});
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
std::optional<u32> PatchManager::GetGameVersion() const {
|
|
const auto update_tid = GetUpdateTitleID(title_id);
|
|
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
|
const auto version = content_provider.GetEntryVersion(update_tid);
|
|
return version || !parent_title_id ? version : content_provider.GetEntryVersion(GetUpdateTitleID(*parent_title_id));
|
|
}
|
|
|
|
const auto version = content_provider.GetEntryVersion(title_id);
|
|
return version || !parent_title_id ? version : content_provider.GetEntryVersion(*parent_title_id);
|
|
}
|
|
|
|
PatchManager::Metadata PatchManager::GetControlMetadata() const {
|
|
const auto base_control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control);
|
|
if (base_control_nca == nullptr) {
|
|
if (parent_title_id) {
|
|
const auto control_id = content_provider.HasEntry(*parent_title_id, ContentRecordType::Control) ? *parent_title_id : GetUpdateTitleID(*parent_title_id);
|
|
const PatchManager parent{control_id, fs_controller, content_provider};
|
|
return parent.GetControlMetadata();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
return ParseControlNCA(*base_control_nca);
|
|
}
|
|
|
|
PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const {
|
|
const auto base_romfs = nca.GetRomFS();
|
|
if (base_romfs == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
const auto romfs = PatchRomFS(&nca, base_romfs, ContentRecordType::Control);
|
|
if (romfs == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
const auto extracted = ExtractRomFS(romfs);
|
|
if (extracted == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
auto nacp_file = extracted->GetFile("control.nacp");
|
|
if (nacp_file == nullptr) {
|
|
nacp_file = extracted->GetFile("Control.nacp");
|
|
}
|
|
|
|
auto nacp = nacp_file == nullptr ? nullptr : std::make_unique<NACP>(nacp_file);
|
|
|
|
// Get language code from settings
|
|
const auto language_code = Service::Set::GetLanguageCodeFromIndex(
|
|
static_cast<u32>(Settings::values.language_index.GetValue()));
|
|
|
|
// Convert to application language and get priority list
|
|
const auto application_language =
|
|
Service::NS::ConvertToApplicationLanguage(language_code)
|
|
.value_or(Service::NS::ApplicationLanguage::AmericanEnglish);
|
|
const auto language_priority_list =
|
|
Service::NS::GetApplicationLanguagePriorityList(application_language);
|
|
|
|
// Convert to language names
|
|
auto priority_language_names = FileSys::LANGUAGE_NAMES; // Copy
|
|
if (language_priority_list) {
|
|
for (size_t i = 0; i < priority_language_names.size(); ++i) {
|
|
// Relies on FileSys::LANGUAGE_NAMES being in the same order as
|
|
// Service::NS::ApplicationLanguage
|
|
const auto language_index = static_cast<u8>(language_priority_list->at(i));
|
|
|
|
if (language_index < FileSys::LANGUAGE_NAMES.size()) {
|
|
priority_language_names[i] = FileSys::LANGUAGE_NAMES[language_index];
|
|
} else {
|
|
// Not a catastrophe, unlikely to happen
|
|
LOG_WARNING(Loader, "Invalid language index {}", language_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get first matching icon
|
|
VirtualFile icon_file;
|
|
for (const auto& language : priority_language_names) {
|
|
icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat"));
|
|
if (icon_file != nullptr) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {std::move(nacp), icon_file};
|
|
}
|
|
|
|
[[nodiscard]] PatchManager::Metadata PatchManager::GetMetadataFromBaseOrUpdate(Core::System& system, u64 application_id) noexcept {
|
|
const FileSys::PatchManager pm{application_id, system.GetFileSystemController(), system.GetContentProvider()};
|
|
auto metadata = pm.GetControlMetadata();
|
|
if (metadata.first != nullptr)
|
|
return metadata;
|
|
const FileSys::PatchManager pm_update{FileSys::GetUpdateTitleID(application_id), system.GetFileSystemController(), system.GetContentProvider()};
|
|
return pm_update.GetControlMetadata();
|
|
}
|
|
|
|
} // namespace FileSys
|