NdsForge.NET 1.0.1
Read, validate, edit, compare, and build Nintendo DS and DSi images from .NET
Loading...
Searching...
No Matches
NdsBannerBuilder.cs
1using System.Buffers.Binary;
2using System.Text;
3
4namespace NdsForge;
5
7public sealed class NdsBannerBuilder
8{
10 private readonly Dictionary<NdsBannerLanguage, string> _titles = [];
12 private byte[] _paletteIndices = new byte[32 * 32];
14 private ushort[] _palette = new ushort[16];
16 private readonly byte[][] _animatedPaletteIndices = Enumerable.Range(0, 8)
17 .Select(static _ => new byte[32 * 32])
18 .ToArray();
20 private readonly ushort[][] _animatedPalettes = Enumerable.Range(0, 8)
21 .Select(static _ => new ushort[16])
22 .ToArray();
24 private NdsBannerAnimationStep[] _animationSteps = [];
25
28 public NdsBannerBuilder(ushort version = 1)
29 {
30 if (version is not (1 or 2 or 3 or 0x0103))
31 {
32 throw new ArgumentOutOfRangeException(nameof(version), "Banner version must be 1, 2, 3, or 0x0103.");
33 }
34
35 Version = version;
36 }
37
39 public ushort Version { get; }
40
45 public NdsBannerBuilder SetTitle(NdsBannerLanguage language, string title)
46 {
47 ArgumentNullException.ThrowIfNull(title);
48 if ((int)language >= GetLanguageCount() ||
49 title.Length > 127 ||
50 title.Contains('\0', StringComparison.Ordinal))
51 {
52 throw new ArgumentException("The title language or length is not supported by this banner version.", nameof(title));
53 }
54
55 _titles[language] = title;
56 return this;
57 }
58
64 ReadOnlySpan<byte> paletteIndices,
65 ReadOnlySpan<ushort> bgr555Palette)
66 {
67 ValidateIcon(paletteIndices, bgr555Palette);
68 _paletteIndices = paletteIndices.ToArray();
69 _palette = bgr555Palette.ToArray();
70 return this;
71 }
72
83 int frame,
84 ReadOnlySpan<byte> paletteIndices,
85 ReadOnlySpan<ushort> bgr555Palette)
86 {
87 EnsureAnimated();
88 ArgumentOutOfRangeException.ThrowIfNegative(frame);
89 ArgumentOutOfRangeException.ThrowIfGreaterThan(frame, 7);
90 ValidateIcon(paletteIndices, bgr555Palette);
91 _animatedPaletteIndices[frame] = paletteIndices.ToArray();
92 _animatedPalettes[frame] = bgr555Palette.ToArray();
93 return this;
94 }
95
103 public NdsBannerBuilder SetAnimationSequence(IEnumerable<NdsBannerAnimationStep> steps)
104 {
105 EnsureAnimated();
106 ArgumentNullException.ThrowIfNull(steps);
107 NdsBannerAnimationStep[] materialized = steps.ToArray();
108 if (materialized.Length > 63)
109 {
110 throw new ArgumentException("A DSi animation accepts at most 63 poses plus its required terminator.", nameof(steps));
111 }
112
113 foreach (NdsBannerAnimationStep step in materialized)
114 {
115 step.Validate();
116 }
117
118 _animationSteps = materialized;
119 return this;
120 }
121
125 {
126 byte[] data = new byte[NdsBanner.GetSize(Version)];
127 NdsBinary.WriteUInt16(data, 0, Version);
128 WriteIcon(data);
129 foreach ((NdsBannerLanguage language, string title) in _titles)
130 {
131 int offset = 0x240 + ((int)language * 0x100);
132 Encoding.Unicode.GetBytes(title, data.AsSpan(offset, 0xFE));
133 }
134
135 if (Version == 0x0103)
136 {
137 WriteAnimation(data);
138 }
139
140 int crcCount = Version == 0x0103 ? 4 : Version;
141 for (int slot = 0; slot < crcCount; slot++)
142 {
143 (int offset, int length) = NdsBanner.GetCrcRegion(slot);
144 BinaryPrimitives.WriteUInt16LittleEndian(
145 data.AsSpan(2 + (slot * 2)),
146 NdsChecksums.ComputeCrc16(data.AsSpan(offset, length)));
147 }
148
149 return NdsBanner.Parse(data);
150 }
151
154 private void WriteIcon(Span<byte> data)
155 {
156 WriteTiles(data.Slice(0x20, 0x200), _paletteIndices);
157 WritePalette(data.Slice(0x220, 0x20), _palette);
158 }
159
162 private void WriteAnimation(Span<byte> data)
163 {
164 for (int frame = 0; frame < 8; frame++)
165 {
166 WriteTiles(data.Slice(0x1240 + (frame * 0x200), 0x200), _animatedPaletteIndices[frame]);
167 WritePalette(data.Slice(0x2240 + (frame * 0x20), 0x20), _animatedPalettes[frame]);
168 }
169
170 for (int index = 0; index < _animationSteps.Length; index++)
171 {
172 BinaryPrimitives.WriteUInt16LittleEndian(data[(0x2340 + (index * 2))..], _animationSteps[index].Pack());
173 }
174 }
175
179 private static void WriteTiles(Span<byte> tiles, ReadOnlySpan<byte> paletteIndices)
180 {
181 for (int y = 0; y < 32; y++)
182 {
183 for (int x = 0; x < 32; x += 2)
184 {
185 int tileOffset = ((((y / 8) * 4) + (x / 8)) * 32) + ((y % 8) * 4) + ((x % 8) / 2);
186 byte low = paletteIndices[(y * 32) + x];
187 byte high = paletteIndices[(y * 32) + x + 1];
188 tiles[tileOffset] = (byte)(low | (high << 4));
189 }
190 }
191 }
192
196 private static void WritePalette(Span<byte> destination, ReadOnlySpan<ushort> palette)
197 {
198 for (int index = 0; index < palette.Length; index++)
199 {
200 BinaryPrimitives.WriteUInt16LittleEndian(destination[(index * 2)..], palette[index]);
201 }
202 }
203
207 private static void ValidateIcon(ReadOnlySpan<byte> paletteIndices, ReadOnlySpan<ushort> bgr555Palette)
208 {
209 if (paletteIndices.Length != 32 * 32 || bgr555Palette.Length != 16)
210 {
211 throw new ArgumentException("A banner icon requires 1024 indices and 16 palette colors.");
212 }
213
214 if (paletteIndices.ContainsAnyExceptInRange((byte)0, (byte)15))
215 {
216 throw new ArgumentException("Banner palette indices must be between zero and 15.", nameof(paletteIndices));
217 }
218 }
219
221 private void EnsureAnimated()
222 {
223 if (Version != 0x0103)
224 {
225 throw new InvalidOperationException("Animated frames require DSi banner version 0x0103.");
226 }
227 }
228
231 private int GetLanguageCount() => Version switch
232 {
233 1 => 6,
234 2 => 7,
235 3 or 0x0103 => 8,
236 _ => 0,
237 };
238}
Describes one timed DSi menu-icon pose. Tile and palette frames are selected independently so an anim...
NdsBannerBuilder SetIndexedIcon(ReadOnlySpan< byte > paletteIndices, ReadOnlySpan< ushort > bgr555Palette)
Accepts developer-friendly row-major pixels and converts them to tiled 4-bpp banner storage during bu...
NdsBanner Build()
Builds a checksummed immutable banner.
NdsBannerBuilder(ushort version=1)
Selects a fixed layout whose language slots and CRC coverage expand through the DSi revision.
NdsBannerBuilder SetTitle(NdsBannerLanguage language, string title)
Assigns one version-supported UTF-16LE title while reserving a terminating NUL code unit.
NdsBannerBuilder SetAnimatedFrame(int frame, ReadOnlySpan< byte > paletteIndices, ReadOnlySpan< ushort > bgr555Palette)
Assigns one of the eight DSi tile-and-palette slots from conventional row-major indexed pixels....
NdsBannerBuilder SetAnimationSequence(IEnumerable< NdsBannerAnimationStep > steps)
Replaces the ordered DSi playback sequence. At most 63 poses are accepted so the serialized 64-word t...
ushort Version
Controls output length, title-slot count, and how many cumulative CRC fields are populated.
Represents a versioned menu icon, localized titles, and optional DSi animation.
Definition NdsBanner.cs:9
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.
NdsBannerLanguage
Identifies a localized Nintendo DS banner title slot.