//! Headless runner for testing: `daedalian --headless OUT_DIR SCRIPT...` //! Script tokens: `N` run N frames; `BUTTON:N` hold a button for N frames //! (up down left right a b start select); `shot:NAME` save OUT_DIR/NAME.png. //! All audio is written to OUT_DIR/audio.wav. use crate::machine::*; use std::fs; use std::path::Path; fn crc32(data: &[u8]) -> u32 { let mut crc = 0xFFFF_FFFFu32; for &b in data { crc ^= b as u32; for _ in 0..8 { crc = if crc & 1 != 0 { 0xEDB8_8320 ^ (crc >> 1) } else { crc >> 1 }; } } !crc } fn write_png(path: &Path, w: usize, h: usize, gray: &[u8]) { let mut raw = Vec::new(); for y in 0..h { raw.push(0); raw.extend_from_slice(&gray[y * w..(y + 1) * w]); } // zlib stream with stored (uncompressed) deflate blocks let mut z = vec![0x78, 0x01]; for (i, chunk) in raw.chunks(65535).enumerate() { let last = (i + 1) * 65535 >= raw.len(); z.push(last as u8); z.extend_from_slice(&(chunk.len() as u16).to_le_bytes()); z.extend_from_slice(&(!(chunk.len() as u16)).to_le_bytes()); z.extend_from_slice(chunk); } let (mut a, mut b) = (1u32, 0u32); for &x in &raw { a = (a + x as u32) % 65521; b = (b + a) % 65521; } z.extend_from_slice(&(b << 16 | a).to_be_bytes()); let mut png = b"\x89PNG\r\n\x1a\n".to_vec(); let mut chunk = |t: &[u8], d: &[u8]| { png.extend_from_slice(&(d.len() as u32).to_be_bytes()); let mut td = t.to_vec(); td.extend_from_slice(d); png.extend_from_slice(&td); png.extend_from_slice(&crc32(&td).to_be_bytes()); }; let mut ihdr = Vec::new(); ihdr.extend_from_slice(&(w as u32).to_be_bytes()); ihdr.extend_from_slice(&(h as u32).to_be_bytes()); ihdr.extend_from_slice(&[8, 0, 0, 0, 0]); chunk(b"IHDR", &ihdr); chunk(b"IDAT", &z); chunk(b"IEND", &[]); fs::write(path, png).unwrap(); } fn write_wav(path: &Path, rate: u32, samples: &[f32]) { let mut d = Vec::new(); let data_len = samples.len() as u32 * 2; d.extend_from_slice(b"RIFF"); d.extend_from_slice(&(36 + data_len).to_le_bytes()); d.extend_from_slice(b"WAVEfmt "); d.extend_from_slice(&16u32.to_le_bytes()); d.extend_from_slice(&1u16.to_le_bytes()); d.extend_from_slice(&2u16.to_le_bytes()); d.extend_from_slice(&rate.to_le_bytes()); d.extend_from_slice(&(rate * 4).to_le_bytes()); d.extend_from_slice(&4u16.to_le_bytes()); d.extend_from_slice(&16u16.to_le_bytes()); d.extend_from_slice(b"data"); d.extend_from_slice(&data_len.to_le_bytes()); for s in samples { d.extend_from_slice(&((s.clamp(-1.0, 1.0) * 32767.0) as i16).to_le_bytes()); } fs::write(path, d).unwrap(); } pub fn run(rom: &[u8], args: &[String]) { let out = Path::new(&args[0]); fs::create_dir_all(out).unwrap(); let rate = 44_100; let mut m = Machine::new(rom, rate); let mut audio = Vec::new(); let mut frames = 0u64; let run = |m: &mut Machine, n: u64, audio: &mut Vec, frames: &mut u64| { for _ in 0..n { m.run_frame(); audio.append(&mut m.apu.out); *frames += 1; } }; for tok in &args[1..] { if let Ok(n) = tok.parse::() { run(&mut m, n, &mut audio, &mut frames); } else if let Some(name) = tok.strip_prefix("shot:") { let gray: Vec = m.framebuffer.iter().map(|&s| [0xFF, 0xAA, 0x55, 0x00][s as usize]).collect(); write_png(&out.join(format!("{name}.png")), SCREEN_W, SCREEN_H, &gray); println!("frame {frames}: {name}.png (pc ${:04X})", m.pc); } else if let Some(spec) = tok.strip_prefix("goto:") { // steer the puzzle cursor (wPieceY/wPieceX at $C0A1/$C0A2) to a position let (ty, tx) = spec.split_once(':').unwrap(); let (ty, tx) = (u8::from_str_radix(ty, 16).unwrap(), u8::from_str_radix(tx, 16).unwrap()); for _ in 0..100 { let (cy, cx) = (m.r8(0xC0A1), m.r8(0xC0A2)); let bit = if cy < ty { BTN_DOWN } else if cy > ty { BTN_UP } else if cx < tx { BTN_RIGHT } else if cx > tx { BTN_LEFT } else { break }; m.buttons |= bit; run(&mut m, 1, &mut audio, &mut frames); m.buttons &= !bit; run(&mut m, 5, &mut audio, &mut frames); } } else if let Some(spec) = tok.strip_prefix("peek:") { let (addr, n) = spec.split_once(':').unwrap_or((spec, "1")); let addr = u16::from_str_radix(addr, 16).unwrap(); let vals: Vec = (0..n.parse::().unwrap()) .map(|i| format!("{:02X}", m.r8(addr + i))) .collect(); println!("frame {frames}: ${addr:04X}: {}", vals.join(" ")); } else if let Some((btn, n)) = tok.split_once(':') { let bit = match btn { "up" => BTN_UP, "down" => BTN_DOWN, "left" => BTN_LEFT, "right" => BTN_RIGHT, "a" => BTN_A, "b" => BTN_B, "start" => BTN_START, "select" => BTN_SELECT, _ => panic!("unknown token {tok}"), }; m.buttons |= bit; run(&mut m, n.parse().unwrap(), &mut audio, &mut frames); m.buttons &= !bit; } else { panic!("unknown token {tok}"); } } write_wav(&out.join("audio.wav"), rate, &audio); println!("{frames} frames, {:.1}s of audio", audio.len() as f32 / 2.0 / rate as f32); }