use std::fs;
use std::ops::Range;
use std::path::Path;
use anyhow::{Context, bail, ensure};
use crate::graphics::{cheep_cheep_frames, mode7, podium_mode7};
use crate::maps::{THEMES, bytes, long, near, stream, word};
use crate::rom::{Region, Rom};
use crate::textures::{palette, theme_stream, write_png};
const ITEMS: usize = 0x40594;
const ITEM_SETS: [(&str, &[usize], u16); 6] = [
("banana", &[0], 6),
("green-shell", &[1, 2], 6),
("red-shell", &[1, 2], 5),
("poison-mushroom", &[3], 5),
("fireball", &[4], 6),
("egg", &[5], 5),
];
const HAZARD_TILES: usize = 2;
const HAZARD_BASE: usize = 0xC0;
const BLANK: u16 = 0x84;
const GHOST_VALLEY: usize = 0;
const MARIO_CIRCUIT: usize = 1;
const DONUT_PLAINS: usize = 2;
const CHOCO_ISLAND: usize = 3;
const KOOPA_BEACH: usize = 5;
const BOWSER_CASTLE: usize = 6;
const PIRANHA: usize = 0;
const THWOMP: usize = 2;
const MOLE: usize = 3;
const PIPE: usize = 5;
const PILLAR: usize = 6;
const SHADOW: usize = 7;
type Scripted = &'static [(usize, u16)];
const HAZARDS: [(&str, usize, usize, Scripted); 7] = [
("pipe", MARIO_CIRCUIT, MARIO_CIRCUIT, &[(PIPE, 0)]),
("orange-pipe", MARIO_CIRCUIT, DONUT_PLAINS, &[(PIPE, 0x400)]),
("pillar", GHOST_VALLEY, GHOST_VALLEY, &[(PILLAR, 0)]),
(
"monty-mole",
DONUT_PLAINS,
DONUT_PLAINS,
&[(MOLE, 0), (MOLE + 1, 0)],
),
(
"piranha-plant",
CHOCO_ISLAND,
CHOCO_ISLAND,
&[(PIRANHA, 0), (PIRANHA + 1, 0)],
),
(
"cheep-cheep",
KOOPA_BEACH,
KOOPA_BEACH,
&[(PIRANHA, 0), (PIRANHA + 1, 0)],
),
(
"thwomp",
BOWSER_CASTLE,
BOWSER_CASTLE,
&[(THWOMP, 0x600), (SHADOW, 0)],
),
];
struct Bank0 {
routines: usize,
scripts: [usize; 8],
drivers: usize,
}
impl Bank0 {
fn of(region: Region) -> Bank0 {
match region {
Region::Japan => Bank0 {
routines: 0xBC34,
scripts: [
0xE0E1, 0xE0FD, 0xE2E8, 0xE3E1, 0xE3FD, 0xE5A7, 0xE71C, 0xED31,
],
drivers: 0x9069,
},
Region::Usa => Bank0 {
routines: 0xBC0F,
scripts: [
0xE0BC, 0xE0D8, 0xE2C3, 0xE3BC, 0xE3D8, 0xE582, 0xE716, 0xED2B,
],
drivers: 0x9090,
},
Region::Europe => Bank0 {
routines: 0xBC34,
scripts: [
0xE0E1, 0xE0FD, 0xE2E8, 0xE3E1, 0xE3FD, 0xE5A7, 0xE73B, 0xED50,
],
drivers: 0x9077,
},
}
}
}
pub const DRIVERS: [(&str, u16); 8] = [
("mario", 1),
("luigi", 2),
("bowser", 0),
("peach", 3),
("dk-jr", 3),
("koopa-troopa", 2),
("toad", 1),
("yoshi", 0),
];
const LUIGI: usize = 1;
const SCALES: usize = 11;
const ANGLES: usize = 16;
const SPECIALS: [&str; 5] = ["cheer-back", "look-back", "look-side", "cheer", "dazed"];
const FRAME_TABLE: usize = 0x16A;
const LUIGI_TABLE: usize = 0x202;
const LUIGI_TOPS: usize = 0x36C;
const LUIGI_BANK: usize = 0x84;
const MARIO_BANK: usize = 0xC0;
const RACE_SPRITES: usize = 0x10000;
const LAKITU: usize = 0x5E264;
const LAKITU_FRAMES: usize = 14;
pub const LAKITU_THEME: usize = MARIO_CIRCUIT;
const LAP_SIGN: usize = 11;
const LAP_DIGIT: u16 = 0xA1;
fn podium_heads(region: Region) -> usize {
match region {
Region::Usa => 0x59EEF,
Region::Japan => 0x59EB2,
Region::Europe => 0x59EF9,
}
}
const PODIUM_OBJ: usize = 0x2800;
const HEAD_TILES: [usize; 8] = [0x140, 0x160, 0x144, 0x148, 0x14C, 0x164, 0x168, 0x16C];
fn podium_tables(region: Region) -> (usize, PodiumTables) {
match region {
Region::Usa => (0x5A330, PODIUM_TABLES),
Region::Japan => (0x5A2DE, PODIUM_TABLES_JAPAN),
Region::Europe => (0x5A33A, PODIUM_TABLES),
}
}
type PodiumTables = &'static [(usize, &'static [usize])];
const PODIUM_TABLES: PodiumTables = &[
(0, &[94]),
(0, &[90]),
(0, &[82, 82, 83, 84]),
(2, &[94]),
(2, &[90]),
(3, &[94]),
(4, &[94, 94, 94, 94, 84]),
(4, &[90]),
(1, &[94]),
(1, &[90]),
(1, &[90, 82, 84]),
(5, &[94]),
(5, &[90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 84]),
(6, &[94]),
(6, &[90]),
(7, &[94]),
];
const PODIUM_TABLES_JAPAN: PodiumTables = &[
(0, &[94]),
(0, &[90]),
(0, &[82, 82, 83, 84]),
(2, &[94]),
(2, &[90]),
(2, &[82]),
(3, &[94, 94, 92]),
(3, &[90]),
(4, &[94, 94, 94, 94, 84]),
(4, &[90]),
(1, &[94]),
(1, &[90]),
(1, &[90, 82, 84]),
(5, &[94]),
(5, &[90, 90, 90, 90, 90, 90, 90, 90, 90, 90, 84]),
(6, &[94]),
(6, &[90]),
(7, &[94]),
];
fn big(slot: usize) -> bool {
matches!(slot, 80..=83 | 90 | 91 | 94..)
}
fn podium_singles(region: Region) -> [(&'static str, usize, usize, bool); 4] {
let [cups, trophies, confetti, pop] = match region {
Region::Usa => [0x5A040, 0x5A32A, 0x5AB43, 0x5AB89],
Region::Japan => [0x59FCE, 0x5A2D8, 0x5AB1F, 0x5AB65],
Region::Europe => [0x5A04A, 0x5A334, 0x5AB4D, 0x5AB93],
};
[
("cups", cups, 3, true),
("trophies", trophies, 3, true),
("confetti", confetti, 18, false),
("pop", pop, 2, true),
]
}
const HIDDEN: i32 = 0xE0;
const EFFECT_TILES: usize = 0x903;
fn dust_tiles(region: Region) -> usize {
match region {
Region::Usa => 0x49C19,
Region::Japan | Region::Europe => 0x49C1C,
}
}
const EFFECT_TABLES: usize = 0x5EE00;
const EFFECTS: [(&str, usize, u16, Range<usize>); 11] = [
("dust", 5, 0x100, ALL),
("smoke", 5, 0x500, ALL),
("dust-vs-ghost", 0, 0x100, 0..48),
("smoke-vs-ghost", 0, 0x500, 0..48),
("dust-small", 0, 0x100, 48..ALL.end),
("smoke-small", 0, 0x500, 48..ALL.end),
("spray", 1, 0, ALL),
("splash", 2, 0, ALL),
("grass", 3, 0, ALL),
("smoke-puff", 4, 0x100, ALL),
("dust-puff", 4, 0x500, ALL),
];
const ALL: Range<usize> = 0..usize::MAX;
#[derive(Clone, Copy)]
pub struct Sprite {
pub x: i32,
pub y: i32,
pub tile: u16,
pub big: bool,
}
pub fn items(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let tiles = stream(&rom.data, ITEMS)?;
ensure!(tiles.len() == 6 * 640, "item graphics are not 6 items");
let palette = palette(rom, MARIO_CIRCUIT)?;
for (name, blocks, pal) in ITEM_SETS {
let mut frames = Vec::new();
for block in blocks {
for k in 0..8 {
let tile = (block * 640 / 32 + if k < 4 { k * 4 } else { 16 + k - 4 }) as u16;
let sprite = |x, y, tile| Sprite {
x,
y,
tile: tile | pal << 9,
big: false,
};
frames.push(if k < 4 {
vec![
sprite(0, 0, tile),
sprite(8, 0, tile + 1),
sprite(0, 8, tile + 2),
sprite(8, 8, tile + 3),
]
} else {
vec![sprite(0, 0, tile)]
});
}
}
write_frames(
&folder.join("sprites/items").join(name),
&frames,
&tiles,
&palette,
)?;
}
Ok(())
}
pub fn hazards(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let Bank0 {
routines, scripts, ..
} = Bank0::of(rom.region);
for (name, graphics, colours, uses) in HAZARDS {
let mut frames = Vec::new();
for &(script, xor) in uses {
frames.extend(script_frames(&rom.data, routines, scripts[script], xor)?);
}
write_frames(
&folder.join("sprites/hazards").join(name),
&frames,
&theme_stream(rom, HAZARD_TILES, graphics)?,
&palette(rom, colours)?,
)?;
}
Ok(())
}
pub fn race_tiles(rom: &Rom) -> anyhow::Result<Vec<u8>> {
let stream = stream(&rom.data, RACE_SPRITES)?;
ensure!(stream.len() >= 144 * 32, "race sprites stop short");
let mut tiles = vec![0; 0x100 * 32];
tiles[0x40 * 32..0xC0 * 32].copy_from_slice(&stream[16 * 32..144 * 32]);
tiles[0xFB * 32..0xFF * 32].copy_from_slice(&stream[12 * 32..16 * 32]);
Ok(tiles)
}
pub fn cast(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let mut frames = (0..LAKITU_FRAMES)
.map(|k| metasprite(&rom.data, LAKITU + word(&rom.data, LAKITU + k * 2)?))
.collect::<anyhow::Result<Vec<_>>>()?;
for lap in 2..=4 {
let mut sign = frames[LAP_SIGN].clone();
let digit = sign.last_mut().context("the LAP sign has no sprites")?;
digit.tile = digit.tile & 0xFF00 | (LAP_DIGIT + lap);
frames.push(sign);
}
write_frames(
&folder.join("sprites/cast/lakitu"),
&frames,
&race_tiles(rom)?,
&palette(rom, LAKITU_THEME)?,
)?;
let (podium, tiles, colours) = podium_mode7(rom)?;
let objects = podium_tiles(rom, &podium)?;
for (driver, frames) in podium_frames(rom)?.iter().enumerate() {
write_frames(
&folder.join("sprites/cast/podium").join(DRIVERS[driver].0),
frames,
&objects,
&colours,
)?;
}
for (name, at, count, big) in podium_singles(rom.region) {
let frames = (0..count)
.map(|k| {
let tile = word(&rom.data, at + k * 2)? as u16;
Ok(vec![Sprite {
x: 0,
y: 0,
tile,
big,
}])
})
.collect::<anyhow::Result<Vec<_>>>()?;
write_frames(
&folder.join("sprites/cast/podium").join(name),
&frames,
&objects,
&colours,
)?;
}
let dir = folder.join("sprites/cast/cheep-cheep");
fs::create_dir_all(&dir)?;
for (i, pieces) in cheep_cheep_frames(rom)?.iter().enumerate() {
let (width, height, pixels) = mode7(&tiles, pieces);
write_png(
&dir.join(format!("{i:03}.png")),
width,
height,
&colours,
&pixels,
)?;
}
Ok(())
}
fn podium_tiles(rom: &Rom, podium: &[u8]) -> anyhow::Result<Vec<u8>> {
let mut tiles = vec![0; 0x200 * 32];
tiles[..PODIUM_OBJ].copy_from_slice(&podium[..PODIUM_OBJ]);
let heads = podium_heads(rom.region);
for (driver, tile) in HEAD_TILES.into_iter().enumerate() {
let at = long(&rom.data, heads + driver * 3)?;
let second = if driver == LUIGI { 0x2100 } else { 0x5900 };
for row in 0..2 {
for (k, head) in [0x40, second].into_iter().enumerate() {
let to = (tile + row * 16 + k * 2) * 32;
tiles[to..to + 0x40].copy_from_slice(bytes(
&rom.data,
at + head + row * 0x200,
0x40,
)?);
}
}
}
Ok(tiles)
}
fn podium_frames(rom: &Rom) -> anyhow::Result<Vec<Vec<Vec<Sprite>>>> {
let (mut at, tables) = podium_tables(rom.region);
let mut frames = vec![Vec::new(); DRIVERS.len()];
for &(driver, slots) in tables {
let mut pointers: Vec<usize> = Vec::new();
while pointers.iter().all(|&p| at + pointers.len() * 2 < p) {
ensure!(pointers.len() < 16, "podium table at {at:#X} runs long");
pointers.push(near(&rom.data, at + pointers.len() * 2)?);
}
let mut seen = Vec::new();
for (k, &pointer) in pointers.iter().enumerate() {
let slot = slots[k.min(slots.len() - 1)];
let mut sprites = Vec::new();
let mut entry = pointer;
while word(&rom.data, entry)? != 0xFFFF {
let [x, y, tile, attr] = bytes(&rom.data, entry, 4)?.try_into()?;
if i32::from(y) < HIDDEN {
sprites.push(Sprite {
x: i32::from(x),
y: i32::from(y),
tile: u16::from(tile) | u16::from(attr) << 8,
big: big(slot + (entry - pointer) / 4),
});
}
entry += 4;
}
at = at.max(entry + 2);
if !seen.contains(&pointer) {
seen.push(pointer);
frames[driver].push(sprites);
}
}
}
Ok(frames)
}
pub fn effects(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let mut tiles = vec![0; 0x200 * 32];
let water = stream(&rom.data, EFFECT_TILES)?;
let dust = stream(&rom.data, dust_tiles(rom.region))?;
ensure!(
water.len() <= 0x100 * 32 && dust.len() <= 0x100 * 32,
"effect tiles run long"
);
tiles[..water.len()].copy_from_slice(&water);
tiles[0x100 * 32..0x100 * 32 + dust.len()].copy_from_slice(&dust);
let tables = stream(&rom.data, EFFECT_TABLES)?;
let mut starts = vec![0];
for _ in 1..6 {
let table = starts[starts.len() - 1];
let mut last = 0;
for k in 0..word(&tables, table)? / 2 {
last = last.max(word(&tables, table + k * 2)?);
}
let count = usize::from(bytes(&tables, table + last, 1)?[0]);
starts.push(table + last + 1 + count * 4 + count / 4 + 1);
}
let palettes = (0..THEMES.len())
.map(|theme| palette(rom, theme))
.collect::<anyhow::Result<Vec<_>>>()?;
for (name, table, xor, range) in EFFECTS {
let start = starts[table];
let mut frames = Vec::new();
for k in range.start..range.end.min(word(&tables, start)? / 2) {
let mut sprites = metasprite(&tables, start + word(&tables, start + k * 2)?)?;
for sprite in &mut sprites {
sprite.tile ^= xor;
}
frames.push(draw(&sprites, &tiles)?);
}
let colours = |palette: &[u8]| -> Vec<u8> {
frames
.iter()
.flat_map(|(_, _, pixels)| pixels)
.filter(|&&p| p != 0)
.flat_map(|&p| palette[usize::from(p) * 3..usize::from(p) * 3 + 3].to_vec())
.collect()
};
let same = palettes
.iter()
.all(|p| colours(p) == colours(&palettes[MARIO_CIRCUIT]));
let dir = folder.join("sprites/effects").join(name);
let themes: Vec<_> = if same {
vec![(dir, &palettes[MARIO_CIRCUIT])]
} else {
THEMES
.iter()
.zip(&palettes)
.map(|(theme, palette)| (dir.join(theme), palette))
.collect()
};
for (dir, palette) in themes {
fs::create_dir_all(&dir)?;
for (i, (width, height, pixels)) in frames.iter().enumerate() {
write_png(
&dir.join(format!("{i:03}.png")),
*width,
*height,
palette,
pixels,
)?;
}
}
}
Ok(())
}
fn metasprite(data: &[u8], at: usize) -> anyhow::Result<Vec<Sprite>> {
let count = usize::from(bytes(data, at, 1)?[0]);
let sizes = bytes(data, at + 1 + count * 4, count.div_ceil(4))?;
(0..count)
.map(|i| {
let [x, y, tile, attr] = bytes(data, at + 1 + i * 4, 4)?.try_into()?;
Ok(Sprite {
x: i32::from(x as i8),
y: i32::from(y as i8),
tile: u16::from(tile) | u16::from(attr) << 8,
big: sizes[i / 4] >> (i % 4 * 2 + 1) & 1 != 0,
})
})
.collect()
}
pub fn drivers(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let codes = Bank0::of(rom.region).drivers;
let palette = palette(rom, MARIO_CIRCUIT)?;
for (driver, (name, pal)) in DRIVERS.into_iter().enumerate() {
let dir = folder.join("sprites/drivers").join(name);
let mut frames = Vec::new();
for scale in 0..SCALES {
let mut seen = Vec::new();
for angle in 0..ANGLES {
let code = scale * ANGLES + angle;
if !seen.contains(&rom.data[codes + code * 2]) {
seen.push(rom.data[codes + code * 2]);
frames.push((code, format!("scale-{scale:02}/angle-{angle:02}.png")));
}
}
}
for (k, special) in SPECIALS.iter().enumerate() {
frames.push((SCALES * ANGLES + k, format!("{special}.png")));
}
for (code, file) in frames {
let path = dir.join(file);
fs::create_dir_all(path.parent().unwrap())?;
let (tiles, layout) = slot(&rom.data, codes, driver, code)?;
let mut sprites = sprites_of(layout)?;
for sprite in &mut sprites {
sprite.tile |= pal << 9;
}
let (width, height, pixels) = draw(&sprites, &tiles)?;
write_png(&path, width, height, &palette, &pixels)?;
}
}
Ok(())
}
fn slot(data: &[u8], codes: usize, driver: usize, code: usize) -> anyhow::Result<(Vec<u8>, u8)> {
let frame = word(
data,
codes + FRAME_TABLE + usize::from(data[codes + code * 2]),
)?;
let mut layout = data[codes + code * 2 + 1];
let luigi = word(data, codes + LUIGI_TABLE + code * 2)?;
if luigi & 0x8200 == 0x200 {
layout = 0x44;
}
let at = |bank: usize, address: usize| (bank & 0x3F) << 16 | address;
let rows: Vec<(usize, usize)> = if driver == LUIGI && luigi & 0x8000 == 0 {
let top = word(data, codes + LUIGI_TOPS + (luigi & 0xFF))?;
let size = if luigi & 0x200 != 0 { 0x40 } else { 0x80 };
let mut rows = vec![
(at(LUIGI_BANK, top), size),
(at(LUIGI_BANK, top + 0x200), size),
];
if luigi & 0x400 == 0 {
rows.push((at(MARIO_BANK, frame + 0x400), size));
rows.push((at(MARIO_BANK, frame + 0x600), size));
}
rows
} else {
let bank = word(
data,
codes - 16 + if driver == LUIGI { 0 } else { driver * 2 },
)?;
(0..usize::from(layout & 15))
.map(|r| (at(bank, frame + r * 0x200), usize::from(layout & 0xF0)))
.collect()
};
let mut slot = vec![0; 4 * 0x200];
for (r, (source, size)) in rows.into_iter().enumerate() {
slot[r * 0x200..r * 0x200 + size].copy_from_slice(bytes(data, source, size)?);
}
Ok((slot, layout))
}
fn sprites_of(layout: u8) -> anyhow::Result<Vec<Sprite>> {
let s = |x, y, tile, big| Sprite { x, y, tile, big };
Ok(match layout {
0x84 => vec![
s(0, 0, 0, true),
s(16, 0, 2, true),
s(0, 16, 32, true),
s(16, 16, 34, true),
],
0x82 => vec![s(0, 0, 0, true), s(16, 0, 2, true)],
0x44 => vec![
s(0, 0, 0, true),
s(16, 0, 0x4000, true),
s(0, 16, 32, true),
s(16, 16, 0x4000 | 32, true),
],
0x63 => vec![
s(0, 0, 0, true),
s(8, 0, 1, true),
s(0, 8, 16, true),
s(8, 8, 17, true),
],
0x43 => vec![
s(0, 0, 0, true),
s(7, 0, 0x4000, true),
s(0, 8, 16, true),
s(7, 8, 0x4000 | 16, true),
],
0x42 => vec![s(0, 0, 0, true)],
0x41 => vec![s(0, 0, 0, false), s(0, 8, 1, false)],
0x21 => vec![s(0, 0, 0, false)],
_ => bail!("unknown driver layout {layout:#X}"),
})
}
fn script_frames(
data: &[u8],
routines: usize,
script: usize,
xor: u16,
) -> anyhow::Result<Vec<Vec<Sprite>>> {
let mut frames = Vec::new();
let mut last = None;
for size in 0..11 {
let record = word(data, script + 4 + size * 2)?;
if last.replace(record) == Some(record) {
continue;
}
let mut sprites = frame(data, routines, record)?;
sprites.retain(|s| s.tile & 0x1FF != BLANK);
for sprite in &mut sprites {
let tile = usize::from(sprite.tile & 0x1FF)
.checked_sub(HAZARD_BASE)
.with_context(|| {
format!("frame at {record:#X} uses a tile outside the hazard's")
})?;
sprite.tile = (sprite.tile & !0x1FF | tile as u16) ^ xor;
}
if !sprites.is_empty() {
frames.push(sprites);
}
}
Ok(frames)
}
fn frame(data: &[u8], routines: usize, record: usize) -> anyhow::Result<Vec<Sprite>> {
let routine = word(data, record)?;
let (count, big) = match routine.wrapping_sub(routines) {
0x00 | 0x08 => (2, true),
0x0D | 0x15 => (2, false),
0x6F | 0x77 => (4, true),
0x7C | 0x84 => (4, false),
0x147 | 0x14F | 0x157 => (1, true),
0x15F | 0x167 | 0x16F => (1, false),
0xAD4 => return Ok(Vec::new()),
_ => bail!("frame at {record:#X} uses unknown routine {routine:#X}"),
};
if count == 1 {
let tile = word(data, record + 2)? as u16;
return Ok(vec![Sprite {
x: 0,
y: 0,
tile,
big,
}]);
}
let (offsets, tiles) = (word(data, record + 2)?, word(data, record + 4)?);
(0..count)
.map(|i| {
Ok(Sprite {
x: i32::from(word(data, offsets + i * 4 + 2)? as i16),
y: i32::from(word(data, offsets + i * 4)? as i16),
tile: word(data, tiles + i * 2)? as u16,
big,
})
})
.collect()
}
fn write_frames(
dir: &Path,
frames: &[Vec<Sprite>],
tiles: &[u8],
palette: &[u8],
) -> anyhow::Result<()> {
fs::create_dir_all(dir)?;
for (i, sprites) in frames.iter().enumerate() {
let (width, height, pixels) = draw(sprites, tiles)?;
write_png(
&dir.join(format!("{i:03}.png")),
width,
height,
palette,
&pixels,
)?;
}
Ok(())
}
pub fn draw(sprites: &[Sprite], tiles: &[u8]) -> anyhow::Result<(u32, u32, Vec<u8>)> {
let size = |s: &Sprite| if s.big { 16 } else { 8 };
let left = sprites.iter().map(|s| s.x).min().unwrap_or(0);
let top = sprites.iter().map(|s| s.y).min().unwrap_or(0);
let width = sprites.iter().map(|s| s.x + size(s)).max().unwrap_or(0) - left;
let height = sprites.iter().map(|s| s.y + size(s)).max().unwrap_or(0) - top;
let mut pixels = vec![0; (width * height) as usize];
for s in sprites {
let n = size(s);
let colour = 128 + (s.tile >> 9 & 7) as u8 * 16;
for y in 0..n {
for x in 0..n {
let sx = if s.tile & 0x4000 != 0 { n - 1 - x } else { x };
let sy = if s.tile & 0x8000 != 0 { n - 1 - y } else { y };
let tile = usize::from(s.tile & 0x1FF) + (sx / 8 + sy / 8 * 16) as usize;
let row = bytes(tiles, tile * 32 + (sy % 8) as usize * 2, 18)?;
let bit = 7 - sx % 8;
let v = (row[0] >> bit & 1)
| (row[1] >> bit & 1) << 1
| (row[16] >> bit & 1) << 2
| (row[17] >> bit & 1) << 3;
let at = ((s.y - top + y) * width + s.x - left + x) as usize;
if v != 0 && pixels[at] == 0 {
pixels[at] = colour + v;
}
}
}
}
Ok((width as u32, height as u32, pixels))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draw_flips_crops_and_keeps_the_first_sprite() {
let mut tiles = vec![0; 64];
tiles[0] = 0x80;
tiles[32..34].copy_from_slice(&[0x80, 0x80]);
tiles[48..50].copy_from_slice(&[0x80, 0x80]);
let sprites = [
Sprite {
x: -8,
y: -8,
tile: 0xC400,
big: false,
},
Sprite {
x: -8,
y: -8,
tile: 0x0001,
big: false,
},
Sprite {
x: 0,
y: 0,
tile: 0x0001,
big: false,
},
];
let (width, height, pixels) = draw(&sprites, &tiles).unwrap();
assert_eq!((width, height), (16, 16));
assert_eq!(pixels[7 * 16 + 7], 128 + 32 + 1);
assert_eq!(pixels[0], 128 + 15);
assert_eq!(pixels[8 * 16 + 8], 128 + 15);
assert_eq!(pixels.iter().filter(|&&p| p != 0).count(), 3);
}
#[test]
fn smk_rom_sprites() {
let Ok(path) = std::env::var("SMK_ROM") else {
return;
};
let rom = Rom::parse(fs::read(path).unwrap()).unwrap();
let folder = std::env::temp_dir().join(format!("desmack-sprites-{}", std::process::id()));
drivers(&rom, &folder).unwrap();
items(&rom, &folder).unwrap();
hazards(&rom, &folder).unwrap();
cast(&rom, &folder).unwrap();
effects(&rom, &folder).unwrap();
let count = |dir: &str| {
fs::read_dir(folder.join("sprites").join(dir))
.unwrap()
.count()
};
assert_eq!(count("drivers/luigi"), SCALES + SPECIALS.len());
assert_eq!(count("drivers/luigi/scale-00"), 12);
assert_eq!(count("drivers/luigi/scale-10"), 2);
assert_eq!(count("cast/lakitu"), LAKITU_FRAMES + 3);
assert_eq!(count("cast/cheep-cheep"), 2);
assert_eq!(count("cast/podium"), DRIVERS.len() + 4);
assert_eq!(count("cast/podium/mario"), 2 + 3 + 7);
assert_eq!(count("cast/podium/confetti"), 18);
assert_eq!(count("effects/smoke"), 48);
assert_eq!(count("effects/smoke-small"), 12);
assert_eq!(count("effects/spray"), THEMES.len());
assert_eq!(count("effects/spray/koopa-beach"), 48);
assert_eq!(count("items/green-shell"), 16);
assert_eq!(count("hazards/pipe"), 11);
assert_eq!(count("hazards/piranha-plant"), 20);
assert_eq!(count("hazards/thwomp"), 11 + 5);
fs::remove_dir_all(&folder).unwrap();
}
}