earthbound_battle_backgrounds/
lib.rs

1#![doc = include_str!("../README.md")]
2#![allow(mixed_script_confusables)]
3#![allow(unused_comparisons)]
4#![warn(missing_docs)]
5
6use std::cell::RefCell;
7use std::rc::Rc;
8
9use rom::background_layer::BackgroundLayer;
10
11use crate::engine::Engine;
12use crate::rom::Rom;
13use crate::utils::LayerParamOptions;
14
15mod engine;
16mod rom;
17mod utils;
18
19pub use engine::{SNES_HEIGHT, SNES_WIDTH};
20
21/// The maximum layer ID.
22pub const MAX_LAYER: u16 = 326;
23
24/// Configuration options.
25#[derive(Debug, Default)]
26pub struct Options {
27    /// Layer styles.
28    pub layers: Layers,
29
30    /// Letterboxing configuration.
31    pub aspect_ratio: AspectRatio,
32}
33
34/// Layer styles.
35#[derive(Debug)]
36pub struct Layers {
37    layer1: u16,
38    layer2: u16,
39}
40
41impl Layers {
42    /// Configure layer styles.
43    ///
44    /// Each layer style is identified by a number from 0 to [`MAX_LAYER`] (inclusive). The layer
45    /// IDs are interchangeable; `Layers::new(50, 300)` is equivalent to ``Layers::new(300, 50)`.
46    /// This results in over 52,650 combinations. However, the SNES is only able to render 3,176 of
47    /// those combinations, and of those, only 225 are used in-game.
48    ///
49    /// # Panics
50    ///
51    /// This function will panic if layer IDs greater than [`MAX_LAYER`] are provided.
52    pub fn new(layer1: u16, layer2: u16) -> Self {
53        assert!(layer1 <= MAX_LAYER, "layer1 must be <= {}", MAX_LAYER);
54        assert!(layer2 <= MAX_LAYER, "layer2 must be <= {}", MAX_LAYER);
55
56        Layers { layer1, layer2 }
57    }
58}
59
60impl Default for Layers {
61    fn default() -> Self {
62        Layers {
63            layer1: 270,
64            layer2: 269,
65        }
66    }
67}
68
69/// Aspect ratio used to render a frame. If a value other than [`Self::Full`] is used, the
70/// resulting image will have letterboxing applied.
71#[derive(Debug, Default, Copy, Clone)]
72pub enum AspectRatio {
73    /// Full screen (8:7).
74    #[default]
75    Full,
76
77    /// Wide letterbox (4:3).
78    WideLetterbox,
79
80    /// Medium letterbox (2:1).
81    MediumLetterbox,
82
83    /// Narrow letterbox (8:3).
84    NarrowLetterbox,
85}
86
87/// Handles rendering of the battle backgrounds.
88#[derive(Debug)]
89pub struct Emulator {
90    engine: Engine,
91}
92
93impl Emulator {
94    /// Construct a new [`Emulator`].
95    pub fn new(options: Options) -> Self {
96        Emulator {
97            engine: setup_engine(options),
98        }
99    }
100
101    /// Draw a new frame of the image into the provided pixel buffer.
102    ///
103    /// This method should be called whenever a new frame is requested by the windowing system.
104    ///
105    /// # Panics
106    ///
107    /// Panics if the provided buffer is not large enough to draw a full frame.
108    pub fn draw_frame(&mut self, pixels: &mut [u8]) {
109        assert_eq!(pixels.len(), usize::from(SNES_WIDTH * SNES_HEIGHT) * 4);
110        self.engine.draw_frame(pixels, false);
111    }
112}
113
114fn setup_engine(options: Options) -> Engine {
115    let layer_1_val = utils::parse_layer_param(
116        Some(options.layers.layer1),
117        LayerParamOptions { first_layer: true },
118    );
119    let layer_2_val = utils::parse_layer_param(
120        Some(options.layers.layer2),
121        LayerParamOptions { first_layer: false },
122    );
123    let frameskip = utils::parse_frameskip_param(None);
124    let aspect_ratio = utils::parse_aspect_ratio_param(Some(match options.aspect_ratio {
125        AspectRatio::Full => 0,
126        AspectRatio::WideLetterbox => 16,
127        AspectRatio::MediumLetterbox => 48,
128        AspectRatio::NarrowLetterbox => 64,
129    }));
130    let debug = false;
131
132    let fps = 30;
133    let mut alpha = 0.5;
134
135    if layer_2_val == 0 {
136        alpha = 1.0;
137    }
138
139    let rom = Rc::new(RefCell::new(Rom::new()));
140
141    let layer1 = BackgroundLayer::new(layer_1_val as usize, rom.clone());
142    let layer2 = BackgroundLayer::new(layer_2_val as usize, rom.clone());
143
144    let mut engine = Engine::new(
145        vec![layer1, layer2],
146        engine::Options {
147            fps,
148            aspect_ratio,
149            frame_skip: frameskip,
150            alpha: vec![alpha, alpha],
151            canvas: (),
152        },
153    );
154
155    engine.animate(debug);
156
157    engine
158}