mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-23 15:53:59 +00:00
This fixed the delete button enabled for external content (which is auto handled and the proper way to get rid of them is either by removing its folder from ext content list, or removing the file itself) by streaming patch source thru jni. Along the way stumbled upon another bug: If you have an external content update installed (say latest version for example) and you NAND install a previous update (like in silksong's hard mode update), the newest update version string would leak to the previous one. Did videos for both. Fixed both. Seems good to go. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3691 Reviewed-by: crueter <crueter@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev> Reviewed-by: DraVee <chimera@dravee.dev> Co-authored-by: xbzk <xbzk@eden-emu.dev> Co-committed-by: xbzk <xbzk@eden-emu.dev>
1159 lines
46 KiB
C++
1159 lines
46 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/hex_util.h"
|
|
#include "common/logging/log.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();
|
|
}
|
|
} // Anonymous namespace
|
|
|
|
PatchManager::PatchManager(u64 title_id_,
|
|
const Service::FileSystem::FileSystemController& fs_controller_,
|
|
const ContentProvider& content_provider_)
|
|
: title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_} {}
|
|
|
|
PatchManager::~PatchManager() = default;
|
|
|
|
u64 PatchManager::GetTitleID() const {
|
|
return title_id;
|
|
}
|
|
|
|
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 = Settings::values.disabled_addons[title_id];
|
|
|
|
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 = fs_controller.GetModificationLoadRoot(title_id);
|
|
const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
|
|
|
|
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 = Settings::values.disabled_addons[title_id];
|
|
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};
|
|
if (!compiler.IsValid())
|
|
continue;
|
|
|
|
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 = fs_controller.GetModificationLoadRoot(title_id);
|
|
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 = fs_controller.GetModificationLoadRoot(title_id);
|
|
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 = fs_controller.GetModificationLoadRoot(title_id);
|
|
if (load_dir == nullptr) {
|
|
LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
|
|
return {};
|
|
}
|
|
|
|
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(); });
|
|
|
|
// <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;
|
|
}
|
|
|
|
static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType type,
|
|
const Service::FileSystem::FileSystemController& fs_controller) {
|
|
const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
|
|
const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
|
|
if ((type != ContentRecordType::Program && type != ContentRecordType::Data &&
|
|
type != ContentRecordType::HtmlDocument) ||
|
|
(load_dir == nullptr && sdmc_load_dir == nullptr)) {
|
|
return;
|
|
}
|
|
|
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
|
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));
|
|
if (layered == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext));
|
|
|
|
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 = Settings::values.disabled_addons[title_id];
|
|
|
|
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, title_id, type, fs_controller);
|
|
}
|
|
|
|
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, "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 (!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)) {
|
|
return content_provider.GetEntryVersion(update_tid);
|
|
}
|
|
|
|
return content_provider.GetEntryVersion(title_id);
|
|
}
|
|
|
|
PatchManager::Metadata PatchManager::GetControlMetadata() const {
|
|
const auto base_control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control);
|
|
if (base_control_nca == nullptr) {
|
|
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};
|
|
}
|
|
} // namespace FileSys
|