earthbound-battle-backgrounds 0.1.0

Emulate and render the battle backgrounds from EarthBound / Mother 2.
Documentation
use std::cell::{LazyCell, RefCell};
use std::rc::Rc;

use battle_background::BattleBackground;
use block::Block;

use crate::rom::background_graphics::BackgroundGraphics;
use crate::rom::background_palette::BackgroundPalette;

pub mod background_graphics;
pub mod background_layer;
pub mod background_palette;
pub mod battle_background;
pub mod block;
pub mod distorter;
pub mod distortion_effect;
pub mod palette_cycle;
pub mod rom_graphics;

const MINIMUM_INDEX: usize = 0;
const MAXIMUM_INDEX: usize = 326;

const UNCOMPRESSED_BLOCK: u8 = 0;
const RUN_LENGTH_ENCODED_BYTE: u8 = 1;
const RUN_LENGTH_ENCODED_SHORT: u8 = 2;
const INCREMENTAL_SEQUENCE: u8 = 3;
const REPEAT_PREVIOUS_DATA: u8 = 4;
const REVERSE_BITS: u8 = 5;
const UNKNOWN_1: u8 = 6;
const UNKNOWN_2: u8 = 7;

fn generate_reversed_bytes() -> [i16; 256] {
    let mut reversed_bytes = [0i16; 256];

    for i in 0..reversed_bytes.len() {
        let binary = format!("{:08b}", i);
        let reversed = binary.chars().rev().collect::<String>();
        let value = i16::from_str_radix(&reversed, 2).unwrap();
        reversed_bytes[i] = value;
    }

    reversed_bytes
}

const REVERSED_BYTES: LazyCell<[i16; 256]> = LazyCell::new(generate_reversed_bytes);

pub fn snes_to_hex(address: usize, header: bool) -> usize {
    let mut new_address = address;
    if new_address >= 0x400000 && new_address < 0x600000 {
        new_address -= 0x0;
    } else if new_address >= 0xC00000 && new_address < 0x1000000 {
        new_address -= 0xC00000;
    } else {
        panic!("SNES address out of range: {}", new_address);
    }
    if header {
        new_address += 0x200;
    }

    new_address - 0xA0200
}

/// Returns a readable block at the given location. Nominally, should also
/// handle tracking free space depending on the type of read requested.
/// (i. e., an object may be interested in read-only access anywhere, but if
/// an object is reading its own data, it should specify this so the ROM can
/// mark the read data as "free")
//
/// `location` - The address from which to read
//
/// Returns a readable block.
pub fn read_block(location: usize) -> Block {
    Block::new(location)
}

