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 = std::result::Result>; 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, /// 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, } struct Zretro { emu: Pin>, 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>, snowflakes: Vec, mode: GuiState, event_pump: EventPump, } impl Zretro { fn new(opt: Opt) -> Result { 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(()) }