NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsBanner.cs
1using System.Buffers.Binary;
2using System.Collections.ObjectModel;
3using System.Text;
4
5namespace NdsForge;
6
8public sealed class NdsBanner
9{
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;
24
27 internal NdsBanner(ReadOnlyMemory<byte> data)
28 {
29 _data = data;
30 Version = NdsBinary.ReadUInt16(data.Span, 0);
31 StoredCrcs = Enumerable.Range(0, 4)
32 .Select(index => NdsBinary.ReadUInt16(data.Span, 2 + (index * 2)))
33 .ToArray();
34
35 var titles = new Dictionary<NdsBannerLanguage, string>();
36 for (int index = 0; index < LanguageCount; index++)
37 {
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]));
41 }
42
43 Titles = new ReadOnlyDictionary<NdsBannerLanguage, string>(titles);
44 }
45
49 public static NdsBanner Parse(ReadOnlyMemory<byte> data)
50 {
51 if (data.Length < 2)
52 {
53 throw new InvalidDataException("A banner is too small to contain a version.");
54 }
55
56 ushort version = NdsBinary.ReadUInt16(data.Span, 0);
57 int expected = GetSize(version);
58 if (data.Length != expected)
59 {
60 throw new InvalidDataException(
61 $"Banner version 0x{version:X4} requires 0x{expected:X} bytes, not 0x{data.Length:X}.");
62 }
63
64 return new(data.ToArray());
65 }
66
68 public ushort Version { get; }
69
71 public ReadOnlyMemory<byte> RawData => _data;
72
74 public IReadOnlyList<ushort> StoredCrcs { get; }
75
77 public IReadOnlyDictionary<NdsBannerLanguage, string> Titles { get; }
78
80 public bool IsAnimated => Version == 0x0103;
81
83 public int LanguageCount => Version switch
84 {
85 1 => 6,
86 2 => 7,
87 3 or 0x0103 => 8,
88 _ => 0,
89 };
90
93 public byte[] RenderIconRgba32() => Render(
94 _data.Span.Slice(StaticTilesOffset, 0x200),
95 _data.Span.Slice(StaticPaletteOffset, 0x20));
96
101 public byte[] RenderAnimatedIconRgba32(int tileFrame, int paletteFrame)
102 {
103 if (!IsAnimated)
104 {
105 throw new InvalidOperationException("This banner has no animated icon.");
106 }
107
108 ArgumentOutOfRangeException.ThrowIfNegative(tileFrame);
109 ArgumentOutOfRangeException.ThrowIfGreaterThan(tileFrame, 7);
110 ArgumentOutOfRangeException.ThrowIfNegative(paletteFrame);
111 ArgumentOutOfRangeException.ThrowIfGreaterThan(paletteFrame, 7);
112 return Render(
113 _data.Span.Slice(AnimatedTilesOffset + (tileFrame * 0x200), 0x200),
114 _data.Span.Slice(AnimatedPalettesOffset + (paletteFrame * 0x20), 0x20));
115 }
116
124 {
125 step.Validate();
126 byte[] pixels = RenderAnimatedIconRgba32(step.TileFrame, step.PaletteFrame);
127 if (!step.FlipHorizontal && !step.FlipVertical)
128 {
129 return pixels;
130 }
131
132 var flipped = new byte[pixels.Length];
133 for (int y = 0; y < 32; y++)
134 {
135 int destinationY = step.FlipVertical ? 31 - y : y;
136 for (int x = 0; x < 32; x++)
137 {
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));
141 }
142 }
143
144 return flipped;
145 }
146
149 public ushort[] GetAnimationSequence()
150 {
151 if (!IsAnimated)
152 {
153 return [];
154 }
155
156 var sequence = new ushort[64];
157 for (int index = 0; index < sequence.Length; index++)
158 {
159 sequence[index] = NdsBinary.ReadUInt16(_data.Span, AnimationSequenceOffset + (index * 2));
160 }
161
162 return sequence;
163 }
164
170 public IReadOnlyList<NdsBannerAnimationStep> GetAnimationSteps()
171 {
172 if (!IsAnimated)
173 {
174 return [];
175 }
176
177 var steps = new List<NdsBannerAnimationStep>();
178 foreach (ushort word in GetAnimationSequence())
179 {
180 if (word == 0)
181 {
182 break;
183 }
184
185 steps.Add(NdsBannerAnimationStep.FromPacked(word));
186 }
187
188 return steps.AsReadOnly();
189 }
190
196 public NdsBanner WithRepairedCrcs()
197 {
198 byte[] data = _data.ToArray();
199 int slots = GetCrcCount(Version);
200 for (int slot = 0; slot < slots; slot++)
201 {
202 (int offset, int length) = GetCrcRegion(slot);
203 BinaryPrimitives.WriteUInt16LittleEndian(
204 data.AsSpan(2 + (slot * 2)),
205 NdsChecksums.ComputeCrc16(data.AsSpan(offset, length)));
206 }
207
208 return Parse(data);
209 }
210
214 internal IEnumerable<NdsDiagnostic> ValidateCrcs(uint bannerOffset)
215 {
216 int slots = GetCrcCount(Version);
217
218 for (int slot = 0; slot < slots; slot++)
219 {
220 (int offset, int length) = GetCrcRegion(slot);
221 ushort calculated = NdsChecksums.ComputeCrc16(_data.Span.Slice(offset, length));
222 if (calculated != StoredCrcs[slot])
223 {
224 yield return new(
225 $"NDS130{slot + 1}",
227 $"Banner CRC slot {slot} stores 0x{StoredCrcs[slot]:X4}, but the calculated value is 0x{calculated:X4}.",
228 new(bannerOffset + offset, length));
229 }
230 }
231 }
232
236 internal static int GetSize(ushort version) => version switch
237 {
238 1 => 0x840,
239 2 => 0x940,
240 3 => 0xA40,
241 0x0103 => 0x23C0,
242 _ => throw new InvalidDataException($"Unsupported banner version 0x{version:X4}."),
243 };
244
248 internal static (int Offset, int Length) GetCrcRegion(int slot) => slot switch
249 {
250 0 => (0x20, 0x820),
251 1 => (0x20, 0x920),
252 2 => (0x20, 0xA20),
253 3 => (0x1240, 0x1180),
254 _ => throw new ArgumentOutOfRangeException(nameof(slot)),
255 };
256
260 private static int GetCrcCount(ushort version) => version switch
261 {
262 1 => 1,
263 2 => 2,
264 3 => 3,
265 0x0103 => 4,
266 _ => 0,
267 };
268
272 private static int FindUtf16Terminator(ReadOnlySpan<byte> bytes)
273 {
274 for (int index = 0; index < bytes.Length; index += 2)
275 {
276 if (bytes[index] == 0 && bytes[index + 1] == 0)
277 {
278 return index;
279 }
280 }
281
282 return bytes.Length;
283 }
284
289 private static byte[] Render(ReadOnlySpan<byte> tiles, ReadOnlySpan<byte> palette)
290 {
291 var rgba = new byte[32 * 32 * 4];
292 for (int y = 0; y < 32; y++)
293 {
294 for (int x = 0; x < 32; x++)
295 {
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;
304 }
305 }
306
307 return rgba;
308 }
309
313 private static byte ExpandFiveBits(int value) => (byte)((value << 3) | (value >> 2));
314}
Describes one timed DSi menu-icon pose. Tile and palette frames are selected independently so an anim...
static NdsBannerAnimationStep FromPacked(ushort value)
Interprets one nonzero on-disk sequence word without needing a containing banner.
byte[] RenderIconRgba32()
Renders the static 32-by-32 icon as row-major RGBA32 pixels.
Definition NdsBanner.cs:93
ushort[] GetAnimationSequence()
Returns the 64 packed DSi sequence words without interpreting duration, frame, palette,...
Definition NdsBanner.cs:149
NdsBanner WithRepairedCrcs()
Returns a byte-preserving banner copy with only the version-defined CRC fields recalculated....
Definition NdsBanner.cs:196
IReadOnlyList< NdsBannerAnimationStep > GetAnimationSteps()
Decodes meaningful DSi animation poses up to the first zero terminator. This excludes unused fixed-ca...
Definition NdsBanner.cs:170
bool IsAnimated
Gets whether this banner contains DSi animated-icon data.
Definition NdsBanner.cs:80
int LanguageCount
Maps historical revisions to six, seven, or eight fixed 0x100-byte UTF-16LE slots.
Definition NdsBanner.cs:83
ReadOnlyMemory< byte > RawData
Preserves reserved fields and animation bits not projected into typed properties, enabling lossless e...
Definition NdsBanner.cs:71
byte[] RenderAnimationStepRgba32(NdsBannerAnimationStep step)
Renders the exact pose described by a typed sequence entry, including hardware horizontal and vertica...
Definition NdsBanner.cs:123
byte[] RenderAnimatedIconRgba32(int tileFrame, int paletteFrame)
Renders one DSi animated-icon tile and palette frame as RGBA32.
Definition NdsBanner.cs:101
IReadOnlyDictionary< NdsBannerLanguage, string > Titles
Gets localized titles supported by this banner version.
Definition NdsBanner.cs:77
IReadOnlyList< ushort > StoredCrcs
Gets stored version CRCs in slot order.
Definition NdsBanner.cs:74
ushort Version
Selects title count and CRC layout: 1-3 are static, while 0x0103 adds DSi animation.
Definition NdsBanner.cs:68
static NdsBanner Parse(ReadOnlyMemory< byte > data)
Parses a complete raw banner from memory.
Definition NdsBanner.cs:49
Calculates checksums used by Nintendo DS image structures.
static ushort ComputeCrc16(ReadOnlySpan< byte > data, ushort seed=ushort.MaxValue)
Calculates the CRC-16 used by Nintendo DS headers and banners.
NdsDiagnosticSeverity
Indicates the impact of a validation finding.
NdsBannerLanguage
Identifies a localized Nintendo DS banner title slot.