260 lines
8.6 KiB
Rust
260 lines
8.6 KiB
Rust
extern crate ferretro_components;
|
|
extern crate sdl2;
|
|
|
|
use std::ffi::CStr;
|
|
use std::path::PathBuf;
|
|
use core::pin::Pin;
|
|
|
|
use sdl2::{
|
|
EventPump,
|
|
event::{Event, WindowEvent},
|
|
keyboard::Keycode,
|
|
mouse::{Cursor, MouseUtil},
|
|
pixels::{Color, PixelFormatEnum},
|
|
rect::{Point, Rect},
|
|
render::{WindowCanvas, BlendMode},
|
|
surface::Surface,
|
|
};
|
|
|
|
use rand::{SeedableRng, Rng};
|
|
use rand_xoshiro::Xoshiro128Plus;
|
|
|
|
use structopt::StructOpt;
|
|
use itertools::Itertools;
|
|
|
|
use ferretro_components::base::ControlFlow;
|
|
use ferretro_components::prelude::*;
|
|
use ferretro_components::provided::{
|
|
sdl2::{Sdl2SurfaceComponent, SimpleSdl2AudioComponent},
|
|
stdlib::{SleepFramerateLimitComponent, PathBufComponent},
|
|
};
|
|
|
|
use crate::gui::*;
|
|
|
|
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
|
|
|
mod gui;
|
|
|
|
#[derive(StructOpt)]
|
|
struct Opt {
|
|
/// Core module to use. If not provided, falls back to statically-linked core.
|
|
#[structopt(short, long, parse(from_os_str))]
|
|
core: Option<PathBuf>,
|
|
/// ROM to load using the core.
|
|
#[structopt(parse(from_os_str))]
|
|
rom: PathBuf,
|
|
/// System directory, often containing BIOS files
|
|
#[structopt(short, long, parse(from_os_str))]
|
|
system: Option<PathBuf>,
|
|
}
|
|
|
|
struct Zretro {
|
|
emu: Pin<Box<RetroComponentBase>>,
|
|
canvas: WindowCanvas,
|
|
font: SurfaceFont<'static>,
|
|
mouse_util: MouseUtil,
|
|
mouse_cursor: Cursor,
|
|
mouse_shadow_surf: Surface<'static>,
|
|
mouse_shadow_pos: Rect,
|
|
rng: Xoshiro128Plus,
|
|
ui_bg: Option<Surface<'static>>,
|
|
snowflakes: Vec<Point>,
|
|
mode: GuiState,
|
|
event_pump: EventPump,
|
|
}
|
|
|
|
impl Zretro {
|
|
fn new(opt: Opt) -> Result<Self> {
|
|
let mut emu = match &opt.core {
|
|
Some(core_path) => RetroComponentBase::new(core_path),
|
|
None if cfg!(feature = "static") => RetroComponentBase::new_static(),
|
|
_ => panic!("--core is required when frontend is not built with the 'static' feature."),
|
|
};
|
|
|
|
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()?;
|
|
|
|
emu.register_component(Sdl2SurfaceComponent::new()?)?;
|
|
|
|
emu.register_component(SimpleSdl2AudioComponent::new(&mut sdl_context)?)?;
|
|
|
|
emu.register_component(SleepFramerateLimitComponent::default())?;
|
|
|
|
emu.register_component(PathBufComponent {
|
|
sys_path: opt.system.clone(),
|
|
libretro_path: opt.core.clone(),
|
|
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 SystemAvInfo { geometry, .. } = emu.libretro_core().get_system_av_info();
|
|
|
|
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 font = SurfaceFont::load_zfont()?;
|
|
|
|
let mouse_surf = surface_asset(MOUSE_PNG)?;
|
|
let mouse_shadow_surf = surface_asset(MOUSE_SHADOW_PNG)?;
|
|
|
|
let mouse_util = sdl_context.mouse();
|
|
let mouse_cursor = Cursor::from_surface(&mouse_surf, 0, 0)?;
|
|
mouse_cursor.set();
|
|
let mouse_shadow_pos = mouse_shadow_surf.rect();
|
|
|
|
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,
|
|
mouse_util,
|
|
mouse_cursor,
|
|
mouse_shadow_surf,
|
|
mouse_shadow_pos,
|
|
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?")?;
|
|
let mut bg_color = BG_COLOR;
|
|
bg_color.a = 128;
|
|
ui_bg.fill_rect(None, bg_color)?;
|
|
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 toggle_mode(&mut self) -> Result<()> {
|
|
self.mode.toggle();
|
|
match self.mode {
|
|
GuiState::Menus => {
|
|
self.mouse_cursor.set();
|
|
self.mouse_util.show_cursor(true);
|
|
}
|
|
GuiState::Game => {
|
|
self.mouse_util.show_cursor(false);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn run(&mut self) -> Result<()> {
|
|
let tc = self.canvas.texture_creator();
|
|
let font_tx = tc.create_texture_from_surface(&self.font.surf)?;
|
|
let font_rect = self.font.surf.rect();
|
|
'outer: loop {
|
|
let events = self.event_pump.poll_iter().collect_vec();
|
|
for event in events {
|
|
match event {
|
|
Event::Quit { .. } => break 'outer,
|
|
Event::KeyDown { keycode: Some(keycode), .. } => {
|
|
match keycode {
|
|
Keycode::Escape => self.toggle_mode()?,
|
|
_ => {}
|
|
}
|
|
}
|
|
Event::KeyUp { .. } => {}
|
|
Event::DropFile { filename, .. } => {
|
|
self.emu.libretro_core().unload_game();
|
|
self.emu.load_game(filename)?;
|
|
}
|
|
Event::MouseMotion { x, y, xrel, yrel, .. } => {
|
|
self.mouse_shadow_pos.set_x(x + xrel + 5);
|
|
self.mouse_shadow_pos.set_y(y + yrel + 7);
|
|
},
|
|
Event::Window { win_event: WindowEvent::Leave, .. } => {
|
|
self.mouse_shadow_pos.set_x(-(self.mouse_shadow_surf.width() as i32));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
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_mut = self.ui_bg.as_mut().ok_or("no bg?")?;
|
|
|
|
let _ = self.mouse_shadow_surf.blit(None, ui_bg_mut, self.mouse_shadow_pos);
|
|
|
|
self.font.blit_text(
|
|
ui_bg_mut,
|
|
"Hello world\nfrom zfon't!! ^_^;",
|
|
Rect::new(144, 32, 100, 20)
|
|
)?;
|
|
let mut ui_tx = tc.create_texture_from_surface(ui_bg_mut)?;
|
|
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(())
|
|
}
|