use std::fs;
use std::path::Path;
use anyhow::{Context, ensure};
use crate::decompress::decompress;
use crate::maps::{bytes, stream, word};
use crate::rom::{Region, Rom};
use crate::sprites::{DRIVERS, LAKITU_THEME, Sprite, draw, race_tiles};
use crate::textures::{colours, palette, write_png};
const FONT: usize = 0x70000;
const FONT_PALETTE: usize = 128 + 4 * 16;
fn title(region: Region) -> usize {
match region {
Region::Usa | Region::Europe => 0x71996,
Region::Japan => 0x71C48,
}
}
const TITLE_CGRAM: usize = 0x4000;
const RESULTS_CGRAM: usize = 0x4200;
const TITLE_BG1: usize = 0x2000;
const TITLE_BG2: usize = 0x2800;
fn select_screens(region: Region) -> usize {
match region {
Region::Usa | Region::Europe => 0x70B29,
Region::Japan => 0x70D31,
}
}
const DRIVER_BG1: usize = 0x0000;
const DRIVER_BG2: usize = 0x0800;
const DRIVER_BG3: usize = 0x1800;
const DRIVER_CGRAM: usize = 0x2800;
const DRIVER_TILES: usize = 0x2A00;
const DRIVER_FILL: u16 = 0x3014;
const CUP_MAP: usize = 0x2F00;
const CUP_CGRAM: usize = 0x3700;
const CUP_TILES: usize = 0x3900;
fn cup_add(region: Region) -> u16 {
match region {
Region::Usa | Region::Europe => 0xF440,
Region::Japan => 0x1040,
}
}
const CUP_TILE_COUNT: usize = 96;
const CUP_FONT: usize = 0x40;
fn cup_icons(region: Region) -> usize {
match region {
Region::Usa => 0x4F81F,
Region::Japan => 0x4F697,
Region::Europe => 0x4F738,
}
}
const CUPS: [&str; 3] = ["mushroom", "flower", "star"];
const TROPHIES: usize = 0x14;
const CLASSES: [(&str, usize); 3] = [("100cc", 0), ("50cc", 8), ("150cc", 16)];
const RANKS_WON: [&str; 3] = ["bronze", "silver", "gold"];
const HEIGHT: usize = 224;
const HUD_TILES: usize = 0x112F8;
const HUD_FIRST: usize = 0x80;
const HUD_COLOURS: usize = 32;
const BAR: u16 = 0x3C00;
fn hud_table(region: Region, usa: usize) -> usize {
usa - match region {
Region::Usa => 0,
Region::Japan => 0x144,
Region::Europe => 0x164,
}
}
const BAR_TOP: usize = 0x1B826;
const BOXES: [(&str, usize); 2] = [("player-1", 0x1B836), ("player-2", 0x1B84A)];
const BOX_COLUMN: usize = 18;
const ICONS: usize = 0x1B320;
const ICON_NAMES: [&str; 11] = [
"mushroom",
"feather",
"star",
"banana",
"green-shell",
"red-shell",
"boo",
"coin",
"lightning",
"stolen",
"unused",
];
const EMPTY: [u16; 2] = [0x3CE9, 0x3CE8];
const TIMER_TOP: u16 = 0x80;
const TIMER_BOTTOM: u16 = 0x90;
const MINUTES: u16 = 0x8A;
const SECONDS: u16 = 0x8B;
const BLANK: u16 = 0x8F;
const RANKS: usize = 0x5E770;
const RANK_SETS: [(&str, usize, u8, usize); 2] = [("race", 20, 0xE0, 16), ("finish", 64, 0xC0, 64)];
const COUNTERS: usize = 0x1BB5F;
const COUNTER_DIGITS: usize = 0x1BABF;
const LADDER: usize = 0x30000;
const FACES: [&str; 3] = ["happy", "sad", "neutral"];
const LADDER_DIGIT: u16 = 0x60;
const LADDER_PALETTES: [(&str, u16); 3] = [("", 1), ("-red", 5), ("-orange", 6)];
fn podium(region: Region) -> usize {
match region {
Region::Usa | Region::Europe => 0x737DA,
Region::Japan => 0x73A63,
}
}
const PODIUM_SIZE: usize = 0x4A00;
const PODIUM_BG2_MAP: usize = 0x3800;
const PODIUM_BG2_TILES: usize = 0x4000;
const PODIUM_CGRAM: usize = 0x4800;
const PODIUM_SPLIT: usize = 136;
const PODIUM_MODE7: [(usize, usize, usize, usize); 2] =
[(0x2400, 152, 0, 0xC0), (0x0C00, 64, 160, 0x20)];
fn cheep_cheep(region: Region) -> usize {
match region {
Region::Usa => 0x59F19,
Region::Japan => 0x59EDC,
Region::Europe => 0x59F23,
}
}
const CHEEP_CHEEP_ROWS: usize = 12;
const TAIL: (usize, usize, usize) = (4, 8, 6);
const MODE7_FILL: u8 = 0x6F;
fn banner(region: Region) -> (usize, [(usize, usize); 2]) {
match region {
Region::Usa => (0x59FD3, [(12, 12), (14, 7)]),
Region::Japan => (0x59F96, [(15, 11), (15, 13)]),
Region::Europe => (0x59FDD, [(12, 12), (14, 7)]),
}
}
pub fn font(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let glyphs = stream(&rom.data, FONT)?;
ensure!(glyphs.len() == 256 * 16, "the font is not 256 glyphs");
let palette = colours(bytes(
&stream(&rom.data, title(rom.region))?,
TITLE_CGRAM + FONT_PALETTE * 2,
8,
)?);
let mut pixels = vec![0; 128 * 128];
for glyph in 0..256 {
for y in 0..8 {
let planes = &glyphs[glyph * 16 + y * 2..];
for x in 0..8 {
pixels[(glyph / 16 * 8 + y) * 128 + glyph % 16 * 8 + x] =
(planes[0] >> (7 - x) & 1) | (planes[1] >> (7 - x) & 1) << 1;
}
}
}
let dir = folder.join("graphics/font");
fs::create_dir_all(&dir)?;
write_png(&dir.join("font.png"), 128, 128, &palette, &pixels)
}
pub fn screens(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let dir = folder.join("graphics/screens");
let vram = stream(&rom.data, title(rom.region))?;
write_screen(
&dir,
"title",
&colours(bytes(&vram, TITLE_CGRAM, 512)?),
&[
layer(&vram, &vram[TITLE_BG1..], 1, 4, 0)?,
layer(&vram, &vram[TITLE_BG2..], 2, 4, 0)?,
],
)?;
write_screen(
&dir,
"results",
&colours(bytes(&vram, RESULTS_CGRAM, 512)?),
&[Vec::new(), layer(&vram, &vram[TITLE_BG2..], 2, 4, 0)?],
)?;
let screens = stream(&rom.data, select_screens(rom.region))?;
ensure!(
screens.len() == 0x4000,
"the select screen graphics are not 16 KB"
);
let tiles = &screens[DRIVER_TILES..CUP_MAP];
let fill = [DRIVER_FILL.to_le_bytes(); 32 * 32].concat();
write_screen(
&dir,
"driver-select",
&colours(&screens[DRIVER_CGRAM..DRIVER_TILES]),
&[
layer(tiles, &screens[DRIVER_BG1..], 1, 2, 0)?,
layer(tiles, &screens[DRIVER_BG2..], 1, 2, 32)?,
layer(tiles, &screens[DRIVER_BG3..], 1, 2, 64)?,
layer(tiles, &fill, 1, 2, 96)?,
],
)?;
let font = stream(&rom.data, FONT)?;
let mut tiles = vec![0; 0x400 * 32];
for (tile, planes) in font.chunks(16).enumerate() {
tiles[(CUP_FONT + tile) * 32..][..16].copy_from_slice(planes);
}
for (tile, planes) in screens[CUP_TILES..CUP_TILES + CUP_TILE_COUNT * 16]
.chunks(16)
.enumerate()
{
tiles[tile * 32..][..16].copy_from_slice(planes);
}
let map: Vec<u8> = screens[CUP_MAP..CUP_CGRAM]
.chunks(2)
.flat_map(|e| {
(u16::from_le_bytes([e[0], e[1]]).wrapping_add(cup_add(rom.region))).to_le_bytes()
})
.collect();
let palette = colours(&screens[CUP_CGRAM..CUP_TILES]);
write_screen(
&dir,
"cup-select",
&palette,
&[layer(&tiles, &map, 1, 4, 0)?],
)?;
let icons = folder.join("graphics/icons");
fs::create_dir_all(&icons)?;
let at = cup_icons(rom.region);
let mut names: Vec<(String, usize)> = CUPS
.iter()
.enumerate()
.map(|(k, &cup)| (cup.into(), at + k * 2))
.collect();
for (class, offset) in CLASSES {
for (k, rank) in RANKS_WON.iter().enumerate() {
names.push((format!("{class}-{rank}"), at + TROPHIES + offset + k * 2));
}
}
for (name, at) in names {
let icon = Sprite {
x: 0,
y: 0,
tile: word(&rom.data, at)? as u16,
big: true,
};
let (width, height, pixels) = draw(&[icon], &tiles)?;
write_png(
&icons.join(format!("{name}.png")),
width,
height,
&palette,
&pixels,
)?;
}
let (podium, tiles, palette) = podium_mode7(rom)?;
let (at, places) = banner(rom.region);
let mut pieces = vec![(0, 0, vec![vec![MODE7_FILL; 32]; HEIGHT / 8])];
pieces.extend(banner_pieces(&rom.data, at, 0, places)?);
let (_, _, mut bg1) = mode7(&tiles, &pieces);
bg1[PODIUM_SPLIT * 256..].fill(0);
let mut map = vec![0; PODIUM_SPLIT / 8 * 64];
map.extend(&podium[PODIUM_BG2_MAP + map.len()..PODIUM_BG2_TILES]);
let mut bg2 = layer(&podium[PODIUM_BG2_TILES..PODIUM_CGRAM], &map, 1, 4, 0)?;
bg2[..PODIUM_SPLIT * 256].fill(0);
write_screen(&dir, "podium", &palette, &[bg1, bg2])?;
for rank in 0..3 {
let (width, height, pixels) = mode7(&tiles, &banner_pieces(&rom.data, at, rank, places)?);
let path = dir.join(format!("podium/banner-{}.png", rank + 1));
write_png(&path, width, height, &palette, &pixels)?;
}
Ok(())
}
pub fn podium_mode7(rom: &Rom) -> anyhow::Result<(Vec<u8>, Vec<u8>, Vec<u8>)> {
let podium = decompress(&stream(&rom.data, podium(rom.region))?)?;
ensure!(
podium.len() == PODIUM_SIZE,
"the podium is not {PODIUM_SIZE:#X} bytes"
);
let mut tiles = vec![0; 256 * 64];
for (from, count, first, base) in PODIUM_MODE7 {
for t in 0..count {
let pixels = tile(&podium[from..], t as u16, 4, base)?;
tiles[(first + t) * 64..][..64].copy_from_slice(&pixels);
}
}
let palette = colours(&podium[PODIUM_CGRAM..]);
Ok((podium, tiles, palette))
}
pub fn cheep_cheep_frames(rom: &Rom) -> anyhow::Result<Vec<Vec<Piece>>> {
let at = cheep_cheep(rom.region);
let pointer = |k: usize| word(&rom.data, at + k * 2).map(|p| at & !0xFFFF | p);
let body = mode7_rows(&rom.data, pointer(2)?, CHEEP_CHEEP_ROWS)?;
(0..2)
.map(|k| {
let tail = mode7_rows(&rom.data, pointer(k)?, TAIL.2)?;
Ok(vec![(0, 0, body.clone()), (TAIL.0, TAIL.1, tail)])
})
.collect()
}
fn banner_pieces(
data: &[u8],
at: usize,
rank: usize,
places: [(usize, usize); 2],
) -> anyhow::Result<Vec<Piece>> {
let pointer = |k: usize| word(data, at + k * 2).map(|p| at & !0xFFFF | p);
let [(row, column), (text_row, text_column)] = places;
Ok(vec![
(row, column, mode7_rows(data, pointer(rank)?, 2)?),
(text_row, text_column, mode7_rows(data, pointer(3)?, 2)?),
])
}
pub type Piece = (usize, usize, Vec<Vec<u8>>);
fn mode7_rows(data: &[u8], mut at: usize, count: usize) -> anyhow::Result<Vec<Vec<u8>>> {
(0..count)
.map(|_| {
let rest = data
.get(at..)
.context("a Mode 7 row is past the ROM's end")?;
let len = rest
.iter()
.position(|&b| b == 0xFF)
.context("a Mode 7 row has no end")?;
at += len + 1;
Ok(rest[..len].to_vec())
})
.collect()
}
pub fn mode7(tiles: &[u8], pieces: &[Piece]) -> (u32, u32, Vec<u8>) {
let top = pieces.iter().map(|p| p.0).min().unwrap_or(0);
let left = pieces.iter().map(|p| p.1).min().unwrap_or(0);
let bottom = pieces.iter().map(|p| p.0 + p.2.len()).max().unwrap_or(0);
let right = pieces
.iter()
.flat_map(|p| p.2.iter().map(|r| p.1 + r.len()))
.max()
.unwrap_or(0);
let width = (right - left) * 8;
let mut pixels = vec![0; width * (bottom - top) * 8];
for (row, column, rows) in pieces {
for (r, line) in rows.iter().enumerate() {
for (c, &t) in line.iter().enumerate() {
let (y, x) = ((row + r - top) * 8, (column + c - left) * 8);
for (i, &p) in tiles[usize::from(t) * 64..][..64].iter().enumerate() {
pixels[(y + i / 8) * width + x + i % 8] = p;
}
}
}
}
(width as u32, ((bottom - top) * 8) as u32, pixels)
}
pub fn hud(rom: &Rom, folder: &Path) -> anyhow::Result<()> {
let dir = folder.join("graphics/hud");
let palette = palette(rom, LAKITU_THEME)?;
let mut tiles = vec![0; 0x100 * 16];
let hud = stream(&rom.data, HUD_TILES)?;
ensure!(hud.len() <= 0x80 * 16, "the HUD tiles run long");
tiles[HUD_FIRST * 16..][..hud.len()].copy_from_slice(&hud);
let at = |usa| hud_table(rom.region, usa);
let top = run_length(&rom.data, at(BAR_TOP))?;
let top = bytes_to_words(&top)?;
ensure!(
top.len() >= BOX_COLUMN + 4,
"the bar's top line stops short"
);
let top = top[BOX_COLUMN..BOX_COLUMN + 4].to_vec();
fs::create_dir_all(dir.join("box"))?;
let mut player = Vec::new();
for (name, list) in BOXES {
let lines = copy_list(&rom.data, at(list))?;
ensure!(lines.len() >= 64 + 8, "{name}'s box stops short");
let lower = [lines[..8].to_vec(), lines[64..72].to_vec()].map(|l| bytes_to_words(&l));
let [middle, bottom] = lower;
let rows = vec![top.clone(), middle?, bottom?];
write_block(
&dir.join(format!("box/{name}.png")),
&tiles,
&rows,
&palette,
)?;
player.push(rows);
}
let with = |inner: [u16; 4]| {
let mut rows = player[0].clone();
rows[1][1..3].copy_from_slice(&inner[..2]);
rows[2][1..3].copy_from_slice(&inner[2..]);
rows
};
for (k, name) in ICON_NAMES.into_iter().enumerate() {
let e = word(&rom.data, at(ICONS) + k * 2)? as u16;
let rows = with([e, e + 1, e + 2, e + 3]);
write_block(
&dir.join(format!("box/{name}.png")),
&tiles,
&rows,
&palette,
)?;
}
let [upper, lower] = EMPTY;
let rows = with([upper, upper, lower, lower]);
write_block(&dir.join("box/empty.png"), &tiles, &rows, &palette)?;
fs::create_dir_all(dir.join("timer"))?;
let digit = |d: u16| [BAR | TIMER_TOP | d, BAR | TIMER_BOTTOM | d];
let [zero, under] = digit(0);
let rows = vec![
vec![
zero,
zero,
BAR | MINUTES,
zero,
zero,
BAR | SECONDS,
zero,
zero,
],
vec![
under,
under,
BAR | BLANK,
under,
under,
BAR | BLANK,
under,
under,
],
];
write_block(&dir.join("timer.png"), &tiles, &rows, &palette)?;
for d in 0..10 {
let [upper, lower] = digit(d);
let rows = vec![vec![upper], vec![lower]];
write_block(&dir.join(format!("timer/{d}.png")), &tiles, &rows, &palette)?;
}
let ranks = stream(&rom.data, RANKS)?;
let mut start = 0;
for (set, lines, left, width) in RANK_SETS {
fs::create_dir_all(dir.join("rank").join(set))?;
for rank in 1..=8 {
let digit = bytes(&ranks, start, lines * 4)?;
let pixels: Vec<u8> = digit
.chunks(4)
.flat_map(|w| {
(0..width).map(move |x| {
let x = left.wrapping_add(x as u8);
u8::from(w[0] <= x && x <= w[1] || w[2] <= x && x <= w[3])
})
})
.collect();
write_png(
&dir.join(format!("rank/{set}/{rank}.png")),
width as u32,
lines as u32,
&[0, 0, 0, 255, 255, 255],
&pixels,
)?;
start += lines * 4;
}
}
let race = race_tiles(rom)?;
let mut sprites = Vec::new();
let mut row = at(COUNTERS);
loop {
let [head] = bytes(&rom.data, row, 1)?.try_into()?;
if head == 0 {
break;
}
ensure!(
head & 0x60 == 0,
"a counter row steps down or uses 16×16 sprites"
);
let [attr, x, y] = bytes(&rom.data, row + 1, 3)?.try_into()?;
let count = usize::from(head);
for (i, &tile) in bytes(&rom.data, row + 4, count)?.iter().enumerate() {
sprites.push(Sprite {
x: i32::from(x) + i as i32 * 8,
y: i32::from(y),
tile: u16::from(tile) | u16::from(attr) << 8,
big: false,
});
}
row += 4 + count;
}
let attr = sprites
.first()
.context("the counters have no sprites")?
.tile & 0xFF00;
let (width, height, pixels) = draw(&sprites, &race)?;
write_png(&dir.join("counters.png"), width, height, &palette, &pixels)?;
fs::create_dir_all(dir.join("counters"))?;
for (d, &tile) in bytes(&rom.data, at(COUNTER_DIGITS), 10)?.iter().enumerate() {
let digit = Sprite {
x: 0,
y: 0,
tile: u16::from(tile) | attr,
big: false,
};
let (width, height, pixels) = draw(&[digit], &race)?;
write_png(
&dir.join(format!("counters/{d}.png")),
width,
height,
&palette,
&pixels,
)?;
}
let ladder = stream(&rom.data, LADDER)?;
ensure!(ladder.len() == 120 * 32, "the ladder is not 120 tiles");
fs::create_dir_all(dir.join("ladder"))?;
for (row, face) in FACES.into_iter().enumerate() {
for (driver, (name, pal)) in DRIVERS.into_iter().enumerate() {
let sprite = Sprite {
x: 0,
y: 0,
tile: (row * 32 + driver * 2) as u16 | pal << 9,
big: true,
};
let (width, height, pixels) = draw(&[sprite], &ladder)?;
let path = dir.join(format!("ladder/{name}-{face}.png"));
write_png(&path, width, height, &palette, &pixels)?;
}
}
for d in 0..8 {
for (suffix, pal) in LADDER_PALETTES {
let sprite = |y, tile| Sprite {
x: 0,
y,
tile: (tile + d) | pal << 9,
big: false,
};
let digit = [sprite(0, LADDER_DIGIT), sprite(8, LADDER_DIGIT + 16)];
let (width, height, pixels) = draw(&digit, &ladder)?;
let path = dir.join(format!("ladder/{}{suffix}.png", d + 1));
write_png(&path, width, height, &palette, &pixels)?;
}
}
Ok(())
}
fn run_length(data: &[u8], mut at: usize) -> anyhow::Result<Vec<u8>> {
let mut out = Vec::new();
loop {
let [n] = bytes(data, at, 1)?.try_into()?;
at += 1;
let count = usize::from(n / 2);
match n {
0 => return Ok(out),
_ if n % 2 == 1 => {
out.extend(bytes(data, at, 2)?.repeat(count));
at += 2;
}
_ => {
out.extend(bytes(data, at, count * 2)?);
at += count * 2;
}
}
}
}
fn copy_list(data: &[u8], mut at: usize) -> anyhow::Result<Vec<u8>> {
let mut out = Vec::new();
loop {
let w = word(data, at)?;
at += 2;
if w & 0x8000 == 0 {
out.extend((w as u16).to_le_bytes());
} else if w & 0xFF == 0 {
return Ok(out);
} else {
out.resize(out.len() + (w & 0xFF), 0);
}
}
}
fn bytes_to_words(bytes: &[u8]) -> anyhow::Result<Vec<u16>> {
ensure!(
bytes.len().is_multiple_of(2),
"a tile map line has an odd length"
);
Ok(bytes
.chunks(2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.collect())
}
fn write_block(path: &Path, tiles: &[u8], rows: &[Vec<u16>], palette: &[u8]) -> anyhow::Result<()> {
let width = rows[0].len() * 8;
let mut pixels = vec![0; width * rows.len() * 8];
for (r, row) in rows.iter().enumerate() {
for (c, &e) in row.iter().enumerate() {
for (i, p) in tile(tiles, e, 2, HUD_COLOURS)?.into_iter().enumerate() {
pixels[(r * 8 + i / 8) * width + c * 8 + i % 8] = p;
}
}
}
write_png(
path,
width as u32,
(rows.len() * 8) as u32,
palette,
&pixels,
)
}
fn write_screen(dir: &Path, name: &str, palette: &[u8], layers: &[Vec<u8>]) -> anyhow::Result<()> {
fs::create_dir_all(dir.join(name))?;
let mut screen = vec![0; 256 * HEIGHT];
for (n, pixels) in layers.iter().enumerate().rev() {
if pixels.is_empty() {
continue;
}
let width = pixels.len() / HEIGHT;
write_png(
&dir.join(name).join(format!("bg{}.png", n + 1)),
width as u32,
HEIGHT as u32,
palette,
pixels,
)?;
for (row, line) in screen.chunks_mut(256).zip(pixels.chunks(width)) {
for (pixel, &over) in row.iter_mut().zip(line) {
if over != 0 {
*pixel = over;
}
}
}
}
write_png(
&dir.join(format!("{name}.png")),
256,
HEIGHT as u32,
palette,
&screen,
)
}
fn layer(
tiles: &[u8],
map: &[u8],
screens: usize,
bpp: usize,
base: usize,
) -> anyhow::Result<Vec<u8>> {
let width = screens * 256;
let mut pixels = vec![0; width * HEIGHT];
for row in 0..HEIGHT / 8 {
for column in 0..screens * 32 {
let at = (column / 32 * 32 * 32 + row * 32 + column % 32) * 2;
let e = bytes(map, at, 2)?;
let e = u16::from(e[0]) | u16::from(e[1]) << 8;
for (i, p) in tile(tiles, e, bpp, base)?.into_iter().enumerate() {
if p != 0 {
pixels[(row * 8 + i / 8) * width + column * 8 + i % 8] = p;
}
}
}
}
Ok(pixels)
}
fn tile(tiles: &[u8], e: u16, bpp: usize, base: usize) -> anyhow::Result<[u8; 64]> {
let e = usize::from(e);
let palette = base + ((e >> 10 & 7) << bpp);
let mut pixels = [0; 64];
for y in 0..8 {
let line = if e & 0x8000 != 0 { 7 - y } else { y };
let planes = bytes(
tiles,
(e & 0x3FF) * bpp * 8 + line * 2,
(bpp / 2 - 1) * 16 + 2,
)?;
for x in 0..8 {
let bit = if e & 0x4000 != 0 { x } else { 7 - x };
let v = (0..bpp).fold(0, |v, plane| {
v | (planes[plane / 2 * 16 + plane % 2] >> bit & 1) << plane
});
if v != 0 {
pixels[y * 8 + x] = (palette | usize::from(v)) as u8;
}
}
}
Ok(pixels)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layer_reads_4bpp_planes_palettes_and_flips() {
let mut tiles = vec![0; 64];
tiles[32] = 0x80;
tiles[32 + 17] = 0x80;
let mut map = vec![0; 2 * 2048];
map[2048..2050].copy_from_slice(&0xCC01_u16.to_le_bytes());
let pixels = layer(&tiles, &map, 2, 4, 0).unwrap();
assert_eq!(pixels[7 * 512 + 256 + 7], 3 * 16 + 9);
assert_eq!(pixels.iter().filter(|&&p| p != 0).count(), 1);
}
#[test]
fn mode7_rows_overlay_and_crop() {
let rows = mode7_rows(&[1, 2, 0xFF, 1, 0xFF], 0, 2).unwrap();
assert_eq!(rows, [vec![1, 2], vec![1]]);
let mut tiles = vec![0; 3 * 64];
tiles[64..128].fill(5);
tiles[128..].fill(6);
let (width, height, pixels) = mode7(&tiles, &[(1, 1, rows), (1, 2, vec![vec![1]])]);
assert_eq!((width, height), (16, 16));
assert_eq!(pixels[8], 5);
assert_eq!(pixels[8 * 16], 5);
assert_eq!(pixels[8 * 16 + 8], 0);
}
#[test]
fn run_length_and_copy_lists() {
let list = [0x04, 1, 0, 2, 0, 0x07, 3, 0, 0x00];
assert_eq!(
run_length(&list, 0).unwrap(),
[1, 0, 2, 0, 3, 0, 3, 0, 3, 0]
);
let list = [1, 0, 0x02, 0x80, 2, 0, 0x00, 0x80];
assert_eq!(copy_list(&list, 0).unwrap(), [1, 0, 0, 0, 2, 0]);
}
#[test]
fn smk_rom_graphics() {
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-graphics-{}", std::process::id()));
font(&rom, &folder).unwrap();
screens(&rom, &folder).unwrap();
hud(&rom, &folder).unwrap();
let graphics = folder.join("graphics");
let size = |path: &str| {
let reader = png::Decoder::new(fs::File::open(graphics.join(path)).unwrap())
.read_info()
.unwrap();
(reader.info().width, reader.info().height)
};
assert_eq!(size("font/font.png"), (128, 128));
assert_eq!(size("screens/title.png"), (256, 224));
assert_eq!(size("screens/title/bg2.png"), (512, 224));
assert_eq!(size("screens/results/bg2.png"), (512, 224));
assert!(!graphics.join("screens/results/bg1.png").exists());
assert_eq!(size("screens/driver-select/bg4.png"), (256, 224));
assert_eq!(size("screens/cup-select.png"), (256, 224));
assert_eq!(size("screens/podium/bg2.png"), (256, 224));
assert_eq!(size("icons/star.png"), (16, 16));
assert_eq!(size("icons/150cc-gold.png"), (16, 16));
assert_eq!(size("hud/box/star.png"), (32, 24));
assert_eq!(size("hud/timer.png"), (64, 16));
assert_eq!(size("hud/rank/race/8.png"), (16, 20));
assert_eq!(size("hud/rank/finish/1.png"), (64, 64));
assert_eq!(size("hud/counters.png"), (40, 16));
assert_eq!(size("hud/ladder/yoshi-sad.png"), (16, 16));
assert_eq!(size("hud/ladder/8.png"), (8, 16));
fs::remove_dir_all(&folder).unwrap();
}
}