mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-05-25 15:27:02 +02:00
[android/fs] external content loader + nca/xci patches (#3596)
Foreword: WHY DON'T EVERYBODY USE ONE FOLDER FOR EACH GAME+CONTENTS? AIN'T THIS THE FORMAT GAMES COME WHEN YOU BUE THEM? DO YOU LIVE WITH ALL YOUR FRIENDS AND HAVE A 2ND HOUSE FOR ALL THE CHILDREN? Nice, i feel better now. This feat extends Maufeat's work on external content loading. It harmonically additions: "...also, if in each game folder X, you find a folder Y, and in this folder Y you detect ONLY a single game, then mount all external content for that game found in that folder Y and its subfolders." Permanent (not toggleable). External Content folders are supported equally. Also: -Reworked several routines for preserving single source of truth between android and other systems; -Fixed the annoying unknown format error for content files, by providing proper format detection. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3596 Reviewed-by: MaranBr <maranbr@eden-emu.dev> Reviewed-by: Lizzie <lizzie@eden-emu.dev> Co-authored-by: xbzk <xbzk@eden-emu.dev> Co-committed-by: xbzk <xbzk@eden-emu.dev>
This commit is contained in:
parent
c682306788
commit
7f5de6bcd6
18 changed files with 477 additions and 424 deletions
|
|
@ -607,6 +607,12 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun addFileToFilesystemProvider(path: String)
|
external fun addFileToFilesystemProvider(path: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a game-folder file to the manual filesystem provider, respecting the internal gate for
|
||||||
|
* game-folder external-content mounting.
|
||||||
|
*/
|
||||||
|
external fun addGameFolderFileToFilesystemProvider(path: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all files added to the manual filesystem provider in our EmulationSession instance
|
* Clears all files added to the manual filesystem provider in our EmulationSession instance
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,6 @@ class AddonViewModel : ViewModel() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are multiple update versions
|
|
||||||
val updates = _patchList.value.filter { PatchType.from(it.type) == PatchType.Update }
|
|
||||||
val hasMultipleUpdates = updates.size > 1
|
|
||||||
|
|
||||||
NativeConfig.setDisabledAddons(
|
NativeConfig.setDisabledAddons(
|
||||||
game!!.programId,
|
game!!.programId,
|
||||||
_patchList.value.mapNotNull {
|
_patchList.value.mapNotNull {
|
||||||
|
|
@ -140,7 +136,7 @@ class AddonViewModel : ViewModel() {
|
||||||
if (PatchType.from(it.type) == PatchType.Update) {
|
if (PatchType.from(it.type) == PatchType.Update) {
|
||||||
if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) {
|
if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) {
|
||||||
it.name
|
it.name
|
||||||
} else if (hasMultipleUpdates) {
|
} else if (it.numericVersion != 0L) {
|
||||||
"Update@${it.numericVersion}"
|
"Update@${it.numericVersion}"
|
||||||
} else {
|
} else {
|
||||||
it.name
|
it.name
|
||||||
|
|
|
||||||
|
|
@ -424,7 +424,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
)
|
)
|
||||||
|
|
||||||
val uriString = result.toString()
|
val uriString = result.toString()
|
||||||
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
val folder = gamesViewModel.folders.value.firstOrNull {
|
||||||
|
it.uriString == uriString && it.type == org.yuzu.yuzu_emu.model.DirectoryType.EXTERNAL_CONTENT
|
||||||
|
}
|
||||||
if (folder != null) {
|
if (folder != null) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
|
|
|
||||||
|
|
@ -51,11 +51,24 @@ object GameHelper {
|
||||||
|
|
||||||
// Scan External Content directories and register all NSP/XCI files
|
// Scan External Content directories and register all NSP/XCI files
|
||||||
val externalContentDirs = NativeConfig.getExternalContentDirs()
|
val externalContentDirs = NativeConfig.getExternalContentDirs()
|
||||||
for (externalDir in externalContentDirs) {
|
val uniqueExternalContentDirs = linkedSetOf<String>()
|
||||||
|
externalContentDirs.forEach { externalDir ->
|
||||||
|
if (externalDir.isNotEmpty()) {
|
||||||
|
uniqueExternalContentDirs.add(externalDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mountedContainerUris = mutableSetOf<String>()
|
||||||
|
for (externalDir in uniqueExternalContentDirs) {
|
||||||
if (externalDir.isNotEmpty()) {
|
if (externalDir.isNotEmpty()) {
|
||||||
val externalDirUri = externalDir.toUri()
|
val externalDirUri = externalDir.toUri()
|
||||||
if (FileUtil.isTreeUriValid(externalDirUri)) {
|
if (FileUtil.isTreeUriValid(externalDirUri)) {
|
||||||
scanExternalContentRecursive(FileUtil.listFiles(externalDirUri), 3)
|
scanContentContainersRecursive(FileUtil.listFiles(externalDirUri), 3) {
|
||||||
|
val containerUri = it.uri.toString()
|
||||||
|
if (mountedContainerUris.add(containerUri)) {
|
||||||
|
NativeLibrary.addFileToFilesystemProvider(containerUri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,10 +78,13 @@ object GameHelper {
|
||||||
val gameDirUri = gameDir.uriString.toUri()
|
val gameDirUri = gameDir.uriString.toUri()
|
||||||
val isValid = FileUtil.isTreeUriValid(gameDirUri)
|
val isValid = FileUtil.isTreeUriValid(gameDirUri)
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
|
val scanDepth = if (gameDir.deepScan) 3 else 1
|
||||||
|
|
||||||
addGamesRecursive(
|
addGamesRecursive(
|
||||||
games,
|
games,
|
||||||
FileUtil.listFiles(gameDirUri),
|
FileUtil.listFiles(gameDirUri),
|
||||||
if (gameDir.deepScan) 3 else 1
|
scanDepth,
|
||||||
|
mountedContainerUris
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
badDirs.add(index)
|
badDirs.add(index)
|
||||||
|
|
@ -103,9 +119,10 @@ object GameHelper {
|
||||||
// be done better imo.
|
// be done better imo.
|
||||||
private val externalContentExtensions = setOf("nsp", "xci")
|
private val externalContentExtensions = setOf("nsp", "xci")
|
||||||
|
|
||||||
private fun scanExternalContentRecursive(
|
private fun scanContentContainersRecursive(
|
||||||
files: Array<MinimalDocumentFile>,
|
files: Array<MinimalDocumentFile>,
|
||||||
depth: Int
|
depth: Int,
|
||||||
|
onContainerFound: (MinimalDocumentFile) -> Unit
|
||||||
) {
|
) {
|
||||||
if (depth <= 0) {
|
if (depth <= 0) {
|
||||||
return
|
return
|
||||||
|
|
@ -113,14 +130,15 @@ object GameHelper {
|
||||||
|
|
||||||
files.forEach {
|
files.forEach {
|
||||||
if (it.isDirectory) {
|
if (it.isDirectory) {
|
||||||
scanExternalContentRecursive(
|
scanContentContainersRecursive(
|
||||||
FileUtil.listFiles(it.uri),
|
FileUtil.listFiles(it.uri),
|
||||||
depth - 1
|
depth - 1,
|
||||||
|
onContainerFound
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val extension = FileUtil.getExtension(it.uri).lowercase()
|
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||||
if (externalContentExtensions.contains(extension)) {
|
if (externalContentExtensions.contains(extension)) {
|
||||||
NativeLibrary.addFileToFilesystemProvider(it.uri.toString())
|
onContainerFound(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +147,8 @@ object GameHelper {
|
||||||
private fun addGamesRecursive(
|
private fun addGamesRecursive(
|
||||||
games: MutableList<Game>,
|
games: MutableList<Game>,
|
||||||
files: Array<MinimalDocumentFile>,
|
files: Array<MinimalDocumentFile>,
|
||||||
depth: Int
|
depth: Int,
|
||||||
|
mountedContainerUris: MutableSet<String>
|
||||||
) {
|
) {
|
||||||
if (depth <= 0) {
|
if (depth <= 0) {
|
||||||
return
|
return
|
||||||
|
|
@ -140,11 +159,20 @@ object GameHelper {
|
||||||
addGamesRecursive(
|
addGamesRecursive(
|
||||||
games,
|
games,
|
||||||
FileUtil.listFiles(it.uri),
|
FileUtil.listFiles(it.uri),
|
||||||
depth - 1
|
depth - 1,
|
||||||
|
mountedContainerUris
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
|
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||||
val game = getGame(it.uri, true)
|
val filePath = it.uri.toString()
|
||||||
|
|
||||||
|
if (externalContentExtensions.contains(extension) &&
|
||||||
|
mountedContainerUris.add(filePath)) {
|
||||||
|
NativeLibrary.addGameFolderFileToFilesystemProvider(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Game.extensions.contains(extension)) {
|
||||||
|
val game = getGame(it.uri, true, false)
|
||||||
if (game != null) {
|
if (game != null) {
|
||||||
games.add(game)
|
games.add(game)
|
||||||
}
|
}
|
||||||
|
|
@ -153,14 +181,20 @@ object GameHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGame(uri: Uri, addedToLibrary: Boolean): Game? {
|
fun getGame(
|
||||||
|
uri: Uri,
|
||||||
|
addedToLibrary: Boolean,
|
||||||
|
registerFilesystemProvider: Boolean = true
|
||||||
|
): Game? {
|
||||||
val filePath = uri.toString()
|
val filePath = uri.toString()
|
||||||
if (!GameMetadata.getIsValid(filePath)) {
|
if (!GameMetadata.getIsValid(filePath)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Needed to update installed content information
|
if (registerFilesystemProvider) {
|
||||||
NativeLibrary.addFileToFilesystemProvider(filePath)
|
// Needed to update installed content information
|
||||||
|
NativeLibrary.addFileToFilesystemProvider(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
var name = GameMetadata.getTitle(filePath)
|
var name = GameMetadata.getTitle(filePath)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ void AndroidConfig::ReadAndroidValues() {
|
||||||
if (global) {
|
if (global) {
|
||||||
ReadAndroidUIValues();
|
ReadAndroidUIValues();
|
||||||
ReadUIValues();
|
ReadUIValues();
|
||||||
|
BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage));
|
||||||
|
Settings::values.ext_content_from_game_dirs = ReadBooleanSetting(
|
||||||
|
std::string("ext_content_from_game_dirs"),
|
||||||
|
std::make_optional(
|
||||||
|
Settings::values.ext_content_from_game_dirs.GetDefault()));
|
||||||
|
EndGroup();
|
||||||
ReadOverlayValues();
|
ReadOverlayValues();
|
||||||
}
|
}
|
||||||
ReadDriverValues();
|
ReadDriverValues();
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,11 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) &&
|
||||||
|
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
u64 program_id = 0;
|
u64 program_id = 0;
|
||||||
Loader::ResultStatus res = loader->ReadProgramId(program_id);
|
Loader::ResultStatus res = loader->ReadProgramId(program_id);
|
||||||
if (res != Loader::ResultStatus::Success) {
|
if (res != Loader::ResultStatus::Success) {
|
||||||
|
|
|
||||||
|
|
@ -217,107 +217,8 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto extension = Common::ToLower(filepath.substr(filepath.find_last_of('.') + 1));
|
if (m_manual_provider->AddEntriesFromContainer(file)) {
|
||||||
|
return;
|
||||||
if (extension == "nsp") {
|
|
||||||
auto nsp = std::make_shared<FileSys::NSP>(file);
|
|
||||||
if (nsp->GetStatus() == Loader::ResultStatus::Success) {
|
|
||||||
std::map<u64, u32> nsp_versions;
|
|
||||||
std::map<u64, std::string> nsp_version_strings;
|
|
||||||
|
|
||||||
for (const auto& [title_id, nca_map] : nsp->GetNCAs()) {
|
|
||||||
for (const auto& [type_pair, nca] : nca_map) {
|
|
||||||
const auto& [title_type, content_type] = type_pair;
|
|
||||||
|
|
||||||
if (content_type == FileSys::ContentRecordType::Meta) {
|
|
||||||
const auto meta_nca = std::make_shared<FileSys::NCA>(nca->GetBaseFile());
|
|
||||||
if (meta_nca->GetStatus() == Loader::ResultStatus::Success) {
|
|
||||||
const auto section0 = meta_nca->GetSubdirectories();
|
|
||||||
if (!section0.empty()) {
|
|
||||||
for (const auto& meta_file : section0[0]->GetFiles()) {
|
|
||||||
if (meta_file->GetExtension() == "cnmt") {
|
|
||||||
FileSys::CNMT cnmt(meta_file);
|
|
||||||
nsp_versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content_type == FileSys::ContentRecordType::Control &&
|
|
||||||
title_type == FileSys::TitleType::Update) {
|
|
||||||
auto romfs = nca->GetRomFS();
|
|
||||||
if (romfs) {
|
|
||||||
auto extracted = FileSys::ExtractRomFS(romfs);
|
|
||||||
if (extracted) {
|
|
||||||
auto nacp_file = extracted->GetFile("control.nacp");
|
|
||||||
if (!nacp_file) {
|
|
||||||
nacp_file = extracted->GetFile("Control.nacp");
|
|
||||||
}
|
|
||||||
if (nacp_file) {
|
|
||||||
FileSys::NACP nacp(nacp_file);
|
|
||||||
auto ver_str = nacp.GetVersionString();
|
|
||||||
if (!ver_str.empty()) {
|
|
||||||
nsp_version_strings[title_id] = ver_str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& [title_id, nca_map] : nsp->GetNCAs()) {
|
|
||||||
for (const auto& [type_pair, nca] : nca_map) {
|
|
||||||
const auto& [title_type, content_type] = type_pair;
|
|
||||||
|
|
||||||
if (title_type == FileSys::TitleType::Update) {
|
|
||||||
u32 version = 0;
|
|
||||||
auto ver_it = nsp_versions.find(title_id);
|
|
||||||
if (ver_it != nsp_versions.end()) {
|
|
||||||
version = ver_it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string version_string;
|
|
||||||
auto str_it = nsp_version_strings.find(title_id);
|
|
||||||
if (str_it != nsp_version_strings.end()) {
|
|
||||||
version_string = str_it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_manual_provider->AddEntryWithVersion(
|
|
||||||
title_type, content_type, title_id, version, version_string,
|
|
||||||
nca->GetBaseFile());
|
|
||||||
|
|
||||||
LOG_DEBUG(Frontend, "Added NSP update entry - TitleID: {:016X}, Version: {}, VersionStr: {}",
|
|
||||||
title_id, version, version_string);
|
|
||||||
} else {
|
|
||||||
// Use regular AddEntry for non-updates
|
|
||||||
m_manual_provider->AddEntry(title_type, content_type, title_id,
|
|
||||||
nca->GetBaseFile());
|
|
||||||
LOG_DEBUG(Frontend, "Added NSP entry - TitleID: {:016X}, TitleType: {}, ContentType: {}",
|
|
||||||
title_id, static_cast<int>(title_type), static_cast<int>(content_type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle XCI files
|
|
||||||
if (extension == "xci") {
|
|
||||||
FileSys::XCI xci{file};
|
|
||||||
if (xci.GetStatus() == Loader::ResultStatus::Success) {
|
|
||||||
const auto nsp = xci.GetSecurePartitionNSP();
|
|
||||||
if (nsp) {
|
|
||||||
for (const auto& title : nsp->GetNCAs()) {
|
|
||||||
for (const auto& entry : title.second) {
|
|
||||||
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
|
||||||
entry.second->GetBaseFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto loader = Loader::GetLoader(m_system, file);
|
auto loader = Loader::GetLoader(m_system, file);
|
||||||
|
|
@ -339,6 +240,13 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EmulationSession::ConfigureFilesystemProviderFromGameFolder(const std::string& filepath) {
|
||||||
|
if (!Settings::values.ext_content_from_game_dirs.GetValue()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ConfigureFilesystemProvider(filepath);
|
||||||
|
}
|
||||||
|
|
||||||
void EmulationSession::InitializeSystem(bool reload) {
|
void EmulationSession::InitializeSystem(bool reload) {
|
||||||
if (!reload) {
|
if (!reload) {
|
||||||
// Initialize logging system
|
// Initialize logging system
|
||||||
|
|
@ -1609,6 +1517,12 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* e
|
||||||
Common::Android::GetJString(env, jpath));
|
Common::Android::GetJString(env, jpath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_addGameFolderFileToFilesystemProvider(
|
||||||
|
JNIEnv* env, jobject jobj, jstring jpath) {
|
||||||
|
EmulationSession::GetInstance().ConfigureFilesystemProviderFromGameFolder(
|
||||||
|
Common::Android::GetJString(env, jpath));
|
||||||
|
}
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) {
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) {
|
||||||
EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
|
EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
@ -46,6 +49,7 @@ public:
|
||||||
const Core::PerfStatsResults& PerfStats();
|
const Core::PerfStatsResults& PerfStats();
|
||||||
int ShadersBuilding();
|
int ShadersBuilding();
|
||||||
void ConfigureFilesystemProvider(const std::string& filepath);
|
void ConfigureFilesystemProvider(const std::string& filepath);
|
||||||
|
void ConfigureFilesystemProviderFromGameFolder(const std::string& filepath);
|
||||||
void InitializeSystem(bool reload);
|
void InitializeSystem(bool reload);
|
||||||
void SetAppletId(int applet_id);
|
void SetAppletId(int applet_id);
|
||||||
Core::SystemResultStatus InitializeEmulation(const std::string& filepath,
|
Core::SystemResultStatus InitializeEmulation(const std::string& filepath,
|
||||||
|
|
|
||||||
|
|
@ -756,6 +756,8 @@ struct Values {
|
||||||
Category::DataStorage};
|
Category::DataStorage};
|
||||||
Setting<std::string> gamecard_path{linkage, std::string(), "gamecard_path",
|
Setting<std::string> gamecard_path{linkage, std::string(), "gamecard_path",
|
||||||
Category::DataStorage};
|
Category::DataStorage};
|
||||||
|
Setting<bool> ext_content_from_game_dirs{linkage, true, "ext_content_from_game_dirs",
|
||||||
|
Category::DataStorage};
|
||||||
std::vector<std::string> external_content_dirs;
|
std::vector<std::string> external_content_dirs;
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,12 @@ void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
|
||||||
bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
|
bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
|
||||||
return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
|
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();
|
||||||
|
}
|
||||||
} // Anonymous namespace
|
} // Anonymous namespace
|
||||||
|
|
||||||
PatchManager::PatchManager(u64 title_id_,
|
PatchManager::PatchManager(u64 title_id_,
|
||||||
|
|
@ -155,8 +161,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
||||||
if (!update_versions.empty()) {
|
if (!update_versions.empty()) {
|
||||||
checked_external = true;
|
checked_external = true;
|
||||||
for (const auto& update_entry : update_versions) {
|
for (const auto& update_entry : update_versions) {
|
||||||
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
||||||
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
|
||||||
update_disabled = false;
|
update_disabled = false;
|
||||||
enabled_version = update_entry.version;
|
enabled_version = update_entry.version;
|
||||||
break;
|
break;
|
||||||
|
|
@ -175,8 +180,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
||||||
if (!manual_update_versions.empty()) {
|
if (!manual_update_versions.empty()) {
|
||||||
checked_manual = true;
|
checked_manual = true;
|
||||||
for (const auto& update_entry : manual_update_versions) {
|
for (const auto& update_entry : manual_update_versions) {
|
||||||
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
||||||
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
|
||||||
update_disabled = false;
|
update_disabled = false;
|
||||||
enabled_version = update_entry.version;
|
enabled_version = update_entry.version;
|
||||||
break;
|
break;
|
||||||
|
|
@ -580,8 +584,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
|
||||||
if (!update_versions.empty()) {
|
if (!update_versions.empty()) {
|
||||||
checked_external = true;
|
checked_external = true;
|
||||||
for (const auto& update_entry : update_versions) {
|
for (const auto& update_entry : update_versions) {
|
||||||
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
||||||
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
|
||||||
update_disabled = false;
|
update_disabled = false;
|
||||||
enabled_version = update_entry.version;
|
enabled_version = update_entry.version;
|
||||||
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
||||||
|
|
@ -600,8 +603,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
|
||||||
if (!manual_update_versions.empty()) {
|
if (!manual_update_versions.empty()) {
|
||||||
checked_manual = true;
|
checked_manual = true;
|
||||||
for (const auto& update_entry : manual_update_versions) {
|
for (const auto& update_entry : manual_update_versions) {
|
||||||
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
|
||||||
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
|
||||||
update_disabled = false;
|
update_disabled = false;
|
||||||
enabled_version = update_entry.version;
|
enabled_version = update_entry.version;
|
||||||
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
||||||
|
|
@ -704,9 +706,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
||||||
version_str = FormatTitleVersion(update_entry.version);
|
version_str = FormatTitleVersion(update_entry.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
|
||||||
const auto update_disabled =
|
const auto update_disabled =
|
||||||
std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend();
|
IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
|
||||||
|
|
||||||
Patch update_patch = {.enabled = !update_disabled,
|
Patch update_patch = {.enabled = !update_disabled,
|
||||||
.name = "Update",
|
.name = "Update",
|
||||||
|
|
@ -732,9 +733,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
||||||
version_str = FormatTitleVersion(update_entry.version);
|
version_str = FormatTitleVersion(update_entry.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
|
||||||
const auto update_disabled =
|
const auto update_disabled =
|
||||||
std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend();
|
IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
|
||||||
|
|
||||||
|
|
||||||
Patch update_patch = {.enabled = !update_disabled,
|
Patch update_patch = {.enabled = !update_disabled,
|
||||||
|
|
@ -771,7 +771,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
||||||
std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
|
std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
|
||||||
|
|
||||||
for (const auto& [slot, entry] : all_updates) {
|
for (const auto& [slot, entry] : all_updates) {
|
||||||
if (slot == ContentProviderUnionSlot::External) {
|
if (slot == ContentProviderUnionSlot::External ||
|
||||||
|
slot == ContentProviderUnionSlot::FrontendManual) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,206 @@ static std::string GetCNMTName(TitleType type, u64 title_id) {
|
||||||
return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
|
return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::shared_ptr<NSP> OpenContainerAsNsp(const VirtualFile& file, Loader::FileType type) {
|
||||||
|
if (!file) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == Loader::FileType::Unknown || type == Loader::FileType::Error) {
|
||||||
|
type = Loader::IdentifyFile(file);
|
||||||
|
if (type == Loader::FileType::Unknown) {
|
||||||
|
type = Loader::GuessFromFilename(file->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == Loader::FileType::NSP) {
|
||||||
|
auto nsp = std::make_shared<NSP>(file);
|
||||||
|
return nsp->GetStatus() == Loader::ResultStatus::Success ? nsp : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == Loader::FileType::XCI) {
|
||||||
|
XCI xci(file);
|
||||||
|
if (xci.GetStatus() != Loader::ResultStatus::Success) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto secure_partition = xci.GetSecurePartitionNSP();
|
||||||
|
if (secure_partition == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return secure_partition;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAF-backed files can occasionally fail type-guessing despite being valid NSP/XCI.
|
||||||
|
// As a last resort, probe both container parsers directly.
|
||||||
|
{
|
||||||
|
auto nsp = std::make_shared<NSP>(file);
|
||||||
|
if (nsp->GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
return nsp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
XCI xci(file);
|
||||||
|
if (xci.GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
auto secure_partition = xci.GetSecurePartitionNSP();
|
||||||
|
if (secure_partition != nullptr) {
|
||||||
|
return secure_partition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Callback>
|
||||||
|
bool ForEachContainerEntry(const std::shared_ptr<NSP>& nsp, bool only_content,
|
||||||
|
std::optional<u64> base_program_id, Callback&& on_entry) {
|
||||||
|
if (!nsp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& ncas = nsp->GetNCAs();
|
||||||
|
if (ncas.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<u64, u32> versions;
|
||||||
|
std::map<u64, std::string> version_strings;
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : ncas) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
if (!nca) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (content_type == ContentRecordType::Meta) {
|
||||||
|
const auto subdirs = nca->GetSubdirectories();
|
||||||
|
if (!subdirs.empty()) {
|
||||||
|
for (const auto& inner_file : subdirs[0]->GetFiles()) {
|
||||||
|
if (inner_file->GetExtension() == "cnmt") {
|
||||||
|
const CNMT cnmt(inner_file);
|
||||||
|
versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title_type == TitleType::Update && content_type == ContentRecordType::Control) {
|
||||||
|
const auto romfs = nca->GetRomFS();
|
||||||
|
if (!romfs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto extracted = ExtractRomFS(romfs);
|
||||||
|
if (!extracted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nacp_file = extracted->GetFile("control.nacp");
|
||||||
|
if (!nacp_file) {
|
||||||
|
nacp_file = extracted->GetFile("Control.nacp");
|
||||||
|
}
|
||||||
|
if (!nacp_file) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NACP nacp(nacp_file);
|
||||||
|
auto version_string = nacp.GetVersionString();
|
||||||
|
if (!version_string.empty()) {
|
||||||
|
version_strings[title_id] = std::move(version_string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool added_entries = false;
|
||||||
|
for (const auto& [title_id, nca_map] : ncas) {
|
||||||
|
if (base_program_id.has_value() && GetBaseTitleID(title_id) != *base_program_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
if (only_content && title_type != TitleType::Update && title_type != TitleType::AOC) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto entry_file = nca ? nca->GetBaseFile() : nullptr;
|
||||||
|
if (!entry_file) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 version = 0;
|
||||||
|
std::string version_string;
|
||||||
|
|
||||||
|
if (title_type == TitleType::Update) {
|
||||||
|
if (const auto version_it = versions.find(title_id); version_it != versions.end()) {
|
||||||
|
version = version_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto version_str_it = version_strings.find(title_id);
|
||||||
|
version_str_it != version_strings.end()) {
|
||||||
|
version_string = version_str_it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on_entry(title_type, content_type, title_id, entry_file, version, version_string);
|
||||||
|
added_entries = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return added_entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void UpsertExternalVersionEntry(std::vector<ExternalUpdateEntry>& multi_version_entries,
|
||||||
|
u64 title_id, u32 version,
|
||||||
|
const std::string& version_string,
|
||||||
|
ContentRecordType content_type, const VirtualFile& file) {
|
||||||
|
auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(),
|
||||||
|
[title_id, version](const ExternalUpdateEntry& entry) {
|
||||||
|
return entry.title_id == title_id && entry.version == version;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it == multi_version_entries.end()) {
|
||||||
|
ExternalUpdateEntry update_entry;
|
||||||
|
update_entry.title_id = title_id;
|
||||||
|
update_entry.version = version;
|
||||||
|
update_entry.version_string = version_string;
|
||||||
|
update_entry.files[static_cast<std::size_t>(content_type)] = file;
|
||||||
|
multi_version_entries.push_back(std::move(update_entry));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
it->files[static_cast<std::size_t>(content_type)] = file;
|
||||||
|
if (it->version_string.empty() && !version_string.empty()) {
|
||||||
|
it->version_string = version_string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename EntryMap, typename VersionMap>
|
||||||
|
static bool AddExternalEntriesFromContainer(const std::shared_ptr<NSP>& nsp, EntryMap& entries,
|
||||||
|
VersionMap& versions,
|
||||||
|
std::vector<ExternalUpdateEntry>& multi_version_entries) {
|
||||||
|
return ForEachContainerEntry(
|
||||||
|
nsp, true, std::nullopt,
|
||||||
|
[&entries, &versions,
|
||||||
|
&multi_version_entries](TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||||
|
const VirtualFile& file, u32 version,
|
||||||
|
const std::string& version_string) {
|
||||||
|
entries[{title_id, content_type, title_type}] = file;
|
||||||
|
|
||||||
|
if (title_type == TitleType::Update) {
|
||||||
|
versions[title_id] = version;
|
||||||
|
UpsertExternalVersionEntry(multi_version_entries, title_id, version, version_string,
|
||||||
|
content_type, file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
|
ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case NCAContentType::Program:
|
case NCAContentType::Program:
|
||||||
|
|
@ -1008,6 +1208,26 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_content,
|
||||||
|
std::optional<u64> base_program_id) {
|
||||||
|
const auto nsp = OpenContainerAsNsp(file, Loader::FileType::Unknown);
|
||||||
|
if (!nsp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ForEachContainerEntry(
|
||||||
|
nsp, only_content, base_program_id,
|
||||||
|
[this](TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||||
|
const VirtualFile& entry_file, u32 version, const std::string& version_string) {
|
||||||
|
if (title_type == TitleType::Update) {
|
||||||
|
AddEntryWithVersion(title_type, content_type, title_id, version, version_string,
|
||||||
|
entry_file);
|
||||||
|
} else {
|
||||||
|
AddEntry(title_type, content_type, title_id, entry_file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void ManualContentProvider::ClearAllEntries() {
|
void ManualContentProvider::ClearAllEntries() {
|
||||||
entries.clear();
|
entries.clear();
|
||||||
multi_version_entries.clear();
|
multi_version_entries.clear();
|
||||||
|
|
@ -1091,14 +1311,6 @@ VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecor
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ManualContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
|
|
||||||
size_t count = 0;
|
|
||||||
for (const auto& entry : multi_version_entries)
|
|
||||||
if (entry.title_id == title_id && entry.files[size_t(type)])
|
|
||||||
++count;
|
|
||||||
return count > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
|
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
|
||||||
: load_dirs(std::move(load_directories)) {
|
: load_dirs(std::move(load_directories)) {
|
||||||
ExternalContentProvider::Refresh();
|
ExternalContentProvider::Refresh();
|
||||||
|
|
@ -1159,247 +1371,22 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
|
void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
|
||||||
auto nsp = NSP(file);
|
const auto nsp = OpenContainerAsNsp(file, Loader::FileType::NSP);
|
||||||
if (nsp.GetStatus() != Loader::ResultStatus::Success) {
|
if (!nsp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
|
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
|
||||||
|
AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
|
||||||
const auto ncas = nsp.GetNCAs();
|
|
||||||
|
|
||||||
std::map<u64, u32> nsp_versions;
|
|
||||||
std::map<u64, std::string> nsp_version_strings; // title_id -> NACP version string
|
|
||||||
|
|
||||||
for (const auto& [title_id, nca_map] : ncas) {
|
|
||||||
for (const auto& [type_pair, nca] : nca_map) {
|
|
||||||
const auto& [title_type, content_type] = type_pair;
|
|
||||||
|
|
||||||
if (content_type == ContentRecordType::Meta) {
|
|
||||||
const auto subdirs = nca->GetSubdirectories();
|
|
||||||
if (!subdirs.empty()) {
|
|
||||||
const auto section0 = subdirs[0];
|
|
||||||
const auto files = section0->GetFiles();
|
|
||||||
for (const auto& inner_file : files) {
|
|
||||||
if (inner_file->GetExtension() == "cnmt") {
|
|
||||||
const CNMT cnmt(inner_file);
|
|
||||||
const auto cnmt_title_id = cnmt.GetTitleID();
|
|
||||||
const auto version = cnmt.GetTitleVersion();
|
|
||||||
nsp_versions[cnmt_title_id] = version;
|
|
||||||
versions[cnmt_title_id] = version;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content_type == ContentRecordType::Control && title_type == TitleType::Update) {
|
|
||||||
auto romfs = nca->GetRomFS();
|
|
||||||
if (romfs) {
|
|
||||||
auto extracted = ExtractRomFS(romfs);
|
|
||||||
if (extracted) {
|
|
||||||
auto nacp_file = extracted->GetFile("control.nacp");
|
|
||||||
if (!nacp_file) {
|
|
||||||
nacp_file = extracted->GetFile("Control.nacp");
|
|
||||||
}
|
|
||||||
if (nacp_file) {
|
|
||||||
NACP nacp(nacp_file);
|
|
||||||
auto ver_str = nacp.GetVersionString();
|
|
||||||
if (!ver_str.empty()) {
|
|
||||||
nsp_version_strings[title_id] = ver_str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
|
|
||||||
|
|
||||||
for (const auto& [title_id, nca_map] : ncas) {
|
|
||||||
for (const auto& [type_pair, nca] : nca_map) {
|
|
||||||
const auto& [title_type, content_type] = type_pair;
|
|
||||||
|
|
||||||
if (title_type != TitleType::AOC && title_type != TitleType::Update) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto nca_file = nsp.GetNCAFile(title_id, content_type, title_type);
|
|
||||||
if (nca_file != nullptr) {
|
|
||||||
entries[{title_id, content_type, title_type}] = nca_file;
|
|
||||||
|
|
||||||
if (title_type == TitleType::Update) {
|
|
||||||
u32 version = 0;
|
|
||||||
auto ver_it = nsp_versions.find(title_id);
|
|
||||||
if (ver_it != nsp_versions.end()) {
|
|
||||||
version = ver_it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
version_files[{title_id, version}][size_t(content_type)] = nca_file;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG(Service_FS, "Added entry - Title ID: {:016X}, Type: {}, Content: {}",
|
|
||||||
title_id, static_cast<int>(title_type), static_cast<int>(content_type));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& [key, files_map] : version_files) {
|
|
||||||
const auto& [title_id, version] = key;
|
|
||||||
|
|
||||||
std::string ver_str;
|
|
||||||
auto str_it = nsp_version_strings.find(title_id);
|
|
||||||
if (str_it != nsp_version_strings.end()) {
|
|
||||||
ver_str = str_it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool version_exists = false;
|
|
||||||
for (auto& existing : multi_version_entries) {
|
|
||||||
if (existing.title_id == title_id && existing.version == version) {
|
|
||||||
existing.files = files_map;
|
|
||||||
if (existing.version_string.empty() && !ver_str.empty()) {
|
|
||||||
existing.version_string = ver_str;
|
|
||||||
}
|
|
||||||
version_exists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!version_exists && !files_map.empty()) {
|
|
||||||
ExternalUpdateEntry update_entry{
|
|
||||||
.title_id = title_id,
|
|
||||||
.version = version,
|
|
||||||
.version_string = ver_str,
|
|
||||||
.files = files_map
|
|
||||||
};
|
|
||||||
multi_version_entries.push_back(update_entry);
|
|
||||||
LOG_DEBUG(Service_FS, "Added multi-version update - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}",
|
|
||||||
title_id, version, ver_str, files_map.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
|
void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
|
||||||
auto xci = XCI(file);
|
const auto nsp = OpenContainerAsNsp(file, Loader::FileType::XCI);
|
||||||
if (xci.GetStatus() != Loader::ResultStatus::Success) {
|
if (!nsp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto nsp = xci.GetSecurePartitionNSP();
|
AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
|
||||||
if (nsp == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto ncas = nsp->GetNCAs();
|
|
||||||
|
|
||||||
std::map<u64, u32> xci_versions;
|
|
||||||
std::map<u64, std::string> xci_version_strings;
|
|
||||||
|
|
||||||
for (const auto& [title_id, nca_map] : ncas) {
|
|
||||||
for (const auto& [type_pair, nca] : nca_map) {
|
|
||||||
const auto& [title_type, content_type] = type_pair;
|
|
||||||
|
|
||||||
if (content_type == ContentRecordType::Meta) {
|
|
||||||
const auto subdirs = nca->GetSubdirectories();
|
|
||||||
if (!subdirs.empty()) {
|
|
||||||
const auto section0 = subdirs[0];
|
|
||||||
const auto files = section0->GetFiles();
|
|
||||||
for (const auto& inner_file : files) {
|
|
||||||
if (inner_file->GetExtension() == "cnmt") {
|
|
||||||
const CNMT cnmt(inner_file);
|
|
||||||
const auto cnmt_title_id = cnmt.GetTitleID();
|
|
||||||
const auto version = cnmt.GetTitleVersion();
|
|
||||||
xci_versions[cnmt_title_id] = version;
|
|
||||||
versions[cnmt_title_id] = version;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (content_type == ContentRecordType::Control && title_type == TitleType::Update) {
|
|
||||||
auto romfs = nca->GetRomFS();
|
|
||||||
if (romfs) {
|
|
||||||
auto extracted = ExtractRomFS(romfs);
|
|
||||||
if (extracted) {
|
|
||||||
auto nacp_file = extracted->GetFile("control.nacp");
|
|
||||||
if (!nacp_file) {
|
|
||||||
nacp_file = extracted->GetFile("Control.nacp");
|
|
||||||
}
|
|
||||||
if (nacp_file) {
|
|
||||||
NACP nacp(nacp_file);
|
|
||||||
auto ver_str = nacp.GetVersionString();
|
|
||||||
if (!ver_str.empty()) {
|
|
||||||
xci_version_strings[title_id] = ver_str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
|
|
||||||
|
|
||||||
for (const auto& [title_id, nca_map] : ncas) {
|
|
||||||
for (const auto& [type_pair, nca] : nca_map) {
|
|
||||||
const auto& [title_type, content_type] = type_pair;
|
|
||||||
|
|
||||||
if (title_type != TitleType::AOC && title_type != TitleType::Update) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto nca_file = nsp->GetNCAFile(title_id, content_type, title_type);
|
|
||||||
if (nca_file != nullptr) {
|
|
||||||
entries[{title_id, content_type, title_type}] = nca_file;
|
|
||||||
|
|
||||||
if (title_type == TitleType::Update) {
|
|
||||||
u32 version = 0;
|
|
||||||
auto ver_it = xci_versions.find(title_id);
|
|
||||||
if (ver_it != xci_versions.end()) {
|
|
||||||
version = ver_it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
version_files[{title_id, version}][size_t(content_type)] = nca_file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& [key, files_map] : version_files) {
|
|
||||||
const auto& [title_id, version] = key;
|
|
||||||
|
|
||||||
std::string ver_str;
|
|
||||||
auto str_it = xci_version_strings.find(title_id);
|
|
||||||
if (str_it != xci_version_strings.end()) {
|
|
||||||
ver_str = str_it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool version_exists = false;
|
|
||||||
for (auto& existing : multi_version_entries) {
|
|
||||||
if (existing.title_id == title_id && existing.version == version) {
|
|
||||||
existing.files = files_map;
|
|
||||||
if (existing.version_string.empty() && !ver_str.empty()) {
|
|
||||||
existing.version_string = ver_str;
|
|
||||||
}
|
|
||||||
version_exists = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!version_exists && !files_map.empty()) {
|
|
||||||
ExternalUpdateEntry update_entry{
|
|
||||||
.title_id = title_id,
|
|
||||||
.version = version,
|
|
||||||
.version_string = ver_str,
|
|
||||||
.files = files_map
|
|
||||||
};
|
|
||||||
multi_version_entries.push_back(update_entry);
|
|
||||||
LOG_DEBUG(Service_FS, "Added multi-version update from XCI - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}",
|
|
||||||
title_id, version, ver_str, files_map.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const {
|
bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const {
|
||||||
|
|
@ -1491,12 +1478,4 @@ VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRec
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExternalContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
|
|
||||||
size_t count = 0;
|
|
||||||
for (const auto& entry : multi_version_entries)
|
|
||||||
if (entry.title_id == title_id && entry.files[size_t(type)])
|
|
||||||
++count;
|
|
||||||
return count > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace FileSys
|
} // namespace FileSys
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <ankerl/unordered_dense.h>
|
#include <ankerl/unordered_dense.h>
|
||||||
|
|
@ -262,6 +263,8 @@ public:
|
||||||
VirtualFile file);
|
VirtualFile file);
|
||||||
void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||||
u32 version, const std::string& version_string, VirtualFile file);
|
u32 version, const std::string& version_string, VirtualFile file);
|
||||||
|
bool AddEntriesFromContainer(VirtualFile file, bool only_content = false,
|
||||||
|
std::optional<u64> base_program_id = std::nullopt);
|
||||||
void ClearAllEntries();
|
void ClearAllEntries();
|
||||||
|
|
||||||
void Refresh() override;
|
void Refresh() override;
|
||||||
|
|
@ -276,7 +279,6 @@ public:
|
||||||
|
|
||||||
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
|
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
|
||||||
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
|
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
|
||||||
bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
|
std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
|
||||||
|
|
@ -303,7 +305,6 @@ public:
|
||||||
|
|
||||||
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
|
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
|
||||||
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
|
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
|
||||||
bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ScanDirectory(const VirtualDir& dir);
|
void ScanDirectory(const VirtualDir& dir);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
|
|
@ -9,11 +9,15 @@
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
|
#include <algorithm>
|
||||||
#include "common/concepts.h"
|
#include "common/concepts.h"
|
||||||
#include "common/fs/path_util.h"
|
#include "common/fs/path_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
|
#include "core/file_sys/card_image.h"
|
||||||
|
#include "core/file_sys/common_funcs.h"
|
||||||
|
#include "core/file_sys/submission_package.h"
|
||||||
#include "core/hle/kernel/k_process.h"
|
#include "core/hle/kernel/k_process.h"
|
||||||
#include "core/loader/deconstructed_rom_directory.h"
|
#include "core/loader/deconstructed_rom_directory.h"
|
||||||
#include "core/loader/kip.h"
|
#include "core/loader/kip.h"
|
||||||
|
|
@ -37,6 +41,49 @@ std::optional<FileType> IdentifyFileLoader(FileSys::VirtualFile file) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, FileType type,
|
||||||
|
u64 program_id = 0,
|
||||||
|
std::size_t program_index = 0) {
|
||||||
|
if (!file) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == FileType::NSP) {
|
||||||
|
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
|
||||||
|
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == FileType::XCI) {
|
||||||
|
FileSys::XCI xci{file, program_id, program_index};
|
||||||
|
if (xci.GetStatus() != ResultStatus::Success) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto secure_nsp = xci.GetSecurePartitionNSP();
|
||||||
|
if (secure_nsp == nullptr || secure_nsp->GetStatus() != ResultStatus::Success) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return secure_nsp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasApplicationProgramContent(const std::shared_ptr<FileSys::NSP>& nsp) {
|
||||||
|
if (!nsp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& ncas = nsp->GetNCAs();
|
||||||
|
return std::any_of(ncas.cbegin(), ncas.cend(), [](const auto& title_entry) {
|
||||||
|
const auto& nca_map = title_entry.second;
|
||||||
|
return nca_map.find(
|
||||||
|
{FileSys::TitleType::Application, FileSys::ContentRecordType::Program}) !=
|
||||||
|
nca_map.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
FileType IdentifyFile(FileSys::VirtualFile file) {
|
FileType IdentifyFile(FileSys::VirtualFile file) {
|
||||||
|
|
@ -62,6 +109,27 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsContainerType(FileType type) {
|
||||||
|
return type == FileType::NSP || type == FileType::XCI;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
|
||||||
|
std::size_t program_index) {
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == FileType::Unknown) {
|
||||||
|
type = IdentifyFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsContainerType(type)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HasApplicationProgramContent(OpenContainerAsNsp(file, type, program_id, program_index));
|
||||||
|
}
|
||||||
|
|
||||||
FileType GuessFromFilename(const std::string& name) {
|
FileType GuessFromFilename(const std::string& name) {
|
||||||
if (name == "main")
|
if (name == "main")
|
||||||
return FileType::DeconstructedRomDirectory;
|
return FileType::DeconstructedRomDirectory;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
@ -46,12 +49,29 @@ enum class FileType {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies the type of a bootable file based on the magic value in its header.
|
* Identifies the type of a supported file/container based on its structure.
|
||||||
* @param file open file
|
* @param file open file
|
||||||
* @return FileType of file
|
* @return FileType of file
|
||||||
*/
|
*/
|
||||||
FileType IdentifyFile(FileSys::VirtualFile file);
|
FileType IdentifyFile(FileSys::VirtualFile file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the file type represents a container format that can bundle multiple titles
|
||||||
|
* (currently NSP/XCI).
|
||||||
|
*/
|
||||||
|
bool IsContainerType(FileType type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a container file is bootable as a game (has Application/Program content).
|
||||||
|
*
|
||||||
|
* @param file open file
|
||||||
|
* @param type optional file type; if Unknown it is auto-detected.
|
||||||
|
* @param program_id optional program id hint for multi-program containers.
|
||||||
|
* @param program_index optional program index hint for multi-program containers.
|
||||||
|
*/
|
||||||
|
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type = FileType::Unknown,
|
||||||
|
u64 program_id = 0, std::size_t program_index = 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guess the type of a bootable file from its name
|
* Guess the type of a bootable file from its name
|
||||||
* @param name String name of bootable file
|
* @param name String name of bootable file
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
@ -55,19 +58,30 @@ AppLoader_NSP::~AppLoader_NSP() = default;
|
||||||
FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) {
|
FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) {
|
||||||
const FileSys::NSP nsp(nsp_file);
|
const FileSys::NSP nsp(nsp_file);
|
||||||
|
|
||||||
if (nsp.GetStatus() == ResultStatus::Success) {
|
if (nsp.GetStatus() != ResultStatus::Success) {
|
||||||
// Extracted Type case
|
return FileType::Error;
|
||||||
if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr &&
|
}
|
||||||
FileSys::IsDirectoryExeFS(nsp.GetExeFS())) {
|
|
||||||
return FileType::NSP;
|
// Extracted Type case
|
||||||
|
if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr &&
|
||||||
|
FileSys::IsDirectoryExeFS(nsp.GetExeFS())) {
|
||||||
|
return FileType::NSP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-extracted NSPs can legitimately contain only update/DLC content.
|
||||||
|
// Identify the container format itself; bootability is validated by Load().
|
||||||
|
if (!nsp.GetNCAs().empty()) {
|
||||||
|
return FileType::NSP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback when NCAs couldn't be parsed (e.g. missing keys) but the PFS still contains NCAs.
|
||||||
|
for (const auto& entry : nsp.GetFiles()) {
|
||||||
|
if (entry == nullptr) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-Extracted Type case
|
const auto& name = entry->GetName();
|
||||||
const auto program_id = nsp.GetProgramTitleID();
|
if (name.size() >= 4 && name.substr(name.size() - 4) == ".nca") {
|
||||||
if (!nsp.IsExtractedType() &&
|
|
||||||
nsp.GetNCA(program_id, FileSys::ContentRecordType::Program) != nullptr &&
|
|
||||||
AppLoader_NCA::IdentifyType(
|
|
||||||
nsp.GetNCAFile(program_id, FileSys::ContentRecordType::Program)) == FileType::NCA) {
|
|
||||||
return FileType::NSP;
|
return FileType::NSP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
|
@ -44,10 +47,13 @@ AppLoader_XCI::~AppLoader_XCI() = default;
|
||||||
FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& xci_file) {
|
FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& xci_file) {
|
||||||
const FileSys::XCI xci(xci_file);
|
const FileSys::XCI xci(xci_file);
|
||||||
|
|
||||||
if (xci.GetStatus() == ResultStatus::Success &&
|
if (xci.GetStatus() != ResultStatus::Success) {
|
||||||
xci.GetNCAByType(FileSys::NCAContentType::Program) != nullptr &&
|
return FileType::Error;
|
||||||
AppLoader_NCA::IdentifyType(xci.GetNCAFileByType(FileSys::NCAContentType::Program)) ==
|
}
|
||||||
FileType::NCA) {
|
|
||||||
|
// Identify XCI as a valid container even when it does not include a bootable Program NCA.
|
||||||
|
// Bootability is handled by AppLoader_XCI::Load().
|
||||||
|
if (xci.GetSecurePartitionNSP() != nullptr) {
|
||||||
return FileType::XCI;
|
return FileType::XCI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
@ -16,14 +17,17 @@
|
||||||
|
|
||||||
#include "common/fs/fs.h"
|
#include "common/fs/fs.h"
|
||||||
#include "common/fs/path_util.h"
|
#include "common/fs/path_util.h"
|
||||||
|
#include "common/settings.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/file_sys/card_image.h"
|
#include "core/file_sys/card_image.h"
|
||||||
|
#include "core/file_sys/common_funcs.h"
|
||||||
#include "core/file_sys/content_archive.h"
|
#include "core/file_sys/content_archive.h"
|
||||||
#include "core/file_sys/control_metadata.h"
|
#include "core/file_sys/control_metadata.h"
|
||||||
#include "core/file_sys/fs_filesystem.h"
|
#include "core/file_sys/fs_filesystem.h"
|
||||||
#include "core/file_sys/nca_metadata.h"
|
#include "core/file_sys/nca_metadata.h"
|
||||||
#include "core/file_sys/patch_manager.h"
|
#include "core/file_sys/patch_manager.h"
|
||||||
#include "core/file_sys/registered_cache.h"
|
#include "core/file_sys/registered_cache.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
#include "core/file_sys/submission_package.h"
|
#include "core/file_sys/submission_package.h"
|
||||||
#include "core/loader/loader.h"
|
#include "core/loader/loader.h"
|
||||||
#include "yuzu/compatibility_list.h"
|
#include "yuzu/compatibility_list.h"
|
||||||
|
|
@ -375,6 +379,12 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (target == ScanTarget::PopulateGameList &&
|
||||||
|
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP) &&
|
||||||
|
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
u64 program_id = 0;
|
u64 program_id = 0;
|
||||||
const auto res2 = loader->ReadProgramId(program_id);
|
const auto res2 = loader->ReadProgramId(program_id);
|
||||||
|
|
||||||
|
|
@ -383,18 +393,10 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
|
||||||
provider->AddEntry(FileSys::TitleType::Application,
|
provider->AddEntry(FileSys::TitleType::Application,
|
||||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||||
program_id, file);
|
program_id, file);
|
||||||
} else if (res2 == Loader::ResultStatus::Success &&
|
} else if (Settings::values.ext_content_from_game_dirs.GetValue() &&
|
||||||
(file_type == Loader::FileType::XCI ||
|
(file_type == Loader::FileType::XCI ||
|
||||||
file_type == Loader::FileType::NSP)) {
|
file_type == Loader::FileType::NSP)) {
|
||||||
const auto nsp = file_type == Loader::FileType::NSP
|
void(provider->AddEntriesFromContainer(file));
|
||||||
? std::make_shared<FileSys::NSP>(file)
|
|
||||||
: FileSys::XCI{file}.GetSecurePartitionNSP();
|
|
||||||
for (const auto& title : nsp->GetNCAs()) {
|
|
||||||
for (const auto& entry : title.second) {
|
|
||||||
provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
|
||||||
entry.second->GetBaseFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
std::vector<u64> program_ids;
|
std::vector<u64> program_ids;
|
||||||
|
|
|
||||||
|
|
@ -2019,6 +2019,10 @@ void MainWindow::ConfigureFilesystemProvider(const std::string& filepath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (QtCommon::provider->AddEntriesFromContainer(file)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto loader = Loader::GetLoader(*QtCommon::system, file);
|
auto loader = Loader::GetLoader(*QtCommon::system, file);
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -2033,19 +2037,8 @@ void MainWindow::ConfigureFilesystemProvider(const std::string& filepath) {
|
||||||
const auto res2 = loader->ReadProgramId(program_id);
|
const auto res2 = loader->ReadProgramId(program_id);
|
||||||
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
|
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
|
||||||
QtCommon::provider->AddEntry(FileSys::TitleType::Application,
|
QtCommon::provider->AddEntry(FileSys::TitleType::Application,
|
||||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()), program_id,
|
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||||
file);
|
program_id, file);
|
||||||
} else if (res2 == Loader::ResultStatus::Success &&
|
|
||||||
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
|
|
||||||
const auto nsp = file_type == Loader::FileType::NSP
|
|
||||||
? std::make_shared<FileSys::NSP>(file)
|
|
||||||
: FileSys::XCI{file}.GetSecurePartitionNSP();
|
|
||||||
for (const auto& title : nsp->GetNCAs()) {
|
|
||||||
for (const auto& entry : title.second) {
|
|
||||||
QtCommon::provider->AddEntry(entry.first.first, entry.first.second, title.first,
|
|
||||||
entry.second->GetBaseFile());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue