From 25d7db7bbe008e9a336d6554bd8eadea61f095b0 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 28 Jul 2025 10:38:23 +0200 Subject: [PATCH] Z3DS: Mark compressed files in UI and other minor fixes (#1249) --- .../citra/citra_emu/adapters/GameAdapter.kt | 5 +- .../java/org/citra/citra_emu/model/Game.kt | 5 +- .../org/citra/citra_emu/model/GameInfo.kt | 11 +- .../org/citra/citra_emu/utils/GameHelper.kt | 21 ++-- src/android/app/src/main/jni/game_info.cpp | 119 +++++++++++++----- .../src/main/res/layout/dialog_about_game.xml | 11 +- .../app/src/main/res/values/strings.xml | 4 +- .../configuration/configure_per_game.cpp | 4 +- src/citra_qt/game_list_worker.cpp | 8 +- src/core/loader/3dsx.cpp | 4 + src/core/loader/3dsx.h | 2 + src/core/loader/loader.cpp | 10 +- src/core/loader/loader.h | 6 +- src/core/loader/ncch.cpp | 16 ++- src/core/loader/ncch.h | 2 + 15 files changed, 160 insertions(+), 68 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt index f43165a8a..d03ff2936 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt @@ -343,8 +343,9 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater: bottomSheetView.findViewById(R.id.about_game_title).text = game.title bottomSheetView.findViewById(R.id.about_game_company).text = game.company bottomSheetView.findViewById(R.id.about_game_region).text = game.regions - bottomSheetView.findViewById(R.id.about_game_id).text = "ID: " + String.format("%016X", game.titleId) - bottomSheetView.findViewById(R.id.about_game_filename).text = "File: " + game.filename + bottomSheetView.findViewById(R.id.about_game_id).text = context.getString(R.string.game_context_id) + " " + String.format("%016X", game.titleId) + bottomSheetView.findViewById(R.id.about_game_filename).text = context.getString(R.string.game_context_file) + " " + game.filename + bottomSheetView.findViewById(R.id.about_game_filetype).text = context.getString(R.string.game_context_type) + " " + game.fileType GameIconUtils.loadGameIcon(activity, game, bottomSheetView.findViewById(R.id.game_icon)) bottomSheetView.findViewById(R.id.about_game_play).setOnClickListener { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt index 7fb6825dd..85f483896 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt @@ -1,4 +1,4 @@ -// Copyright Citra Emulator Project / Lime3DS Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -26,7 +26,8 @@ class Game( val isSystemTitle: Boolean = false, val isVisibleSystemTitle: Boolean = false, val icon: IntArray? = null, - val filename: String + val fileType: String = "", + val filename: String, ) : Parcelable { val keyAddedToLibraryTime get() = "${filename}_AddedToLibraryTime" val keyLastPlayedTime get() = "${filename}_LastPlayed" diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt index 469ace945..817e5fdec 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/GameInfo.kt @@ -13,25 +13,30 @@ class GameInfo(path: String) { init { pointer = initialize(path) - if (pointer == 0L) { - throw IOException() - } } protected external fun finalize() external fun getTitle(): String + external fun isValid(): Boolean + external fun isEncrypted(): Boolean + external fun getTitleID(): Long + external fun getRegions(): String external fun getCompany(): String external fun getIcon(): IntArray? + external fun isSystemTitle(): Boolean + external fun getIsVisibleSystemTitle(): Boolean + external fun getFileType(): String + companion object { @JvmStatic private external fun initialize(path: String): Long diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt index 771064d74..ffbeaf394 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt @@ -70,29 +70,26 @@ object GameHelper { fun getGame(uri: Uri, isInstalled: Boolean, addedToLibrary: Boolean): Game { val filePath = uri.toString() - var gameInfo: GameInfo? = try { - GameInfo(filePath) - } catch (e: IOException) { - null + var gameInfo: GameInfo? = GameInfo(filePath) + + if (gameInfo?.isValid() == false) { + gameInfo = null } - var isEncrypted = false - if (gameInfo?.isEncrypted() == true) { - gameInfo = null - isEncrypted = true - } + val isEncrypted = gameInfo?.isEncrypted() == true val newGame = Game( (gameInfo?.getTitle() ?: FileUtil.getFilename(uri)).replace("[\\t\\n\\r]+".toRegex(), " "), filePath.replace("\n", " "), filePath, - NativeLibrary.getTitleId(filePath), + gameInfo?.getTitleID() ?: 0, gameInfo?.getCompany() ?: "", - gameInfo?.getRegions() ?: (if (isEncrypted) { CitraApplication.appContext.getString(R.string.unsupported_encrypted) } else { CitraApplication.appContext.getString(R.string.invalid_region) }), + if (isEncrypted) { CitraApplication.appContext.getString(R.string.unsupported_encrypted) } else { gameInfo?.getRegions() ?: "" }, isInstalled, - NativeLibrary.getIsSystemTitle(filePath), + gameInfo?.isSystemTitle() ?: false, gameInfo?.getIsVisibleSystemTitle() ?: false, gameInfo?.getIcon(), + gameInfo?.getFileType() ?: "", if (FileUtil.isNativePath(filePath)) { CitraApplication.documentsTree.getFilename(filePath) } else { diff --git a/src/android/app/src/main/jni/game_info.cpp b/src/android/app/src/main/jni/game_info.cpp index 3f5855452..a8c27e53a 100644 --- a/src/android/app/src/main/jni/game_info.cpp +++ b/src/android/app/src/main/jni/game_info.cpp @@ -17,28 +17,41 @@ namespace { -std::vector GetSMDHData(const std::string& path, bool& is_encrypted) { - std::unique_ptr loader = Loader::GetLoader(path); - if (!loader) { - return {}; - } +static constexpr u64 UPDATE_TID_HIGH = 0x0004000e00000000; +struct GameInfoData { + Loader::SMDH smdh; + u64 title_id = 0; + bool loaded = false; + bool is_encrypted = false; + std::string file_type = ""; +}; + +GameInfoData* GetNewGameInfoData(const std::string& path) { + std::unique_ptr loader = Loader::GetLoader(path); u64 program_id = 0; - loader->ReadProgramId(program_id); + bool is_encrypted = false; + + if (!loader || loader->ReadProgramId(program_id) != Loader::ResultStatus::Success) { + GameInfoData* gid = new GameInfoData(); + memset(&gid->smdh, 0, sizeof(Loader::SMDH)); + return gid; + } std::vector smdh = [program_id, &loader, &is_encrypted]() -> std::vector { std::vector original_smdh; auto result = loader->ReadIcon(original_smdh); - if (result == Loader::ResultStatus::ErrorEncrypted) { - is_encrypted = true; - return original_smdh; + if (result != Loader::ResultStatus::Success) { + is_encrypted = result == Loader::ResultStatus::ErrorEncrypted; + return {}; } if (program_id < 0x00040000'00000000 || program_id > 0x00040000'FFFFFFFF) return original_smdh; - std::string update_path = Service::AM::GetTitleContentPath( - Service::FS::MediaType::SDMC, program_id + 0x0000000E'00000000); + u64 update_tid = (program_id & 0xFFFFFFFFULL) | UPDATE_TID_HIGH; + std::string update_path = + Service::AM::GetTitleContentPath(Service::FS::MediaType::SDMC, update_tid); if (!FileUtil::Exists(update_path)) return original_smdh; @@ -49,41 +62,50 @@ std::vector GetSMDHData(const std::string& path, bool& is_encrypted) { return original_smdh; std::vector update_smdh; - update_loader->ReadIcon(update_smdh); + result = update_loader->ReadIcon(update_smdh); + if (result != Loader::ResultStatus::Success) { + is_encrypted = result == Loader::ResultStatus::ErrorEncrypted; + return {}; + } return update_smdh; }(); - return smdh; + GameInfoData* gid = new GameInfoData(); + if (smdh.empty()) { + std::memset(&gid->smdh, 0, sizeof(Loader::SMDH)); + } else { + std::memcpy(&gid->smdh, smdh.data(), smdh.size()); + } + gid->loaded = true; + gid->is_encrypted = is_encrypted; + gid->title_id = program_id; + gid->file_type = Loader::GetFileTypeString(loader->GetFileType(), loader->IsFileCompressed()); + + return gid; } } // namespace extern "C" { -static Loader::SMDH* GetPointer(JNIEnv* env, jobject obj) { - return reinterpret_cast(env->GetLongField(obj, IDCache::GetGameInfoPointer())); +static GameInfoData* GetPointer(JNIEnv* env, jobject obj) { + return reinterpret_cast(env->GetLongField(obj, IDCache::GetGameInfoPointer())); } JNIEXPORT jlong JNICALL Java_org_citra_citra_1emu_model_GameInfo_initialize(JNIEnv* env, jclass, jstring j_path) { - bool is_encrypted = false; - std::vector smdh_data = GetSMDHData(GetJString(env, j_path), is_encrypted); + GameInfoData* game_info_data = GetNewGameInfoData(GetJString(env, j_path)); + return reinterpret_cast(game_info_data); +} - Loader::SMDH* smdh = nullptr; - if (is_encrypted) { - smdh = new Loader::SMDH; - smdh->magic = 0xDEADDEAD; - } else if (Loader::IsValidSMDH(smdh_data)) { - smdh = new Loader::SMDH; - std::memcpy(smdh, smdh_data.data(), sizeof(Loader::SMDH)); - } - return reinterpret_cast(smdh); +JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_model_GameInfo_isValid(JNIEnv* env, + jobject obj) { + return GetPointer(env, obj)->loaded; } JNIEXPORT jboolean JNICALL Java_org_citra_citra_1emu_model_GameInfo_isEncrypted(JNIEnv* env, jobject obj) { - Loader::SMDH* smdh = GetPointer(env, obj); - return smdh->magic == 0xDEADDEAD; + return GetPointer(env, obj)->is_encrypted; } JNIEXPORT void JNICALL Java_org_citra_citra_1emu_model_GameInfo_finalize(JNIEnv* env, jobject obj) { @@ -91,7 +113,11 @@ JNIEXPORT void JNICALL Java_org_citra_citra_1emu_model_GameInfo_finalize(JNIEnv* } jstring Java_org_citra_citra_1emu_model_GameInfo_getTitle(JNIEnv* env, jobject obj) { - Loader::SMDH* smdh = GetPointer(env, obj); + Loader::SMDH* smdh = &GetPointer(env, obj)->smdh; + if (!smdh->IsValid()) { + return ToJString(env, ""); + } + Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English; // Get the title from SMDH in UTF-16 format @@ -102,7 +128,11 @@ jstring Java_org_citra_citra_1emu_model_GameInfo_getTitle(JNIEnv* env, jobject o } jstring Java_org_citra_citra_1emu_model_GameInfo_getCompany(JNIEnv* env, jobject obj) { - Loader::SMDH* smdh = GetPointer(env, obj); + Loader::SMDH* smdh = &GetPointer(env, obj)->smdh; + if (!smdh->IsValid()) { + return ToJString(env, ""); + } + Loader::SMDH::TitleLanguage language = Loader::SMDH::TitleLanguage::English; // Get the Publisher's name from SMDH in UTF-16 format @@ -113,8 +143,15 @@ jstring Java_org_citra_citra_1emu_model_GameInfo_getCompany(JNIEnv* env, jobject return ToJString(env, Common::UTF16ToUTF8(publisher).data()); } +jlong Java_org_citra_citra_1emu_model_GameInfo_getTitleID(JNIEnv* env, jobject obj) { + return static_cast(GetPointer(env, obj)->title_id); +} + jstring Java_org_citra_citra_1emu_model_GameInfo_getRegions(JNIEnv* env, jobject obj) { - Loader::SMDH* smdh = GetPointer(env, obj); + Loader::SMDH* smdh = &GetPointer(env, obj)->smdh; + if (!smdh->IsValid()) { + return ToJString(env, ""); + } using GameRegion = Loader::SMDH::GameRegion; static const std::map regions_map = { @@ -147,7 +184,10 @@ jstring Java_org_citra_citra_1emu_model_GameInfo_getRegions(JNIEnv* env, jobject } jintArray Java_org_citra_citra_1emu_model_GameInfo_getIcon(JNIEnv* env, jobject obj) { - Loader::SMDH* smdh = GetPointer(env, obj); + Loader::SMDH* smdh = &GetPointer(env, obj)->smdh; + if (!smdh->IsValid()) { + return nullptr; + } // Always get a 48x48(large) icon std::vector icon_data = smdh->GetIcon(true); @@ -162,12 +202,23 @@ jintArray Java_org_citra_citra_1emu_model_GameInfo_getIcon(JNIEnv* env, jobject return icon; } +jboolean Java_org_citra_citra_1emu_model_GameInfo_isSystemTitle(JNIEnv* env, jobject obj) { + return ((GetPointer(env, obj)->title_id >> 32) & 0xFFFFFFFF) == 0x00040010; +} + jboolean Java_org_citra_citra_1emu_model_GameInfo_getIsVisibleSystemTitle(JNIEnv* env, jobject obj) { - Loader::SMDH* smdh = GetPointer(env, obj); - if (smdh == nullptr) { + Loader::SMDH* smdh = &GetPointer(env, obj)->smdh; + if (!smdh->IsValid()) { return false; } + return smdh->flags & Loader::SMDH::Flags::Visible; } + +jstring Java_org_citra_citra_1emu_model_GameInfo_getFileType(JNIEnv* env, jobject obj) { + std::string& file_type = GetPointer(env, obj)->file_type; + + return ToJString(env, file_type); +} } diff --git a/src/android/app/src/main/res/layout/dialog_about_game.xml b/src/android/app/src/main/res/layout/dialog_about_game.xml index 76df39868..feb95cf2d 100644 --- a/src/android/app/src/main/res/layout/dialog_about_game.xml +++ b/src/android/app/src/main/res/layout/dialog_about_game.xml @@ -86,12 +86,21 @@ tools:text="Application Filename" /> + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index cfc0565b1..e04cdb0a9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -539,7 +539,6 @@ Save/Load Error Fatal Error A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs. - Invalid region Unsupported encrypted application @@ -565,6 +564,9 @@ Create Shortcut Shortcut name cannot be empty Stretch to fit image + ID: + File: + Type: Show Performance Overlay diff --git a/src/citra_qt/configuration/configure_per_game.cpp b/src/citra_qt/configuration/configure_per_game.cpp index 1a943d732..29c0d3175 100644 --- a/src/citra_qt/configuration/configure_per_game.cpp +++ b/src/citra_qt/configuration/configure_per_game.cpp @@ -173,8 +173,8 @@ void ConfigurePerGame::LoadConfiguration() { ui->display_filepath->setText(QString::fromStdString(filename)); - ui->display_format->setText( - QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))); + ui->display_format->setText(QString::fromStdString( + Loader::GetFileTypeString(loader->GetFileType(), loader->IsFileCompressed()))); const auto valueText = ReadableByteSize(FileUtil::GetSize(filename)); ui->display_size->setText(valueText); diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp index 96c8baa70..b6376261f 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/citra_qt/game_list_worker.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2018 yuzu emulator team // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -112,8 +116,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign res == Loader::ResultStatus::ErrorEncrypted), new GameListItemCompat(compatibility), new GameListItemRegion(smdh), - new GameListItem( - QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), + new GameListItem(QString::fromStdString(Loader::GetFileTypeString( + loader->GetFileType(), loader->IsFileCompressed()))), new GameListItemSize(FileUtil::GetSize(physical_name)), new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)), }, diff --git a/src/core/loader/3dsx.cpp b/src/core/loader/3dsx.cpp index 0a3a7b662..e989ee7e2 100644 --- a/src/core/loader/3dsx.cpp +++ b/src/core/loader/3dsx.cpp @@ -346,6 +346,10 @@ AppLoader::CompressFileInfo AppLoader_THREEDSX::GetCompressFileInfo() { return info; } +bool AppLoader_THREEDSX::IsFileCompressed() { + return FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file.get()) != std::nullopt; +} + ResultStatus AppLoader_THREEDSX::ReadIcon(std::vector& buffer) { if (!file->IsOpen()) return ResultStatus::Error; diff --git a/src/core/loader/3dsx.h b/src/core/loader/3dsx.h index 4403a6763..e7cf2ab74 100644 --- a/src/core/loader/3dsx.h +++ b/src/core/loader/3dsx.h @@ -41,6 +41,8 @@ public: CompressFileInfo GetCompressFileInfo() override; + bool IsFileCompressed() override; + private: std::string filename; std::string filepath; diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp index 205f81a00..3b23f7762 100644 --- a/src/core/loader/loader.cpp +++ b/src/core/loader/loader.cpp @@ -63,18 +63,18 @@ FileType GuessFromExtension(const std::string& extension_) { return FileType::Unknown; } -const char* GetFileTypeString(FileType type) { +const char* GetFileTypeString(FileType type, bool is_compressed) { switch (type) { case FileType::CCI: - return "NCSD"; + return is_compressed ? "NCSD (Z)" : "NCSD"; case FileType::CXI: - return "NCCH"; + return is_compressed ? "NCCH (Z)" : "NCCH"; case FileType::CIA: - return "CIA"; + return is_compressed ? "CIA (Z)" : "CIA"; case FileType::ELF: return "ELF"; case FileType::THREEDSX: - return "3DSX"; + return is_compressed ? "3DSX (Z)" : "3DSX"; case FileType::ARTIC: return "ARTIC"; case FileType::Error: diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index a98536d1c..2c5ef952f 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -60,7 +60,7 @@ FileType GuessFromExtension(const std::string& extension); /** * Convert a FileType into a string which can be displayed to the user. */ -const char* GetFileTypeString(FileType type); +const char* GetFileTypeString(FileType type, bool is_compressed = false); /// Return type for functions in Loader namespace enum class ResultStatus { @@ -294,6 +294,10 @@ public: return info; } + virtual bool IsFileCompressed() { + return false; + } + protected: Core::System& system; std::unique_ptr file; diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 05fe13dee..ef33c42d3 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -60,9 +60,12 @@ FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile* file) { if (MakeMagic('N', 'C', 'C', 'H') == magic) return FileType::CXI; - std::optional magic_zstd; - if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file) != std::nullopt || - FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file_crypto.get()) != std::nullopt) { + std::optional magic_zstd = FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file); + if (!magic_zstd.has_value()) { + magic_zstd = FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file_crypto.get()); + } + + if (magic_zstd.has_value()) { if (MakeMagic('N', 'C', 'S', 'D') == magic_zstd) return FileType::CCI; @@ -437,4 +440,11 @@ AppLoader::CompressFileInfo AppLoader_NCCH::GetCompressFileInfo() { return info; } +bool AppLoader_NCCH::IsFileCompressed() { + if (base_ncch.LoadHeader() != ResultStatus::Success) { + return false; + } + return base_ncch.IsFileCompressed(); +} + } // namespace Loader diff --git a/src/core/loader/ncch.h b/src/core/loader/ncch.h index 2610baeca..d36e4c249 100644 --- a/src/core/loader/ncch.h +++ b/src/core/loader/ncch.h @@ -76,6 +76,8 @@ public: CompressFileInfo GetCompressFileInfo() override; + bool IsFileCompressed() override; + private: /** * Loads .code section into memory for booting