diff options
7 files changed, 183 insertions, 5 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index 22af9e435..4be9ade14 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt @@ -227,6 +227,8 @@ object NativeLibrary { external fun setAppDirectory(directory: String) + external fun installFileToNand(filename: String): Int + external fun initializeGpuDriver( hookLibDir: String?, customDriverDir: String?, @@ -507,4 +509,15 @@ object NativeLibrary { const val RELEASED = 0 const val PRESSED = 1 } + + /** + * Result from installFileToNand + */ + object InstallFileToNandResult { + const val Success = 0 + const val SuccessFileOverwritten = 1 + const val Error = 2 + const val ErrorBaseGame = 3 + const val ErrorFilenameExtension = 4 + } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index bdc337501..536163eb6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -95,6 +95,11 @@ class HomeSettingsFragment : Fragment() { R.drawable.ic_nfc ) { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }, HomeSetting( + R.string.install_game_content, + R.string.install_game_content_description, + R.drawable.ic_system_update_alt + ) { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }, + HomeSetting( R.string.select_games_folder, R.string.select_games_folder_description, R.drawable.ic_add @@ -103,7 +108,12 @@ class HomeSettingsFragment : Fragment() { R.string.manage_save_data, R.string.import_export_saves_description, R.drawable.ic_save - ) { ImportExportSavesFragment().show(parentFragmentManager, ImportExportSavesFragment.TAG) }, + ) { + ImportExportSavesFragment().show( + parentFragmentManager, + ImportExportSavesFragment.TAG + ) + }, HomeSetting( R.string.install_prod_keys, R.string.install_prod_keys_description, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 3fca0a7e6..041d16f3a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -467,4 +467,62 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } } } + + val installGameUpdate = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { + if (it == null) + return@registerForActivityResult + + IndeterminateProgressDialogFragment.newInstance( + this@MainActivity, + R.string.install_game_content + ) { + val result = NativeLibrary.installFileToNand(it.toString()) + lifecycleScope.launch { + withContext(Dispatchers.Main) { + when (result) { + NativeLibrary.InstallFileToNandResult.Success -> { + Toast.makeText( + applicationContext, + R.string.install_game_content_success, + Toast.LENGTH_SHORT + ).show() + } + + NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { + Toast.makeText( + applicationContext, + R.string.install_game_content_success_overwrite, + Toast.LENGTH_SHORT + ).show() + } + + NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { + MessageDialogFragment.newInstance( + R.string.install_game_content_failure, + R.string.install_game_content_failure_base + ).show(supportFragmentManager, MessageDialogFragment.TAG) + } + + NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { + MessageDialogFragment.newInstance( + R.string.install_game_content_failure, + R.string.install_game_content_failure_file_extension, + R.string.install_game_content_help_link + ).show(supportFragmentManager, MessageDialogFragment.TAG) + } + + else -> { + MessageDialogFragment.newInstance( + R.string.install_game_content_failure, + R.string.install_game_content_failure_description, + R.string.install_game_content_help_link + ).show(supportFragmentManager, MessageDialogFragment.TAG) + } + } + } + } + return@newInstance result + }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + } } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 7ebed5e6a..4091c23d1 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -28,7 +28,10 @@ #include "core/core.h" #include "core/cpu_manager.h" #include "core/crypto/key_manager.h" +#include "core/file_sys/card_image.h" #include "core/file_sys/registered_cache.h" +#include "core/file_sys/submission_package.h" +#include "core/file_sys/vfs.h" #include "core/file_sys/vfs_real.h" #include "core/frontend/applets/cabinet.h" #include "core/frontend/applets/controller.h" @@ -94,6 +97,74 @@ public: m_native_window = native_window; } + int InstallFileToNand(std::string filename) { + const auto copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest, + std::size_t block_size) { + if (src == nullptr || dest == nullptr) { + return false; + } + if (!dest->Resize(src->GetSize())) { + return false; + } + + using namespace Common::Literals; + std::vector<u8> buffer(1_MiB); + + for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) { + const auto read = src->Read(buffer.data(), buffer.size(), i); + dest->Write(buffer.data(), read, i); + } + return true; + }; + + enum InstallResult { + Success = 0, + SuccessFileOverwritten = 1, + InstallError = 2, + ErrorBaseGame = 3, + ErrorFilenameExtension = 4, + }; + + m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); + m_system.GetFileSystemController().CreateFactories(*m_vfs); + + std::shared_ptr<FileSys::NSP> nsp; + if (filename.ends_with("nsp")) { + nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); + if (nsp->IsExtractedType()) { + return InstallError; + } + } else if (filename.ends_with("xci")) { + const auto xci = + std::make_shared<FileSys::XCI>(m_vfs->OpenFile(filename, FileSys::Mode::Read)); + nsp = xci->GetSecurePartitionNSP(); + } else { + return ErrorFilenameExtension; + } + + if (!nsp) { + return InstallError; + } + + if (nsp->GetStatus() != Loader::ResultStatus::Success) { + return InstallError; + } + + const auto res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry( + *nsp, true, copy_func); + + switch (res) { + case FileSys::InstallResult::Success: + return Success; + case FileSys::InstallResult::OverwriteExisting: + return SuccessFileOverwritten; + case FileSys::InstallResult::ErrorBaseInstall: + return ErrorBaseGame; + default: + return InstallError; + } + } + void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, const std::string& custom_driver_name, const std::string& file_redirect_dir) { @@ -154,14 +225,14 @@ public: m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library); + m_system.SetFilesystem(m_vfs); + // Initialize system. auto android_keyboard = std::make_unique<SoftwareKeyboard::AndroidKeyboard>(); m_software_keyboard = android_keyboard.get(); m_system.SetShuttingDown(false); m_system.ApplySettings(); m_system.HIDCore().ReloadInputDevices(); - m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); - m_system.SetFilesystem(std::make_shared<FileSys::RealVfsFilesystem>()); m_system.SetAppletFrontendSet({ nullptr, // Amiibo Settings nullptr, // Controller Selector @@ -173,7 +244,8 @@ public: std::move(android_keyboard), // Software Keyboard nullptr, // Web Browser }); - m_system.GetFileSystemController().CreateFactories(*m_system.GetFilesystem()); + m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>()); + m_system.GetFileSystemController().CreateFactories(*m_vfs); // Initialize account manager m_profile_manager = std::make_unique<Service::Account::ProfileManager>(); @@ -398,7 +470,7 @@ private: InputCommon::InputSubsystem m_input_subsystem; Common::DetachedTasks m_detached_tasks; Core::PerfStatsResults m_perf_stats{}; - std::shared_ptr<FileSys::RealVfsFilesystem> m_vfs; + std::shared_ptr<FileSys::VfsFilesystem> m_vfs; Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; bool m_is_running{}; SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; @@ -466,6 +538,12 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, Common::FS::SetAppDirectory(GetJString(env, j_directory)); } +int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, + [[maybe_unused]] jclass clazz, + jstring j_file) { + return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file)); +} + void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver( JNIEnv* env, [[maybe_unused]] jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir, jstring custom_driver_name, jstring file_redirect_dir) { diff --git a/src/android/app/src/main/res/drawable/ic_system_update_alt.xml b/src/android/app/src/main/res/drawable/ic_system_update_alt.xml new file mode 100644 index 000000000..0f6adfdb8 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_system_update_alt.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="#FF000000" + android:pathData="M140,800q-24,0 -42,-18t-18,-42v-520q0,-24 18,-42t42,-18h250v60L140,220v520h680v-520L570,220v-60h250q24,0 42,18t18,42v520q0,24 -18,42t-42,18L140,800ZM480,615L280,415l43,-43 127,127v-339h60v339l127,-127 43,43 -200,200Z"/> +</vector> diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 6e9d47557..7dae63dcb 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -102,6 +102,15 @@ <string name="share_log">Share debug logs</string> <string name="share_log_description">Share yuzu\'s log file to debug issues</string> <string name="share_log_missing">No log file found</string> + <string name="install_game_content">Install game content</string> + <string name="install_game_content_description">Install game updates or DLC</string> + <string name="install_game_content_failure">Error installing file to NAND</string> + <string name="install_game_content_failure_description">Game content installation failed. Please ensure content is valid and that the prod.keys file is installed.</string> + <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts. Please select an update or DLC instead.</string> + <string name="install_game_content_failure_file_extension">The selected file type is not supported. Only NSP and XCI content is supported for this action. Please verify the game content is valid.</string> + <string name="install_game_content_success">Game content installed successfully</string> + <string name="install_game_content_success_overwrite">Game content was overwritten successfully</string> + <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string> <!-- About screen strings --> <string name="gaia_is_not_real">Gaia isn\'t real</string> diff --git a/src/core/file_sys/submission_package.h b/src/core/file_sys/submission_package.h index 3226b884a..27f97c725 100644 --- a/src/core/file_sys/submission_package.h +++ b/src/core/file_sys/submission_package.h @@ -8,6 +8,7 @@ #include <set> #include <vector> #include "common/common_types.h" +#include "core/file_sys/nca_metadata.h" #include "core/file_sys/vfs.h" namespace Core::Crypto { |