mirror of
https://git.eden-emu.dev/eden-emu/eden
synced 2026-04-15 04:38:57 +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)
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -127,10 +127,6 @@ class AddonViewModel : ViewModel() {
|
|||
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(
|
||||
game!!.programId,
|
||||
_patchList.value.mapNotNull {
|
||||
|
|
@ -140,7 +136,7 @@ class AddonViewModel : ViewModel() {
|
|||
if (PatchType.from(it.type) == PatchType.Update) {
|
||||
if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) {
|
||||
it.name
|
||||
} else if (hasMultipleUpdates) {
|
||||
} else if (it.numericVersion != 0L) {
|
||||
"Update@${it.numericVersion}"
|
||||
} else {
|
||||
it.name
|
||||
|
|
|
|||
|
|
@ -424,7 +424,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||
)
|
||||
|
||||
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) {
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
|
|
|
|||
|
|
@ -51,11 +51,24 @@ object GameHelper {
|
|||
|
||||
// Scan External Content directories and register all NSP/XCI files
|
||||
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()) {
|
||||
val externalDirUri = externalDir.toUri()
|
||||
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 isValid = FileUtil.isTreeUriValid(gameDirUri)
|
||||
if (isValid) {
|
||||
val scanDepth = if (gameDir.deepScan) 3 else 1
|
||||
|
||||
addGamesRecursive(
|
||||
games,
|
||||
FileUtil.listFiles(gameDirUri),
|
||||
if (gameDir.deepScan) 3 else 1
|
||||
scanDepth,
|
||||
mountedContainerUris
|
||||
)
|
||||
} else {
|
||||
badDirs.add(index)
|
||||
|
|
@ -103,9 +119,10 @@ object GameHelper {
|
|||
// be done better imo.
|
||||
private val externalContentExtensions = setOf("nsp", "xci")
|
||||
|
||||
private fun scanExternalContentRecursive(
|
||||
private fun scanContentContainersRecursive(
|
||||
files: Array<MinimalDocumentFile>,
|
||||
depth: Int
|
||||
depth: Int,
|
||||
onContainerFound: (MinimalDocumentFile) -> Unit
|
||||
) {
|
||||
if (depth <= 0) {
|
||||
return
|
||||
|
|
@ -113,14 +130,15 @@ object GameHelper {
|
|||
|
||||
files.forEach {
|
||||
if (it.isDirectory) {
|
||||
scanExternalContentRecursive(
|
||||
scanContentContainersRecursive(
|
||||
FileUtil.listFiles(it.uri),
|
||||
depth - 1
|
||||
depth - 1,
|
||||
onContainerFound
|
||||
)
|
||||
} else {
|
||||
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||
if (externalContentExtensions.contains(extension)) {
|
||||
NativeLibrary.addFileToFilesystemProvider(it.uri.toString())
|
||||
onContainerFound(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +147,8 @@ object GameHelper {
|
|||
private fun addGamesRecursive(
|
||||
games: MutableList<Game>,
|
||||
files: Array<MinimalDocumentFile>,
|
||||
depth: Int
|
||||
depth: Int,
|
||||
mountedContainerUris: MutableSet<String>
|
||||
) {
|
||||
if (depth <= 0) {
|
||||
return
|
||||
|
|
@ -140,11 +159,20 @@ object GameHelper {
|
|||
addGamesRecursive(
|
||||
games,
|
||||
FileUtil.listFiles(it.uri),
|
||||
depth - 1
|
||||
depth - 1,
|
||||
mountedContainerUris
|
||||
)
|
||||
} else {
|
||||
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
|
||||
val game = getGame(it.uri, true)
|
||||
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||
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) {
|
||||
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()
|
||||
if (!GameMetadata.getIsValid(filePath)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Needed to update installed content information
|
||||
NativeLibrary.addFileToFilesystemProvider(filePath)
|
||||
if (registerFilesystemProvider) {
|
||||
// Needed to update installed content information
|
||||
NativeLibrary.addFileToFilesystemProvider(filePath)
|
||||
}
|
||||
|
||||
var name = GameMetadata.getTitle(filePath)
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ void AndroidConfig::ReadAndroidValues() {
|
|||
if (global) {
|
||||
ReadAndroidUIValues();
|
||||
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();
|
||||
}
|
||||
ReadDriverValues();
|
||||
|
|
|
|||
|
|
@ -96,6 +96,11 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj
|
|||
return false;
|
||||
}
|
||||
|
||||
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) &&
|
||||
!Loader::IsBootableGameContainer(file, file_type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
u64 program_id = 0;
|
||||
Loader::ResultStatus res = loader->ReadProgramId(program_id);
|
||||
if (res != Loader::ResultStatus::Success) {
|
||||
|
|
|
|||
|
|
@ -217,107 +217,8 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
|||
return;
|
||||
}
|
||||
|
||||
const auto extension = Common::ToLower(filepath.substr(filepath.find_last_of('.') + 1));
|
||||
|
||||
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;
|
||||
}
|
||||
if (m_manual_provider->AddEntriesFromContainer(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!reload) {
|
||||
// Initialize logging system
|
||||
|
|
@ -1609,6 +1517,12 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* e
|
|||
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) {
|
||||
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-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
|
|
@ -46,6 +49,7 @@ public:
|
|||
const Core::PerfStatsResults& PerfStats();
|
||||
int ShadersBuilding();
|
||||
void ConfigureFilesystemProvider(const std::string& filepath);
|
||||
void ConfigureFilesystemProviderFromGameFolder(const std::string& filepath);
|
||||
void InitializeSystem(bool reload);
|
||||
void SetAppletId(int applet_id);
|
||||
Core::SystemResultStatus InitializeEmulation(const std::string& filepath,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue