diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
index c8b3d8fee..583e59863 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
@@ -182,6 +182,10 @@ object NativeLibrary {
external fun uninstallSystemFiles(old3DS: Boolean)
+ external fun isFullConsoleLinked(): Boolean
+
+ external fun unlinkConsole()
+
private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object()
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
index 1f8328a74..7b76149d3 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
@@ -4,6 +4,7 @@
package org.citra.citra_emu.fragments
+import android.content.DialogInterface
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.Gravity
@@ -157,6 +158,22 @@ class SystemFilesFragment : Fragment() {
movementMethod = LinkMovementMethod.getInstance()
}
+ binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
+ binding.buttonUnlinkConsoleData.setOnClickListener {
+ MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.delete_system_files)
+ .setMessage(HtmlCompat.fromHtml(
+ requireContext().getString(R.string.delete_system_files_description),
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ ))
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+ NativeLibrary.unlinkConsole()
+ binding.buttonUnlinkConsoleData.isEnabled = NativeLibrary.isFullConsoleLinked()
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
binding.buttonSetUpSystemFiles.setOnClickListener {
val inflater = LayoutInflater.from(context)
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index a88a4036c..02cc329af 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -36,6 +36,7 @@
#include "core/frontend/camera/factory.h"
#include "core/hle/service/am/am.h"
#include "core/hle/service/nfc/nfc.h"
+#include "core/hw/unique_data.h"
#include "core/loader/loader.h"
#include "core/savestate.h"
#include "core/system_titles.h"
@@ -772,4 +773,12 @@ void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIE
LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
}
+jboolean Java_org_citra_citra_1emu_NativeLibrary_isFullConsoleLinked(JNIEnv* env, jobject obj) {
+ return HW::UniqueData::IsFullConsoleLinked();
+}
+
+void Java_org_citra_citra_1emu_NativeLibrary_unlinkConsole(JNIEnv* env, jobject obj) {
+ HW::UniqueData::UnlinkConsole();
+}
+
} // extern "C"
diff --git a/src/android/app/src/main/res/layout/fragment_system_files.xml b/src/android/app/src/main/res/layout/fragment_system_files.xml
index 32e8b5358..bae1cda1c 100644
--- a/src/android/app/src/main/res/layout/fragment_system_files.xml
+++ b/src/android/app/src/main/res/layout/fragment_system_files.xml
@@ -61,6 +61,13 @@
android:layout_height="wrap_content"
android:text="@string/setup_tool_connect" />
+
+
System Files
Perform system file operations such as installing system files or booting the Home Menu
Connect to Artic Setup Tool
- Azahar Artic Setup Tool. Notes:This operation will install console unique files to Azahar, do not share your user or nand folders after performing the setup process! Old 3DS setup is needed for the New 3DS setup to work. Both setup modes will work regardless of the model of the console running the setup tool. ]]>
+ Azahar Artic Setup Tool. Notes:This operation will install console unique data to Azahar, do not share your user or nand folders after performing the setup process! While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the console later from the System Files tab in the emulator options menu. Do not go online with both Azahar and your 3DS console at the same time after setting up system files, as this could cause issues. Old 3DS setup is needed for the New 3DS setup to work (setting up both is recommended). Both setup modes will work regardless of the model of the console running the setup tool. ]]>
Fetching current system files status, please wait...
+ Unlink Console Unique Data
+ Your OTP, SecureInfo and LocalFriendCodeSeed will be removed from Azahar. Your friend list will reset and you will be logged out of your NNID/PNID account. System files and eshop titles obtained through Azahar will become inaccessible until the same console is linked again using the setup tool (save data will not be lost). Continue?]]>
Old 3DS Setup
New 3DS Setup
Setup is possible.
diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp
index 8dfb2dae7..94318da55 100644
--- a/src/citra_qt/citra_qt.cpp
+++ b/src/citra_qt/citra_qt.cpp
@@ -2112,15 +2112,18 @@ void GMainWindow::OnMenuSetUpSystemFiles() {
QVBoxLayout layout(&dialog);
QLabel label_description(
- tr("Azahar needs files from a real console to be able to use some of its features. "
- "You can get such files with the Azahar needs console unique data and firmware files from a real console to be "
+ "able to use some of its features. Such files and data can be set up with the Azahar "
- "Artic Setup Tool Notes:
This operation will install console unique "
- "files "
- "to Azahar, do not share your user or nand folders after performing the setup "
- "process! Old 3DS setup is needed for the New 3DS setup to "
- "work. Both setup modes will work regardless of the model of the console "
- "running the setup tool.
"),
+ "Artic Setup Tool Notes:This operation will install console unique "
+ "data to Azahar, do not share your user or nand folders after performing the setup "
+ "process! While doing the setup process, Azahar will link to the console "
+ "running the setup tool. You can unlink the console later from the System tab in the "
+ "emulator configuration menu. Do not go online with both Azahar and your 3DS "
+ "console at the same time after setting up system files, as it could cause "
+ "issues. Old 3DS setup is needed for the New 3DS setup to work (doing both "
+ "setup modes is recommended). Both setup modes will work regardless of the "
+ "model of the console running the setup tool. "),
&dialog);
label_description.setOpenExternalLinks(true);
layout.addWidget(&label_description);
diff --git a/src/citra_qt/configuration/configure_system.cpp b/src/citra_qt/configuration/configure_system.cpp
index 005ebe394..2f097aaa3 100644
--- a/src/citra_qt/configuration/configure_system.cpp
+++ b/src/citra_qt/configuration/configure_system.cpp
@@ -238,6 +238,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
&ConfigureSystem::RefreshConsoleID);
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
+ connect(ui->button_linked_console, &QPushButton::clicked, this,
+ &ConfigureSystem::UnlinkConsole);
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
ui->button_secure_info->setEnabled(false);
@@ -561,6 +563,25 @@ void ConfigureSystem::RefreshMAC() {
ui->label_mac->setText(tr("MAC: %1").arg(QString::fromStdString(mac_address)));
}
+void ConfigureSystem::UnlinkConsole() {
+ QMessageBox::StandardButton reply;
+ QString warning_text =
+ tr("This action will unlink your real console from Azahar, with the following "
+ "consequences:Your OTP, SecureInfo and LocalFriendCodeSeed will be removed "
+ "from Azahar. Your friend list will reset and you will be logged out of your "
+ "NNID/PNID account. System files and eshop titles obtained through Azahar will "
+ "become inaccessible until the same console is linked again (save data will not be "
+ "lost). Continue?");
+ reply =
+ QMessageBox::warning(this, tr("Warning"), warning_text, QMessageBox::No | QMessageBox::Yes);
+ if (reply == QMessageBox::No) {
+ return;
+ }
+
+ HW::UniqueData::UnlinkConsole();
+ RefreshSecureDataStatus();
+}
+
void ConfigureSystem::InstallSecureData(const std::string& from_path, const std::string& to_path) {
std::string from =
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
@@ -601,6 +622,15 @@ void ConfigureSystem::RefreshSecureDataStatus() {
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadOTP())).c_str()));
ui->label_movable_status->setText(
tr((std::string("Status: ") + status_to_str(HW::UniqueData::LoadMovable())).c_str()));
+
+ if (HW::UniqueData::IsFullConsoleLinked()) {
+ ui->linked_console->setVisible(true);
+ ui->button_otp->setEnabled(false);
+ ui->button_secure_info->setEnabled(false);
+ ui->button_friend_code_seed->setEnabled(false);
+ } else {
+ ui->linked_console->setVisible(false);
+ }
}
void ConfigureSystem::RetranslateUI() {
@@ -625,6 +655,7 @@ void ConfigureSystem::SetupPerGameUI() {
ui->label_init_ticks_type->setVisible(false);
ui->label_init_ticks_value->setVisible(false);
ui->label_console_id->setVisible(false);
+ ui->label_mac->setVisible(false);
ui->label_sound->setVisible(false);
ui->label_language->setVisible(false);
ui->label_country->setVisible(false);
@@ -646,6 +677,7 @@ void ConfigureSystem::SetupPerGameUI() {
ui->edit_init_ticks_value->setVisible(false);
ui->toggle_system_setup->setVisible(false);
ui->button_regenerate_console_id->setVisible(false);
+ ui->button_regenerate_mac->setVisible(false);
// Apps can change the state of the plugin loader, so plugins load
// to a chainloaded app with specific parameters. Don't allow
// the plugin loader state to be configured per-game as it may
@@ -653,6 +685,7 @@ void ConfigureSystem::SetupPerGameUI() {
ui->label_plugin_loader->setVisible(false);
ui->plugin_loader->setVisible(false);
ui->allow_plugin_loader->setVisible(false);
+ ui->group_real_console_unique_data->setVisible(false);
ConfigurationShared::SetColoredTristate(ui->toggle_new_3ds, Settings::values.is_new_3ds,
is_new_3ds);
diff --git a/src/citra_qt/configuration/configure_system.h b/src/citra_qt/configuration/configure_system.h
index 086b84a22..26d640f01 100644
--- a/src/citra_qt/configuration/configure_system.h
+++ b/src/citra_qt/configuration/configure_system.h
@@ -52,6 +52,7 @@ private:
void UpdateInitTicks(int init_ticks_type);
void RefreshConsoleID();
void RefreshMAC();
+ void UnlinkConsole();
void InstallSecureData(const std::string& from_path, const std::string& to_path);
void RefreshSecureDataStatus();
diff --git a/src/citra_qt/configuration/configure_system.ui b/src/citra_qt/configuration/configure_system.ui
index ad35e9804..792a4ace4 100644
--- a/src/citra_qt/configuration/configure_system.ui
+++ b/src/citra_qt/configuration/configure_system.ui
@@ -518,122 +518,157 @@ online features (if installed)
Real Console Unique Data
+ -
+
+
+ -
+
+
+ -
+
+
+ Your real console is linked to Azahar.
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Unlink
+
+
+
+
+
+
+ -
+
+
+ OTP
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+ -
+
+
+ SecureInfo_A/B
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+ -
+
+
+ LocalFriendCodeSeed_A/B
+
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Qt::RightToLeft
+
+
+ Choose
+
+
+
+
+
+
+
+
+
-
-
-
- SecureInfo_A/B
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
-
- -
-
-
- LocalFriendCodeSeed_A/B
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
-
- -
-
-
- OTP
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Qt::RightToLeft
-
-
- Choose
-
-
-
-
-
-
- -
movable.sed
- -
+
-
-
diff --git a/src/core/hw/unique_data.cpp b/src/core/hw/unique_data.cpp
index 3f11da2a7..988c42e19 100644
--- a/src/core/hw/unique_data.cpp
+++ b/src/core/hw/unique_data.cpp
@@ -5,6 +5,7 @@
#include
#include "common/common_paths.h"
#include "common/logging/log.h"
+#include "core/file_sys/archive_systemsavedata.h"
#include "core/file_sys/certificate.h"
#include "core/file_sys/otp.h"
#include "core/hw/aes/key.h"
@@ -262,4 +263,30 @@ std::unique_ptr OpenUniqueCryptoFile(const std::string& filena
return std::make_unique(filename, openmode, key, ctr, flags);
}
+bool IsFullConsoleLinked() {
+ return GetOTP().Valid() && GetSecureInfoA().IsValid() && GetLocalFriendCodeSeedB().IsValid();
+}
+
+void UnlinkConsole() {
+ // Remove all console unique data, as well as the act, nim and frd savefiles
+ const std::string system_save_data_path =
+ FileSys::GetSystemSaveDataContainerPath(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir));
+ constexpr std::array, 3> save_data_ids{{
+ {0x00, 0x00, 0x00, 0x00, 0x2C, 0x00, 0x01, 0x00},
+ {0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x01, 0x00},
+ {0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x01, 0x00},
+ }};
+
+ for (auto& id : save_data_ids) {
+ const std::string final_path = FileSys::GetSystemSaveDataPath(system_save_data_path, id);
+ FileUtil::DeleteDirRecursively(final_path, 2);
+ }
+
+ FileUtil::Delete(GetOTPPath());
+ FileUtil::Delete(GetSecureInfoAPath());
+ FileUtil::Delete(GetLocalFriendCodeSeedBPath());
+
+ InvalidateSecureData();
+}
+
} // namespace HW::UniqueData
diff --git a/src/core/hw/unique_data.h b/src/core/hw/unique_data.h
index 294484ffa..463d8e8e0 100644
--- a/src/core/hw/unique_data.h
+++ b/src/core/hw/unique_data.h
@@ -154,4 +154,7 @@ void InvalidateSecureData();
std::unique_ptr OpenUniqueCryptoFile(const std::string& filename,
const char openmode[], UniqueCryptoFileID id,
int flags = 0);
+
+bool IsFullConsoleLinked();
+void UnlinkConsole();
} // namespace HW::UniqueData
\ No newline at end of file
diff --git a/src/core/loader/artic.cpp b/src/core/loader/artic.cpp
index 5f4bb28f9..ab2888f7a 100644
--- a/src/core/loader/artic.cpp
+++ b/src/core/loader/artic.cpp
@@ -342,7 +342,8 @@ void Apploader_Artic::EnsureClientConnected() {
if (is_initial_setup) {
// Ensure we are running the initial setup app in the correct version
- auto req = client->NewRequest("System_IsAzaharInitialSetup");
+ auto req = client->NewRequest("System_ArticSetupVersion");
+ req.AddParameterU32(SETUP_TOOL_VERSION);
auto resp = client->Send(req);
if (!resp.has_value()) {
client_connected = false;
@@ -355,7 +356,15 @@ void Apploader_Artic::EnsureClientConnected() {
return;
}
- client_connected = *reinterpret_cast(ret_buf->first) == INITIAL_SETUP_APP_VERSION;
+ if (*reinterpret_cast(ret_buf->first) != SETUP_TOOL_VERSION) {
+ system.SetStatus(Core::System::ResultStatus::ErrorArticDisconnected,
+ "\nIncompatible Artic Setup Tool version.\nCheck for Artic Setup Tool "
+ "or Azahar updates.");
+ client_connected = false;
+ client->Stop();
+ } else {
+ client_connected = true;
+ }
}
}
@@ -385,6 +394,20 @@ ResultStatus Apploader_Artic::Load(std::shared_ptr& process) {
if (is_initial_setup) {
+ // If there is already a console linked, check it's the same device.
+ // Otherwise it could cause weird issues with account save data.
+ if (HW::UniqueData::IsFullConsoleLinked()) {
+ auto req = client->NewRequest("System_ReportDeviceID");
+ req.AddParameterU32(HW::UniqueData::GetOTP().GetDeviceID());
+
+ auto resp = client->Send(req);
+ if (!resp.has_value() || !resp->Succeeded())
+ return ResultStatus::ErrorArtic;
+
+ if (resp->GetMethodResult() != 0)
+ return ResultStatus::ErrorArtic;
+ }
+
// Request console unique data
for (int i = 0; i < 6; i++) {
std::string path;
diff --git a/src/core/loader/artic.h b/src/core/loader/artic.h
index d102babcc..f5b148364 100644
--- a/src/core/loader/artic.h
+++ b/src/core/loader/artic.h
@@ -93,7 +93,7 @@ public:
}
private:
- static constexpr u32 INITIAL_SETUP_APP_VERSION = 0;
+ static constexpr u32 SETUP_TOOL_VERSION = 1;
/**
* Loads .code section into memory for booting
* @param process The newly created process