diff --git a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs
index f5b7fc0f1..8e7a44005 100644
--- a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs
+++ b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs
@@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState
SimplifiedChinese,
TraditionalChinese,
BrazilianPortuguese,
+ Polish,
+ Thai,
}
}
diff --git a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs
index 91277232c..74378b153 100644
--- a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs
+++ b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs
@@ -23,7 +23,9 @@ namespace Ryujinx.HLE.HOS.SystemState
"es-419",
"zh-Hans",
"zh-Hant",
- "pt-BR"
+ "pt-BR",
+ "pl",
+ "th"
];
internal long DesiredKeyboardLayout { get; private set; }
diff --git a/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs b/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs
index e3bfb9165..e651410ce 100644
--- a/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs
+++ b/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs
@@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState
TraditionalChinese,
SimplifiedChinese,
BrazilianPortuguese,
+ Polish,
+ Thai,
}
}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs
index 7373e2f45..e3a2ae69e 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs
@@ -8,6 +8,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.Loaders.Executables;
+using Ryujinx.HLE.Utilities;
using Ryujinx.Memory;
using System.Linq;
using static Ryujinx.HLE.HOS.ModLoader;
@@ -86,16 +87,33 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
if (!isHomebrew && programId > 0x010000000000FFFF)
{
- programName = nacpData.Value.Title[(int)device.System.State.DesiredTitleLanguage].NameString.ToString();
+ int langIdx = (int)device.System.State.DesiredTitleLanguage;
+ var decompressedTitles = NacpHelper.IsCompressed(in nacpData.Value)
+ ? NacpHelper.DecompressTitleEntries(in nacpData.Value) : null;
+ int titleCount = decompressedTitles?.Length ?? 16;
+
+ if (langIdx < titleCount)
+ {
+ programName = decompressedTitles != null
+ ? decompressedTitles[langIdx].NameString.ToString()
+ : nacpData.Value.Title[langIdx].NameString.ToString();
+ }
if (string.IsNullOrWhiteSpace(programName))
{
- foreach (ApplicationControlProperty.ApplicationTitle appTitle in nacpData.Value.Title)
+ for (int i = 0; i < titleCount; i++)
{
- if (appTitle.Name[0] != 0)
- continue;
+ bool empty = decompressedTitles != null
+ ? decompressedTitles[i].Name[0] == 0
+ : nacpData.Value.Title[i].Name[0] == 0;
- programName = appTitle.NameString.ToString();
+ if (!empty)
+ {
+ programName = decompressedTitles != null
+ ? decompressedTitles[i].NameString.ToString()
+ : nacpData.Value.Title[i].NameString.ToString();
+ break;
+ }
}
}
}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
index 48b5b724c..513eacac4 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
@@ -194,20 +194,39 @@ namespace Ryujinx.HLE.Loaders.Processes
{
nacpStorage.Read(0, nacpData.ByteSpan);
- programName = nacpData.Value.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString();
+ int langIdx = (int)_device.System.State.DesiredTitleLanguage;
+ var decompressedTitles = Utilities.NacpHelper.IsCompressed(in nacpData.Value)
+ ? Utilities.NacpHelper.DecompressTitleEntries(in nacpData.Value) : null;
+ int titleCount = decompressedTitles?.Length ?? 16;
- if ("Switch Verification" ==
- nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString())
+ if (langIdx < titleCount)
+ {
+ programName = decompressedTitles != null
+ ? decompressedTitles[langIdx].NameString.ToString()
+ : nacpData.Value.Title[langIdx].NameString.ToString();
+ }
+
+ string verifyName = decompressedTitles != null
+ ? decompressedTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()
+ : nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString();
+ if ("Switch Verification" == verifyName)
throw new InvalidOperationException();
if (string.IsNullOrWhiteSpace(programName))
{
- foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in nacpData.Value.Title)
+ for (int i = 0; i < titleCount; i++)
{
- if (nacpTitles.Name[0] != 0)
- continue;
+ bool empty = decompressedTitles != null
+ ? decompressedTitles[i].Name[0] == 0
+ : nacpData.Value.Title[i].Name[0] == 0;
- programName = nacpTitles.NameString.ToString();
+ if (!empty)
+ {
+ programName = decompressedTitles != null
+ ? decompressedTitles[i].NameString.ToString()
+ : nacpData.Value.Title[i].NameString.ToString();
+ break;
+ }
}
}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs
index d6e492317..84288917f 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs
@@ -5,6 +5,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
using Ryujinx.Horizon.Common;
namespace Ryujinx.HLE.Loaders.Processes
@@ -54,16 +55,33 @@ namespace Ryujinx.HLE.Loaders.Processes
{
ulong programId = metaLoader.ProgramId;
- Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString();
+ int langIdx = (int)titleLanguage;
+ var decompressedTitles = NacpHelper.IsCompressed(in ApplicationControlProperties)
+ ? NacpHelper.DecompressTitleEntries(in ApplicationControlProperties) : null;
+ int titleCount = decompressedTitles?.Length ?? 16;
+
+ if (langIdx < titleCount)
+ {
+ Name = decompressedTitles != null
+ ? decompressedTitles[langIdx].NameString.ToString()
+ : ApplicationControlProperties.Title[langIdx].NameString.ToString();
+ }
if (string.IsNullOrWhiteSpace(Name))
{
- foreach (ApplicationControlProperty.ApplicationTitle appTitle in ApplicationControlProperties.Title)
+ for (int i = 0; i < titleCount; i++)
{
- if (appTitle.Name[0] != 0)
- continue;
+ bool empty = decompressedTitles != null
+ ? decompressedTitles[i].Name[0] == 0
+ : ApplicationControlProperties.Title[i].Name[0] == 0;
- Name = appTitle.NameString.ToString();
+ if (!empty)
+ {
+ Name = decompressedTitles != null
+ ? decompressedTitles[i].NameString.ToString()
+ : ApplicationControlProperties.Title[i].NameString.ToString();
+ break;
+ }
}
}
diff --git a/src/Ryujinx.HLE/Utilities/NacpHelper.cs b/src/Ryujinx.HLE/Utilities/NacpHelper.cs
new file mode 100644
index 000000000..a88ec2bce
--- /dev/null
+++ b/src/Ryujinx.HLE/Utilities/NacpHelper.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.IO.Compression;
+using System.Runtime.InteropServices;
+using LibHac.Ns;
+
+namespace Ryujinx.HLE.Utilities
+{
+ public static class NacpHelper
+ {
+ private const int CompressedTitleCount = 32;
+ private const int UncompressedTitleCount = 16;
+ private const int TitleEntrySize = 0x300;
+ private const int TitleCompressionByteIndex = 1; // offset within Reserved3214
+
+ public static bool IsCompressed(ref readonly ApplicationControlProperty nacp)
+ => nacp.Reserved3214[TitleCompressionByteIndex] == 1;
+
+ public static int GetTitleCount(ref readonly ApplicationControlProperty nacp)
+ => IsCompressed(in nacp) ? CompressedTitleCount : UncompressedTitleCount;
+
+ ///
+ /// Decompresses a zlib-compressed NACP title block into 32 title entries.
+ /// Only call this when returns true.
+ ///
+ public static ApplicationControlProperty.ApplicationTitle[] DecompressTitleEntries(
+ ref readonly ApplicationControlProperty nacp)
+ {
+ ReadOnlySpan titleBytes = MemoryMarshal.AsBytes(
+ (ReadOnlySpan)nacp.Title);
+
+ ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
+ ReadOnlySpan compressedBlob = titleBytes.Slice(2, compressedBlobSize);
+
+ byte[] decompressed = new byte[CompressedTitleCount * TitleEntrySize];
+
+ using (var compressedStream = new MemoryStream(compressedBlob.ToArray()))
+ using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
+ {
+ int totalRead = 0;
+ while (totalRead < decompressed.Length)
+ {
+ int read = deflateStream.Read(decompressed, totalRead, decompressed.Length - totalRead);
+ if (read == 0)
+ break;
+ totalRead += read;
+ }
+ }
+
+ var result = new ApplicationControlProperty.ApplicationTitle[CompressedTitleCount];
+ for (int i = 0; i < CompressedTitleCount; i++)
+ {
+ result[i] = MemoryMarshal.Read(
+ decompressed.AsSpan(i * TitleEntrySize, TitleEntrySize));
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs
index 7640967c6..a58f1bae5 100644
--- a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs
+++ b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs
@@ -58,7 +58,10 @@ namespace Ryujinx.Horizon.Sdk.Ns
public RepairFlagValue RepairFlag;
public byte ProgramIndex;
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
- public Array4 Reserved3214;
+ public byte ApplicationErrorCodePrefix;
+ public TitleCompressionValue TitleCompression;
+ public byte AcdIndex;
+ public byte ApparentPlatform;
public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
public ApplicationJitConfiguration JitConfiguration;
public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
@@ -130,6 +133,8 @@ namespace Ryujinx.Horizon.Sdk.Ns
TraditionalChinese = 13,
SimplifiedChinese = 14,
BrazilianPortuguese = 15,
+ Polish = 16,
+ Thai = 17,
}
public enum Organization
@@ -302,5 +307,11 @@ namespace Ryujinx.Horizon.Sdk.Ns
Deny = 0,
Allow = 1,
}
+
+ public enum TitleCompressionValue : byte
+ {
+ Disable = 0,
+ Enable = 1,
+ }
}
}
diff --git a/src/Ryujinx/Headless/Windows/WindowBase.cs b/src/Ryujinx/Headless/Windows/WindowBase.cs
index 49b2a389a..f97b56ae5 100644
--- a/src/Ryujinx/Headless/Windows/WindowBase.cs
+++ b/src/Ryujinx/Headless/Windows/WindowBase.cs
@@ -167,7 +167,17 @@ namespace Ryujinx.Headless
ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage;
- string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}";
+ string titleName;
+ if (Ryujinx.HLE.Utilities.NacpHelper.IsCompressed(in nacp))
+ {
+ var decompressed = Ryujinx.HLE.Utilities.NacpHelper.DecompressTitleEntries(in nacp);
+ titleName = desiredLanguage < decompressed.Length ? decompressed[desiredLanguage].NameString.ToString() : string.Empty;
+ }
+ else
+ {
+ titleName = desiredLanguage < 16 ? nacp.Title[desiredLanguage].NameString.ToString() : string.Empty;
+ }
+ string titleNameSection = string.IsNullOrWhiteSpace(titleName) ? string.Empty : $" - {titleName}";
string titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs
index 2831802fe..275f06a1f 100644
--- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs
+++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs
@@ -22,9 +22,9 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
+using Ryujinx.HLE.Utilities;
using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.HLE.Loaders.Processes.Extensions;
-using Ryujinx.HLE.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
@@ -440,8 +440,10 @@ namespace Ryujinx.Ava.Systems.AppLibrary
GetApplicationInformation(ref controlHolder.Value, ref application);
- if ("Switch Verification" == controlHolder.Value
- .Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString())
+ string verifyName = NacpHelper.IsCompressed(in controlHolder.Value)
+ ? NacpHelper.DecompressTitleEntries(in controlHolder.Value)[(int)TitleLanguage.AmericanEnglish].NameString.ToString()
+ : controlHolder.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString();
+ if ("Switch Verification" == verifyName)
return false;
}
else
@@ -1391,10 +1393,23 @@ namespace Ryujinx.Ava.Systems.AppLibrary
{
_ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
- if (controlData.Title.Length > (int)desiredTitleLanguage)
+ int langIdx = (int)desiredTitleLanguage;
+ var decompressedTitles = NacpHelper.IsCompressed(in controlData)
+ ? NacpHelper.DecompressTitleEntries(in controlData) : null;
+ int titleCount = decompressedTitles?.Length ?? 16;
+
+ if (langIdx < titleCount)
{
- data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
- data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
+ if (decompressedTitles != null)
+ {
+ data.Name = decompressedTitles[langIdx].NameString.ToString();
+ data.Developer = decompressedTitles[langIdx].PublisherString.ToString();
+ }
+ else
+ {
+ data.Name = controlData.Title[langIdx].NameString.ToString();
+ data.Developer = controlData.Title[langIdx].PublisherString.ToString();
+ }
}
else
{
@@ -1404,11 +1419,17 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Name))
{
- foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
+ for (int i = 0; i < titleCount; i++)
{
- if (!controlTitle.NameString.IsEmpty())
+ bool empty = decompressedTitles != null
+ ? decompressedTitles[i].NameString.IsEmpty()
+ : controlData.Title[i].NameString.IsEmpty();
+
+ if (!empty)
{
- data.Name = controlTitle.NameString.ToString();
+ data.Name = decompressedTitles != null
+ ? decompressedTitles[i].NameString.ToString()
+ : controlData.Title[i].NameString.ToString();
break;
}
@@ -1417,11 +1438,17 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Developer))
{
- foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title)
+ for (int i = 0; i < titleCount; i++)
{
- if (!controlTitle.PublisherString.IsEmpty())
+ bool empty = decompressedTitles != null
+ ? decompressedTitles[i].PublisherString.IsEmpty()
+ : controlData.Title[i].PublisherString.IsEmpty();
+
+ if (!empty)
{
- data.Developer = controlTitle.PublisherString.ToString();
+ data.Developer = decompressedTitles != null
+ ? decompressedTitles[i].PublisherString.ToString()
+ : controlData.Title[i].PublisherString.ToString();
break;
}
diff --git a/src/Ryujinx/Systems/Configuration/System/Language.cs b/src/Ryujinx/Systems/Configuration/System/Language.cs
index dd44dff37..4cf91feb6 100644
--- a/src/Ryujinx/Systems/Configuration/System/Language.cs
+++ b/src/Ryujinx/Systems/Configuration/System/Language.cs
@@ -24,6 +24,8 @@ namespace Ryujinx.Ava.Systems.Configuration.System
SimplifiedChinese,
TraditionalChinese,
BrazilianPortuguese,
+ Polish,
+ Thai,
}
public static class LanguageEnumHelper
diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
index f35d72b6f..c05aca629 100644
--- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
+++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs
@@ -168,7 +168,18 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!ViewModel.IsGameRunning)
return;
- string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString();
+ int langIdx = (int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage;
+ ref readonly ApplicationControlProperty nacp = ref ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties;
+ string name;
+ if (Ryujinx.HLE.Utilities.NacpHelper.IsCompressed(in nacp))
+ {
+ var decompressed = Ryujinx.HLE.Utilities.NacpHelper.DecompressTitleEntries(in nacp);
+ name = langIdx < decompressed.Length ? decompressed[langIdx].NameString.ToString() : string.Empty;
+ }
+ else
+ {
+ name = langIdx < 16 ? nacp.Title[langIdx].NameString.ToString() : string.Empty;
+ }
await StyleableAppWindow.ShowAsync(
new CheatWindow(