Z3DS: Mark compressed files in UI and other minor fixes (#1249)

This commit is contained in:
PabloMK7 2025-07-28 10:38:23 +02:00 committed by GitHub
parent c95b942ec2
commit 25d7db7bbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 160 additions and 68 deletions

View File

@ -343,8 +343,9 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater:
bottomSheetView.findViewById<TextView>(R.id.about_game_title).text = game.title
bottomSheetView.findViewById<TextView>(R.id.about_game_company).text = game.company
bottomSheetView.findViewById<TextView>(R.id.about_game_region).text = game.regions
bottomSheetView.findViewById<TextView>(R.id.about_game_id).text = "ID: " + String.format("%016X", game.titleId)
bottomSheetView.findViewById<TextView>(R.id.about_game_filename).text = "File: " + game.filename
bottomSheetView.findViewById<TextView>(R.id.about_game_id).text = context.getString(R.string.game_context_id) + " " + String.format("%016X", game.titleId)
bottomSheetView.findViewById<TextView>(R.id.about_game_filename).text = context.getString(R.string.game_context_file) + " " + game.filename
bottomSheetView.findViewById<TextView>(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<MaterialButton>(R.id.about_game_play).setOnClickListener {

View File

@ -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"

View File

@ -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

View File

@ -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 {

View File

@ -17,28 +17,41 @@
namespace {
std::vector<u8> GetSMDHData(const std::string& path, bool& is_encrypted) {
std::unique_ptr<Loader::AppLoader> 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::AppLoader> 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<u8> smdh = [program_id, &loader, &is_encrypted]() -> std::vector<u8> {
std::vector<u8> 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<u8> GetSMDHData(const std::string& path, bool& is_encrypted) {
return original_smdh;
std::vector<u8> 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<Loader::SMDH*>(env->GetLongField(obj, IDCache::GetGameInfoPointer()));
static GameInfoData* GetPointer(JNIEnv* env, jobject obj) {
return reinterpret_cast<GameInfoData*>(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<u8> smdh_data = GetSMDHData(GetJString(env, j_path), is_encrypted);
GameInfoData* game_info_data = GetNewGameInfoData(GetJString(env, j_path));
return reinterpret_cast<jlong>(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<jlong>(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<jlong>(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<GameRegion, const char*> 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<u16> 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);
}
}

View File

@ -86,12 +86,21 @@
tools:text="Application Filename" />
<TextView
android:id="@+id/about_game_playtime"
android:id="@+id/about_game_filetype"
style="?attr/textAppearanceBodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/about_game_title"
app:layout_constraintTop_toBottomOf="@+id/about_game_filename"
tools:text="Game Filetype" />
<TextView
android:id="@+id/about_game_playtime"
style="?attr/textAppearanceBodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/about_game_title"
app:layout_constraintTop_toBottomOf="@+id/about_game_filetype"
tools:text="Game Playtime" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -539,7 +539,6 @@
<string name="save_load_error">Save/Load Error</string>
<string name="fatal_error">Fatal Error</string>
<string name="fatal_error_message">A fatal error occurred. Check the log for details.\nContinuing emulation may result in crashes and bugs.</string>
<string name="invalid_region">Invalid region</string>
<string name="unsupported_encrypted">Unsupported encrypted application</string>
<!-- Disk Shader Cache -->
@ -565,6 +564,9 @@
<string name="create_shortcut">Create Shortcut</string>
<string name="shortcut_name_empty">Shortcut name cannot be empty</string>
<string name="shortcut_image_stretch_toggle">Stretch to fit image</string>
<string name="game_context_id">ID:</string>
<string name="game_context_file">File:</string>
<string name="game_context_type">Type:</string>
<!-- Performance Overlay settings -->
<string name="performance_overlay_show">Show Performance Overlay</string>

View File

@ -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);

View File

@ -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)),
},

View File

@ -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<u8>& buffer) {
if (!file->IsOpen())
return ResultStatus::Error;

View File

@ -41,6 +41,8 @@ public:
CompressFileInfo GetCompressFileInfo() override;
bool IsFileCompressed() override;
private:
std::string filename;
std::string filepath;

View File

@ -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:

View File

@ -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<FileUtil::IOFile> file;

View File

@ -60,9 +60,12 @@ FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile* file) {
if (MakeMagic('N', 'C', 'C', 'H') == magic)
return FileType::CXI;
std::optional<u32> magic_zstd;
if (FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file) != std::nullopt ||
FileUtil::Z3DSReadIOFile::GetUnderlyingFileMagic(file_crypto.get()) != std::nullopt) {
std::optional<u32> 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

View File

@ -76,6 +76,8 @@ public:
CompressFileInfo GetCompressFileInfo() override;
bool IsFileCompressed() override;
private:
/**
* Loads .code section into memory for booting