// Do not try to understand what this is doing. It will hurt you.
// The only documentation for this decompression routine is a 65816
// disassembly.
// This function can return the following error codes:
//
// ERROR MEANING
// -1 Something went wrong
// -2 I dunno
// -3 No idea
// -4 Something went _very_ wrong
// -5 Bad stuff
// -6 Out of ninjas error
// -7 Ask somebody else
// -8 Unexpected end of data
/// - `start`
/// - `data`
/// - `output` - Must already be allocated with at least enough space
/// - `read` - "Out" parameter which receives the number of bytes of compressed data read
/// Returns the size of the decompressed data if successful, empty otherwise
pub fn decompress(
    start: usize,
    data: &'static [u8],
    mut output: Vec<i16>,
    read: &mut usize,
) -> Option<Vec<i16>> {
    let max_length = output.len() as i16;
    let mut pos = start;
    let mut bpos = 0i16;
    let mut bpos2 = 0i16;
    let new_read = read;
    while data[pos] != 0xFF {
        // Data overflow before end of compressed data
        if pos >= data.len() {
            *new_read = pos - start + 1;
            return None;
        }
        let mut command_type = data[pos] >> 5;
        let mut len = i16::from((data[pos] & 0x1F) + 1);
        if command_type == 7 {
            command_type = (data[pos] & 0x1C) >> 2;
            len = ((i16::from(data[pos]) & 3) << 8) + i16::from(data[pos + 1]) + 1;
            pos += 1;
        }
        // Error: block length would overflow max_length, or block endpos negative?
        if bpos + len > max_length || bpos + len < 0 {
            *new_read = pos - start + 1;
            return None;
        }
        pos += 1;
        if command_type >= 4 {
            bpos2 = (i16::from(data[pos]) << 8) + i16::from(data[pos + 1]);
            if bpos2 >= max_length || bpos2 < 0 {
                *new_read = pos - start + 1;
                return None;
            }
            pos += 2;
        }

        match command_type {
            UNCOMPRESSED_BLOCK => {
                while {
                    let old_len = len;
                    len -= 1;
                    old_len
                } != 0
                {
                    output[bpos as usize] = data[pos].into();
                    bpos += 1;
                    pos += 1;
                }
            }
            RUN_LENGTH_ENCODED_BYTE => {
                while {
                    let old_len = len;
                    len -= 1;
                    old_len
                } != 0
                {
                    output[bpos as usize] = data[pos].into();
                    bpos += 1;
                }
                pos += 1;
            }
            RUN_LENGTH_ENCODED_SHORT => {
                if bpos + 2 * len > max_length || bpos < 0 {
                    *new_read = pos - start + 1;
                    return None;
                }
                while {
                    let old_len = len;
                    len -= 1;
                    old_len
                } != 0
                {
                    output[bpos as usize] = data[pos].into();
                    bpos += 1;
                    output[bpos as usize] = data[pos + 1].into();
                    bpos += 1;
                }
                pos += 2;
            }
            INCREMENTAL_SEQUENCE => {
                let mut tmp = data[pos];
                pos += 1;
                while {
                    let old_len = len;
                    len -= 1;
                    old_len
                } != 0
                {
                    output[bpos as usize] = tmp as i16;
                    bpos += 1;
                    tmp += 1;
                }
            }
            REPEAT_PREVIOUS_DATA => {
                if bpos + len > max_length || bpos2 < 0 {
                    *new_read = pos - start + 1;
                    return None;
                }
                for i in 0..len {
                    output[bpos as usize] = output[(bpos2 + i) as usize];
                    bpos += 1;
                }
            }
            REVERSE_BITS => {
                if bpos2 + len > max_length || bpos2 < 0 {
                    *new_read = pos - start + 1;
                    return None;
                }
                while {
                    let old_len = len;
                    len -= 1;
                    old_len
                } != 0
                {
                    output[bpos as usize] =
                        REVERSED_BYTES[(output[bpos2 as usize] & 0xFF) as usize];
                    bpos2 += 1;
                    bpos += 1;
                }
            }
            UNKNOWN_1 => {
                if bpos2 - len + 1 < 0 {
                    *new_read = pos - start + 1;
                    return None;
                }
                while {
                    let old_len = len;
                    len -= 1;
                    old_len
                } != 0
                {
                    output[bpos as usize] = output[bpos2 as usize];
                    bpos += 1;
                    bpos2 -= 1;
                }
            }
            UNKNOWN_2 | _ => {
                *new_read = pos - start + 1;
                return None;
            }
        }
    }

    *new_read = pos - start + 1;

    Some(output)
}

