use super::{hex, hex_bytes, music_from_mem, sfx_from_mem, SFX_MEM_LEN};
use crate::{
assets::{SHEET_H, SHEET_W},
clipboard::{tagged, Pasted, PixelRect, SfxClip, Slotted},
};
use anyhow::{bail, Result};
pub fn parse_clipboard(text: &str) -> Result<Pasted> {
if let Some(inner) = tagged(text, "gfx") {
return Ok(Pasted::Sprites {
rect: parse_gfx(inner)?,
flags: None,
});
}
if let Some(inner) = tagged(text, "sfx") {
return Ok(Pasted::Sfx(parse_sfx(inner)?));
}
bail!("no sprite or sound on the clipboard");
}
fn parse_gfx(inner: &str) -> Result<PixelRect> {
let d: Vec<u8> = inner.trim().chars().map(|c| hex(c).unwrap_or(0)).collect();
if d.len() < 4 {
bail!("clipboard sprite data is truncated");
}
let w = (d[0] << 4 | d[1]) as usize;
let h = (d[2] << 4 | d[3]) as usize;
if w == 0 || h == 0 || w > SHEET_W || h > SHEET_H {
bail!("clipboard sprite size {w}x{h} is out of range");
}
if d.len() < 4 + w * h {
bail!("clipboard sprite data is truncated");
}
Ok(PixelRect {
w,
h,
pixels: d[4..4 + w * h].to_vec(),
})
}
fn parse_sfx(inner: &str) -> Result<SfxClip> {
let b = hex_bytes(inner);
if b.len() < 2 {
bail!("clipboard SFX data is truncated");
}
let rec = 1 + SFX_MEM_LEN; let count = (b[0] as usize).min(b.len().saturating_sub(2) / rec);
if count == 0 && b[2..].len() < 4 {
bail!("clipboard SFX data is empty");
}
let mut records = Vec::with_capacity(count);
for i in 0..count {
let off = 2 + i * rec;
records.push(Slotted {
src: b[off],
value: sfx_from_mem(&b[off + 1..off + 1 + SFX_MEM_LEN]),
});
}
let patterns = b[2 + count * rec..]
.chunks(4)
.filter(|c| c.len() == 4)
.map(|c| music_from_mem([c[0], c[1], c[2], c[3]]))
.collect();
Ok(SfxClip { records, patterns })
}
#[cfg(test)]
mod tests {
use super::*;
const MUSIC: &str = "[sfx]020108090a0900090a0900090a0900090a0000090a0900090a0900090a0900090\
a0900090a0900090a0900090a0900090a0900090a0900090a0900090a0900090a09000008002009090\
a0e000e0a0e00100a1000150a1000180a1000130a1000150a1000100a1000130a10000e0a1000100a1\
000130a1000090a10000e0a0d00100a0000150a100000080000080943440[/sfx]";
#[test]
fn gfx_header_then_nibbles() {
let p = parse_clipboard("[gfx]0202abcd[/gfx]").unwrap();
let Pasted::Sprites { rect: r, .. } = p else {
panic!("not sprites")
};
assert_eq!((r.w, r.h), (2, 2));
assert_eq!(r.pixels, vec![0xa, 0xb, 0xc, 0xd]);
}
#[test]
fn sfx_records_carry_source_slots() {
let p = parse_clipboard(MUSIC).unwrap();
let Pasted::Sfx(clip) = p else {
panic!("not sfx")
};
assert_eq!(clip.records.len(), 2);
assert_eq!(clip.records[0].src, 8);
assert_eq!(clip.records[1].src, 9);
assert_eq!(clip.patterns.len(), 1);
assert_eq!(clip.patterns[0].channels, [Some(8), Some(9), None, None]);
}
#[test]
fn unknown_clipboard_is_an_error() {
assert!(parse_clipboard("hello world").is_err());
}
#[test]
fn footer_only_sfx_blob_decodes_to_an_empty_pattern() {
let Pasted::Sfx(clip) = parse_clipboard("[sfx]000140404040[/sfx]").unwrap() else {
panic!("not sfx")
};
assert!(clip.records.is_empty());
assert_eq!(clip.patterns.len(), 1);
assert_eq!(clip.patterns[0].channels, [None, None, None, None]);
}
}