ui: Made rom loading errors more clear and user friendly (#2097)

This commit is contained in:
PabloMK7 2026-05-07 20:39:30 +02:00 committed by GitHub
parent b540725090
commit 921ea178b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 141 additions and 98 deletions

View File

@ -465,26 +465,51 @@ object NativeLibrary {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
emulationActivity = requireActivity() as EmulationActivity
val result = requireArguments().getInt(RESULT_CODE)
var captionString = getString(R.string.loader_error_invalid_format)
if (result == CoreError.ErrorLoader_ErrorEncrypted.value) {
captionString = getString(R.string.loader_error_encrypted)
}
if (result == CoreError.ErrorArticDisconnected.value) {
captionString = getString(R.string.artic_base)
var coreError = CoreError.fromInt(requireArguments().getInt(RESULT_CODE))
val title: String
val message: String
when (coreError) {
CoreError.ErrorGetLoader, CoreError.ErrorLoader_ErrorInvalidFormat, CoreError.ErrorSystemMode -> {
title = getString(R.string.loader_error_invalid_format)
message = getString(R.string.loader_error_invalid_format_description)
}
CoreError.ErrorLoader_ErrorEncrypted -> {
title = getString(R.string.loader_error_encrypted)
message = getString(R.string.loader_error_encrypted_description)
}
CoreError.ErrorArticDisconnected -> {
title = getString(R.string.artic_base)
message = getString(R.string.artic_server_comm_error)
}
CoreError.ErrorN3DSApplication -> {
title = getString(R.string.loader_error_invalid_system_mode)
message = getString(R.string.loader_error_invalid_system_mode_description)
}
CoreError.ErrorLoader_ErrorPatches -> {
title = getString(R.string.loader_error_applying_patches)
message = getString(R.string.loader_error_applying_patches_description)
}
CoreError.ErrorLoader_ErrorPatchesInvalidTitle -> {
title = getString(R.string.loader_error_applying_patches)
message = getString(R.string.loader_error_patch_wrong_application)
}
else -> {
title = getString(R.string.loader_error_generic_title)
message = getString(R.string.loader_error_generic,
getString(coreError.stringRes), coreError.value)
}
}
val alert = MaterialAlertDialogBuilder(requireContext())
.setTitle(captionString)
.setTitle(title)
.setMessage(
Html.fromHtml(
if (result == CoreError.ErrorArticDisconnected.value)
getString(R.string.artic_server_comm_error)
else if (result == CoreError.ErrorLoader_ErrorEncrypted.value)
getString(R.string.loader_error_encrypted_desc)
else
getString(R.string.loader_error_generic,
getString(CoreError.fromInt(result).stringRes), result),
Html.fromHtml(message,
Html.FROM_HTML_MODE_LEGACY
)
)
@ -861,14 +886,16 @@ object NativeLibrary {
ErrorLoader_ErrorEncrypted(5, R.string.core_error_loader_encrypted),
ErrorLoader_ErrorInvalidFormat(6, R.string.core_error_loader_invalid_format),
ErrorLoader_ErrorGBATitle(7, R.string.core_error_loader_gba_title),
ErrorSystemFiles(8, R.string.core_error_system_files),
ErrorSavestate(9, R.string.core_error_savestate),
ErrorArticDisconnected(10, R.string.core_error_artic_disconnected),
ErrorN3DSApplication(11, R.string.core_error_n3ds_application),
ErrorCoreExceptionRaised(12, R.string.core_error_core_exception_raised),
ErrorMemoryExceptionRaised(13, R.string.core_error_memory_exception_raised),
ShutdownRequested(14, R.string.core_error_shutdown_requested),
ErrorUnknown(15, R.string.core_error_unknown);
ErrorLoader_ErrorPatches(8, R.string.core_error_loader_error_patches),
ErrorLoader_ErrorPatchesInvalidTitle(9, R.string.core_error_loader_patches_invalid_title),
ErrorSystemFiles(10, R.string.core_error_system_files),
ErrorSavestate(11, R.string.core_error_savestate),
ErrorArticDisconnected(12, R.string.core_error_artic_disconnected),
ErrorN3DSApplication(13, R.string.core_error_n3ds_application),
ErrorCoreExceptionRaised(14, R.string.core_error_core_exception_raised),
ErrorMemoryExceptionRaised(15, R.string.core_error_memory_exception_raised),
ShutdownRequested(16, R.string.core_error_shutdown_requested),
ErrorUnknown(17, R.string.core_error_unknown);
companion object {
fun fromInt(value: Int): CoreError {

View File

@ -435,11 +435,19 @@
<string name="preferences_layout">Layout</string>
<!-- ROM loading errors -->
<string name="loader_error_generic_title">Error loading application</string>
<string name="loader_error_invalid_format">Invalid application format</string>
<string name="loader_error_invalid_format_description"><![CDATA[The application file format not supported.<br>Please make sure you are using one of the compatible file formats:<ul><li>Cartridge images: <b>.cci/.zcci/.3ds</b></li><li>Installable archives: <b>.cia/.zcia</b></li><li>Homebrew titles: <b>.3dsx/.z3dsx</b></li><li>NCCH containers: <b>.cxi/.zcxi/.app</b></li><li>ELF files: <b>.elf/.axf</b></li></ul>]]></string>
<string name="loader_error_invalid_system_mode">Invalid system mode</string>
<string name="loader_error_invalid_system_mode_description">New 3DS exclusive applications cannot be loaded without enabling the New 3DS mode.</string>
<string name="loader_error_applying_patches">Error applying patches</string>
<string name="loader_error_applying_patches_description">A generic error occurred while applying a patch to the application. Please check the log for more details.</string>
<string name="loader_error_patch_wrong_application">Failed to apply a patch because it is designed for a different application. Please make sure you are using the patches for the right application, region and version.</string>
<string name="loader_error_encrypted">Your ROM is Encrypted</string>
<string name="loader_error_encrypted_desc"><![CDATA[Azahar does not support encrypted ROMS. Read our <a href="https://azahar-emu.org/blog/game-loading-changes/">blog post</a> for more information.]]></string>
<string name="loader_error_invalid_format">Invalid ROM format</string>
<string name="loader_error_encrypted_description"><![CDATA[Azahar does not support encrypted applications. Read our <a href="https://azahar-emu.org/blog/game-loading-changes/">blog post</a> for more information.]]></string>
<string name="loader_error_file_not_found">ROM file does not exist</string>
<string name="no_game_present">No bootable game present!</string>
<string name="loader_error_generic">An error occurred while loading ROM: \"%s (%d)\"</string>
<string name="core_error_success">Success</string>
<string name="core_error_not_initialized">Not initialized</string>
@ -449,6 +457,8 @@
<string name="core_error_loader_encrypted">Encrypted file</string>
<string name="core_error_loader_invalid_format">Corrupted file</string>
<string name="core_error_loader_gba_title">File is GBA title</string>
<string name="core_error_loader_error_patches">Error applying patches</string>
<string name="core_error_loader_patches_invalid_title">Patches are for a different application</string>
<string name="core_error_system_files">Missing system files</string>
<string name="core_error_savestate">Savestate failed</string>
<string name="core_error_artic_disconnected">Artic Base disconnected</string>

View File

@ -1366,61 +1366,39 @@ bool GMainWindow::LoadROM(const QString& filename) {
system.Load(*render_window, filename.toStdString(), secondary_window)};
if (result != Core::System::ResultStatus::Success) {
QString invalid_format = tr("Invalid application format");
QString invalid_format_description =
tr("The application file format not supported.<br>Please make sure you are using one "
"of the compatible file formats:<ul><li>Cartridge images: "
"<b>.cci/.zcci/.3ds</b></li><li>Installable archives: "
"<b>.cia/.zcia</b></li><li>Homebrew titles: <b>.3dsx/.z3dsx</b></li><li>NCCH "
"containers: <b>.cxi/.zcxi/.app</b></li><li>ELF files: <b>.elf/.axf</b></li></ul>");
switch (result) {
case Core::System::ResultStatus::ErrorGetLoader:
LOG_CRITICAL(Frontend, "Failed to obtain loader for {}", filename.toStdString());
QMessageBox::critical(
this, tr("Invalid App Format"),
tr("Your app format is not supported.<br/>Please follow the guides to redump your "
"<a "
"href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
"dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a "
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
"dumping-installed-titles/'>installed "
"titles</a>."));
QMessageBox::critical(this, invalid_format, invalid_format_description);
break;
case Core::System::ResultStatus::ErrorSystemMode:
LOG_CRITICAL(Frontend, "Failed to load App!");
QMessageBox::critical(
this, tr("App Corrupted"),
tr("Your app is corrupted. <br/>Please follow the guides to redump your "
"<a "
"href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
"dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a "
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
"dumping-installed-titles/'>installed "
"titles</a>."));
LOG_CRITICAL(Frontend, "Failed to load application!");
QMessageBox::critical(this, invalid_format, invalid_format_description);
break;
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: {
QMessageBox::critical(this, tr("App Encrypted"),
tr("Your app is encrypted. <br/>"
QMessageBox::critical(this, tr("Encrypted application"),
tr("Encrypted applications are not supported.<br/>"
"<a "
"href='https://azahar-emu.org/blog/game-loading-changes/'>"
"Please check our blog for more info.</a>"));
break;
}
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
QMessageBox::critical(
this, tr("Invalid App Format"),
tr("Your app format is not supported.<br/>Please follow the guides to redump your "
"<a "
"href='https://web.archive.org/web/20240304210021/https://citra-emu.org/wiki/"
"dumping-game-cartridges/'>game "
"cartridges</a> or "
"<a "
"href='https://web.archive.org/web/20240304210011/https://citra-emu.org/wiki/"
"dumping-installed-titles/'>installed "
"titles</a>."));
QMessageBox::critical(this, invalid_format, invalid_format_description);
break;
case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle:
QMessageBox::critical(this, tr("Unsupported App"),
QMessageBox::critical(this, tr("Unsupported application"),
tr("GBA Virtual Console is not supported by Azahar."));
break;
@ -1437,10 +1415,27 @@ bool GMainWindow::LoadROM(const QString& filename) {
tr("New 3DS exclusive applications cannot be loaded without "
"enabling the New 3DS mode."));
break;
case Core::System::ResultStatus::ErrorLoader:
QMessageBox::critical(this, tr("Generic load error"),
tr("An generic load error occurred while loading the "
"application.<br/>Please check the log for more details."));
break;
case Core::System::ResultStatus::ErrorLoader_ErrorPatches:
QMessageBox::critical(this, tr("Error applying patches"),
tr("A generic error occurred while applying a patch to the "
"application.<br/>Please check the log for more details."));
break;
case Core::System::ResultStatus::ErrorLoader_ErrorPatchesInvalidTitle:
QMessageBox::critical(
this, tr("Error applying patches"),
tr("Failed to apply a patch because it is designed for a different "
"application.<br/>Please make sure you are using the patches for "
"the right application, region and version."));
break;
default:
QMessageBox::critical(
this, tr("Error while loading App!"),
tr("An unknown error occurred. Please see the log for more details."));
this, tr("Error while loading application"),
tr("An unknown error occurred.<br/>Please see the log for more details."));
break;
}
return false;
@ -1499,7 +1494,8 @@ void GMainWindow::BootGame(const QString& filename) {
auto loader = Loader::GetLoader(path);
u64 title_id{0};
Loader::ResultStatus res = loader->ReadProgramId(title_id);
Loader::ResultStatus res =
loader ? loader->ReadProgramId(title_id) : Loader::ResultStatus::Error;
if (Loader::ResultStatus::Success == res) {
// Load per game settings
@ -1512,7 +1508,7 @@ void GMainWindow::BootGame(const QString& filename) {
// Artic Server cannot accept a client multiple times, so multiple loaders are not
// possible. Instead register the app loader early and do not create it again on system load.
if (!loader->SupportsMultipleInstancesForSameFile()) {
if (loader && !loader->SupportsMultipleInstancesForSameFile()) {
system.RegisterAppLoaderEarly(loader);
}

View File

@ -434,6 +434,10 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st
return ResultStatus::ErrorLoader_ErrorInvalidFormat;
case Loader::ResultStatus::ErrorGbaTitle:
return ResultStatus::ErrorLoader_ErrorGbaTitle;
case Loader::ResultStatus::ErrorPatches:
return ResultStatus::ErrorLoader_ErrorPatches;
case Loader::ResultStatus::ErrorPatchesInvalidTitle:
return ResultStatus::ErrorLoader_ErrorPatchesInvalidTitle;
case Loader::ResultStatus::ErrorArtic:
return ResultStatus::ErrorArticDisconnected;
default:

View File

@ -99,11 +99,13 @@ public:
/// invalid format
ErrorLoader_ErrorGbaTitle, ///< Error loading the specified application as it is GBA Virtual
///< Console
ErrorSystemFiles, ///< Error in finding system files
ErrorSavestate, ///< Error saving or loading
ErrorArticDisconnected, ///< Error when artic base disconnects
ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode
ErrorCoreExceptionRaised, ///< The CPU emulation raised an exception
ErrorLoader_ErrorPatches, ///< Generic error while loading patches for an application
ErrorLoader_ErrorPatchesInvalidTitle, ///< A patch was loaded for the incorrect application
ErrorSystemFiles, ///< Error in finding system files
ErrorSavestate, ///< Error saving or loading
ErrorArticDisconnected, ///< Error when artic base disconnects
ErrorN3DSApplication, ///< Error launching New 3DS application in Old 3DS mode
ErrorCoreExceptionRaised, ///< The CPU emulation raised an exception
ErrorMemoryExceptionRaised, ///< Unmmaped memory was accessed
ShutdownRequested, ///< Emulated program requested a system shutdown
ErrorUnknown ///< Any other error

View File

@ -1,4 +1,4 @@
// Copyright 2020 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -247,14 +247,14 @@ void LayeredFS::LoadExtRelocations() {
std::vector<u8> buffer(file.relocation.size); // Original size
romfs->ReadFile(file.relocation.original_offset, buffer.size(), buffer.data());
bool ret = false;
Loader::ResultStatus ret{};
if (extension == ".ips") {
ret = Patch::ApplyIpsPatch(patch, buffer);
} else {
ret = Patch::ApplyBpsPatch(patch, buffer);
}
if (ret) {
if (ret == Loader::ResultStatus::Success) {
LOG_INFO(Service_FS, "LayeredFS patched file {}", file_path);
file.relocation.type = 2;

View File

@ -1,4 +1,4 @@
// Copyright 2020 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

View File

@ -518,7 +518,7 @@ Loader::ResultStatus NCCHContainer::LoadSectionExeFS(const char* name, std::vect
Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector<u8>& code) const {
struct PatchLocation {
std::string path;
bool (*patch_fn)(const std::vector<u8>& patch, std::vector<u8>& code);
Loader::ResultStatus (*patch_fn)(const std::vector<u8>& patch, std::vector<u8>& code);
};
const auto mods_path =
@ -555,11 +555,12 @@ Loader::ResultStatus NCCHContainer::ApplyCodePatch(std::vector<u8>& code) const
std::vector<u8> patch(patch_file.GetSize());
if (patch_file.ReadBytes(patch.data(), patch.size()) != patch.size())
return Loader::ResultStatus::Error;
return Loader::ResultStatus::ErrorPatches;
LOG_INFO(Service_FS, "File {} patching code.bin", info.path);
if (!info.patch_fn(patch, code))
return Loader::ResultStatus::Error;
auto patch_result = info.patch_fn(patch, code);
if (patch_result != Loader::ResultStatus::Success)
return patch_result;
return Loader::ResultStatus::Success;
}

View File

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -14,21 +14,21 @@
namespace FileSys::Patch {
bool ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
Loader::ResultStatus ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
std::size_t cursor = 5;
std::size_t patch_length = ips.size() - 3;
std::string ips_header(ips.begin(), ips.begin() + 5);
if (ips_header != "PATCH") {
LOG_INFO(Service_FS, "Attempted to load invalid IPS");
return false;
return Loader::ResultStatus::ErrorPatches;
}
while (cursor < patch_length) {
std::string eof_check(ips.begin() + cursor, ips.begin() + cursor + 3);
if (eof_check == "EOF")
return false;
break;
std::size_t offset = ips[cursor] << 16 | ips[cursor + 1] << 8 | ips[cursor + 2];
std::size_t length = ips[cursor + 3] << 8 | ips[cursor + 4];
@ -38,7 +38,7 @@ bool ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
length = ips[cursor + 5] << 8 | ips[cursor + 6];
if (buffer.size() < offset + length)
return false;
return Loader::ResultStatus::ErrorPatches;
for (u32 i = 0; i < length; ++i)
buffer[offset + i] = ips[cursor + 7];
@ -49,12 +49,12 @@ bool ApplyIpsPatch(const std::vector<u8>& ips, std::vector<u8>& buffer) {
}
if (buffer.size() < offset + length)
return false;
return Loader::ResultStatus::ErrorPatches;
std::memcpy(&buffer[offset], &ips[cursor + 5], length);
cursor += length + 5;
}
return true;
return Loader::ResultStatus::Success;
}
namespace Bps {
@ -149,11 +149,11 @@ public:
PatchApplier(Stream<const u8> source, Stream<u8> target, Stream<const u8> patch)
: m_source{source}, m_target{target}, m_patch{patch} {}
bool Apply() {
Loader::ResultStatus Apply() {
const auto magic = *m_patch.Read<std::array<char, MagicSize>>();
if (std::string_view(magic.data(), magic.size()) != "BPS1") {
LOG_ERROR(Service_FS, "Invalid BPS magic");
return false;
return Loader::ResultStatus::ErrorPatches;
}
const Bps::Number source_size = m_patch.ReadNumber();
@ -161,7 +161,7 @@ public:
const Bps::Number metadata_size = m_patch.ReadNumber();
if (source_size > m_source.size() || target_size > m_target.size() || metadata_size != 0) {
LOG_ERROR(Service_FS, "Invalid sizes");
return false;
return Loader::ResultStatus::ErrorPatches;
}
const std::size_t command_start_offset = m_patch.Tell();
@ -173,22 +173,22 @@ public:
if (crc32(m_source.data(), source_size) != source_crc32) {
LOG_ERROR(Service_FS, "Unexpected source hash");
return false;
return Loader::ResultStatus::ErrorPatchesInvalidTitle;
}
// Process all patch commands.
std::memset(m_target.data(), 0, m_target.size());
while (m_patch.Tell() < command_end_offset) {
if (!HandleCommand())
return false;
return Loader::ResultStatus::ErrorPatches;
}
if (crc32(m_target.data(), target_size) != target_crc32) {
LOG_ERROR(Service_FS, "Unexpected target hash");
return false;
return Loader::ResultStatus::ErrorPatches;
}
return true;
return Loader::ResultStatus::Success;
}
private:
@ -257,7 +257,7 @@ private:
} // namespace Bps
bool ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer) {
Loader::ResultStatus ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer) {
Bps::Stream patch_stream{patch.data(), patch.size()};
// Move the offset past the file format marker (i.e. "BPS1")

View File

@ -1,4 +1,4 @@
// Copyright 2019 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,11 +7,12 @@
#include <vector>
#include "common/common_types.h"
#include "core/loader/loader.h"
namespace FileSys::Patch {
bool ApplyIpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
Loader::ResultStatus ApplyIpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
bool ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
Loader::ResultStatus ApplyBpsPatch(const std::vector<u8>& patch, std::vector<u8>& buffer);
} // namespace FileSys::Patch

View File

@ -76,6 +76,8 @@ enum class ResultStatus {
ErrorGbaTitle,
ErrorArtic,
ErrorNotFound,
ErrorPatches,
ErrorPatchesInvalidTitle,
};
constexpr u32 MakeMagic(char a, char b, char c, char d) {