zretro/src/main.rs

236 lines
7.7 KiB
Rust

extern crate ferretro_components;
extern crate sdl2;
use std::ffi::CStr;
use std::path::PathBuf;
use std::pin::Pin;
use sdl2::{
EventPump,
event::Event,
image::ImageRWops,
keyboard::Keycode,
pixels::{Color, PixelFormatEnum},
rect::Point,
render::{WindowCanvas, BlendMode},
surface::Surface,
};
use rand::{SeedableRng, Rng};
use rand_xoshiro::Xoshiro128Plus;
use structopt::StructOpt;
use ferretro_components::base::ControlFlow;
use ferretro_components::prelude::*;
use ferretro_components::provided::{
sdl2::{Sdl2SurfaceComponent, SimpleSdl2AudioComponent},
stdlib::{SleepFramerateLimitComponent, PathBufComponent},
};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
#[derive(StructOpt)]
struct Opt {
/// Core module to use.
#[structopt(short, long, parse(from_os_str))]
core: PathBuf,
/// ROM to load using the core.
#[structopt(short, long, parse(from_os_str))]
rom: PathBuf,
/// System directory, often containing BIOS files
#[structopt(short, long, parse(from_os_str))]
system: Option<PathBuf>,
}
enum GuiState {
Menus,
Game
}
impl GuiState {
fn toggle(&mut self) {
match self {
GuiState::Menus => *self = GuiState::Game,
GuiState::Game => *self = GuiState::Menus,
}
}
}
struct Zretro {
emu: Pin<Box<RetroComponentBase>>,
canvas: WindowCanvas,
font: Surface<'static>,
rng: Xoshiro128Plus,
ui_bg: Option<Surface<'static>>,
snowflakes: Vec<Point>,
mode: GuiState,
event_pump: EventPump,
}
const FONT_PNG: &'static [u8] = include_bytes!("lifont.png");
impl Zretro {
fn new(opt: Opt) -> Result<Self> {
let mut emu = RetroComponentBase::new(&opt.core);
let SystemAvInfo { geometry, .. } = emu.libretro_core().get_system_av_info();
let sys_info = emu.libretro_core().get_system_info();
let title = format!(
"zretro - {}",
unsafe { CStr::from_ptr(sys_info.library_name) }.to_string_lossy()
);
let mut sdl_context = sdl2::init()?;
let window = sdl_context
.video()?
.window(title.as_str(), geometry.base_width, geometry.base_height)
.borderless()
.build()?;
let canvas = window.into_canvas().build()?;
let event_pump = sdl_context.event_pump()?;
let sdl2surf_comp = Sdl2SurfaceComponent::new(emu.libretro_core())?;
emu.register_component(sdl2surf_comp)?;
let sdl2_audio = SimpleSdl2AudioComponent::new(&mut sdl_context, emu.libretro_core());
emu.register_component(sdl2_audio)?;
let sleep_fps = SleepFramerateLimitComponent::new(emu.libretro_core());
emu.register_component(sleep_fps)?;
emu.register_component(PathBufComponent {
sys_path: opt.system.clone(),
libretro_path: Some(opt.core.to_path_buf()),
core_assets_path: None,
save_path: Some(std::env::temp_dir()),
})?;
emu.init().unwrap();
emu.load_game(&opt.rom).unwrap(); // TODO: optional via CLI positional arg, menu-browsable
let rwops = sdl2::rwops::RWops::from_bytes(FONT_PNG)?;
let font_shortlived = rwops.load_png()?;
// there's a nonsemantic inheritance of lifetime from ImageRWops::load_png. TODO: pr
let font = unsafe {
let ptr = font_shortlived.raw();
std::mem::forget(font_shortlived);
Surface::from_ll(ptr)
};
let mut rng = rand_xoshiro::Xoshiro128Plus::from_entropy();
let mut ui_bg = Surface::new(geometry.base_width, geometry.base_height, PixelFormatEnum::ABGR8888)?;
ui_bg.fill_rect(None, Color::RGBA(0, 0, 128, 128))?;
let snowflakes = (0..100).into_iter()
.map(|_| Point::new(
rng.gen_range(0..ui_bg.width() as i32),
rng.gen_range(0..ui_bg.height() as i32),
))
.collect();
Ok(Zretro {
emu,
canvas,
font,
rng,
ui_bg: Some(ui_bg),
snowflakes,
mode: GuiState::Menus,
event_pump,
})
}
fn update_snow(&mut self) -> Result<()> {
let mut ui_bg = self.ui_bg.take().ok_or("no ui_bg?")?;
ui_bg.fill_rect(None, Color::RGBA(0, 0, 128, 128))?;
let w = ui_bg.width() as i32;
let h = ui_bg.height() as i32;
let halflen = self.snowflakes.len() / 2;
for flake in &mut self.snowflakes[..halflen] {
let x = flake.x() + self.rng.gen_range(0..=1);
let y = flake.y() + 1;
*flake = Point::new(x % w, y % h);
}
for flake in &mut self.snowflakes[halflen..] {
let x = flake.x();
let y = flake.y() + self.rng.gen_range(0..=1);
*flake = Point::new(x, y % h);
}
let mut ui_canvas = ui_bg.into_canvas()?;
ui_canvas.set_draw_color(Color::RGBA(255, 255, 255, 160));
ui_canvas.draw_points(&self.snowflakes[..halflen])?;
ui_canvas.set_draw_color(Color::RGBA(255, 255, 255, 128));
ui_canvas.draw_points(&self.snowflakes[halflen..])?;
self.ui_bg = Some(ui_canvas.into_surface());
Ok(())
}
fn run(&mut self) -> Result<()> {
let tc = self.canvas.texture_creator();
let font_tx = tc.create_texture_from_surface(&self.font)?;
let mut font_rect = self.font.rect();
font_rect.set_height(64);
'outer: loop {
for event in self.event_pump.poll_iter() {
match event {
Event::Quit { .. } => break 'outer,
Event::KeyDown { keycode: Some(keycode), .. } => {
match keycode {
Keycode::Escape => {
self.mode.toggle();
let audio: &SimpleSdl2AudioComponent = self.emu.component_ref()?;
match self.mode {
GuiState::Menus => audio.silence_buffer(),
GuiState::Game => {} //audio.resume(),
}
},
_ => {}
}
}
Event::KeyUp { .. } => {}
Event::DropFile { filename, .. } => {
self.emu.libretro_core().unload_game();
self.emu.load_game(filename)?;
}
_ => {}
}
}
if let GuiState::Game = self.mode {
if let ControlFlow::Break = self.emu.run() {
break;
}
} else {
let delay: &mut SleepFramerateLimitComponent = self.emu.component_mut()?;
delay.do_sleep();
}
let surfcomp: &Sdl2SurfaceComponent = self.emu.component_ref()?;
let emu_surf_ref = surfcomp.surface();
let emu_tx = tc.create_texture_from_surface(emu_surf_ref)?;
self.canvas.clear();
self.canvas.copy(&emu_tx, None, None)?;
if let GuiState::Menus = self.mode {
self.update_snow()?;
let ui_bg_surf_ref = self.ui_bg.as_ref().ok_or("no bg?")?;
let mut ui_tx = tc.create_texture_from_surface(ui_bg_surf_ref)?;
ui_tx.set_blend_mode(BlendMode::Blend);
self.canvas.copy(&ui_tx, None, None)?;
self.canvas.copy(&font_tx, font_rect, font_rect)?;
}
self.canvas.present();
}
Ok(())
}
}
fn main() -> Result<()> {
let opt: Opt = Opt::from_args();
let mut frontend = Zretro::new(opt)?;
frontend.run()?;
Ok(())
}