8public sealed class NdsBanner
11 private const int StaticTilesOffset = 0x20;
13 private const int StaticPaletteOffset = 0x220;
15 private const int TitlesOffset = 0x240;
17 private const int AnimatedTilesOffset = 0x1240;
19 private const int AnimatedPalettesOffset = 0x2240;
21 private const int AnimationSequenceOffset = 0x2340;
23 private readonly ReadOnlyMemory<byte> _data;
27 internal NdsBanner(ReadOnlyMemory<byte> data)
30 Version = NdsBinary.ReadUInt16(data.Span, 0);
32 .Select(index => NdsBinary.ReadUInt16(data.Span, 2 + (index * 2)))
35 var titles =
new Dictionary<NdsBannerLanguage, string>();
38 ReadOnlySpan<byte> titleBytes = data.Span.Slice(TitlesOffset + (index * 0x100), 0x100);
39 int length = FindUtf16Terminator(titleBytes);
40 titles.Add((
NdsBannerLanguage)index, Encoding.Unicode.GetString(titleBytes[..length]));
43 Titles =
new ReadOnlyDictionary<NdsBannerLanguage, string>(titles);
49 public static NdsBanner
Parse(ReadOnlyMemory<byte> data)
53 throw new InvalidDataException(
"A banner is too small to contain a version.");
56 ushort version = NdsBinary.ReadUInt16(data.Span, 0);
57 int expected = GetSize(version);
58 if (data.Length != expected)
60 throw new InvalidDataException(
61 $
"Banner version 0x{version:X4} requires 0x{expected:X} bytes, not 0x{data.Length:X}.");
64 return new(data.ToArray());
71 public ReadOnlyMemory<byte>
RawData => _data;
77 public IReadOnlyDictionary<NdsBannerLanguage, string>
Titles {
get; }
94 _data.Span.Slice(StaticTilesOffset, 0x200),
95 _data.Span.Slice(StaticPaletteOffset, 0x20));
105 throw new InvalidOperationException(
"This banner has no animated icon.");
108 ArgumentOutOfRangeException.ThrowIfNegative(tileFrame);
109 ArgumentOutOfRangeException.ThrowIfGreaterThan(tileFrame, 7);
110 ArgumentOutOfRangeException.ThrowIfNegative(paletteFrame);
111 ArgumentOutOfRangeException.ThrowIfGreaterThan(paletteFrame, 7);
113 _data.Span.Slice(AnimatedTilesOffset + (tileFrame * 0x200), 0x200),
114 _data.Span.Slice(AnimatedPalettesOffset + (paletteFrame * 0x20), 0x20));
127 if (!step.FlipHorizontal && !step.FlipVertical)
132 var flipped =
new byte[pixels.Length];
133 for (
int y = 0; y < 32; y++)
135 int destinationY = step.FlipVertical ? 31 - y : y;
136 for (
int x = 0; x < 32; x++)
138 int destinationX = step.FlipHorizontal ? 31 - x : x;
139 pixels.AsSpan(((y * 32) + x) * 4, 4)
140 .CopyTo(flipped.AsSpan(((destinationY * 32) + destinationX) * 4, 4));
156 var sequence =
new ushort[64];
157 for (
int index = 0; index < sequence.Length; index++)
159 sequence[index] = NdsBinary.ReadUInt16(_data.Span, AnimationSequenceOffset + (index * 2));
177 var steps =
new List<NdsBannerAnimationStep>();
188 return steps.AsReadOnly();
198 byte[] data = _data.ToArray();
199 int slots = GetCrcCount(
Version);
200 for (
int slot = 0; slot < slots; slot++)
202 (
int offset,
int length) = GetCrcRegion(slot);
203 BinaryPrimitives.WriteUInt16LittleEndian(
204 data.AsSpan(2 + (slot * 2)),
214 internal IEnumerable<NdsDiagnostic> ValidateCrcs(uint bannerOffset)
216 int slots = GetCrcCount(
Version);
218 for (
int slot = 0; slot < slots; slot++)
220 (
int offset,
int length) = GetCrcRegion(slot);
227 $
"Banner CRC slot {slot} stores 0x{StoredCrcs[slot]:X4}, but the calculated value is 0x{calculated:X4}.",
228 new(bannerOffset + offset, length));
236 internal static int GetSize(ushort version) => version
switch
242 _ =>
throw new InvalidDataException($
"Unsupported banner version 0x{version:X4}."),
248 internal static (
int Offset,
int Length) GetCrcRegion(
int slot) => slot
switch
253 3 => (0x1240, 0x1180),
254 _ =>
throw new ArgumentOutOfRangeException(nameof(slot)),
260 private static int GetCrcCount(ushort version) => version
switch
272 private static int FindUtf16Terminator(ReadOnlySpan<byte> bytes)
274 for (
int index = 0; index < bytes.Length; index += 2)
276 if (bytes[index] == 0 && bytes[index + 1] == 0)
289 private static byte[] Render(ReadOnlySpan<byte> tiles, ReadOnlySpan<byte> palette)
291 var rgba =
new byte[32 * 32 * 4];
292 for (
int y = 0; y < 32; y++)
294 for (
int x = 0; x < 32; x++)
296 int tileOffset = ((((y / 8) * 4) + (x / 8)) * 32) + ((y % 8) * 4) + ((x % 8) / 2);
297 int paletteIndex = (x & 1) == 0 ? tiles[tileOffset] & 0x0F : tiles[tileOffset] >> 4;
298 ushort color = BinaryPrimitives.ReadUInt16LittleEndian(palette[(paletteIndex * 2)..]);
299 int pixelOffset = ((y * 32) + x) * 4;
300 rgba[pixelOffset] = ExpandFiveBits(color & 0x1F);
301 rgba[pixelOffset + 1] = ExpandFiveBits((color >> 5) & 0x1F);
302 rgba[pixelOffset + 2] = ExpandFiveBits((color >> 10) & 0x1F);
303 rgba[pixelOffset + 3] = paletteIndex == 0 ? (byte)0 : byte.MaxValue;
313 private static byte ExpandFiveBits(
int value) => (byte)((value << 3) | (value >> 2));