From 76bb4ae2d0909b449e7bc8536341bffefa5e16e4 Mon Sep 17 00:00:00 2001 From: Mythrax Date: Sat, 21 Feb 2026 15:39:50 +1000 Subject: [PATCH 1/8] nacp: add support for zlib-compressed title blocks Introduced with TLoZ BotW 1.9.0, a compression flag determines whether the first 0x3000 bytes of the NACP title block contain a zlib-compressed blob that decompresses to 0x6000 bytes with up to 32 language entries. Added Polish and Thai language support (indexes 16/17), NacpHelper decompression utility, and updated all title-reading call sites to use resolved entries. --- .../HOS/SystemState/SystemLanguage.cs | 2 + .../HOS/SystemState/SystemStateMgr.cs | 4 +- .../HOS/SystemState/TitleLanguage.cs | 2 + .../Extensions/FileSystemExtensions.cs | 12 +++- .../Loaders/Processes/ProcessLoader.cs | 15 +++-- .../Loaders/Processes/ProcessResult.cs | 11 ++- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 67 +++++++++++++++++++ .../Sdk/Ns/ApplicationControlProperty.cs | 13 +++- src/Ryujinx/Headless/Windows/WindowBase.cs | 4 +- .../Systems/AppLibrary/ApplicationLibrary.cs | 21 +++--- .../Systems/Configuration/System/Language.cs | 2 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 5 +- 12 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 src/Ryujinx.HLE/Utilities/NacpHelper.cs 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..2d822a87e 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,11 +87,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions if (!isHomebrew && programId > 0x010000000000FFFF) { - programName = nacpData.Value.Title[(int)device.System.State.DesiredTitleLanguage].NameString.ToString(); + ApplicationControlProperty.ApplicationTitle[] titles = + NacpHelper.GetTitleEntries(in nacpData.Value); + + int langIdx = (int)device.System.State.DesiredTitleLanguage; + if (langIdx < titles.Length) + { + programName = titles[langIdx].NameString.ToString(); + } if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in nacpData.Value.Title) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 48b5b724c..d34b7fddc 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -194,15 +194,22 @@ namespace Ryujinx.HLE.Loaders.Processes { nacpStorage.Read(0, nacpData.ByteSpan); - programName = nacpData.Value.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString(); + ApplicationControlProperty.ApplicationTitle[] titles = + Utilities.NacpHelper.GetTitleEntries(in nacpData.Value); - if ("Switch Verification" == - nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) + int langIdx = (int)_device.System.State.DesiredTitleLanguage; + if (langIdx < titles.Length) + { + programName = titles[langIdx].NameString.ToString(); + } + + if ((int)TitleLanguage.AmericanEnglish < titles.Length && + "Switch Verification" == titles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) throw new InvalidOperationException(); if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in nacpData.Value.Title) + foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in titles) { if (nacpTitles.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index d6e492317..7782b0a51 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,11 +55,17 @@ namespace Ryujinx.HLE.Loaders.Processes { ulong programId = metaLoader.ProgramId; - Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); + ApplicationControlProperty.ApplicationTitle[] titles = + NacpHelper.GetTitleEntries(in ApplicationControlProperties); + + if ((int)titleLanguage < titles.Length) + { + Name = titles[(int)titleLanguage].NameString.ToString(); + } if (string.IsNullOrWhiteSpace(Name)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in ApplicationControlProperties.Title) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Utilities/NacpHelper.cs b/src/Ryujinx.HLE/Utilities/NacpHelper.cs new file mode 100644 index 000000000..bbcf12708 --- /dev/null +++ b/src/Ryujinx.HLE/Utilities/NacpHelper.cs @@ -0,0 +1,67 @@ +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 UncompressedTitleCount = 16; + private const int CompressedTitleCount = 32; + private const int TitleEntrySize = 0x300; + private const int TitleCompressionByteIndex = 1; // offset within Reserved3214 + + /// + /// Returns the resolved title entries from an NACP. When the title block is zlib-compressed + /// (indicated by a compression flag at NACP offset 0x3215), the first 0x3000 bytes are treated + /// as a raw-deflate compressed blob that decompresses to 0x6000 bytes containing up to 32 title + /// entries. Otherwise the 16 uncompressed entries are returned directly. + /// + public static ApplicationControlProperty.ApplicationTitle[] GetTitleEntries( + ref readonly ApplicationControlProperty nacp) + { + byte titleCompression = nacp.Reserved3214[TitleCompressionByteIndex]; + + if (titleCompression != 1) + { + var titles = new ApplicationControlProperty.ApplicationTitle[UncompressedTitleCount]; + for (int i = 0; i < UncompressedTitleCount; i++) + titles[i] = nacp.Title[i]; + return titles; + } + + 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..062589179 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -167,7 +167,9 @@ 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()}"; + ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries(in nacp); + string titleName = desiredLanguage < titles.Length ? titles[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..b734529bc 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()) + ApplicationControlProperty.ApplicationTitle[] verifyTitles = + NacpHelper.GetTitleEntries(in controlHolder.Value); + if ((int)TitleLanguage.AmericanEnglish < verifyTitles.Length && + "Switch Verification" == verifyTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) return false; } else @@ -1391,10 +1393,13 @@ namespace Ryujinx.Ava.Systems.AppLibrary { _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); - if (controlData.Title.Length > (int)desiredTitleLanguage) + ApplicationControlProperty.ApplicationTitle[] titles = + NacpHelper.GetTitleEntries(in controlData); + + if (titles.Length > (int)desiredTitleLanguage) { - data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + data.Name = titles[(int)desiredTitleLanguage].NameString.ToString(); + data.Developer = titles[(int)desiredTitleLanguage].PublisherString.ToString(); } else { @@ -1404,7 +1409,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Name)) { - foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) + foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) { if (!controlTitle.NameString.IsEmpty()) { @@ -1417,7 +1422,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Developer)) { - foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) + foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) { if (!controlTitle.PublisherString.IsEmpty()) { 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..77ba9969f 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -168,7 +168,10 @@ 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(); + ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries( + in ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties); + int langIdx = (int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage; + string name = langIdx < titles.Length ? titles[langIdx].NameString.ToString() : string.Empty; await StyleableAppWindow.ShowAsync( new CheatWindow( From 21c84439b3409fee3d455df07fca30b1a8ed7485 Mon Sep 17 00:00:00 2001 From: Mythrax Date: Sat, 21 Feb 2026 15:39:50 +1000 Subject: [PATCH 2/8] nacp: add support for zlib-compressed title blocks Introduced with TLoZ BotW 1.9.0, a compression flag determines whether the first 0x3000 bytes of the NACP title block contain a zlib-compressed blob that decompresses to 0x6000 bytes with up to 32 language entries. Added Polish and Thai language support (indexes 16/17), NacpHelper decompression utility, and updated all title-reading call sites to use resolved entries. --- .../HOS/SystemState/SystemLanguage.cs | 2 + .../HOS/SystemState/SystemStateMgr.cs | 4 +- .../HOS/SystemState/TitleLanguage.cs | 2 + .../Extensions/FileSystemExtensions.cs | 28 +++++++-- .../Loaders/Processes/ProcessLoader.cs | 33 +++++++--- .../Loaders/Processes/ProcessResult.cs | 28 +++++++-- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 61 +++++++++++++++++++ .../Sdk/Ns/ApplicationControlProperty.cs | 13 +++- src/Ryujinx/Headless/Windows/WindowBase.cs | 12 +++- .../Systems/AppLibrary/ApplicationLibrary.cs | 51 ++++++++++++---- .../Systems/Configuration/System/Language.cs | 2 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 13 +++- 12 files changed, 216 insertions(+), 33 deletions(-) create mode 100644 src/Ryujinx.HLE/Utilities/NacpHelper.cs 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( From ba81c3131e6dac5d89952b8875e6c84dc1fdc48d Mon Sep 17 00:00:00 2001 From: Mythrax Date: Sat, 21 Feb 2026 16:10:31 +1000 Subject: [PATCH 3/8] Revert "nacp: add support for zlib-compressed title blocks" This reverts commit 76bb4ae2d0909b449e7bc8536341bffefa5e16e4. --- .../HOS/SystemState/SystemLanguage.cs | 2 - .../HOS/SystemState/SystemStateMgr.cs | 4 +- .../HOS/SystemState/TitleLanguage.cs | 2 - .../Extensions/FileSystemExtensions.cs | 12 +--- .../Loaders/Processes/ProcessLoader.cs | 15 ++--- .../Loaders/Processes/ProcessResult.cs | 11 +-- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 67 ------------------- .../Sdk/Ns/ApplicationControlProperty.cs | 13 +--- src/Ryujinx/Headless/Windows/WindowBase.cs | 4 +- .../Systems/AppLibrary/ApplicationLibrary.cs | 21 +++--- .../Systems/Configuration/System/Language.cs | 2 - .../UI/Views/Main/MainMenuBarView.axaml.cs | 5 +- 12 files changed, 20 insertions(+), 138 deletions(-) delete mode 100644 src/Ryujinx.HLE/Utilities/NacpHelper.cs diff --git a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs index 8e7a44005..f5b7fc0f1 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs @@ -20,7 +20,5 @@ 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 74378b153..91277232c 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs @@ -23,9 +23,7 @@ namespace Ryujinx.HLE.HOS.SystemState "es-419", "zh-Hans", "zh-Hant", - "pt-BR", - "pl", - "th" + "pt-BR" ]; 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 e651410ce..e3bfb9165 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs @@ -18,7 +18,5 @@ 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 2d822a87e..7373e2f45 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -8,7 +8,6 @@ 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; @@ -87,18 +86,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions if (!isHomebrew && programId > 0x010000000000FFFF) { - ApplicationControlProperty.ApplicationTitle[] titles = - NacpHelper.GetTitleEntries(in nacpData.Value); - - int langIdx = (int)device.System.State.DesiredTitleLanguage; - if (langIdx < titles.Length) - { - programName = titles[langIdx].NameString.ToString(); - } + programName = nacpData.Value.Title[(int)device.System.State.DesiredTitleLanguage].NameString.ToString(); if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in nacpData.Value.Title) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index d34b7fddc..48b5b724c 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -194,22 +194,15 @@ namespace Ryujinx.HLE.Loaders.Processes { nacpStorage.Read(0, nacpData.ByteSpan); - ApplicationControlProperty.ApplicationTitle[] titles = - Utilities.NacpHelper.GetTitleEntries(in nacpData.Value); + programName = nacpData.Value.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString(); - int langIdx = (int)_device.System.State.DesiredTitleLanguage; - if (langIdx < titles.Length) - { - programName = titles[langIdx].NameString.ToString(); - } - - if ((int)TitleLanguage.AmericanEnglish < titles.Length && - "Switch Verification" == titles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) + if ("Switch Verification" == + nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) throw new InvalidOperationException(); if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in titles) + foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in nacpData.Value.Title) { if (nacpTitles.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 7782b0a51..d6e492317 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -5,7 +5,6 @@ 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 @@ -55,17 +54,11 @@ namespace Ryujinx.HLE.Loaders.Processes { ulong programId = metaLoader.ProgramId; - ApplicationControlProperty.ApplicationTitle[] titles = - NacpHelper.GetTitleEntries(in ApplicationControlProperties); - - if ((int)titleLanguage < titles.Length) - { - Name = titles[(int)titleLanguage].NameString.ToString(); - } + Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); if (string.IsNullOrWhiteSpace(Name)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in ApplicationControlProperties.Title) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Utilities/NacpHelper.cs b/src/Ryujinx.HLE/Utilities/NacpHelper.cs deleted file mode 100644 index bbcf12708..000000000 --- a/src/Ryujinx.HLE/Utilities/NacpHelper.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 UncompressedTitleCount = 16; - private const int CompressedTitleCount = 32; - private const int TitleEntrySize = 0x300; - private const int TitleCompressionByteIndex = 1; // offset within Reserved3214 - - /// - /// Returns the resolved title entries from an NACP. When the title block is zlib-compressed - /// (indicated by a compression flag at NACP offset 0x3215), the first 0x3000 bytes are treated - /// as a raw-deflate compressed blob that decompresses to 0x6000 bytes containing up to 32 title - /// entries. Otherwise the 16 uncompressed entries are returned directly. - /// - public static ApplicationControlProperty.ApplicationTitle[] GetTitleEntries( - ref readonly ApplicationControlProperty nacp) - { - byte titleCompression = nacp.Reserved3214[TitleCompressionByteIndex]; - - if (titleCompression != 1) - { - var titles = new ApplicationControlProperty.ApplicationTitle[UncompressedTitleCount]; - for (int i = 0; i < UncompressedTitleCount; i++) - titles[i] = nacp.Title[i]; - return titles; - } - - 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 a58f1bae5..7640967c6 100644 --- a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs +++ b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs @@ -58,10 +58,7 @@ namespace Ryujinx.Horizon.Sdk.Ns public RepairFlagValue RepairFlag; public byte ProgramIndex; public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag; - public byte ApplicationErrorCodePrefix; - public TitleCompressionValue TitleCompression; - public byte AcdIndex; - public byte ApparentPlatform; + public Array4 Reserved3214; public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration; public ApplicationJitConfiguration JitConfiguration; public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors; @@ -133,8 +130,6 @@ namespace Ryujinx.Horizon.Sdk.Ns TraditionalChinese = 13, SimplifiedChinese = 14, BrazilianPortuguese = 15, - Polish = 16, - Thai = 17, } public enum Organization @@ -307,11 +302,5 @@ 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 062589179..49b2a389a 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -167,9 +167,7 @@ namespace Ryujinx.Headless ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties; int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage; - ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries(in nacp); - string titleName = desiredLanguage < titles.Length ? titles[desiredLanguage].NameString.ToString() : string.Empty; - string titleNameSection = string.IsNullOrWhiteSpace(titleName) ? string.Empty : $" - {titleName}"; + string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}"; 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 b734529bc..2831802fe 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,10 +440,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary GetApplicationInformation(ref controlHolder.Value, ref application); - ApplicationControlProperty.ApplicationTitle[] verifyTitles = - NacpHelper.GetTitleEntries(in controlHolder.Value); - if ((int)TitleLanguage.AmericanEnglish < verifyTitles.Length && - "Switch Verification" == verifyTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) + if ("Switch Verification" == controlHolder.Value + .Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) return false; } else @@ -1393,13 +1391,10 @@ namespace Ryujinx.Ava.Systems.AppLibrary { _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); - ApplicationControlProperty.ApplicationTitle[] titles = - NacpHelper.GetTitleEntries(in controlData); - - if (titles.Length > (int)desiredTitleLanguage) + if (controlData.Title.Length > (int)desiredTitleLanguage) { - data.Name = titles[(int)desiredTitleLanguage].NameString.ToString(); - data.Developer = titles[(int)desiredTitleLanguage].PublisherString.ToString(); + data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { @@ -1409,7 +1404,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Name)) { - foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) + foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) { if (!controlTitle.NameString.IsEmpty()) { @@ -1422,7 +1417,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Developer)) { - foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) + foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) { if (!controlTitle.PublisherString.IsEmpty()) { diff --git a/src/Ryujinx/Systems/Configuration/System/Language.cs b/src/Ryujinx/Systems/Configuration/System/Language.cs index 4cf91feb6..dd44dff37 100644 --- a/src/Ryujinx/Systems/Configuration/System/Language.cs +++ b/src/Ryujinx/Systems/Configuration/System/Language.cs @@ -24,8 +24,6 @@ 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 77ba9969f..f35d72b6f 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -168,10 +168,7 @@ namespace Ryujinx.Ava.UI.Views.Main if (!ViewModel.IsGameRunning) return; - ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries( - in ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties); - int langIdx = (int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage; - string name = langIdx < titles.Length ? titles[langIdx].NameString.ToString() : string.Empty; + string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString(); await StyleableAppWindow.ShowAsync( new CheatWindow( From f0c4e52fc207e6b82e3c827d173512b2f663df83 Mon Sep 17 00:00:00 2001 From: Mythrax Date: Tue, 24 Feb 2026 19:46:51 +1000 Subject: [PATCH 4/8] nacp: centralize title logic in ApplicationControlProperty.Title property --- .../Extensions/FileSystemExtensions.cs | 19 ++---- .../Loaders/Processes/ProcessLoader.cs | 23 ++----- .../Loaders/Processes/ProcessResult.cs | 19 ++---- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 65 +++++-------------- .../Sdk/Ns/ApplicationControlProperty.cs | 56 +++++++++++++++- src/Ryujinx/Headless/Windows/WindowBase.cs | 13 +--- .../Systems/AppLibrary/ApplicationLibrary.cs | 43 +++--------- .../UI/Views/Main/MainMenuBarView.axaml.cs | 12 +--- 8 files changed, 100 insertions(+), 150 deletions(-) diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs index e3a2ae69e..4324e6dbd 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -88,30 +88,21 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions if (!isHomebrew && programId > 0x010000000000FFFF) { 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; + var titles = NacpHelper.GetTitles(in nacpData.Value); + int titleCount = titles.Length; if (langIdx < titleCount) { - programName = decompressedTitles != null - ? decompressedTitles[langIdx].NameString.ToString() - : nacpData.Value.Title[langIdx].NameString.ToString(); + programName = titles[langIdx].NameString.ToString(); } if (string.IsNullOrWhiteSpace(programName)) { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].Name[0] == 0 - : nacpData.Value.Title[i].Name[0] == 0; - - if (!empty) + if (titles[i].Name[0] != 0) { - programName = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : nacpData.Value.Title[i].NameString.ToString(); + programName = titles[i].NameString.ToString(); break; } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 513eacac4..d2f954332 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -195,20 +195,15 @@ namespace Ryujinx.HLE.Loaders.Processes nacpStorage.Read(0, nacpData.ByteSpan); 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; + var titles = Utilities.NacpHelper.GetTitles(in nacpData.Value); + int titleCount = titles.Length; if (langIdx < titleCount) { - programName = decompressedTitles != null - ? decompressedTitles[langIdx].NameString.ToString() - : nacpData.Value.Title[langIdx].NameString.ToString(); + programName = titles[langIdx].NameString.ToString(); } - string verifyName = decompressedTitles != null - ? decompressedTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString() - : nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString(); + string verifyName = titles[(int)TitleLanguage.AmericanEnglish].NameString.ToString(); if ("Switch Verification" == verifyName) throw new InvalidOperationException(); @@ -216,15 +211,9 @@ namespace Ryujinx.HLE.Loaders.Processes { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].Name[0] == 0 - : nacpData.Value.Title[i].Name[0] == 0; - - if (!empty) + if (titles[i].Name[0] != 0) { - programName = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : nacpData.Value.Title[i].NameString.ToString(); + programName = titles[i].NameString.ToString(); break; } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 84288917f..b5f959f25 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -56,30 +56,21 @@ namespace Ryujinx.HLE.Loaders.Processes ulong programId = metaLoader.ProgramId; int langIdx = (int)titleLanguage; - var decompressedTitles = NacpHelper.IsCompressed(in ApplicationControlProperties) - ? NacpHelper.DecompressTitleEntries(in ApplicationControlProperties) : null; - int titleCount = decompressedTitles?.Length ?? 16; + var titles = NacpHelper.GetTitles(in ApplicationControlProperties); + int titleCount = titles.Length; if (langIdx < titleCount) { - Name = decompressedTitles != null - ? decompressedTitles[langIdx].NameString.ToString() - : ApplicationControlProperties.Title[langIdx].NameString.ToString(); + Name = titles[langIdx].NameString.ToString(); } if (string.IsNullOrWhiteSpace(Name)) { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].Name[0] == 0 - : ApplicationControlProperties.Title[i].Name[0] == 0; - - if (!empty) + if (titles[i].Name[0] != 0) { - Name = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : ApplicationControlProperties.Title[i].NameString.ToString(); + Name = titles[i].NameString.ToString(); break; } } diff --git a/src/Ryujinx.HLE/Utilities/NacpHelper.cs b/src/Ryujinx.HLE/Utilities/NacpHelper.cs index a88ec2bce..db2341369 100644 --- a/src/Ryujinx.HLE/Utilities/NacpHelper.cs +++ b/src/Ryujinx.HLE/Utilities/NacpHelper.cs @@ -1,61 +1,28 @@ -using System; -using System.Buffers.Binary; -using System.IO; -using System.IO.Compression; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using LibHac.Ns; +using Ryujinx.Horizon.Sdk.Ns; namespace Ryujinx.HLE.Utilities { + /// + /// Bridges from LibHac's NACP struct to title resolution. All title parsing logic + /// lives on ; this helper + /// reinterprets the same bytes so call sites holding LibHac's struct can use it. + /// 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. + /// Returns the resolved title entries (16 or 32 depending on compression). + /// Uses the same binary layout as Horizon.Sdk so we delegate to its property. /// - public static ApplicationControlProperty.ApplicationTitle[] DecompressTitleEntries( - ref readonly ApplicationControlProperty nacp) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Ryujinx.Horizon.Sdk.Ns.ApplicationControlProperty.ApplicationTitle[] GetTitles( + in LibHac.Ns.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; + ref readonly var libHacRef = ref nacp; + ref var horizonRef = ref Unsafe.As( + ref Unsafe.AsRef(in libHacRef)); + return horizonRef.Title; } } } diff --git a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs index a58f1bae5..5849f5cbf 100644 --- a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs +++ b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs @@ -1,12 +1,16 @@ using Ryujinx.Common.Memory; using System; +using System.Buffers.Binary; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; using System.Text; namespace Ryujinx.Horizon.Sdk.Ns { public struct ApplicationControlProperty { - public Array16 Title; + public Array16 TitleBlock; public Array37 Isbn; public StartupUserAccountValue StartupUserAccount; public UserAccountSwitchLockValue UserAccountSwitchLock; @@ -78,6 +82,56 @@ namespace Ryujinx.Horizon.Sdk.Ns public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0'); public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0'); + private const int UncompressedTitleCount = 16; + private const int CompressedTitleCount = 32; + private const int TitleEntrySize = 0x300; + + /// + /// Returns the resolved title entries. When is + /// , the raw bytes are + /// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries. + /// Otherwise the 16 uncompressed entries from are returned directly. + /// + public readonly ApplicationTitle[] Title + { + get + { + if (TitleCompression != TitleCompressionValue.Enable) + { + var titles = new ApplicationTitle[UncompressedTitleCount]; + for (int i = 0; i < UncompressedTitleCount; i++) + titles[i] = TitleBlock[i]; + return titles; + } + + ReadOnlySpan titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan()); + 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 ApplicationTitle[CompressedTitleCount]; + for (int i = 0; i < CompressedTitleCount; i++) + result[i] = MemoryMarshal.Read( + decompressed.AsSpan(i * TitleEntrySize, TitleEntrySize)); + + return result; + } + } + public struct ApplicationTitle { public ByteArray512 Name; diff --git a/src/Ryujinx/Headless/Windows/WindowBase.cs b/src/Ryujinx/Headless/Windows/WindowBase.cs index f97b56ae5..d6f7ad21d 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -166,17 +166,8 @@ namespace Ryujinx.Headless ProcessResult activeProcess = Device.Processes.ActiveApplication; ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties; int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage; - - 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; - } + var titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitles(in nacp); + string titleName = desiredLanguage < titles.Length ? titles[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()})"; diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index 275f06a1f..6ba767f53 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -440,9 +440,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary GetApplicationInformation(ref controlHolder.Value, ref application); - 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(); + string verifyName = NacpHelper.GetTitles(in controlHolder.Value)[(int)TitleLanguage.AmericanEnglish].NameString.ToString(); if ("Switch Verification" == verifyName) return false; } @@ -1394,22 +1392,13 @@ namespace Ryujinx.Ava.Systems.AppLibrary _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); int langIdx = (int)desiredTitleLanguage; - var decompressedTitles = NacpHelper.IsCompressed(in controlData) - ? NacpHelper.DecompressTitleEntries(in controlData) : null; - int titleCount = decompressedTitles?.Length ?? 16; + var titles = NacpHelper.GetTitles(in controlData); + int titleCount = titles.Length; if (langIdx < titleCount) { - 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(); - } + data.Name = titles[langIdx].NameString.ToString(); + data.Developer = titles[langIdx].PublisherString.ToString(); } else { @@ -1421,16 +1410,9 @@ namespace Ryujinx.Ava.Systems.AppLibrary { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].NameString.IsEmpty() - : controlData.Title[i].NameString.IsEmpty(); - - if (!empty) + if (!string.IsNullOrWhiteSpace(titles[i].NameString)) { - data.Name = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : controlData.Title[i].NameString.ToString(); - + data.Name = titles[i].NameString.ToString(); break; } } @@ -1440,16 +1422,9 @@ namespace Ryujinx.Ava.Systems.AppLibrary { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].PublisherString.IsEmpty() - : controlData.Title[i].PublisherString.IsEmpty(); - - if (!empty) + if (!string.IsNullOrWhiteSpace(titles[i].PublisherString)) { - data.Developer = decompressedTitles != null - ? decompressedTitles[i].PublisherString.ToString() - : controlData.Title[i].PublisherString.ToString(); - + data.Developer = titles[i].PublisherString.ToString(); break; } } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index c05aca629..9537a11ca 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -170,16 +170,8 @@ namespace Ryujinx.Ava.UI.Views.Main 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; - } + var titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitles(in nacp); + string name = langIdx < titles.Length ? titles[langIdx].NameString.ToString() : string.Empty; await StyleableAppWindow.ShowAsync( new CheatWindow( From 6a7b904cd051755ea83015ccf6c2ea5933efaf48 Mon Sep 17 00:00:00 2001 From: Mythrax Date: Sat, 21 Feb 2026 15:39:50 +1000 Subject: [PATCH 5/8] nacp: add support for zlib-compressed title blocks Introduced with TLoZ BotW 1.9.0, a compression flag determines whether the first 0x3000 bytes of the NACP title block contain a zlib-compressed blob that decompresses to 0x6000 bytes with up to 32 language entries. Added Polish and Thai language support (indexes 16/17), NacpHelper decompression utility, and updated all title-reading call sites to use resolved entries. --- .../HOS/SystemState/SystemLanguage.cs | 2 + .../HOS/SystemState/SystemStateMgr.cs | 4 +- .../HOS/SystemState/TitleLanguage.cs | 2 + .../Extensions/FileSystemExtensions.cs | 12 +++- .../Loaders/Processes/ProcessLoader.cs | 15 +++-- .../Loaders/Processes/ProcessResult.cs | 11 ++- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 67 +++++++++++++++++++ .../Sdk/Ns/ApplicationControlProperty.cs | 13 +++- src/Ryujinx/Headless/Windows/WindowBase.cs | 4 +- .../Systems/AppLibrary/ApplicationLibrary.cs | 21 +++--- .../Systems/Configuration/System/Language.cs | 2 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 5 +- 12 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 src/Ryujinx.HLE/Utilities/NacpHelper.cs 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..2d822a87e 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,11 +87,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions if (!isHomebrew && programId > 0x010000000000FFFF) { - programName = nacpData.Value.Title[(int)device.System.State.DesiredTitleLanguage].NameString.ToString(); + ApplicationControlProperty.ApplicationTitle[] titles = + NacpHelper.GetTitleEntries(in nacpData.Value); + + int langIdx = (int)device.System.State.DesiredTitleLanguage; + if (langIdx < titles.Length) + { + programName = titles[langIdx].NameString.ToString(); + } if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in nacpData.Value.Title) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 48b5b724c..d34b7fddc 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -194,15 +194,22 @@ namespace Ryujinx.HLE.Loaders.Processes { nacpStorage.Read(0, nacpData.ByteSpan); - programName = nacpData.Value.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString(); + ApplicationControlProperty.ApplicationTitle[] titles = + Utilities.NacpHelper.GetTitleEntries(in nacpData.Value); - if ("Switch Verification" == - nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) + int langIdx = (int)_device.System.State.DesiredTitleLanguage; + if (langIdx < titles.Length) + { + programName = titles[langIdx].NameString.ToString(); + } + + if ((int)TitleLanguage.AmericanEnglish < titles.Length && + "Switch Verification" == titles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) throw new InvalidOperationException(); if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in nacpData.Value.Title) + foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in titles) { if (nacpTitles.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index d6e492317..7782b0a51 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,11 +55,17 @@ namespace Ryujinx.HLE.Loaders.Processes { ulong programId = metaLoader.ProgramId; - Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); + ApplicationControlProperty.ApplicationTitle[] titles = + NacpHelper.GetTitleEntries(in ApplicationControlProperties); + + if ((int)titleLanguage < titles.Length) + { + Name = titles[(int)titleLanguage].NameString.ToString(); + } if (string.IsNullOrWhiteSpace(Name)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in ApplicationControlProperties.Title) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Utilities/NacpHelper.cs b/src/Ryujinx.HLE/Utilities/NacpHelper.cs new file mode 100644 index 000000000..bbcf12708 --- /dev/null +++ b/src/Ryujinx.HLE/Utilities/NacpHelper.cs @@ -0,0 +1,67 @@ +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 UncompressedTitleCount = 16; + private const int CompressedTitleCount = 32; + private const int TitleEntrySize = 0x300; + private const int TitleCompressionByteIndex = 1; // offset within Reserved3214 + + /// + /// Returns the resolved title entries from an NACP. When the title block is zlib-compressed + /// (indicated by a compression flag at NACP offset 0x3215), the first 0x3000 bytes are treated + /// as a raw-deflate compressed blob that decompresses to 0x6000 bytes containing up to 32 title + /// entries. Otherwise the 16 uncompressed entries are returned directly. + /// + public static ApplicationControlProperty.ApplicationTitle[] GetTitleEntries( + ref readonly ApplicationControlProperty nacp) + { + byte titleCompression = nacp.Reserved3214[TitleCompressionByteIndex]; + + if (titleCompression != 1) + { + var titles = new ApplicationControlProperty.ApplicationTitle[UncompressedTitleCount]; + for (int i = 0; i < UncompressedTitleCount; i++) + titles[i] = nacp.Title[i]; + return titles; + } + + 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..062589179 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -167,7 +167,9 @@ 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()}"; + ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries(in nacp); + string titleName = desiredLanguage < titles.Length ? titles[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..b734529bc 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()) + ApplicationControlProperty.ApplicationTitle[] verifyTitles = + NacpHelper.GetTitleEntries(in controlHolder.Value); + if ((int)TitleLanguage.AmericanEnglish < verifyTitles.Length && + "Switch Verification" == verifyTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) return false; } else @@ -1391,10 +1393,13 @@ namespace Ryujinx.Ava.Systems.AppLibrary { _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); - if (controlData.Title.Length > (int)desiredTitleLanguage) + ApplicationControlProperty.ApplicationTitle[] titles = + NacpHelper.GetTitleEntries(in controlData); + + if (titles.Length > (int)desiredTitleLanguage) { - data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); - data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); + data.Name = titles[(int)desiredTitleLanguage].NameString.ToString(); + data.Developer = titles[(int)desiredTitleLanguage].PublisherString.ToString(); } else { @@ -1404,7 +1409,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Name)) { - foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) + foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) { if (!controlTitle.NameString.IsEmpty()) { @@ -1417,7 +1422,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Developer)) { - foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) + foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) { if (!controlTitle.PublisherString.IsEmpty()) { 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 cb325f0b6..fbb212b4a 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -168,7 +168,10 @@ 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(); + ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries( + in ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties); + int langIdx = (int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage; + string name = langIdx < titles.Length ? titles[langIdx].NameString.ToString() : string.Empty; await StyleableAppWindow.ShowAsync( new CheatWindow( From 81f346f15f1968dbc366e2529dece03f7686eaf3 Mon Sep 17 00:00:00 2001 From: Mythrax Date: Sat, 21 Feb 2026 16:10:31 +1000 Subject: [PATCH 6/8] Revert "nacp: add support for zlib-compressed title blocks" This reverts commit 76bb4ae2d0909b449e7bc8536341bffefa5e16e4. --- .../HOS/SystemState/SystemLanguage.cs | 2 - .../HOS/SystemState/SystemStateMgr.cs | 4 +- .../HOS/SystemState/TitleLanguage.cs | 2 - .../Extensions/FileSystemExtensions.cs | 12 +--- .../Loaders/Processes/ProcessLoader.cs | 15 ++--- .../Loaders/Processes/ProcessResult.cs | 11 +-- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 67 ------------------- .../Sdk/Ns/ApplicationControlProperty.cs | 13 +--- src/Ryujinx/Headless/Windows/WindowBase.cs | 4 +- .../Systems/AppLibrary/ApplicationLibrary.cs | 21 +++--- .../Systems/Configuration/System/Language.cs | 2 - .../UI/Views/Main/MainMenuBarView.axaml.cs | 5 +- 12 files changed, 20 insertions(+), 138 deletions(-) delete mode 100644 src/Ryujinx.HLE/Utilities/NacpHelper.cs diff --git a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs index 8e7a44005..f5b7fc0f1 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/SystemLanguage.cs @@ -20,7 +20,5 @@ 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 74378b153..91277232c 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/SystemStateMgr.cs @@ -23,9 +23,7 @@ namespace Ryujinx.HLE.HOS.SystemState "es-419", "zh-Hans", "zh-Hant", - "pt-BR", - "pl", - "th" + "pt-BR" ]; 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 e651410ce..e3bfb9165 100644 --- a/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs +++ b/src/Ryujinx.HLE/HOS/SystemState/TitleLanguage.cs @@ -18,7 +18,5 @@ 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 2d822a87e..7373e2f45 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -8,7 +8,6 @@ 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; @@ -87,18 +86,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions if (!isHomebrew && programId > 0x010000000000FFFF) { - ApplicationControlProperty.ApplicationTitle[] titles = - NacpHelper.GetTitleEntries(in nacpData.Value); - - int langIdx = (int)device.System.State.DesiredTitleLanguage; - if (langIdx < titles.Length) - { - programName = titles[langIdx].NameString.ToString(); - } + programName = nacpData.Value.Title[(int)device.System.State.DesiredTitleLanguage].NameString.ToString(); if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in nacpData.Value.Title) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index d34b7fddc..48b5b724c 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -194,22 +194,15 @@ namespace Ryujinx.HLE.Loaders.Processes { nacpStorage.Read(0, nacpData.ByteSpan); - ApplicationControlProperty.ApplicationTitle[] titles = - Utilities.NacpHelper.GetTitleEntries(in nacpData.Value); + programName = nacpData.Value.Title[(int)_device.System.State.DesiredTitleLanguage].NameString.ToString(); - int langIdx = (int)_device.System.State.DesiredTitleLanguage; - if (langIdx < titles.Length) - { - programName = titles[langIdx].NameString.ToString(); - } - - if ((int)TitleLanguage.AmericanEnglish < titles.Length && - "Switch Verification" == titles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) + if ("Switch Verification" == + nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) throw new InvalidOperationException(); if (string.IsNullOrWhiteSpace(programName)) { - foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in titles) + foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in nacpData.Value.Title) { if (nacpTitles.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 7782b0a51..d6e492317 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -5,7 +5,6 @@ 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 @@ -55,17 +54,11 @@ namespace Ryujinx.HLE.Loaders.Processes { ulong programId = metaLoader.ProgramId; - ApplicationControlProperty.ApplicationTitle[] titles = - NacpHelper.GetTitleEntries(in ApplicationControlProperties); - - if ((int)titleLanguage < titles.Length) - { - Name = titles[(int)titleLanguage].NameString.ToString(); - } + Name = ApplicationControlProperties.Title[(int)titleLanguage].NameString.ToString(); if (string.IsNullOrWhiteSpace(Name)) { - foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles) + foreach (ApplicationControlProperty.ApplicationTitle appTitle in ApplicationControlProperties.Title) { if (appTitle.Name[0] != 0) continue; diff --git a/src/Ryujinx.HLE/Utilities/NacpHelper.cs b/src/Ryujinx.HLE/Utilities/NacpHelper.cs deleted file mode 100644 index bbcf12708..000000000 --- a/src/Ryujinx.HLE/Utilities/NacpHelper.cs +++ /dev/null @@ -1,67 +0,0 @@ -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 UncompressedTitleCount = 16; - private const int CompressedTitleCount = 32; - private const int TitleEntrySize = 0x300; - private const int TitleCompressionByteIndex = 1; // offset within Reserved3214 - - /// - /// Returns the resolved title entries from an NACP. When the title block is zlib-compressed - /// (indicated by a compression flag at NACP offset 0x3215), the first 0x3000 bytes are treated - /// as a raw-deflate compressed blob that decompresses to 0x6000 bytes containing up to 32 title - /// entries. Otherwise the 16 uncompressed entries are returned directly. - /// - public static ApplicationControlProperty.ApplicationTitle[] GetTitleEntries( - ref readonly ApplicationControlProperty nacp) - { - byte titleCompression = nacp.Reserved3214[TitleCompressionByteIndex]; - - if (titleCompression != 1) - { - var titles = new ApplicationControlProperty.ApplicationTitle[UncompressedTitleCount]; - for (int i = 0; i < UncompressedTitleCount; i++) - titles[i] = nacp.Title[i]; - return titles; - } - - 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 a58f1bae5..7640967c6 100644 --- a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs +++ b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs @@ -58,10 +58,7 @@ namespace Ryujinx.Horizon.Sdk.Ns public RepairFlagValue RepairFlag; public byte ProgramIndex; public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag; - public byte ApplicationErrorCodePrefix; - public TitleCompressionValue TitleCompression; - public byte AcdIndex; - public byte ApparentPlatform; + public Array4 Reserved3214; public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration; public ApplicationJitConfiguration JitConfiguration; public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors; @@ -133,8 +130,6 @@ namespace Ryujinx.Horizon.Sdk.Ns TraditionalChinese = 13, SimplifiedChinese = 14, BrazilianPortuguese = 15, - Polish = 16, - Thai = 17, } public enum Organization @@ -307,11 +302,5 @@ 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 062589179..49b2a389a 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -167,9 +167,7 @@ namespace Ryujinx.Headless ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties; int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage; - ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries(in nacp); - string titleName = desiredLanguage < titles.Length ? titles[desiredLanguage].NameString.ToString() : string.Empty; - string titleNameSection = string.IsNullOrWhiteSpace(titleName) ? string.Empty : $" - {titleName}"; + string titleNameSection = string.IsNullOrWhiteSpace(nacp.Title[desiredLanguage].NameString.ToString()) ? string.Empty : $" - {nacp.Title[desiredLanguage].NameString.ToString()}"; 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 b734529bc..2831802fe 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,10 +440,8 @@ namespace Ryujinx.Ava.Systems.AppLibrary GetApplicationInformation(ref controlHolder.Value, ref application); - ApplicationControlProperty.ApplicationTitle[] verifyTitles = - NacpHelper.GetTitleEntries(in controlHolder.Value); - if ((int)TitleLanguage.AmericanEnglish < verifyTitles.Length && - "Switch Verification" == verifyTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) + if ("Switch Verification" == controlHolder.Value + .Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) return false; } else @@ -1393,13 +1391,10 @@ namespace Ryujinx.Ava.Systems.AppLibrary { _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); - ApplicationControlProperty.ApplicationTitle[] titles = - NacpHelper.GetTitleEntries(in controlData); - - if (titles.Length > (int)desiredTitleLanguage) + if (controlData.Title.Length > (int)desiredTitleLanguage) { - data.Name = titles[(int)desiredTitleLanguage].NameString.ToString(); - data.Developer = titles[(int)desiredTitleLanguage].PublisherString.ToString(); + data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString(); + data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); } else { @@ -1409,7 +1404,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Name)) { - foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) + foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) { if (!controlTitle.NameString.IsEmpty()) { @@ -1422,7 +1417,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary if (string.IsNullOrWhiteSpace(data.Developer)) { - foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles) + foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) { if (!controlTitle.PublisherString.IsEmpty()) { diff --git a/src/Ryujinx/Systems/Configuration/System/Language.cs b/src/Ryujinx/Systems/Configuration/System/Language.cs index 4cf91feb6..dd44dff37 100644 --- a/src/Ryujinx/Systems/Configuration/System/Language.cs +++ b/src/Ryujinx/Systems/Configuration/System/Language.cs @@ -24,8 +24,6 @@ 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 fbb212b4a..cb325f0b6 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -168,10 +168,7 @@ namespace Ryujinx.Ava.UI.Views.Main if (!ViewModel.IsGameRunning) return; - ApplicationControlProperty.ApplicationTitle[] titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitleEntries( - in ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties); - int langIdx = (int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage; - string name = langIdx < titles.Length ? titles[langIdx].NameString.ToString() : string.Empty; + string name = ViewModel.AppHost.Device.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)ViewModel.AppHost.Device.System.State.DesiredTitleLanguage].NameString.ToString(); await StyleableAppWindow.ShowAsync( new CheatWindow( From 772b3fb8005412470261ee6ce45a7dac0a4fbede Mon Sep 17 00:00:00 2001 From: Mythrax Date: Sat, 21 Feb 2026 15:39:50 +1000 Subject: [PATCH 7/8] nacp: add support for zlib-compressed title blocks Introduced with TLoZ BotW 1.9.0, a compression flag determines whether the first 0x3000 bytes of the NACP title block contain a zlib-compressed blob that decompresses to 0x6000 bytes with up to 32 language entries. Added Polish and Thai language support (indexes 16/17), NacpHelper decompression utility, and updated all title-reading call sites to use resolved entries. --- .../HOS/SystemState/SystemLanguage.cs | 2 + .../HOS/SystemState/SystemStateMgr.cs | 4 +- .../HOS/SystemState/TitleLanguage.cs | 2 + .../Extensions/FileSystemExtensions.cs | 28 +++++++-- .../Loaders/Processes/ProcessLoader.cs | 33 +++++++--- .../Loaders/Processes/ProcessResult.cs | 28 +++++++-- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 61 +++++++++++++++++++ .../Sdk/Ns/ApplicationControlProperty.cs | 13 +++- src/Ryujinx/Headless/Windows/WindowBase.cs | 12 +++- .../Systems/AppLibrary/ApplicationLibrary.cs | 51 ++++++++++++---- .../Systems/Configuration/System/Language.cs | 2 + .../UI/Views/Main/MainMenuBarView.axaml.cs | 13 +++- 12 files changed, 216 insertions(+), 33 deletions(-) create mode 100644 src/Ryujinx.HLE/Utilities/NacpHelper.cs 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 cb325f0b6..440e6d9bd 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( From 7ff2874baa4654b3c53f05729378171f542ccf35 Mon Sep 17 00:00:00 2001 From: Mythrax Date: Tue, 24 Feb 2026 19:46:51 +1000 Subject: [PATCH 8/8] nacp: centralize title logic in ApplicationControlProperty.Title property --- .../Extensions/FileSystemExtensions.cs | 19 ++---- .../Loaders/Processes/ProcessLoader.cs | 23 ++----- .../Loaders/Processes/ProcessResult.cs | 19 ++---- src/Ryujinx.HLE/Utilities/NacpHelper.cs | 65 +++++-------------- .../Sdk/Ns/ApplicationControlProperty.cs | 56 +++++++++++++++- src/Ryujinx/Headless/Windows/WindowBase.cs | 13 +--- .../Systems/AppLibrary/ApplicationLibrary.cs | 43 +++--------- .../UI/Views/Main/MainMenuBarView.axaml.cs | 12 +--- 8 files changed, 100 insertions(+), 150 deletions(-) diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs index e3a2ae69e..4324e6dbd 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs @@ -88,30 +88,21 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions if (!isHomebrew && programId > 0x010000000000FFFF) { 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; + var titles = NacpHelper.GetTitles(in nacpData.Value); + int titleCount = titles.Length; if (langIdx < titleCount) { - programName = decompressedTitles != null - ? decompressedTitles[langIdx].NameString.ToString() - : nacpData.Value.Title[langIdx].NameString.ToString(); + programName = titles[langIdx].NameString.ToString(); } if (string.IsNullOrWhiteSpace(programName)) { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].Name[0] == 0 - : nacpData.Value.Title[i].Name[0] == 0; - - if (!empty) + if (titles[i].Name[0] != 0) { - programName = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : nacpData.Value.Title[i].NameString.ToString(); + programName = titles[i].NameString.ToString(); break; } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index 513eacac4..d2f954332 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -195,20 +195,15 @@ namespace Ryujinx.HLE.Loaders.Processes nacpStorage.Read(0, nacpData.ByteSpan); 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; + var titles = Utilities.NacpHelper.GetTitles(in nacpData.Value); + int titleCount = titles.Length; if (langIdx < titleCount) { - programName = decompressedTitles != null - ? decompressedTitles[langIdx].NameString.ToString() - : nacpData.Value.Title[langIdx].NameString.ToString(); + programName = titles[langIdx].NameString.ToString(); } - string verifyName = decompressedTitles != null - ? decompressedTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString() - : nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString(); + string verifyName = titles[(int)TitleLanguage.AmericanEnglish].NameString.ToString(); if ("Switch Verification" == verifyName) throw new InvalidOperationException(); @@ -216,15 +211,9 @@ namespace Ryujinx.HLE.Loaders.Processes { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].Name[0] == 0 - : nacpData.Value.Title[i].Name[0] == 0; - - if (!empty) + if (titles[i].Name[0] != 0) { - programName = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : nacpData.Value.Title[i].NameString.ToString(); + programName = titles[i].NameString.ToString(); break; } } diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index 84288917f..b5f959f25 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -56,30 +56,21 @@ namespace Ryujinx.HLE.Loaders.Processes ulong programId = metaLoader.ProgramId; int langIdx = (int)titleLanguage; - var decompressedTitles = NacpHelper.IsCompressed(in ApplicationControlProperties) - ? NacpHelper.DecompressTitleEntries(in ApplicationControlProperties) : null; - int titleCount = decompressedTitles?.Length ?? 16; + var titles = NacpHelper.GetTitles(in ApplicationControlProperties); + int titleCount = titles.Length; if (langIdx < titleCount) { - Name = decompressedTitles != null - ? decompressedTitles[langIdx].NameString.ToString() - : ApplicationControlProperties.Title[langIdx].NameString.ToString(); + Name = titles[langIdx].NameString.ToString(); } if (string.IsNullOrWhiteSpace(Name)) { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].Name[0] == 0 - : ApplicationControlProperties.Title[i].Name[0] == 0; - - if (!empty) + if (titles[i].Name[0] != 0) { - Name = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : ApplicationControlProperties.Title[i].NameString.ToString(); + Name = titles[i].NameString.ToString(); break; } } diff --git a/src/Ryujinx.HLE/Utilities/NacpHelper.cs b/src/Ryujinx.HLE/Utilities/NacpHelper.cs index a88ec2bce..db2341369 100644 --- a/src/Ryujinx.HLE/Utilities/NacpHelper.cs +++ b/src/Ryujinx.HLE/Utilities/NacpHelper.cs @@ -1,61 +1,28 @@ -using System; -using System.Buffers.Binary; -using System.IO; -using System.IO.Compression; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using LibHac.Ns; +using Ryujinx.Horizon.Sdk.Ns; namespace Ryujinx.HLE.Utilities { + /// + /// Bridges from LibHac's NACP struct to title resolution. All title parsing logic + /// lives on ; this helper + /// reinterprets the same bytes so call sites holding LibHac's struct can use it. + /// 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. + /// Returns the resolved title entries (16 or 32 depending on compression). + /// Uses the same binary layout as Horizon.Sdk so we delegate to its property. /// - public static ApplicationControlProperty.ApplicationTitle[] DecompressTitleEntries( - ref readonly ApplicationControlProperty nacp) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Ryujinx.Horizon.Sdk.Ns.ApplicationControlProperty.ApplicationTitle[] GetTitles( + in LibHac.Ns.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; + ref readonly var libHacRef = ref nacp; + ref var horizonRef = ref Unsafe.As( + ref Unsafe.AsRef(in libHacRef)); + return horizonRef.Title; } } } diff --git a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs index a58f1bae5..5849f5cbf 100644 --- a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs +++ b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs @@ -1,12 +1,16 @@ using Ryujinx.Common.Memory; using System; +using System.Buffers.Binary; +using System.IO; +using System.IO.Compression; +using System.Runtime.InteropServices; using System.Text; namespace Ryujinx.Horizon.Sdk.Ns { public struct ApplicationControlProperty { - public Array16 Title; + public Array16 TitleBlock; public Array37 Isbn; public StartupUserAccountValue StartupUserAccount; public UserAccountSwitchLockValue UserAccountSwitchLock; @@ -78,6 +82,56 @@ namespace Ryujinx.Horizon.Sdk.Ns public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0'); public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0'); + private const int UncompressedTitleCount = 16; + private const int CompressedTitleCount = 32; + private const int TitleEntrySize = 0x300; + + /// + /// Returns the resolved title entries. When is + /// , the raw bytes are + /// decompressed (raw deflate) from 0x3000 into 0x6000 bytes yielding up to 32 entries. + /// Otherwise the 16 uncompressed entries from are returned directly. + /// + public readonly ApplicationTitle[] Title + { + get + { + if (TitleCompression != TitleCompressionValue.Enable) + { + var titles = new ApplicationTitle[UncompressedTitleCount]; + for (int i = 0; i < UncompressedTitleCount; i++) + titles[i] = TitleBlock[i]; + return titles; + } + + ReadOnlySpan titleBytes = MemoryMarshal.AsBytes(TitleBlock.AsSpan()); + 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 ApplicationTitle[CompressedTitleCount]; + for (int i = 0; i < CompressedTitleCount; i++) + result[i] = MemoryMarshal.Read( + decompressed.AsSpan(i * TitleEntrySize, TitleEntrySize)); + + return result; + } + } + public struct ApplicationTitle { public ByteArray512 Name; diff --git a/src/Ryujinx/Headless/Windows/WindowBase.cs b/src/Ryujinx/Headless/Windows/WindowBase.cs index f97b56ae5..d6f7ad21d 100644 --- a/src/Ryujinx/Headless/Windows/WindowBase.cs +++ b/src/Ryujinx/Headless/Windows/WindowBase.cs @@ -166,17 +166,8 @@ namespace Ryujinx.Headless ProcessResult activeProcess = Device.Processes.ActiveApplication; ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties; int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage; - - 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; - } + var titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitles(in nacp); + string titleName = desiredLanguage < titles.Length ? titles[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()})"; diff --git a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs index 275f06a1f..6ba767f53 100644 --- a/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs +++ b/src/Ryujinx/Systems/AppLibrary/ApplicationLibrary.cs @@ -440,9 +440,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary GetApplicationInformation(ref controlHolder.Value, ref application); - 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(); + string verifyName = NacpHelper.GetTitles(in controlHolder.Value)[(int)TitleLanguage.AmericanEnglish].NameString.ToString(); if ("Switch Verification" == verifyName) return false; } @@ -1394,22 +1392,13 @@ namespace Ryujinx.Ava.Systems.AppLibrary _ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); int langIdx = (int)desiredTitleLanguage; - var decompressedTitles = NacpHelper.IsCompressed(in controlData) - ? NacpHelper.DecompressTitleEntries(in controlData) : null; - int titleCount = decompressedTitles?.Length ?? 16; + var titles = NacpHelper.GetTitles(in controlData); + int titleCount = titles.Length; if (langIdx < titleCount) { - 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(); - } + data.Name = titles[langIdx].NameString.ToString(); + data.Developer = titles[langIdx].PublisherString.ToString(); } else { @@ -1421,16 +1410,9 @@ namespace Ryujinx.Ava.Systems.AppLibrary { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].NameString.IsEmpty() - : controlData.Title[i].NameString.IsEmpty(); - - if (!empty) + if (!string.IsNullOrWhiteSpace(titles[i].NameString)) { - data.Name = decompressedTitles != null - ? decompressedTitles[i].NameString.ToString() - : controlData.Title[i].NameString.ToString(); - + data.Name = titles[i].NameString.ToString(); break; } } @@ -1440,16 +1422,9 @@ namespace Ryujinx.Ava.Systems.AppLibrary { for (int i = 0; i < titleCount; i++) { - bool empty = decompressedTitles != null - ? decompressedTitles[i].PublisherString.IsEmpty() - : controlData.Title[i].PublisherString.IsEmpty(); - - if (!empty) + if (!string.IsNullOrWhiteSpace(titles[i].PublisherString)) { - data.Developer = decompressedTitles != null - ? decompressedTitles[i].PublisherString.ToString() - : controlData.Title[i].PublisherString.ToString(); - + data.Developer = titles[i].PublisherString.ToString(); break; } } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 440e6d9bd..385e843d8 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -170,16 +170,8 @@ namespace Ryujinx.Ava.UI.Views.Main 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; - } + var titles = Ryujinx.HLE.Utilities.NacpHelper.GetTitles(in nacp); + string name = langIdx < titles.Length ? titles[langIdx].NameString.ToString() : string.Empty; await StyleableAppWindow.ShowAsync( new CheatWindow(