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.
This commit is contained in:
Mythrax 2026-02-21 15:39:50 +10:00
parent d1205dc95d
commit 76bb4ae2d0
No known key found for this signature in database
GPG key ID: 3D310DF18BB7ADB9
12 changed files with 138 additions and 20 deletions

View file

@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState
SimplifiedChinese, SimplifiedChinese,
TraditionalChinese, TraditionalChinese,
BrazilianPortuguese, BrazilianPortuguese,
Polish,
Thai,
} }
} }

View file

@ -23,7 +23,9 @@ namespace Ryujinx.HLE.HOS.SystemState
"es-419", "es-419",
"zh-Hans", "zh-Hans",
"zh-Hant", "zh-Hant",
"pt-BR" "pt-BR",
"pl",
"th"
]; ];
internal long DesiredKeyboardLayout { get; private set; } internal long DesiredKeyboardLayout { get; private set; }

View file

@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState
TraditionalChinese, TraditionalChinese,
SimplifiedChinese, SimplifiedChinese,
BrazilianPortuguese, BrazilianPortuguese,
Polish,
Thai,
} }
} }

View file

@ -8,6 +8,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu;
using Ryujinx.HLE.Loaders.Executables; using Ryujinx.HLE.Loaders.Executables;
using Ryujinx.HLE.Utilities;
using Ryujinx.Memory; using Ryujinx.Memory;
using System.Linq; using System.Linq;
using static Ryujinx.HLE.HOS.ModLoader; using static Ryujinx.HLE.HOS.ModLoader;
@ -86,11 +87,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
if (!isHomebrew && programId > 0x010000000000FFFF) 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)) if (string.IsNullOrWhiteSpace(programName))
{ {
foreach (ApplicationControlProperty.ApplicationTitle appTitle in nacpData.Value.Title) foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles)
{ {
if (appTitle.Name[0] != 0) if (appTitle.Name[0] != 0)
continue; continue;

View file

@ -194,15 +194,22 @@ namespace Ryujinx.HLE.Loaders.Processes
{ {
nacpStorage.Read(0, nacpData.ByteSpan); 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" == int langIdx = (int)_device.System.State.DesiredTitleLanguage;
nacpData.Value.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) 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(); throw new InvalidOperationException();
if (string.IsNullOrWhiteSpace(programName)) if (string.IsNullOrWhiteSpace(programName))
{ {
foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in nacpData.Value.Title) foreach (ApplicationControlProperty.ApplicationTitle nacpTitles in titles)
{ {
if (nacpTitles.Name[0] != 0) if (nacpTitles.Name[0] != 0)
continue; continue;

View file

@ -5,6 +5,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.Cpu; using Ryujinx.Cpu;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using Ryujinx.Horizon.Common; using Ryujinx.Horizon.Common;
namespace Ryujinx.HLE.Loaders.Processes namespace Ryujinx.HLE.Loaders.Processes
@ -54,11 +55,17 @@ namespace Ryujinx.HLE.Loaders.Processes
{ {
ulong programId = metaLoader.ProgramId; 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)) if (string.IsNullOrWhiteSpace(Name))
{ {
foreach (ApplicationControlProperty.ApplicationTitle appTitle in ApplicationControlProperties.Title) foreach (ApplicationControlProperty.ApplicationTitle appTitle in titles)
{ {
if (appTitle.Name[0] != 0) if (appTitle.Name[0] != 0)
continue; continue;

View file

@ -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
/// <summary>
/// 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.
/// </summary>
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<byte> titleBytes = MemoryMarshal.AsBytes(
(ReadOnlySpan<ApplicationControlProperty.ApplicationTitle>)nacp.Title);
ushort compressedBlobSize = BinaryPrimitives.ReadUInt16LittleEndian(titleBytes);
ReadOnlySpan<byte> 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<ApplicationControlProperty.ApplicationTitle>(
decompressed.AsSpan(i * TitleEntrySize, TitleEntrySize));
}
return result;
}
}
}

View file

@ -58,7 +58,10 @@ namespace Ryujinx.Horizon.Sdk.Ns
public RepairFlagValue RepairFlag; public RepairFlagValue RepairFlag;
public byte ProgramIndex; public byte ProgramIndex;
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag; public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
public Array4<byte> Reserved3214; public byte ApplicationErrorCodePrefix;
public TitleCompressionValue TitleCompression;
public byte AcdIndex;
public byte ApparentPlatform;
public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration; public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
public ApplicationJitConfiguration JitConfiguration; public ApplicationJitConfiguration JitConfiguration;
public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors; public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
@ -130,6 +133,8 @@ namespace Ryujinx.Horizon.Sdk.Ns
TraditionalChinese = 13, TraditionalChinese = 13,
SimplifiedChinese = 14, SimplifiedChinese = 14,
BrazilianPortuguese = 15, BrazilianPortuguese = 15,
Polish = 16,
Thai = 17,
} }
public enum Organization public enum Organization
@ -302,5 +307,11 @@ namespace Ryujinx.Horizon.Sdk.Ns
Deny = 0, Deny = 0,
Allow = 1, Allow = 1,
} }
public enum TitleCompressionValue : byte
{
Disable = 0,
Enable = 1,
}
} }
} }

View file

@ -167,7 +167,9 @@ namespace Ryujinx.Headless
ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties; ApplicationControlProperty nacp = activeProcess.ApplicationControlProperties;
int desiredLanguage = (int)Device.System.State.DesiredTitleLanguage; 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 titleVersionSection = string.IsNullOrWhiteSpace(nacp.DisplayVersionString.ToString()) ? string.Empty : $" v{nacp.DisplayVersionString.ToString()}";
string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})"; string titleIdSection = string.IsNullOrWhiteSpace(activeProcess.ProgramIdText) ? string.Empty : $" ({activeProcess.ProgramIdText.ToUpper()})";
string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)"; string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";

View file

@ -22,9 +22,9 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState; using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Utilities;
using Ryujinx.HLE.Loaders.Npdm; using Ryujinx.HLE.Loaders.Npdm;
using Ryujinx.HLE.Loaders.Processes.Extensions; using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.HLE.Utilities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -440,8 +440,10 @@ namespace Ryujinx.Ava.Systems.AppLibrary
GetApplicationInformation(ref controlHolder.Value, ref application); GetApplicationInformation(ref controlHolder.Value, ref application);
if ("Switch Verification" == controlHolder.Value ApplicationControlProperty.ApplicationTitle[] verifyTitles =
.Title[(int)TitleLanguage.AmericanEnglish].NameString.ToString()) NacpHelper.GetTitleEntries(in controlHolder.Value);
if ((int)TitleLanguage.AmericanEnglish < verifyTitles.Length &&
"Switch Verification" == verifyTitles[(int)TitleLanguage.AmericanEnglish].NameString.ToString())
return false; return false;
} }
else else
@ -1391,10 +1393,13 @@ namespace Ryujinx.Ava.Systems.AppLibrary
{ {
_ = Enum.TryParse(DesiredLanguage.ToString(), out TitleLanguage desiredTitleLanguage); _ = 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.Name = titles[(int)desiredTitleLanguage].NameString.ToString();
data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString(); data.Developer = titles[(int)desiredTitleLanguage].PublisherString.ToString();
} }
else else
{ {
@ -1404,7 +1409,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Name)) if (string.IsNullOrWhiteSpace(data.Name))
{ {
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles)
{ {
if (!controlTitle.NameString.IsEmpty()) if (!controlTitle.NameString.IsEmpty())
{ {
@ -1417,7 +1422,7 @@ namespace Ryujinx.Ava.Systems.AppLibrary
if (string.IsNullOrWhiteSpace(data.Developer)) if (string.IsNullOrWhiteSpace(data.Developer))
{ {
foreach (ref readonly ApplicationControlProperty.ApplicationTitle controlTitle in controlData.Title) foreach (ApplicationControlProperty.ApplicationTitle controlTitle in titles)
{ {
if (!controlTitle.PublisherString.IsEmpty()) if (!controlTitle.PublisherString.IsEmpty())
{ {

View file

@ -24,6 +24,8 @@ namespace Ryujinx.Ava.Systems.Configuration.System
SimplifiedChinese, SimplifiedChinese,
TraditionalChinese, TraditionalChinese,
BrazilianPortuguese, BrazilianPortuguese,
Polish,
Thai,
} }
public static class LanguageEnumHelper public static class LanguageEnumHelper

View file

@ -168,7 +168,10 @@ namespace Ryujinx.Ava.UI.Views.Main
if (!ViewModel.IsGameRunning) if (!ViewModel.IsGameRunning)
return; 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( await StyleableAppWindow.ShowAsync(
new CheatWindow( new CheatWindow(