earthbound-battle-backgrounds 0.1.0

Emulate and render the battle backgrounds from EarthBound / Mother 2.
Documentation
#![doc = include_str!("../README.md")]
#![allow(mixed_script_confusables)]
#![allow(unused_comparisons)]
#![warn(missing_docs)]

use std::cell::RefCell;
use std::rc::Rc;

use rom::background_layer::BackgroundLayer;

use crate::engine::Engine;
use crate::rom::Rom;
use crate::utils::LayerParamOptions;

mod engine;
mod rom;
mod utils;

pub use engine::{SNES_HEIGHT, SNES_WIDTH};

/// The maximum layer ID.
pub const MAX_LAYER: u16 = 326;

/// Configuration options.
#[derive(Debug, Default)]
pub struct Options {
    /// Layer styles.
    pub layers: Layers,

    /// Letterboxing configuration.
    pub aspect_ratio: AspectRatio,
}

/// Layer styles.
#[derive(Debug)]
pub struct Layers {
    layer1: u16,
    layer2: u16,
}

impl Layers {
    /// Configure layer styles.
    ///
    /// Each layer style is identified by a number from 0 to [`MAX_LAYER`] (inclusive). The layer
    /// IDs are interchangeable; `Layers::new(50, 300)` is equivalent to ``Layers::new(300, 50)`.
    /// This results in over 52,650 combinations. However, the SNES is only able to render 3,176 of
    /// those combinations, and of those, only 225 are used in-game.
    ///
    /// # Panics
    ///
    /// This function will panic if layer IDs greater than [`MAX_LAYER`] are provided.
    pub fn new(layer1: u16, layer2: u16) -> Self {
        assert!(layer1 <= MAX_LAYER, "layer1 must be <= {}", MAX_LAYER);
        assert!(layer2 <= MAX_LAYER, "layer2 must be <= {}", MAX_LAYER);

        Layers { layer1, layer2 }
    }
}

impl Default for Layers {
    fn default() -> Self {
        Layers {
            layer1: 270,
            layer2: 269,
        }
    }
}

/// Aspect ratio used to render a frame. If a value other than [`Self::Full`] is used, the
/// resulting image will have letterboxing applied.
#[derive(Debug, Default, Copy, Clone)]
pub enum AspectRatio {
    /// Full screen (8:7).
    #[default]
    Full,

    /// Wide letterbox (4:3).
    WideLetterbox,

    /// Medium letterbox (2:1).
    MediumLetterbox,

    /// Narrow letterbox (8:3).
    NarrowLetterbox,
}

/// Handles rendering of the battle backgrounds.
#[derive(Debug)]
pub struct Emulator {
    engine: Engine,
}

impl Emulator {
    /// Construct a new [`Emulator`].
    pub fn new(options: Options) -> Self {
        Emulator {
            engine: setup_engine(options),
        }
    }

    /// Draw a new frame of the image into the provided pixel buffer.
    ///
    /// This method should be called whenever a new frame is requested by the windowing system.
    ///
    /// # Panics
    ///
    /// Panics if the provided buffer is not large enough to draw a full frame.
    pub fn draw_frame(&mut self, pixels: &mut [u8]) {
        assert_eq!(pixels.len(), usize::from(SNES_WIDTH * SNES_HEIGHT) * 4);
        self.engine.draw_frame(pixels, false);
    }
}

fn setup_engine(options: Options) -> Engine {
    let layer_1_val = utils::parse_layer_param(
        Some(options.layers.layer1),
        LayerParamOptions { first_layer: true },
    );
    let layer_2_val = utils::parse_layer_param(
        Some(options.layers.layer2),
        LayerParamOptions { first_layer: false },
    );
    let frameskip = utils::parse_frameskip_param(None);
    let aspect_ratio = utils::parse_aspect_ratio_param(Some(match options.aspect_ratio {
        AspectRatio::Full => 0,
        AspectRatio::WideLetterbox => 16,
        AspectRatio::MediumLetterbox => 48,
        AspectRatio::NarrowLetterbox => 64,
    }));
    let debug = false;

    let fps = 30;
    let mut alpha = 0.5;

    if layer_2_val == 0 {
        alpha = 1.0;
    }

    let rom = Rc::new(RefCell::new(Rom::new()));

    let layer1 = BackgroundLayer::new(layer_1_val as usize, rom.clone());
    let layer2 = BackgroundLayer::new(layer_2_val as usize, rom.clone());

    let mut engine = Engine::new(
        vec![layer1, layer2],
        engine::Options {
            fps,
            aspect_ratio,
            frame_skip: frameskip,
            alpha: vec![alpha, alpha],
            canvas: (),
        },
    );

    engine.animate(debug);

    engine
}