mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-03-11 17:55:44 +00:00
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:
parent
d1205dc95d
commit
21c84439b3
12 changed files with 216 additions and 33 deletions
|
|
@ -20,5 +20,7 @@ namespace Ryujinx.HLE.HOS.SystemState
|
|||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -18,5 +18,7 @@ namespace Ryujinx.HLE.HOS.SystemState
|
|||
TraditionalChinese,
|
||||
SimplifiedChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
61
src/Ryujinx.HLE/Utilities/NacpHelper.cs
Normal file
61
src/Ryujinx.HLE/Utilities/NacpHelper.cs
Normal file
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses a zlib-compressed NACP title block into 32 title entries.
|
||||
/// Only call this when <see cref="IsCompressed"/> returns <c>true</c>.
|
||||
/// </summary>
|
||||
public static ApplicationControlProperty.ApplicationTitle[] DecompressTitleEntries(
|
||||
ref readonly ApplicationControlProperty nacp)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,10 @@ namespace Ryujinx.Horizon.Sdk.Ns
|
|||
public RepairFlagValue RepairFlag;
|
||||
public byte ProgramIndex;
|
||||
public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
|
||||
public Array4<byte> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ namespace Ryujinx.Ava.Systems.Configuration.System
|
|||
SimplifiedChinese,
|
||||
TraditionalChinese,
|
||||
BrazilianPortuguese,
|
||||
Polish,
|
||||
Thai,
|
||||
}
|
||||
|
||||
public static class LanguageEnumHelper
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue