ryubing-ryujinx/src/Ryujinx.HLE/Utilities/NacpHelper.cs
Mythrax 772b3fb800
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.
2026-02-24 19:51:44 +10:00

61 lines
2.5 KiB
C#

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;
}
}
}