use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
pub const SPRITE_SIZE: usize = 8;
pub const SHEET_W: usize = 128;
pub const SHEET_H: usize = 128;
pub const SPRITES_PER_ROW: usize = SHEET_W / SPRITE_SIZE;
pub const SPRITE_COUNT: usize = 256;
pub const MAP_W: usize = 128;
pub const MAP_H: usize = 64;
pub const SFX_COUNT: usize = 64;
pub const MUSIC_COUNT: usize = 64;
pub const SFX_LEN: usize = 32;
pub const CHANNELS: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SpriteId(pub u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SfxId(pub u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MusicId(pub u8);
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpriteSheet {
#[serde(with = "crate::wire::pixel_rows")]
pub pixels: Vec<u8>,
#[serde(with = "crate::wire::hex_string")]
pub flags: Vec<u8>,
}
impl Default for SpriteSheet {
fn default() -> Self {
Self {
pixels: vec![0; SHEET_W * SHEET_H],
flags: vec![0; SPRITE_COUNT],
}
}
}
impl SpriteSheet {
pub fn get(&self, x: i32, y: i32) -> u8 {
if (0..SHEET_W as i32).contains(&x) && (0..SHEET_H as i32).contains(&y) {
self.pixels[(y as usize) * SHEET_W + x as usize]
} else {
0
}
}
pub fn set(&mut self, x: i32, y: i32, color: u8) {
if (0..SHEET_W as i32).contains(&x) && (0..SHEET_H as i32).contains(&y) {
self.pixels[(y as usize) * SHEET_W + x as usize] = color & 0x0f;
}
}
pub fn sprite_pixel(&self, n: u32, px: i32, py: i32) -> u8 {
let n = (n as usize) % SPRITE_COUNT;
let sx = (n % SPRITES_PER_ROW * SPRITE_SIZE) as i32 + px;
let sy = (n / SPRITES_PER_ROW * SPRITE_SIZE) as i32 + py;
self.get(sx, sy)
}
pub fn flags(&self, n: u32) -> u8 {
self.flags[(n as usize) % SPRITE_COUNT]
}
pub fn set_flag(&mut self, n: u32, flag: u8, value: bool) {
let f = &mut self.flags[(n as usize) % SPRITE_COUNT];
if value {
*f |= 1 << (flag & 7);
} else {
*f &= !(1 << (flag & 7));
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MapData {
#[serde(with = "crate::wire::tile_rows")]
pub tiles: Vec<u8>,
}
impl Default for MapData {
fn default() -> Self {
Self {
tiles: vec![0; MAP_W * MAP_H],
}
}
}
impl MapData {
pub fn get(&self, x: i32, y: i32) -> u8 {
if (0..MAP_W as i32).contains(&x) && (0..MAP_H as i32).contains(&y) {
self.tiles[(y as usize) * MAP_W + x as usize]
} else {
0
}
}
pub fn set(&mut self, x: i32, y: i32, tile: u8) {
if (0..MAP_W as i32).contains(&x) && (0..MAP_H as i32).contains(&y) {
self.tiles[(y as usize) * MAP_W + x as usize] = tile;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum Waveform {
Triangle = 0,
TiltedSaw = 1,
Saw = 2,
Square = 3,
Pulse = 4,
Organ = 5,
Noise = 6,
Phaser = 7,
}
impl Waveform {
pub fn from_u8(v: u8) -> Self {
match v & 7 {
0 => Self::Triangle,
1 => Self::TiltedSaw,
2 => Self::Saw,
3 => Self::Square,
4 => Self::Pulse,
5 => Self::Organ,
6 => Self::Noise,
_ => Self::Phaser,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum SfxEffect {
None = 0,
Slide = 1,
Vibrato = 2,
Drop = 3,
FadeIn = 4,
FadeOut = 5,
ArpFast = 6,
ArpSlow = 7,
}
impl SfxEffect {
pub fn from_u8(v: u8) -> Self {
match v & 7 {
0 => Self::None,
1 => Self::Slide,
2 => Self::Vibrato,
3 => Self::Drop,
4 => Self::FadeIn,
5 => Self::FadeOut,
6 => Self::ArpFast,
_ => Self::ArpSlow,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "[u8; 4]", from = "[u8; 4]")]
pub struct Note {
pub pitch: u8,
pub wave: u8,
pub volume: u8,
pub effect: u8,
}
pub const NOTE_CUSTOM_FLAG: u8 = 0x08;
impl Note {
pub fn wave_index(&self) -> u8 {
self.wave & 7
}
pub fn instrument(&self) -> Option<u8> {
(self.wave & NOTE_CUSTOM_FLAG != 0).then_some(self.wave & 7)
}
}
impl From<Note> for [u8; 4] {
fn from(n: Note) -> Self {
[n.pitch, n.wave, n.volume, n.effect]
}
}
impl From<[u8; 4]> for Note {
fn from([pitch, wave, volume, effect]: [u8; 4]) -> Self {
Self {
pitch,
wave,
volume,
effect,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomWave {
pub samples: [i8; SFX_LEN],
pub bass: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Sfx {
pub notes: [Note; SFX_LEN],
pub speed: u8,
pub loop_start: u8,
pub loop_end: u8,
pub noiz: bool,
pub buzz: bool,
pub detune: u8,
pub reverb: u8,
pub dampen: u8,
#[serde(default)]
pub custom_wave: Option<CustomWave>,
}
impl Default for Sfx {
fn default() -> Self {
Self {
notes: [Note::default(); SFX_LEN],
speed: 16,
loop_start: 0,
loop_end: 0,
noiz: false,
buzz: false,
detune: 0,
reverb: 0,
dampen: 0,
custom_wave: None,
}
}
}
impl Sfx {
pub fn is_empty(&self) -> bool {
self.notes.iter().all(|n| n.volume == 0)
}
pub fn set_filters(&mut self, byte: u8) {
self.noiz = byte & 2 != 0;
self.buzz = byte & 4 != 0;
self.detune = byte / 8 % 3;
self.reverb = byte / 24 % 3;
self.dampen = byte / 72 % 3;
}
pub fn filters_byte(&self) -> u8 {
let mut byte = 0u8;
if self.noiz {
byte |= 2;
}
if self.buzz {
byte |= 4;
}
byte += self.detune * 8;
byte += self.reverb * 24;
byte += self.dampen * 72;
byte
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MusicPattern {
pub channels: [Option<u8>; CHANNELS],
pub loop_back: bool,
pub loop_start: bool,
pub stop_at_end: bool,
}
impl Default for MusicPattern {
fn default() -> Self {
Self {
channels: [None; CHANNELS],
loop_back: false,
loop_start: false,
stop_at_end: false,
}
}
}
impl MusicPattern {
pub fn is_empty(&self) -> bool {
self.channels.iter().all(|c| c.is_none())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub name: String,
pub author: String,
pub version: String,
}
impl Default for Metadata {
fn default() -> Self {
Self {
name: "untitled".into(),
author: String::new(),
version: "0.1.0".into(),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Assets {
pub meta: Metadata,
pub sprites: SpriteSheet,
pub map: MapData,
pub sfx: Vec<Sfx>,
pub music: Vec<MusicPattern>,
#[serde(with = "crate::wire::pixel_rows_opt", default)]
pub label: Option<Vec<u8>>,
}
impl Default for Assets {
fn default() -> Self {
Self {
meta: Metadata::default(),
sprites: SpriteSheet::default(),
map: MapData::default(),
sfx: vec![Sfx::default(); SFX_COUNT],
music: vec![MusicPattern::default(); MUSIC_COUNT],
label: None,
}
}
}
pub fn validate(assets: &Assets) -> Result<()> {
if assets.sprites.pixels.len() != SHEET_W * SHEET_H
|| assets.sprites.flags.len() != SPRITE_COUNT
|| assets.map.tiles.len() != MAP_W * MAP_H
|| assets.sfx.len() != SFX_COUNT
|| assets.music.len() != MUSIC_COUNT
{
bail!("Cart assets have invalid dimensions");
}
if let Some(label) = &assets.label {
if label.len() != SHEET_W * SHEET_H {
bail!("Cart label has invalid dimensions");
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_accepts_default_and_rejects_bad_dimensions() {
assert!(validate(&Assets::default()).is_ok());
let mut a = Assets::default();
a.sprites.pixels.truncate(10);
assert!(validate(&a).is_err(), "short sprite sheet must be rejected");
let mut a = Assets::default();
a.sfx.pop();
assert!(validate(&a).is_err(), "missing sfx slot must be rejected");
let a = Assets {
label: Some(vec![0; 8]),
..Default::default()
};
assert!(validate(&a).is_err(), "wrong-size label must be rejected");
let a = Assets {
label: Some(vec![0; SHEET_W * SHEET_H]),
..Default::default()
};
assert!(validate(&a).is_ok(), "correctly-sized label is allowed");
}
#[test]
fn assets_json_roundtrip() {
let mut a = Assets::default();
a.meta.name = "test cart".into();
a.sprites.set(3, 4, 9);
a.sprites.set_flag(1, 2, true);
a.map.set(10, 5, 42);
a.sfx[0].notes[0] = Note {
pitch: 33,
wave: 3,
volume: 5,
effect: 0,
};
a.music[0].channels[0] = Some(0);
let bytes = serde_json::to_vec(&a).unwrap();
let b: Assets = serde_json::from_slice(&bytes).unwrap();
assert_eq!(b.meta.name, "test cart");
assert_eq!(b.sprites.get(3, 4), 9);
assert_eq!(b.sprites.flags(1), 0b100);
assert_eq!(b.map.get(10, 5), 42);
assert_eq!(b.sfx[0].notes[0].pitch, 33);
assert_eq!(b.music[0].channels[0], Some(0));
}
#[test]
fn note_custom_instrument_flag() {
let builtin = Note {
wave: 3,
..Default::default()
};
assert_eq!(builtin.wave_index(), 3);
assert_eq!(builtin.instrument(), None);
let custom = Note {
wave: NOTE_CUSTOM_FLAG | 2,
..Default::default()
};
assert_eq!(custom.wave_index(), 2);
assert_eq!(custom.instrument(), Some(2));
}
#[test]
fn sfx_filter_byte_decodes() {
let mut s = Sfx::default();
s.set_filters(0x86);
assert!(s.noiz && s.buzz);
assert_eq!((s.detune, s.reverb, s.dampen), (1, 2, 1));
let mut off = Sfx::default();
off.set_filters(0x01); assert!(!off.noiz && !off.buzz);
assert_eq!((off.detune, off.reverb, off.dampen), (0, 0, 0));
}
#[test]
fn sprite_pixel_addresses_sheet() {
let mut s = SpriteSheet::default();
s.set(8, 8, 12);
assert_eq!(s.sprite_pixel(17, 0, 0), 12);
assert_eq!(s.sprite_pixel(16, 8, 0), 12);
}
#[test]
fn custom_wave_roundtrips_and_defaults_none() {
assert!(Sfx::default().custom_wave.is_none());
let mut a = Assets::default();
a.sfx[0].custom_wave = Some(CustomWave {
samples: [3; SFX_LEN],
bass: true,
});
let bytes = serde_json::to_vec(&a).unwrap();
let b: Assets = serde_json::from_slice(&bytes).unwrap();
let w = b.sfx[0].custom_wave.as_ref().expect("wave kept");
assert_eq!(w.samples[0], 3);
assert!(w.bass);
assert!(b.sfx[1].custom_wave.is_none());
}
}