1use anyhow::{bail, Result};
9use serde::{Deserialize, Serialize};
10
11pub const SPRITE_SIZE: usize = 8;
13pub const SHEET_W: usize = 128;
15pub const SHEET_H: usize = 128;
16pub const SPRITES_PER_ROW: usize = SHEET_W / SPRITE_SIZE;
17pub const SPRITE_COUNT: usize = 256;
18
19pub const MAP_W: usize = 128;
21pub const MAP_H: usize = 64;
22
23pub const SFX_COUNT: usize = 64;
25pub const MUSIC_COUNT: usize = 64;
26pub const SFX_LEN: usize = 32;
28pub const CHANNELS: usize = 4;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct SpriteId(pub u8);
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
37pub struct SfxId(pub u8);
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
41pub struct MusicId(pub u8);
42
43#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct SpriteSheet {
46 #[serde(with = "crate::wire::pixel_rows")]
48 pub pixels: Vec<u8>,
49 #[serde(with = "crate::wire::hex_string")]
51 pub flags: Vec<u8>,
52}
53
54impl Default for SpriteSheet {
55 fn default() -> Self {
56 Self {
57 pixels: vec![0; SHEET_W * SHEET_H],
58 flags: vec![0; SPRITE_COUNT],
59 }
60 }
61}
62
63impl SpriteSheet {
64 pub fn get(&self, x: i32, y: i32) -> u8 {
66 if (0..SHEET_W as i32).contains(&x) && (0..SHEET_H as i32).contains(&y) {
67 self.pixels[(y as usize) * SHEET_W + x as usize]
68 } else {
69 0
70 }
71 }
72
73 pub fn set(&mut self, x: i32, y: i32, color: u8) {
75 if (0..SHEET_W as i32).contains(&x) && (0..SHEET_H as i32).contains(&y) {
76 self.pixels[(y as usize) * SHEET_W + x as usize] = color & 0x0f;
77 }
78 }
79
80 pub fn sprite_pixel(&self, n: u32, px: i32, py: i32) -> u8 {
83 let n = (n as usize) % SPRITE_COUNT;
84 let sx = (n % SPRITES_PER_ROW * SPRITE_SIZE) as i32 + px;
85 let sy = (n / SPRITES_PER_ROW * SPRITE_SIZE) as i32 + py;
86 self.get(sx, sy)
87 }
88
89 pub fn flags(&self, n: u32) -> u8 {
91 self.flags[(n as usize) % SPRITE_COUNT]
92 }
93
94 pub fn set_flag(&mut self, n: u32, flag: u8, value: bool) {
96 let f = &mut self.flags[(n as usize) % SPRITE_COUNT];
97 if value {
98 *f |= 1 << (flag & 7);
99 } else {
100 *f &= !(1 << (flag & 7));
101 }
102 }
103}
104
105#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct MapData {
108 #[serde(with = "crate::wire::tile_rows")]
109 pub tiles: Vec<u8>,
110}
111
112impl Default for MapData {
113 fn default() -> Self {
114 Self {
115 tiles: vec![0; MAP_W * MAP_H],
116 }
117 }
118}
119
120impl MapData {
121 pub fn get(&self, x: i32, y: i32) -> u8 {
122 if (0..MAP_W as i32).contains(&x) && (0..MAP_H as i32).contains(&y) {
123 self.tiles[(y as usize) * MAP_W + x as usize]
124 } else {
125 0
126 }
127 }
128
129 pub fn set(&mut self, x: i32, y: i32, tile: u8) {
130 if (0..MAP_W as i32).contains(&x) && (0..MAP_H as i32).contains(&y) {
131 self.tiles[(y as usize) * MAP_W + x as usize] = tile;
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[repr(u8)]
139pub enum Waveform {
140 Triangle = 0,
141 TiltedSaw = 1,
142 Saw = 2,
143 Square = 3,
144 Pulse = 4,
145 Organ = 5,
146 Noise = 6,
147 Phaser = 7,
148}
149
150impl Waveform {
151 pub fn from_u8(v: u8) -> Self {
152 match v & 7 {
153 0 => Self::Triangle,
154 1 => Self::TiltedSaw,
155 2 => Self::Saw,
156 3 => Self::Square,
157 4 => Self::Pulse,
158 5 => Self::Organ,
159 6 => Self::Noise,
160 _ => Self::Phaser,
161 }
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[repr(u8)]
168pub enum SfxEffect {
169 None = 0,
170 Slide = 1,
171 Vibrato = 2,
172 Drop = 3,
173 FadeIn = 4,
174 FadeOut = 5,
175 ArpFast = 6,
176 ArpSlow = 7,
177}
178
179impl SfxEffect {
180 pub fn from_u8(v: u8) -> Self {
181 match v & 7 {
182 0 => Self::None,
183 1 => Self::Slide,
184 2 => Self::Vibrato,
185 3 => Self::Drop,
186 4 => Self::FadeIn,
187 5 => Self::FadeOut,
188 6 => Self::ArpFast,
189 _ => Self::ArpSlow,
190 }
191 }
192}
193
194#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
197#[serde(into = "[u8; 4]", from = "[u8; 4]")]
198pub struct Note {
199 pub pitch: u8,
200 pub wave: u8,
207 pub volume: u8,
208 pub effect: u8,
209}
210
211pub const NOTE_CUSTOM_FLAG: u8 = 0x08;
214
215impl Note {
216 pub fn wave_index(&self) -> u8 {
219 self.wave & 7
220 }
221
222 pub fn instrument(&self) -> Option<u8> {
225 (self.wave & NOTE_CUSTOM_FLAG != 0).then_some(self.wave & 7)
226 }
227}
228
229impl From<Note> for [u8; 4] {
230 fn from(n: Note) -> Self {
231 [n.pitch, n.wave, n.volume, n.effect]
232 }
233}
234
235impl From<[u8; 4]> for Note {
236 fn from([pitch, wave, volume, effect]: [u8; 4]) -> Self {
237 Self {
238 pitch,
239 wave,
240 volume,
241 effect,
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
250pub struct CustomWave {
251 pub samples: [i8; SFX_LEN],
253 pub bass: bool,
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259pub struct Sfx {
260 pub notes: [Note; SFX_LEN],
261 pub speed: u8,
263 pub loop_start: u8,
265 pub loop_end: u8,
267 pub noiz: bool,
270 pub buzz: bool,
272 pub detune: u8,
275 pub reverb: u8,
277 pub dampen: u8,
279 #[serde(default)]
281 pub custom_wave: Option<CustomWave>,
282}
283
284impl Default for Sfx {
285 fn default() -> Self {
286 Self {
287 notes: [Note::default(); SFX_LEN],
288 speed: 16,
289 loop_start: 0,
290 loop_end: 0,
291 noiz: false,
292 buzz: false,
293 detune: 0,
294 reverb: 0,
295 dampen: 0,
296 custom_wave: None,
297 }
298 }
299}
300
301impl Sfx {
302 pub fn is_empty(&self) -> bool {
304 self.notes.iter().all(|n| n.volume == 0)
305 }
306
307 pub fn set_filters(&mut self, byte: u8) {
312 self.noiz = byte & 2 != 0;
313 self.buzz = byte & 4 != 0;
314 self.detune = byte / 8 % 3;
315 self.reverb = byte / 24 % 3;
316 self.dampen = byte / 72 % 3;
317 }
318
319 pub fn filters_byte(&self) -> u8 {
323 let mut byte = 0u8;
324 if self.noiz {
325 byte |= 2;
326 }
327 if self.buzz {
328 byte |= 4;
329 }
330 byte += self.detune * 8;
331 byte += self.reverb * 24;
332 byte += self.dampen * 72;
333 byte
334 }
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
341pub struct MusicPattern {
342 pub channels: [Option<u8>; CHANNELS],
344 pub loop_back: bool,
346 pub loop_start: bool,
348 pub stop_at_end: bool,
350}
351
352impl Default for MusicPattern {
353 fn default() -> Self {
354 Self {
355 channels: [None; CHANNELS],
356 loop_back: false,
357 loop_start: false,
358 stop_at_end: false,
359 }
360 }
361}
362
363impl MusicPattern {
364 pub fn is_empty(&self) -> bool {
365 self.channels.iter().all(|c| c.is_none())
366 }
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct Metadata {
372 pub name: String,
373 pub author: String,
374 pub version: String,
375}
376
377impl Default for Metadata {
378 fn default() -> Self {
379 Self {
380 name: "untitled".into(),
381 author: String::new(),
382 version: "0.1.0".into(),
383 }
384 }
385}
386
387#[derive(Clone, Serialize, Deserialize)]
389pub struct Assets {
390 pub meta: Metadata,
391 pub sprites: SpriteSheet,
392 pub map: MapData,
393 pub sfx: Vec<Sfx>,
394 pub music: Vec<MusicPattern>,
395 #[serde(with = "crate::wire::pixel_rows_opt", default)]
397 pub label: Option<Vec<u8>>,
398}
399
400impl Default for Assets {
401 fn default() -> Self {
402 Self {
403 meta: Metadata::default(),
404 sprites: SpriteSheet::default(),
405 map: MapData::default(),
406 sfx: vec![Sfx::default(); SFX_COUNT],
407 music: vec![MusicPattern::default(); MUSIC_COUNT],
408 label: None,
409 }
410 }
411}
412
413pub fn validate(assets: &Assets) -> Result<()> {
420 if assets.sprites.pixels.len() != SHEET_W * SHEET_H
421 || assets.sprites.flags.len() != SPRITE_COUNT
422 || assets.map.tiles.len() != MAP_W * MAP_H
423 || assets.sfx.len() != SFX_COUNT
424 || assets.music.len() != MUSIC_COUNT
425 {
426 bail!("Cart assets have invalid dimensions");
427 }
428 if let Some(label) = &assets.label {
429 if label.len() != SHEET_W * SHEET_H {
430 bail!("Cart label has invalid dimensions");
431 }
432 }
433 Ok(())
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn validate_accepts_default_and_rejects_bad_dimensions() {
442 assert!(validate(&Assets::default()).is_ok());
443
444 let mut a = Assets::default();
445 a.sprites.pixels.truncate(10);
446 assert!(validate(&a).is_err(), "short sprite sheet must be rejected");
447
448 let mut a = Assets::default();
449 a.sfx.pop();
450 assert!(validate(&a).is_err(), "missing sfx slot must be rejected");
451
452 let a = Assets {
453 label: Some(vec![0; 8]),
454 ..Default::default()
455 };
456 assert!(validate(&a).is_err(), "wrong-size label must be rejected");
457
458 let a = Assets {
459 label: Some(vec![0; SHEET_W * SHEET_H]),
460 ..Default::default()
461 };
462 assert!(validate(&a).is_ok(), "correctly-sized label is allowed");
463 }
464
465 #[test]
466 fn assets_json_roundtrip() {
467 let mut a = Assets::default();
468 a.meta.name = "test cart".into();
469 a.sprites.set(3, 4, 9);
470 a.sprites.set_flag(1, 2, true);
471 a.map.set(10, 5, 42);
472 a.sfx[0].notes[0] = Note {
473 pitch: 33,
474 wave: 3,
475 volume: 5,
476 effect: 0,
477 };
478 a.music[0].channels[0] = Some(0);
479
480 let bytes = serde_json::to_vec(&a).unwrap();
481 let b: Assets = serde_json::from_slice(&bytes).unwrap();
482 assert_eq!(b.meta.name, "test cart");
483 assert_eq!(b.sprites.get(3, 4), 9);
484 assert_eq!(b.sprites.flags(1), 0b100);
485 assert_eq!(b.map.get(10, 5), 42);
486 assert_eq!(b.sfx[0].notes[0].pitch, 33);
487 assert_eq!(b.music[0].channels[0], Some(0));
488 }
489
490 #[test]
491 fn note_custom_instrument_flag() {
492 let builtin = Note {
493 wave: 3,
494 ..Default::default()
495 };
496 assert_eq!(builtin.wave_index(), 3);
497 assert_eq!(builtin.instrument(), None);
498
499 let custom = Note {
500 wave: NOTE_CUSTOM_FLAG | 2,
501 ..Default::default()
502 };
503 assert_eq!(custom.wave_index(), 2);
504 assert_eq!(custom.instrument(), Some(2));
505 }
506
507 #[test]
508 fn sfx_filter_byte_decodes() {
509 let mut s = Sfx::default();
510 s.set_filters(0x86);
513 assert!(s.noiz && s.buzz);
514 assert_eq!((s.detune, s.reverb, s.dampen), (1, 2, 1));
515
516 let mut off = Sfx::default();
517 off.set_filters(0x01); assert!(!off.noiz && !off.buzz);
519 assert_eq!((off.detune, off.reverb, off.dampen), (0, 0, 0));
520 }
521
522 #[test]
523 fn sprite_pixel_addresses_sheet() {
524 let mut s = SpriteSheet::default();
525 s.set(8, 8, 12);
527 assert_eq!(s.sprite_pixel(17, 0, 0), 12);
528 assert_eq!(s.sprite_pixel(16, 8, 0), 12);
529 }
530
531 #[test]
532 fn custom_wave_roundtrips_and_defaults_none() {
533 assert!(Sfx::default().custom_wave.is_none());
535
536 let mut a = Assets::default();
537 a.sfx[0].custom_wave = Some(CustomWave {
538 samples: [3; SFX_LEN],
539 bass: true,
540 });
541 let bytes = serde_json::to_vec(&a).unwrap();
542 let b: Assets = serde_json::from_slice(&bytes).unwrap();
543 let w = b.sfx[0].custom_wave.as_ref().expect("wave kept");
544 assert_eq!(w.samples[0], 3);
545 assert!(w.bass);
546 assert!(b.sfx[1].custom_wave.is_none());
547 }
548}