pub fn get_compressed_size(start: usize, data: &'static [u8]) -> isize {
    let mut bpos = 0;
    let mut pos = start;
    let mut bpos2 = 0;
    while data[pos] != 0xFF {
        // Data overflow before end of compressed data
        if pos >= data.len() {
            return -8;
        }
        let mut command_type = data[pos] >> 5;
        let mut length = usize::from((data[pos] & 0x1F) + 1);
        if command_type == 7 {
            command_type = (data[pos] & 0x1C) >> 2;
            length = ((usize::from(data[pos]) & 3) << 8) + usize::from(data[pos + 1]) + 1;
            pos += 1;
        }
        if bpos + length < 0 {
            return -1;
        }
        pos += 1;

        if command_type >= 4 {
            bpos2 = (usize::from(data[pos]) << 8) + (usize::from(data[pos + 1]));
            if bpos2 < 0 {
                return -2;
            }
            pos += 2;
        }

        match command_type {
            UNCOMPRESSED_BLOCK => {
                bpos += length;
                pos += length;
            }
            RUN_LENGTH_ENCODED_BYTE => {
                bpos += length;
                pos += 1;
            }
            RUN_LENGTH_ENCODED_SHORT => {
                if bpos < 0 {
                    return -3;
                }
                bpos += 2 * length;
                pos += 2;
            }
            INCREMENTAL_SEQUENCE => {
                bpos += length;
                pos += 1;
            }
            REPEAT_PREVIOUS_DATA => {
                if bpos2 < 0 {
                    return -4;
                }
                bpos += length;
            }
            REVERSE_BITS => {
                if bpos2 < 0 {
                    return -5;
                }
                bpos += length;
            }
            UNKNOWN_1 => {
                if bpos - length + 1 < 0 {
                    return -6;
                }
                bpos += length;
            }
            UNKNOWN_2 | _ => {
                return -7;
            }
        }
    }
    bpos as isize
}

pub static DATA: &[u8] = include_bytes!("../data/truncated_backgrounds.dat");

#[derive(Debug)]
pub struct Rom {
    battle_background_objects: Vec<Rc<RefCell<BattleBackground>>>,
    background_palette_objects: Vec<BackgroundPalette>,
    background_graphics_objects: Vec<Rc<RefCell<BackgroundGraphics>>>,
}

#[allow(clippy::new_without_default)]
impl Rom {
    pub fn new() -> Self {
        // The only way to determine the bit depth of each BG palette is to check the bit depth of
        // the backgrounds that use it - so, first we create an array to track Palette bit depths:
        let mut palette_bits = vec![0i32; 114];
        let mut graphics_bits = vec![0i32; 103];

        let mut battle_background_objects = Vec::with_capacity(MAXIMUM_INDEX);
        for i in MINIMUM_INDEX..=MAXIMUM_INDEX {
            let background = Rc::new(RefCell::new(BattleBackground::new(i)));
            battle_background_objects.push(background.clone());

            // Now that the background has been read, update the BPP entry for its palette. We can
            // also check to make sure palettes are used consistently:
            let palette = background.borrow().palette_index();
            let bits_per_pixel = i32::from(background.borrow().bits_per_pixel());
            if palette_bits[palette] != 0 {
                assert_eq!(
                    palette_bits[palette], bits_per_pixel,
                    "BattleBackground palette Error: Inconsistent bit depth"
                );
            }
            palette_bits[palette] = bits_per_pixel;
            graphics_bits[background.borrow().graphics_index()] = bits_per_pixel;
        }
        // Now load palettes
        let mut background_palette_objects = Vec::with_capacity(114);
        for i in 0..114 {
            background_palette_objects.push(BackgroundPalette::new(i, palette_bits[i] as u8));
        }
        // Load graphics
        let mut background_graphics_objects = Vec::with_capacity(103);
        for i in 0..103 {
            background_graphics_objects.push(Rc::new(RefCell::new(BackgroundGraphics::new(
                i,
                graphics_bits[i] as u8,
            ))));
        }

        Rom {
            battle_background_objects,
            background_palette_objects,
            background_graphics_objects,
        }
    }

    pub fn get_battle_background(&self, index: usize) -> Rc<RefCell<BattleBackground>> {
        self.battle_background_objects[index].clone()
    }

    pub fn get_background_graphics(&self, index: usize) -> Rc<RefCell<BackgroundGraphics>> {
        self.background_graphics_objects[index].clone()
    }

    pub fn get_background_palette(&self, index: usize) -> &BackgroundPalette {
        &self.background_palette_objects[index]
    }
}