diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index b0487302b3..565decb390 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -64,7 +64,8 @@
- @string/language_spanish
- @string/language_taiwanese
- @string/language_traditional_chinese
- - @string/language_serbian
+ - @string/language_polish
+ - @string/language_thai
@@ -87,6 +88,7 @@
- 11
- 16
- 18
+ - 19
@@ -407,6 +409,7 @@
- @string/app_language_persian
- @string/app_language_hebrew
- @string/app_language_serbian
+ - @string/app_language_thai
- 0
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 8a1193303e..a34d75d7cb 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -1011,7 +1011,8 @@
简体中文
正體中文
Português do Brasil
- српски
+ Polska
+ แบบไทย
B
@@ -1216,6 +1217,7 @@
فارسی
עברית
Српски
+ แบบไทย
Theme Color
diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h
index de54696497..638be4127f 100644
--- a/src/common/settings_enums.h
+++ b/src/common/settings_enums.h
@@ -120,7 +120,7 @@ ENUM(AudioMode, Mono, Stereo, Surround);
ENUM(Language, Japanese, EnglishAmerican, French, German, Italian, Spanish, Chinese, Korean, Dutch,
Portuguese, Russian, Taiwanese, EnglishBritish, FrenchCanadian, SpanishLatin,
- ChineseSimplified, ChineseTraditional, PortugueseBrazilian, Serbian, Polish, Thai);
+ ChineseSimplified, ChineseTraditional, PortugueseBrazilian, Polish, Thai);
ENUM(Region, Japan, Usa, Europe, Australia, China, Korea, Taiwan);
ENUM(TimeZone, Auto, Default, Cet, Cst6Cdt, Cuba, Eet, Egypt, Eire, Est, Est5Edt, Gb, GbEire, Gmt,
GmtPlusZero, GmtMinusZero, GmtZero, Greenwich, Hongkong, Hst, Iceland, Iran, Israel, Jamaica,
diff --git a/src/core/file_sys/control_metadata.cpp b/src/core/file_sys/control_metadata.cpp
index 1b38bcec12..e9681ab80f 100644
--- a/src/core/file_sys/control_metadata.cpp
+++ b/src/core/file_sys/control_metadata.cpp
@@ -41,71 +41,37 @@ const std::array LANGUAGE_NAMES{{
"Thai",
}};
-namespace {
-constexpr std::size_t LEGACY_LANGUAGE_REGION_SIZE = sizeof(std::array);
-constexpr std::size_t PACKED_LANGUAGE_REGION_MAX_SIZE = sizeof(LanguageEntry) * 32;
+namespace
+{
+ constexpr std::size_t MAX_EXPANDED_LANG_SIZE = sizeof(LanguageEntry) * 32;
-bool InflateRawDeflate(std::span compressed, std::vector& out) {
- if (compressed.empty() || compressed.size() > std::numeric_limits::max()) {
- return false;
- }
- z_stream stream{};
- stream.next_in = const_cast(reinterpret_cast(compressed.data()));
- stream.avail_in = static_cast(compressed.size());
- if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
- return false;
- }
+ bool InflateRawDeflate(std::span compressed, std::vector& out)
+ {
+ if (compressed.empty()) return false;
- std::array chunk{};
- int ret = Z_OK;
- while (ret == Z_OK) {
- stream.next_out = reinterpret_cast(chunk.data());
- stream.avail_out = static_cast(chunk.size());
- ret = inflate(&stream, Z_NO_FLUSH);
- if (ret != Z_OK && ret != Z_STREAM_END) {
- inflateEnd(&stream);
+ z_stream stream{};
+ stream.next_in = const_cast(reinterpret_cast(compressed.data()));
+ stream.avail_in = static_cast(compressed.size());
+ if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
return false;
}
- const auto produced = chunk.size() - static_cast(stream.avail_out);
- if (produced != 0) {
- if (out.size() + produced > PACKED_LANGUAGE_REGION_MAX_SIZE) {
- inflateEnd(&stream);
- return false;
- }
- out.insert(out.end(), chunk.begin(),
- chunk.begin() + static_cast(produced));
- }
- }
+ out.resize(MAX_EXPANDED_LANG_SIZE);
+ stream.next_out = reinterpret_cast(out.data());
+ stream.avail_out = static_cast(out.size());
- inflateEnd(&stream);
- return ret == Z_STREAM_END;
-}
+ int ret = inflate(&stream, Z_FINISH);
+ inflateEnd(&stream);
-void DecodePackedLanguageEntries(RawNACP& raw) {
- auto* packed_region = reinterpret_cast(raw.language_entries.data());
- u16_le compressed_size_le{};
- std::memcpy(&compressed_size_le, packed_region, sizeof(compressed_size_le));
- const auto compressed_size = static_cast(compressed_size_le);
-
- if (compressed_size == 0 || compressed_size > LEGACY_LANGUAGE_REGION_SIZE - sizeof(u16_le)) {
- return;
- }
-
- std::vector decompressed;
- if (!InflateRawDeflate(
- std::span(packed_region + sizeof(u16_le), compressed_size), decompressed)) {
- return;
- }
+ if (ret != Z_STREAM_END && ret != Z_OK) {
+ return false;
+ }
- if (decompressed.size() < LEGACY_LANGUAGE_REGION_SIZE ||
- decompressed.size() % sizeof(LanguageEntry) != 0) {
- return;
+ // Shrink to actual decompressed size
+ out.resize(stream.total_out);
+ return true;
}
-
- std::memcpy(raw.language_entries.data(), decompressed.data(), LEGACY_LANGUAGE_REGION_SIZE);
-}
} // namespace
std::string LanguageEntry::GetApplicationName() const {
@@ -141,55 +107,48 @@ constexpr std::array language_to_codes = {{
NACP::NACP() = default;
-NACP::NACP(VirtualFile file) {
+NACP::NACP(VirtualFile file)
+{
file->ReadObject(&raw);
- DecodePackedLanguageEntries(raw);
+ if (raw.titles_data_format == TitleDataFormat::Compressed) {
+ const u16 compressed_size = raw.language_entries.compressed_data.buffer_size;
+ std::span compressed_payload{raw.language_entries.compressed_data.buffer,
+ compressed_size};
+
+ std::vector decompressed;
+ if (InflateRawDeflate(compressed_payload, decompressed)) {
+ const size_t entry_count = decompressed.size() / sizeof(LanguageEntry);
+ language_entries.resize(entry_count);
+ std::memcpy(language_entries.data(), decompressed.data(), decompressed.size());
+ }
+ } else {
+ language_entries.resize(16);
+ std::memcpy(language_entries.data(), raw.language_entries.language_entries.data(),
+ sizeof(raw.language_entries.language_entries));
+ }
}
NACP::~NACP() = default;
const LanguageEntry& NACP::GetLanguageEntry() const {
- auto const language = []{
- switch (Settings::values.language_index.GetValue()) {
- case Settings::Language::Chinese: return Language::SimplifiedChinese;
- case Settings::Language::ChineseSimplified: return Language::SimplifiedChinese;
- case Settings::Language::ChineseTraditional: return Language::TraditionalChinese;
- case Settings::Language::Dutch: return Language::Dutch;
- case Settings::Language::EnglishAmerican: return Language::AmericanEnglish;
- case Settings::Language::EnglishBritish: return Language::BritishEnglish;
- case Settings::Language::French: return Language::French;
- case Settings::Language::FrenchCanadian: return Language::CanadianFrench;
- case Settings::Language::German: return Language::German;
- case Settings::Language::Italian: return Language::Italian;
- case Settings::Language::Korean: return Language::Korean;
- case Settings::Language::Japanese: return Language::Japanese;
- case Settings::Language::Portuguese: return Language::Portuguese;
- case Settings::Language::PortugueseBrazilian: return Language::BrazilianPortuguese;
- case Settings::Language::Russian: return Language::Russian;
- case Settings::Language::Serbian: return Language::Russian;
- case Settings::Language::Spanish: return Language::Spanish;
- case Settings::Language::SpanishLatin: return Language::LatinAmericanSpanish;
- case Settings::Language::Taiwanese: return Language::SimplifiedChinese;
- case Settings::Language::Thai: return Language::Thai;
- case Settings::Language::Polish: return Language::Polish;
- }
- }();
- {
- const auto& language_entry = raw.language_entries.at(static_cast(language));
- if (!language_entry.GetApplicationName().empty())
- return language_entry;
+ u32 index = static_cast(Settings::values.language_index.GetValue());
+
+ if (index < language_entries.size()) {
+ return language_entries[index];
}
- for (const auto& language_entry : raw.language_entries) {
- if (!language_entry.GetApplicationName().empty())
- return language_entry;
+
+ for (const auto& entry : language_entries) {
+ return entry;
}
- return raw.language_entries.at(static_cast(Language::AmericanEnglish));
+
+ return language_entries.at(static_cast(Language::AmericanEnglish));
}
-std::array NACP::GetApplicationNames() const {
- std::array names{};
- for (size_t i = 0; i < raw.language_entries.size(); ++i) {
- names[i] = raw.language_entries[i].GetApplicationName();
+std::vector NACP::GetApplicationNames() const {
+ std::vector names;
+ names.reserve(language_entries.size());
+ for (const auto& entry : language_entries) {
+ names.push_back(entry.GetApplicationName());
}
return names;
}
diff --git a/src/core/file_sys/control_metadata.h b/src/core/file_sys/control_metadata.h
index 478fd6e5e7..7b90ba235d 100644
--- a/src/core/file_sys/control_metadata.h
+++ b/src/core/file_sys/control_metadata.h
@@ -27,6 +27,24 @@ struct LanguageEntry {
};
static_assert(sizeof(LanguageEntry) == 0x300);
+struct LanguageEntryData {
+ union {
+ // TitleDataFormat::Uncompressed (16 entries)
+ std::array language_entries;
+
+ // TitleDataFormat::Compressed (18+ entries)
+ struct {
+ u16 buffer_size;
+ u8 buffer[0x2FFE];
+ } compressed_data;
+ };
+};
+
+enum class TitleDataFormat : u8 {
+ Uncompressed = 0,
+ Compressed = 1,
+};
+
struct ApplicationNeighborDetectionGroupConfiguration {
u64 group_id; ///< GroupId
std::array key;
@@ -98,6 +116,17 @@ struct AccessibleLaunchRequiredVersion {
};
static_assert(sizeof(AccessibleLaunchRequiredVersion) == 0x40);
+enum class CrashReport : u8 {
+ Deny = 0,
+ Allow = 1,
+};
+
+enum class PlayLogQueryCapability : u8 {
+ None = 0,
+ WhiteList = 1,
+ All = 2,
+};
+
struct ApplicationControlDataConditionData {
u8 priority;
INSERT_PADDING_BYTES(7);
@@ -154,7 +183,7 @@ enum class SupportedLanguage : u32 {
// The raw file format of a NACP file.
struct RawNACP {
- std::array language_entries;
+ LanguageEntryData language_entries;
std::array isbn;
u8 startup_user_account;
u8 user_account_switch_lock;
@@ -165,7 +194,7 @@ struct RawNACP {
bool screenshot_enabled;
u8 video_capture_mode;
bool data_loss_confirmation;
- INSERT_PADDING_BYTES(1);
+ u8 play_log_policy;
u64_le presence_group_id;
std::array rating_age;
std::array version_string;
@@ -181,10 +210,14 @@ struct RawNACP {
u8 logo_type;
u8 logo_handling;
bool runtime_add_on_content_install;
- INSERT_PADDING_BYTES(5);
+ u8 runtime_parameter_delivery;
+ u8 appropriate_age_for_china;
+ INSERT_PADDING_BYTES(1);
+ CrashReport crash_report;
u64_le seed_for_pseudo_device_id;
std::array bcat_passphrase;
- INSERT_PADDING_BYTES(7);
+ u8 startup_user_account_option;
+ INSERT_PADDING_BYTES(6); // ReservedForUserAccountSaveDataOperation
u64_le user_account_save_data_max_size;
u64_le user_account_save_data_max_journal_size;
u64_le device_save_data_max_size;
@@ -194,9 +227,15 @@ struct RawNACP {
u64_le cache_storage_journal_size;
u64_le cache_storage_data_and_journal_max_size;
u16_le cache_storage_max_index;
- INSERT_PADDING_BYTES(0x8B);
+ u8 runtime_upgrade;
+ u32_le supporting_limited_application_licenses;
+ std::array play_log_queryable_application_id;
+ PlayLogQueryCapability play_log_query_capability;
+ u8 repair_flag;
+ u8 program_index;
+ u8 required_network_service_license_on_launch_flag;
u8 app_error_code_prefix;
- u8 title_compression;
+ TitleDataFormat titles_data_format;
u8 acd_index;
ApparentPlatform apparent_platform;
// NeighborDetectionClientConfiguration neighbor_detection_client_configuration;
@@ -219,7 +258,8 @@ struct RawNACP {
u8 has_ingame_voice_chat;
INSERT_PADDING_BYTES(3);
u32_le supported_extra_addon_content_flag;
- INSERT_PADDING_BYTES(0x698);
+ u8 has_karaoke_feature;
+ INSERT_PADDING_BYTES(0x697);
std::array platform_specific_region;
};
static_assert(sizeof(RawNACP) == 0x4000, "RawNACP has incorrect size.");
@@ -243,7 +283,7 @@ public:
u64 GetDefaultNormalSaveSize() const;
u64 GetDefaultJournalSaveSize() const;
u32 GetSupportedLanguages() const;
- std::array GetApplicationNames() const;
+ std::vector GetApplicationNames() const;
std::vector GetRawBytes() const;
bool GetUserAccountSwitchLock() const;
u64 GetDeviceSaveDataSize() const;
@@ -252,6 +292,7 @@ public:
private:
RawNACP raw{};
+ std::vector language_entries;
};
} // namespace FileSys
diff --git a/src/core/hle/service/ns/language.cpp b/src/core/hle/service/ns/language.cpp
index 997e293a80..ff5ccdd6d3 100644
--- a/src/core/hle/service/ns/language.cpp
+++ b/src/core/hle/service/ns/language.cpp
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -329,9 +332,6 @@ constexpr ApplicationLanguagePriorityList priority_list_brazilian_portuguese = {
constexpr ApplicationLanguagePriorityList priority_list_thai = {{
ApplicationLanguage::Thai,
- ApplicationLanguage::BrazilianPortuguese,
- ApplicationLanguage::Portuguese,
- ApplicationLanguage::LatinAmericanSpanish,
ApplicationLanguage::AmericanEnglish,
ApplicationLanguage::BritishEnglish,
ApplicationLanguage::Japanese,
@@ -350,10 +350,6 @@ constexpr ApplicationLanguagePriorityList priority_list_thai = {{
constexpr ApplicationLanguagePriorityList priority_list_polish = {{
ApplicationLanguage::Polish,
- ApplicationLanguage::Thai,
- ApplicationLanguage::BrazilianPortuguese,
- ApplicationLanguage::Portuguese,
- ApplicationLanguage::LatinAmericanSpanish,
ApplicationLanguage::AmericanEnglish,
ApplicationLanguage::BritishEnglish,
ApplicationLanguage::Japanese,
diff --git a/src/core/hle/service/set/settings_types.h b/src/core/hle/service/set/settings_types.h
index 12f1c42185..469b8d745c 100644
--- a/src/core/hle/service/set/settings_types.h
+++ b/src/core/hle/service/set/settings_types.h
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
@@ -193,8 +196,8 @@ enum class LanguageCode : u64 {
ZH_HANS = 0x00736E61482D687A,
ZH_HANT = 0x00746E61482D687A,
PT_BR = 0x00000052422D7470,
- TH = 0x0000000000006874,
PL = 0x000000000000706C,
+ TH = 0x0000000000006874,
};
/// This is nn::settings::system::NotificationVolume
@@ -252,7 +255,7 @@ enum class PlatformRegion : s32 {
Terra = 2,
};
-constexpr std::array available_language_codes = {{
+constexpr std::array available_language_codes = {{
LanguageCode::JA,
LanguageCode::EN_US,
LanguageCode::FR,
@@ -271,9 +274,11 @@ constexpr std::array available_language_codes = {{
LanguageCode::ZH_HANS,
LanguageCode::ZH_HANT,
LanguageCode::PT_BR,
+ LanguageCode::PL,
+ LanguageCode::TH
}};
-static constexpr std::array, 18> language_to_layout{{
+static constexpr std::array, 20> language_to_layout{{
{LanguageCode::JA, KeyboardLayout::Japanese},
{LanguageCode::EN_US, KeyboardLayout::EnglishUs},
{LanguageCode::FR, KeyboardLayout::French},
@@ -292,6 +297,8 @@ static constexpr std::array, 18> languag
{LanguageCode::ZH_HANS, KeyboardLayout::ChineseSimplified},
{LanguageCode::ZH_HANT, KeyboardLayout::ChineseTraditional},
{LanguageCode::PT_BR, KeyboardLayout::Portuguese},
+ {LanguageCode::PL, KeyboardLayout::EnglishUsInternational},
+ {LanguageCode::TH, KeyboardLayout::EnglishUsInternational}
}};
/// This is nn::settings::system::AccountNotificationFlag
diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp
index 78fbce2347..9666092802 100644
--- a/src/qt_common/config/shared_translation.cpp
+++ b/src/qt_common/config/shared_translation.cpp
@@ -651,7 +651,6 @@ std::unique_ptr ComboboxEnumeration(QObject* parent)
PAIR(Language, ChineseSimplified, tr("Simplified Chinese")),
PAIR(Language, ChineseTraditional, tr("Traditional Chinese (正體中文)")),
PAIR(Language, PortugueseBrazilian, tr("Brazilian Portuguese (português do Brasil)")),
- PAIR(Language, Serbian, tr("Serbian (српски)")),
PAIR(Language, Polish, tr("Polish (polska)")),
PAIR(Language, Thai, tr("Thai (แบบไทย)")),
}});