Check that the country setting is valid for selected region (#847)

* Check that the country setting is valid for selected region

* `SystemSaveGame.checkCountryCompatibility` -> `SystemSaveGame.getCountryCompatibility`

* SettingsFragmentPresenter.kt: Moved `checkCountryCompatibility` definition out of `addSystemSettings`

* SettingsFragmentPresenter.kt: Renamed `compat` value to `compatFlags` for better readability

* configure_system.ui: Corrected indentation

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
This commit is contained in:
PabloMK7 2025-03-29 22:50:00 +01:00 committed by OpenSauce04
parent 63ce52d0db
commit f7c284c7a9
16 changed files with 528 additions and 177 deletions

View File

@ -73,7 +73,7 @@ import kotlin.math.roundToInt
class SettingsAdapter( class SettingsAdapter(
private val fragmentView: SettingsFragmentView, private val fragmentView: SettingsFragmentView,
private val context: Context public val context: Context
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener { ) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
private var settings: ArrayList<SettingsItem>? = null private var settings: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null private var clickedItem: SettingsItem? = null

View File

@ -13,6 +13,7 @@ import android.hardware.camera2.CameraManager
import android.os.Build import android.os.Build
import android.text.TextUtils import android.text.TextUtils
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.math.min import kotlin.math.min
import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R import org.citra.citra_emu.R
@ -239,6 +240,30 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
} }
} }
private var countryCompatibilityChanged = true
private fun checkCountryCompatibility() {
if (countryCompatibilityChanged) {
countryCompatibilityChanged = false
val compatFlags = SystemSaveGame.getCountryCompatibility(IntSetting.EMULATED_REGION.int)
if (compatFlags != 0) {
var message = ""
if (compatFlags and 1 != 0) {
message += settingsAdapter.context.getString(R.string.region_mismatch_emulated)
}
if (compatFlags and 2 != 0) {
if (message.isNotEmpty()) message += "\n\n"
message += settingsAdapter.context.getString(R.string.region_mismatch_console)
}
MaterialAlertDialogBuilder(settingsAdapter.context)
.setTitle(R.string.region_mismatch)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
}
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
private fun addSystemSettings(sl: ArrayList<SettingsItem>) { private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system)) settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system))
@ -282,51 +307,45 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
) )
) )
add(HeaderSetting(R.string.profile_settings)) add(HeaderSetting(R.string.profile_settings))
add( val regionSetting = object : AbstractIntSetting {
StringInputSetting( override var int: Int
usernameSetting, get() {
R.string.username, val ret = IntSetting.EMULATED_REGION.int
0, checkCountryCompatibility()
"AZAHAR", return ret
10 }
) set(value) {
) IntSetting.EMULATED_REGION.int = value
countryCompatibilityChanged = true
checkCountryCompatibility()
}
override val key = IntSetting.EMULATED_REGION.key
override val section = null
override val isRuntimeEditable = false
override val valueAsString get() = int.toString()
override val defaultValue = IntSetting.EMULATED_REGION.defaultValue
}
add( add(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.EMULATED_REGION, regionSetting,
R.string.emulated_region, R.string.emulated_region,
0, 0,
R.array.regionNames, R.array.regionNames,
R.array.regionValues, R.array.regionValues,
IntSetting.EMULATED_REGION.key,
IntSetting.EMULATED_REGION.defaultValue
) )
) )
val systemLanguageSetting = object : AbstractIntSetting {
override var int: Int
get() = SystemSaveGame.getSystemLanguage()
set(value) = SystemSaveGame.setSystemLanguage(value)
override val key = null
override val section = null
override val isRuntimeEditable = false
override val valueAsString get() = int.toString()
override val defaultValue = 1
}
add(
SingleChoiceSetting(
systemLanguageSetting,
R.string.emulated_language,
0,
R.array.languageNames,
R.array.languageValues
)
)
val systemCountrySetting = object : AbstractShortSetting { val systemCountrySetting = object : AbstractShortSetting {
override var short: Short override var short: Short
get() = SystemSaveGame.getCountryCode() get() {
set(value) = SystemSaveGame.setCountryCode(value) val ret = SystemSaveGame.getCountryCode()
checkCountryCompatibility()
return ret;
}
set(value) {
SystemSaveGame.setCountryCode(value)
countryCompatibilityChanged = true
checkCountryCompatibility()
}
override val key = null override val key = null
override val section = null override val section = null
override val isRuntimeEditable = false override val isRuntimeEditable = false
@ -348,7 +367,34 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
countries.map { it.second }.toTypedArray() countries.map { it.second }.toTypedArray()
) )
) )
val systemLanguageSetting = object : AbstractIntSetting {
override var int: Int
get() = SystemSaveGame.getSystemLanguage()
set(value) = SystemSaveGame.setSystemLanguage(value)
override val key = null
override val section = null
override val isRuntimeEditable = false
override val valueAsString get() = int.toString()
override val defaultValue = 1
}
add(
SingleChoiceSetting(
systemLanguageSetting,
R.string.emulated_language,
0,
R.array.languageNames,
R.array.languageValues
)
)
add(
StringInputSetting(
usernameSetting,
R.string.username,
0,
"AZAHAR",
10
)
)
val playCoinSettings = object : AbstractIntSetting { val playCoinSettings = object : AbstractIntSetting {
override var int: Int override var int: Int
get() = SystemSaveGame.getPlayCoins() get() = SystemSaveGame.getPlayCoins()

View File

@ -48,6 +48,8 @@ object SystemSaveGame {
external fun getMac(): String external fun getMac(): String
external fun regenerateMac() external fun regenerateMac()
external fun getCountryCompatibility(region: Int): Int
} }
enum class BirthdayMonth(val code: Int, val days: Int) { enum class BirthdayMonth(val code: Int, val days: Int) {

View File

@ -6,6 +6,7 @@
#include <core/core.h> #include <core/core.h>
#include <core/hle/service/cfg/cfg.h> #include <core/hle/service/cfg/cfg.h>
#include <core/hle/service/ptm/ptm.h> #include <core/hle/service/ptm/ptm.h>
#include <core/hw/unique_data.h>
#include "android_common/android_common.h" #include "android_common/android_common.h"
static bool changes_pending = false; static bool changes_pending = false;
@ -138,4 +139,21 @@ void Java_org_citra_citra_1emu_utils_SystemSaveGame_regenerateMac(JNIEnv* env,
cfg->SaveMacAddress(); cfg->SaveMacAddress();
} }
jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getCountryCompatibility(
JNIEnv* env, [[maybe_unused]] jobject obj, jint region) {
int res = 0;
u8 country = cfg->GetCountryCode();
if (region != Settings::REGION_VALUE_AUTO_SELECT &&
!Service::CFG::Module::IsValidRegionCountry(static_cast<u32>(region), country)) {
res |= 1;
}
if (HW::UniqueData::GetSecureInfoA().IsValid()) {
region = static_cast<jint>(cfg->GetRegionValue(true));
if (!Service::CFG::Module::IsValidRegionCountry(static_cast<u32>(region), country)) {
res |= 2;
}
}
return res;
}
} // extern "C" } // extern "C"

View File

@ -200,6 +200,9 @@
<string name="plugin_loader_description">Loads 3GX plugins from the emulated SD card if they are available.</string> <string name="plugin_loader_description">Loads 3GX plugins from the emulated SD card if they are available.</string>
<string name="allow_plugin_loader">Allow Applications to Change Plugin Loader State</string> <string name="allow_plugin_loader">Allow Applications to Change Plugin Loader State</string>
<string name="allow_plugin_loader_description">Allows homebrew apps to enable the plugin loader even when it is disabled.</string> <string name="allow_plugin_loader_description">Allows homebrew apps to enable the plugin loader even when it is disabled.</string>
<string name="region_mismatch">Region Mismatch Warning</string>
<string name="region_mismatch_emulated">The country setting is not valid for the selected emulated region.</string>
<string name="region_mismatch_console">The country setting is not valid for the current linked console.</string>
<!-- Camera settings strings --> <!-- Camera settings strings -->
<string name="inner_camera">Inner Camera</string> <string name="inner_camera">Inner Camera</string>

View File

@ -121,16 +121,6 @@ void ConfigureGeneral::SetConfiguration() {
!UISettings::values.screenshot_path.UsingGlobal()); !UISettings::values.screenshot_path.UsingGlobal());
ConfigurationShared::SetHighlight(ui->emulation_speed_layout, ConfigurationShared::SetHighlight(ui->emulation_speed_layout,
!Settings::values.frame_limit.UsingGlobal()); !Settings::values.frame_limit.UsingGlobal());
ConfigurationShared::SetHighlight(ui->widget_region,
!Settings::values.region_value.UsingGlobal());
const bool is_region_global = Settings::values.region_value.UsingGlobal();
ui->region_combobox->setCurrentIndex(
is_region_global ? ConfigurationShared::USE_GLOBAL_INDEX
: static_cast<int>(Settings::values.region_value.GetValue()) +
ConfigurationShared::USE_GLOBAL_OFFSET + 1);
} else {
// The first item is "auto-select" with actual value -1, so plus one here will do the trick
ui->region_combobox->setCurrentIndex(Settings::values.region_value.GetValue() + 1);
} }
UISettings::values.screenshot_path.SetGlobal(ui->screenshot_combo->currentIndex() == UISettings::values.screenshot_path.SetGlobal(ui->screenshot_combo->currentIndex() ==
@ -160,9 +150,6 @@ void ConfigureGeneral::ResetDefaults() {
} }
void ConfigureGeneral::ApplyConfiguration() { void ConfigureGeneral::ApplyConfiguration() {
ConfigurationShared::ApplyPerGameSetting(&Settings::values.region_value, ui->region_combobox,
[](s32 index) { return index - 1; });
ConfigurationShared::ApplyPerGameSetting( ConfigurationShared::ApplyPerGameSetting(
&Settings::values.frame_limit, ui->emulation_speed_combo, [this](s32) { &Settings::values.frame_limit, ui->emulation_speed_combo, [this](s32) {
const bool is_maximum = ui->frame_limit->value() == ui->frame_limit->maximum(); const bool is_maximum = ui->frame_limit->value() == ui->frame_limit->maximum();
@ -191,7 +178,6 @@ void ConfigureGeneral::RetranslateUI() {
void ConfigureGeneral::SetupPerGameUI() { void ConfigureGeneral::SetupPerGameUI() {
if (Settings::IsConfiguringGlobal()) { if (Settings::IsConfiguringGlobal()) {
ui->region_combobox->setEnabled(Settings::values.region_value.UsingGlobal());
ui->frame_limit->setEnabled(Settings::values.frame_limit.UsingGlobal()); ui->frame_limit->setEnabled(Settings::values.frame_limit.UsingGlobal());
return; return;
} }
@ -212,8 +198,4 @@ void ConfigureGeneral::SetupPerGameUI() {
ui->button_reset_defaults->setVisible(false); ui->button_reset_defaults->setVisible(false);
ui->toggle_gamemode->setVisible(false); ui->toggle_gamemode->setVisible(false);
ui->toggle_update_checker->setVisible(false); ui->toggle_update_checker->setVisible(false);
ConfigurationShared::SetColoredComboBox(
ui->region_combobox, ui->widget_region,
static_cast<u32>(Settings::values.region_value.GetValue(true) + 1));
} }

View File

@ -73,75 +73,6 @@
<string>Emulation</string> <string>Emulation</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWidget" name="widget_region" native="true">
<layout class="QHBoxLayout" name="region_layout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="region_label">
<property name="text">
<string>Region:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="region_combobox">
<item>
<property name="text">
<string>Auto-select</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">JPN</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">USA</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">EUR</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AUS</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">CHN</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">KOR</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TWN</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QWidget" name="emulation_speed_layout" native="true"> <widget class="QWidget" name="emulation_speed_layout" native="true">
<layout class="QHBoxLayout" name="emulation_speed_layout_inner"> <layout class="QHBoxLayout" name="emulation_speed_layout_inner">

View File

@ -240,6 +240,14 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC); connect(ui->button_regenerate_mac, &QPushButton::clicked, this, &ConfigureSystem::RefreshMAC);
connect(ui->button_linked_console, &QPushButton::clicked, this, connect(ui->button_linked_console, &QPushButton::clicked, this,
&ConfigureSystem::UnlinkConsole); &ConfigureSystem::UnlinkConsole);
connect(ui->combo_country, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this](int index) {
CheckCountryValid(static_cast<u8>(ui->combo_country->itemData(index).toInt()));
});
connect(ui->region_combobox, qOverload<int>(&QComboBox::currentIndexChanged), this,
[this]([[maybe_unused]] int index) {
CheckCountryValid(static_cast<u8>(ui->combo_country->currentData().toInt()));
});
connect(ui->button_secure_info, &QPushButton::clicked, this, [this] { connect(ui->button_secure_info, &QPushButton::clicked, this, [this] {
ui->button_secure_info->setEnabled(false); ui->button_secure_info->setEnabled(false);
@ -280,6 +288,8 @@ ConfigureSystem::ConfigureSystem(Core::System& system_, QWidget* parent)
ui->combo_country->addItem(tr(country_names.at(i)), i); ui->combo_country->addItem(tr(country_names.at(i)), i);
} }
} }
ui->label_country_invalid->setVisible(false);
ui->label_country_invalid->setStyleSheet(QStringLiteral("QLabel { color: #ff3333; }"));
SetupPerGameUI(); SetupPerGameUI();
ConfigureTime(); ConfigureTime();
@ -290,6 +300,19 @@ ConfigureSystem::~ConfigureSystem() = default;
void ConfigureSystem::SetConfiguration() { void ConfigureSystem::SetConfiguration() {
enabled = !system.IsPoweredOn(); enabled = !system.IsPoweredOn();
if (!Settings::IsConfiguringGlobal()) {
ConfigurationShared::SetHighlight(ui->region_label,
!Settings::values.region_value.UsingGlobal());
const bool is_region_global = Settings::values.region_value.UsingGlobal();
ui->region_combobox->setCurrentIndex(
is_region_global ? ConfigurationShared::USE_GLOBAL_INDEX
: static_cast<int>(Settings::values.region_value.GetValue()) +
ConfigurationShared::USE_GLOBAL_OFFSET + 1);
} else {
// The first item is "auto-select" with actual value -1, so plus one here will do the trick
ui->region_combobox->setCurrentIndex(Settings::values.region_value.GetValue() + 1);
}
ui->combo_init_clock->setCurrentIndex(static_cast<u8>(Settings::values.init_clock.GetValue())); ui->combo_init_clock->setCurrentIndex(static_cast<u8>(Settings::values.init_clock.GetValue()));
QDateTime date_time; QDateTime date_time;
date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue()); date_time.setSecsSinceEpoch(Settings::values.init_time.GetValue());
@ -351,6 +374,7 @@ void ConfigureSystem::ReadSystemSettings() {
// set the country code // set the country code
country_code = cfg->GetCountryCode(); country_code = cfg->GetCountryCode();
ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code)); ui->combo_country->setCurrentIndex(ui->combo_country->findData(country_code));
CheckCountryValid(country_code);
// set whether system setup is needed // set whether system setup is needed
system_setup = cfg->IsSystemSetupNeeded(); system_setup = cfg->IsSystemSetupNeeded();
@ -373,6 +397,10 @@ void ConfigureSystem::ReadSystemSettings() {
void ConfigureSystem::ApplyConfiguration() { void ConfigureSystem::ApplyConfiguration() {
if (enabled) { if (enabled) {
ConfigurationShared::ApplyPerGameSetting(&Settings::values.region_value,
ui->region_combobox,
[](s32 index) { return index - 1; });
bool modified = false; bool modified = false;
// apply username // apply username
@ -582,6 +610,32 @@ void ConfigureSystem::UnlinkConsole() {
RefreshSecureDataStatus(); RefreshSecureDataStatus();
} }
void ConfigureSystem::CheckCountryValid(u8 country) {
// TODO(PabloMK7): Make this per-game compatible
if (!Settings::IsConfiguringGlobal())
return;
s32 region = ui->region_combobox->currentIndex() - 1;
QString label_text;
if (region != Settings::REGION_VALUE_AUTO_SELECT &&
!cfg->IsValidRegionCountry(static_cast<u32>(region), country)) {
label_text = tr("Invalid country for configured region");
}
if (HW::UniqueData::GetSecureInfoA().IsValid()) {
region = static_cast<u32>(cfg->GetRegionValue(true));
if (!cfg->IsValidRegionCountry(static_cast<u32>(region), country)) {
if (!label_text.isEmpty()) {
label_text += QString::fromStdString("\n");
}
label_text += tr("Invalid country for console unique data");
}
}
ui->label_country_invalid->setText(label_text);
ui->label_country_invalid->setVisible(!label_text.isEmpty());
}
void ConfigureSystem::InstallSecureData(const std::string& from_path, const std::string& to_path) { void ConfigureSystem::InstallSecureData(const std::string& from_path, const std::string& to_path) {
std::string from = std::string from =
FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault); FileUtil::SanitizePath(from_path, FileUtil::DirectorySeparator::PlatformDefault);
@ -644,6 +698,7 @@ void ConfigureSystem::SetupPerGameUI() {
ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal()); ui->toggle_lle_applets->setEnabled(Settings::values.lle_applets.UsingGlobal());
ui->enable_required_online_lle_modules->setEnabled( ui->enable_required_online_lle_modules->setEnabled(
Settings::values.enable_required_online_lle_modules.UsingGlobal()); Settings::values.enable_required_online_lle_modules.UsingGlobal());
ui->region_combobox->setEnabled(Settings::values.region_value.UsingGlobal());
return; return;
} }
@ -694,4 +749,7 @@ void ConfigureSystem::SetupPerGameUI() {
ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules, ConfigurationShared::SetColoredTristate(ui->enable_required_online_lle_modules,
Settings::values.enable_required_online_lle_modules, Settings::values.enable_required_online_lle_modules,
required_online_lle_modules); required_online_lle_modules);
ConfigurationShared::SetColoredComboBox(
ui->region_combobox, ui->region_label,
static_cast<u32>(Settings::values.region_value.GetValue(true) + 1));
} }

View File

@ -53,6 +53,7 @@ private:
void RefreshConsoleID(); void RefreshConsoleID();
void RefreshMAC(); void RefreshMAC();
void UnlinkConsole(); void UnlinkConsole();
void CheckCountryValid(u8 country);
void InstallSecureData(const std::string& from_path, const std::string& to_path); void InstallSecureData(const std::string& from_path, const std::string& to_path);
void RefreshSecureDataStatus(); void RefreshSecureDataStatus();

View File

@ -64,21 +64,21 @@
<string>System Settings</string> <string>System Settings</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="1" column="0"> <item row="0" column="0">
<widget class="QCheckBox" name="toggle_new_3ds"> <widget class="QCheckBox" name="toggle_new_3ds">
<property name="text"> <property name="text">
<string>Enable New 3DS mode</string> <string>Enable New 3DS mode</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="1" column="0">
<widget class="QCheckBox" name="toggle_lle_applets"> <widget class="QCheckBox" name="toggle_lle_applets">
<property name="text"> <property name="text">
<string>Use LLE applets (if installed)</string> <string>Use LLE applets (if installed)</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="2" column="0">
<widget class="QCheckBox" name="enable_required_online_lle_modules"> <widget class="QCheckBox" name="enable_required_online_lle_modules">
<property name="text"> <property name="text">
<string>Enable required LLE modules for online features (if installed)</string> <string>Enable required LLE modules for online features (if installed)</string>
@ -88,6 +88,57 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0">
<widget class="QLabel" name="region_label">
<property name="text">
<string>Region:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="region_combobox">
<item>
<property name="text">
<string>Auto-select</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">JPN</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">USA</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">EUR</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AUS</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">CHN</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">KOR</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TWN</string>
</property>
</item>
</widget>
</item>
<item row="4" column="1"> <item row="4" column="1">
<widget class="QLineEdit" name="edit_username"> <widget class="QLineEdit" name="edit_username">
<property name="sizePolicy"> <property name="sizePolicy">
@ -296,14 +347,21 @@
<item row="8" column="1"> <item row="8" column="1">
<widget class="QComboBox" name="combo_country"/> <widget class="QComboBox" name="combo_country"/>
</item> </item>
<item row="9" column="0"> <item row="9" column="1">
<widget class="QLabel" name="label_country_invalid">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_init_clock"> <widget class="QLabel" name="label_init_clock">
<property name="text"> <property name="text">
<string>Clock</string> <string>Clock</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="1"> <item row="10" column="1">
<widget class="QComboBox" name="combo_init_clock"> <widget class="QComboBox" name="combo_init_clock">
<item> <item>
<property name="text"> <property name="text">
@ -317,28 +375,28 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="10" column="0"> <item row="11" column="0">
<widget class="QLabel" name="label_init_time"> <widget class="QLabel" name="label_init_time">
<property name="text"> <property name="text">
<string>Startup time</string> <string>Startup time</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="10" column="1"> <item row="11" column="1">
<widget class="QDateTimeEdit" name="edit_init_time"> <widget class="QDateTimeEdit" name="edit_init_time">
<property name="displayFormat"> <property name="displayFormat">
<string>yyyy-MM-ddTHH:mm:ss</string> <string>yyyy-MM-ddTHH:mm:ss</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="0"> <item row="12" column="0">
<widget class="QLabel" name="label_init_time_offset"> <widget class="QLabel" name="label_init_time_offset">
<property name="text"> <property name="text">
<string>Offset time</string> <string>Offset time</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="11" column="1"> <item row="12" column="1">
<layout class="QGridLayout" name="edit_init_time_offset_grid"> <layout class="QGridLayout" name="edit_init_time_offset_grid">
<item row="0" column="0"> <item row="0" column="0">
<widget class="QSpinBox" name="edit_init_time_offset_days"> <widget class="QSpinBox" name="edit_init_time_offset_days">
@ -362,14 +420,14 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="12" column="0"> <item row="13" column="0">
<widget class="QLabel" name="label_init_ticks_type"> <widget class="QLabel" name="label_init_ticks_type">
<property name="text"> <property name="text">
<string>Initial System Ticks</string> <string>Initial System Ticks</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="12" column="1"> <item row="13" column="1">
<widget class="QComboBox" name="combo_init_ticks_type"> <widget class="QComboBox" name="combo_init_ticks_type">
<item> <item>
<property name="text"> <property name="text">
@ -383,14 +441,14 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="13" column="0"> <item row="14" column="0">
<widget class="QLabel" name="label_init_ticks_value"> <widget class="QLabel" name="label_init_ticks_value">
<property name="text"> <property name="text">
<string>Initial System Ticks Override</string> <string>Initial System Ticks Override</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="13" column="1"> <item row="14" column="1">
<widget class="QLineEdit" name="edit_init_ticks_value"> <widget class="QLineEdit" name="edit_init_ticks_value">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
@ -403,21 +461,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="14" column="0"> <item row="15" column="0">
<widget class="QLabel" name="label_play_coins"> <widget class="QLabel" name="label_play_coins">
<property name="text"> <property name="text">
<string>Play Coins</string> <string>Play Coins</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="14" column="1"> <item row="15" column="1">
<widget class="QSpinBox" name="spinBox_play_coins"> <widget class="QSpinBox" name="spinBox_play_coins">
<property name="maximum"> <property name="maximum">
<number>300</number> <number>300</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="0"> <item row="16" column="0">
<widget class="QLabel" name="label_steps_per_hour"> <widget class="QLabel" name="label_steps_per_hour">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Number of steps per hour reported by the pedometer. Range from 0 to 65,535.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Number of steps per hour reported by the pedometer. Range from 0 to 65,535.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -427,28 +485,28 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="15" column="1"> <item row="16" column="1">
<widget class="QSpinBox" name="spinBox_steps_per_hour"> <widget class="QSpinBox" name="spinBox_steps_per_hour">
<property name="maximum"> <property name="maximum">
<number>9999</number> <number>9999</number>
</property> </property>
</widget> </widget>
</item> </item>
<item row="16" column="1"> <item row="17" column="1">
<widget class="QCheckBox" name="toggle_system_setup"> <widget class="QCheckBox" name="toggle_system_setup">
<property name="text"> <property name="text">
<string>Run System Setup when Home Menu is launched</string> <string>Run System Setup when Home Menu is launched</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="17" column="0"> <item row="18" column="0">
<widget class="QLabel" name="label_console_id"> <widget class="QLabel" name="label_console_id">
<property name="text"> <property name="text">
<string>Console ID:</string> <string>Console ID:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="17" column="1"> <item row="18" column="1">
<widget class="QPushButton" name="button_regenerate_console_id"> <widget class="QPushButton" name="button_regenerate_console_id">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
@ -464,14 +522,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="18" column="0"> <item row="19" column="0">
<widget class="QLabel" name="label_mac"> <widget class="QLabel" name="label_mac">
<property name="text"> <property name="text">
<string>MAC:</string> <string>MAC:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="18" column="1"> <item row="19" column="1">
<widget class="QPushButton" name="button_regenerate_mac"> <widget class="QPushButton" name="button_regenerate_mac">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
@ -487,21 +545,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="19" column="0"> <item row="20" column="0">
<widget class="QLabel" name="label_plugin_loader"> <widget class="QLabel" name="label_plugin_loader">
<property name="text"> <property name="text">
<string>3GX Plugin Loader:</string> <string>3GX Plugin Loader:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="19" column="1"> <item row="20" column="1">
<widget class="QCheckBox" name="plugin_loader"> <widget class="QCheckBox" name="plugin_loader">
<property name="text"> <property name="text">
<string>Enable 3GX plugin loader</string> <string>Enable 3GX plugin loader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="20" column="1"> <item row="21" column="1">
<widget class="QCheckBox" name="allow_plugin_loader"> <widget class="QCheckBox" name="allow_plugin_loader">
<property name="text"> <property name="text">
<string>Allow applications to change plugin loader state</string> <string>Allow applications to change plugin loader state</string>

View File

@ -86,30 +86,227 @@ static constexpr u16 C(const char code[2]) {
} }
static const std::array<u16, 187> country_codes = {{ static const std::array<u16, 187> country_codes = {{
0, C("JP"), 0, 0, 0, 0, 0, 0, // 0-7 // 0-7 Japan
C("AI"), C("AG"), C("AR"), C("AW"), C("BS"), C("BB"), C("BZ"), C("BO"), // 8-15 0,
C("BR"), C("VG"), C("CA"), C("KY"), C("CL"), C("CO"), C("CR"), C("DM"), // 16-23 C("JP"),
C("DO"), C("EC"), C("SV"), C("GF"), C("GD"), C("GP"), C("GT"), C("GY"), // 24-31 0,
C("HT"), C("HN"), C("JM"), C("MQ"), C("MX"), C("MS"), C("AN"), C("NI"), // 32-39 0,
C("PA"), C("PY"), C("PE"), C("KN"), C("LC"), C("VC"), C("SR"), C("TT"), // 40-47 0,
C("TC"), C("US"), C("UY"), C("VI"), C("VE"), 0, 0, 0, // 48-55 0,
0, 0, 0, 0, 0, 0, 0, 0, // 56-63 0,
C("AL"), C("AU"), C("AT"), C("BE"), C("BA"), C("BW"), C("BG"), C("HR"), // 64-71 0,
C("CY"), C("CZ"), C("DK"), C("EE"), C("FI"), C("FR"), C("DE"), C("GR"), // 72-79
C("HU"), C("IS"), C("IE"), C("IT"), C("LV"), C("LS"), C("LI"), C("LT"), // 80-87 // 8-15 America
C("LU"), C("MK"), C("MT"), C("ME"), C("MZ"), C("NA"), C("NL"), C("NZ"), // 88-95 C("AI"),
C("NO"), C("PL"), C("PT"), C("RO"), C("RU"), C("RS"), C("SK"), C("SI"), // 96-103 C("AG"),
C("ZA"), C("ES"), C("SZ"), C("SE"), C("CH"), C("TR"), C("GB"), C("ZM"), // 104-111 C("AR"),
C("ZW"), C("AZ"), C("MR"), C("ML"), C("NE"), C("TD"), C("SD"), C("ER"), // 112-119 C("AW"),
C("DJ"), C("SO"), C("AD"), C("GI"), C("GG"), C("IM"), C("JE"), C("MC"), // 120-127 C("BS"),
C("TW"), 0, 0, 0, 0, 0, 0, 0, // 128-135 C("BB"),
C("KR"), 0, 0, 0, 0, 0, 0, 0, // 136-143 C("BZ"),
C("HK"), C("MO"), 0, 0, 0, 0, 0, 0, // 144-151 C("BO"),
C("ID"), C("SG"), C("TH"), C("PH"), C("MY"), 0, 0, 0, // 152-159 // 16-23 America
C("CN"), 0, 0, 0, 0, 0, 0, 0, // 160-167 C("BR"),
C("AE"), C("IN"), C("EG"), C("OM"), C("QA"), C("KW"), C("SA"), C("SY"), // 168-175 C("VG"),
C("BH"), C("JO"), 0, 0, 0, 0, 0, 0, // 176-183 C("CA"),
C("SM"), C("VA"), C("BM"), // 184-186 C("KY"),
C("CL"),
C("CO"),
C("CR"),
C("DM"),
// 24-31 America
C("DO"),
C("EC"),
C("SV"),
C("GF"),
C("GD"),
C("GP"),
C("GT"),
C("GY"),
// 32-39 America
C("HT"),
C("HN"),
C("JM"),
C("MQ"),
C("MX"),
C("MS"),
C("AN"),
C("NI"),
// 40-47 America
C("PA"),
C("PY"),
C("PE"),
C("KN"),
C("LC"),
C("VC"),
C("SR"),
C("TT"),
// 48-55 America
C("TC"),
C("US"),
C("UY"),
C("VI"),
C("VE"),
0,
0,
0,
// 56-63 Invalid
0,
0,
0,
0,
0,
0,
0,
0,
// 64-71 Europe
C("AL"),
C("AU"),
C("AT"),
C("BE"),
C("BA"),
C("BW"),
C("BG"),
C("HR"),
// 72-79 Europe
C("CY"),
C("CZ"),
C("DK"),
C("EE"),
C("FI"),
C("FR"),
C("DE"),
C("GR"),
// 80-87 Europe
C("HU"),
C("IS"),
C("IE"),
C("IT"),
C("LV"),
C("LS"),
C("LI"),
C("LT"),
// 88-95 Europe
C("LU"),
C("MK"),
C("MT"),
C("ME"),
C("MZ"),
C("NA"),
C("NL"),
C("NZ"),
// 96-103 Europe
C("NO"),
C("PL"),
C("PT"),
C("RO"),
C("RU"),
C("RS"),
C("SK"),
C("SI"),
// 104-111 Europe
C("ZA"),
C("ES"),
C("SZ"),
C("SE"),
C("CH"),
C("TR"),
C("GB"),
C("ZM"),
// 112-119 Europe
C("ZW"),
C("AZ"),
C("MR"),
C("ML"),
C("NE"),
C("TD"),
C("SD"),
C("ER"),
// 120-127 Europe
C("DJ"),
C("SO"),
C("AD"),
C("GI"),
C("GG"),
C("IM"),
C("JE"),
C("MC"),
// 128-135 Taiwan
C("TW"),
0,
0,
0,
0,
0,
0,
0,
// 136-143 Korea
C("KR"),
0,
0,
0,
0,
0,
0,
0,
// 144-151 China? (Hong Kong & Macao)
C("HK"),
C("MO"),
0,
0,
0,
0,
0,
0,
// 152-159 Southeast Asia
C("ID"),
C("SG"), // USA
C("TH"),
C("PH"),
C("MY"), // USA
0,
0,
0,
// 160-167 China
C("CN"),
0,
0,
0,
0,
0,
0,
0,
// 168-175 Middle East
C("AE"), // USA
C("IN"), // EUR
C("EG"),
C("OM"),
C("QA"),
C("KW"),
C("SA"), // USA
C("SY"),
// 176-183 Middle East
C("BH"),
C("JO"),
0,
0,
0,
0,
0,
0,
// 184-186 European Microstates
C("SM"),
C("VA"),
C("BM"),
}}; }};
// Based on PKHeX's lists of subregions at // Based on PKHeX's lists of subregions at
@ -218,6 +415,29 @@ u32 Module::GetRegionValue(bool from_secure_info) {
return Settings::values.region_value.GetValue(); return Settings::values.region_value.GetValue();
} }
bool Module::IsValidRegionCountry(u32 region, u8 country_code) {
switch (region) {
case 0: // JPN
return country_code == 1;
case 1: // USA
return (country_code >= 8 && country_code <= 52) || country_code == 153 ||
country_code == 156 || country_code == 168 || country_code == 174;
case 2: // EUR
case 3: // AUS
return (country_code >= 64 && country_code <= 127) ||
(country_code >= 184 && country_code <= 186) || country_code == 169;
case 4: // CHN
return country_code == 144 || country_code == 145 || country_code == 160;
case 5: // KOR
return country_code == 136;
case 6: // TWN
return country_code == 128;
default:
break;
}
return false;
}
void Module::Interface::GetRegion(Kernel::HLERequestContext& ctx) { void Module::Interface::GetRegion(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp(ctx); IPC::RequestParser rp(ctx);

View File

@ -486,6 +486,8 @@ private:
public: public:
u32 GetRegionValue(bool from_secure_info); u32 GetRegionValue(bool from_secure_info);
static bool IsValidRegionCountry(u32 region, u8 country_code);
// Utilities for frontend to set config data. // Utilities for frontend to set config data.
// Note: UpdateConfigNANDSavegame should be called after making changes to config data. // Note: UpdateConfigNANDSavegame should be called after making changes to config data.

View File

@ -408,6 +408,7 @@ ResultStatus Apploader_Artic::Load(std::shared_ptr<Kernel::Process>& process) {
return ResultStatus::ErrorArtic; return ResultStatus::ErrorArtic;
} }
auto cfg = system.ServiceManager().GetService<Service::CFG::CFG_U>("cfg:u");
// Request console unique data // Request console unique data
for (int i = 0; i < 6; i++) { for (int i = 0; i < 6; i++) {
std::string path; std::string path;
@ -471,7 +472,6 @@ ResultStatus Apploader_Artic::Load(std::shared_ptr<Kernel::Process>& process) {
memcpy(&console_id, resp_buff->first, sizeof(u64)); memcpy(&console_id, resp_buff->first, sizeof(u64));
memcpy(&random_id, reinterpret_cast<u8*>(resp_buff->first) + sizeof(u64), memcpy(&random_id, reinterpret_cast<u8*>(resp_buff->first) + sizeof(u64),
sizeof(u32)); sizeof(u32));
auto cfg = system.ServiceManager().GetService<Service::CFG::CFG_U>("cfg:u");
if (cfg.get()) { if (cfg.get()) {
auto cfg_module = cfg->GetModule(); auto cfg_module = cfg->GetModule();
cfg_module->SetConsoleUniqueId(random_id, console_id); cfg_module->SetConsoleUniqueId(random_id, console_id);
@ -480,7 +480,6 @@ ResultStatus Apploader_Artic::Load(std::shared_ptr<Kernel::Process>& process) {
} else if (i == 5) { } else if (i == 5) {
std::array<u8, 6> mac; std::array<u8, 6> mac;
memcpy(mac.data(), resp_buff->first, mac.size()); memcpy(mac.data(), resp_buff->first, mac.size());
auto cfg = system.ServiceManager().GetService<Service::CFG::CFG_U>("cfg:u");
if (cfg.get()) { if (cfg.get()) {
auto cfg_module = cfg->GetModule(); auto cfg_module = cfg->GetModule();
cfg_module->GetMacAddress() = Service::CFG::MacToString(mac); cfg_module->GetMacAddress() = Service::CFG::MacToString(mac);
@ -494,10 +493,25 @@ ResultStatus Apploader_Artic::Load(std::shared_ptr<Kernel::Process>& process) {
if (!HW::UniqueData::GetCTCert().IsValid() || !HW::UniqueData::GetMovableSed().IsValid() || if (!HW::UniqueData::GetCTCert().IsValid() || !HW::UniqueData::GetMovableSed().IsValid() ||
!HW::UniqueData::GetSecureInfoA().IsValid() || !HW::UniqueData::GetSecureInfoA().IsValid() ||
!HW::UniqueData::GetLocalFriendCodeSeedB().IsValid()) { !HW::UniqueData::GetLocalFriendCodeSeedB().IsValid()) {
LOG_CRITICAL(Loader, "Some console unique data is invalid, aborting..."); client->LogOnServer(Network::ArticBaseCommon::LogOnServerType::LOG_ERROR,
"Some console unique data is invalid.\n Aborting...");
return ResultStatus::ErrorArtic; return ResultStatus::ErrorArtic;
} }
if (cfg.get()) {
auto cfg_module = cfg->GetModule();
if (!Service::CFG::Module::IsValidRegionCountry(cfg_module->GetRegionValue(true),
cfg_module->GetCountryCode())) {
// Report mismatch to server.
client->LogOnServer(
Network::ArticBaseCommon::LogOnServerType::LOG_ERROR,
"The country configuration does not match\n the console region. "
"Please select a valid\n country from the emulation settings.");
return ResultStatus::ErrorArtic;
}
cfg_module->SetSystemSetupNeeded(false);
}
// Set deliver arg so that System Settings goes to the update screen directly // Set deliver arg so that System Settings goes to the update screen directly
auto apt = Service::APT::GetModule(system); auto apt = Service::APT::GetModule(system);
Service::APT::DeliverArg arg; Service::APT::DeliverArg arg;

View File

@ -471,6 +471,14 @@ std::optional<Client::Response> Client::Send(Request& request) {
return std::optional<Client::Response>(std::move(resp.response)); return std::optional<Client::Response>(std::move(resp.response));
} }
void Client::LogOnServer(ArticBaseCommon::LogOnServerType log_type, const std::string& message) {
auto req = NewRequest("__log");
req.AddParameterS8(static_cast<s8>(log_type));
req.AddParameterBuffer(message.data(), message.size());
Send(req);
}
void Client::SignalCommunicationError(const std::string& msg) { void Client::SignalCommunicationError(const std::string& msg) {
StopImpl(true); StopImpl(true);
LOG_CRITICAL(Network, "Communication error"); LOG_CRITICAL(Network, "Communication error");

View File

@ -165,6 +165,8 @@ public:
ping_enabled = enable; ping_enabled = enable;
} }
void LogOnServer(ArticBaseCommon::LogOnServerType log_type, const std::string& message);
private: private:
static constexpr const int SERVER_VERSION = 2; static constexpr const int SERVER_VERSION = 2;

View File

@ -1,4 +1,4 @@
// Copyright 2024 Citra Emulator Project // Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version // Licensed under GPLv2 or any later version
// Refer to the license.txt file included. // Refer to the license.txt file included.
@ -31,6 +31,12 @@ enum class RequestParameterType : u16 {
IN_SMALL_BUFFER = 4, IN_SMALL_BUFFER = 4,
IN_BIG_BUFFER = 5, IN_BIG_BUFFER = 5,
}; };
enum class LogOnServerType : u8 {
LOG_DEBUG = 0,
LOG_INFO = 1,
LOG_WARNING = 2,
LOG_ERROR = 3,
};
struct RequestParameter { struct RequestParameter {
RequestParameterType type{}; RequestParameterType type{};
union { union {