use std::collections::HashMap; use std::io::{Stdout, Write}; use std::path::{Path, PathBuf}; use std::sync::mpsc::Receiver; use std::time::{Duration, Instant}; use structopt::StructOpt; use ferretro_components::base::ControlFlow; use ferretro_components::prelude::*; use ferretro_components::provided::stdlib::*; use termion::color::DetectColors; use termion::event::Key; use termion::input::TermRead; use termion::raw::{IntoRawMode, RawTerminal}; use termion::screen::AlternateScreen; use termion::terminal_size; use anime_telnet::encoding::Encoder as AnsiArtEncoder; use anime_telnet::encoding::ProcessorPipeline; use anime_telnet::metadata::ColorMode; use fast_image_resize::FilterType; use image::RgbaImage; use sdl2::pixels::PixelFormatEnum; use sdl2::surface::Surface; #[derive(StructOpt)] struct Opt { /// Emulator core to use. #[structopt(short, long, parse(from_os_str))] core: PathBuf, /// Path to ROM. #[structopt(parse(from_os_str))] rom: PathBuf, /// Directory containing BIOS files #[structopt(short, long, parse(from_os_str))] system: Option, } struct AnsiVideoComponent { terminal_width: u32, terminal_height: u32, color_mode: ColorMode, screen: AlternateScreen>, fps: f32, framerate_sampling_start: Instant, frame_count: usize, frame_skip: usize, } impl AnsiArtEncoder for AnsiVideoComponent { fn needs_width(&self) -> u32 { self.terminal_width - 1 } fn needs_height(&self) -> u32 { (self.terminal_height - 1) * 2 // half-blocks? } fn needs_color(&self) -> ColorMode { self.color_mode } } impl Default for AnsiVideoComponent { fn default() -> Self { let output = std::io::stdout().into_raw_mode().unwrap(); let mut screen = AlternateScreen::from(output); write!(screen, "{}", termion::cursor::Hide).unwrap(); let (w, h) = terminal_size().unwrap_or((80, 24)); let (width, height) = (w as u32, h as u32); let colors = screen.available_colors().unwrap_or(16); let color_mode = if colors > 16 { ColorMode::True } else { ColorMode::EightBit }; AnsiVideoComponent { terminal_width: width, terminal_height: height, color_mode, screen, fps: 60.0, framerate_sampling_start: Instant::now(), frame_count: 0, frame_skip: 0, } } } impl RetroCallbacks for AnsiVideoComponent { fn video_refresh(&mut self, frame: &VideoFrame) { match frame { VideoFrame::XRGB1555 { width, height, .. } | VideoFrame::RGB565 { width, height, .. } | VideoFrame::XRGB8888 { width, height, .. } => { let (bytes, pitch) = frame.data_pitch_as_bytes().unwrap(); let pitch = pitch as u32; let format = match frame.pixel_format().unwrap() { PixelFormat::ARGB1555 => sdl2::pixels::PixelFormatEnum::RGB555, PixelFormat::ARGB8888 => sdl2::pixels::PixelFormatEnum::ARGB8888, PixelFormat::RGB565 => sdl2::pixels::PixelFormatEnum::RGB565, }; // dirty, but must be &mut for SDL API. // safety: we don't actually mutate or leak the Surface we construct here. let data = unsafe { core::slice::from_raw_parts_mut(bytes.as_ptr() as *mut u8, bytes.len()) }; // has the screen size changed? let (w, h) = terminal_size().unwrap_or((80, 24)); let (w, h) = (w as u32, h as u32); let force_redraw = if self.terminal_width != w || self.terminal_height != h { self.terminal_width = w as u32; self.terminal_height = h as u32; self.frame_skip = 0; write!(self.screen, "{}", termion::clear::All).unwrap(); true } else { false }; if !force_redraw { if self.frame_skip != 0 && self.frame_count % self.frame_skip != 0 { return; } } if let Ok(surf) = Surface::from_data(data, *width, *height, pitch, format) { let rgba_raw = surf.into_canvas().unwrap().read_pixels(None, PixelFormatEnum::ABGR8888).unwrap(); let rgba_img = RgbaImage::from_raw(*width, *height, rgba_raw).unwrap(); let processor = ProcessorPipeline { filter: FilterType::Hamming, width: self.needs_width(), height: self.needs_height(), color_modes: Some(self.needs_color()).into_iter().collect(), }; let processed = processor.process(&rgba_img).into_iter().next().unwrap().1; write!(self.screen, "{}", termion::cursor::Goto(1, 1)).unwrap(); for line in self.encode_frame(&processed).lines() { write!(self.screen, "{}\r\n", line).unwrap(); } write!(self.screen, "\x1B[0m").unwrap(); self.screen.flush().unwrap(); } else if force_redraw { // TODO: draw last copy } } _ => {} } } fn get_variable(&mut self, key: &str) -> Option { match key { "parallel-n64-gfxplugin" => Some("angrylion".to_string()), _ => None, } } } impl RetroComponent for AnsiVideoComponent { fn post_run(&mut self, _retro: &mut LibretroWrapper) -> ControlFlow { self.frame_count += 1; if self.frame_skip < 10 && self.frame_count > 10 { let now = Instant::now(); let period = now.duration_since(self.framerate_sampling_start).as_secs_f32(); let actual_fps = self.frame_count as f32 / period; if actual_fps < self.fps * 2.0 / 3.0 { self.frame_skip += 1; self.frame_count = 0; self.framerate_sampling_start = now; } } ControlFlow::Continue } fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> ferretro_components::base::Result<()> { self.fps = retro.get_system_av_info().timing.fps as f32; Ok(()) } } struct TermiosInputComponent { //reader: AsyncReader, receiver: Receiver<(Instant, Key)>, want_quit: bool, preferred_pad: Option, button_map: HashMap, axis_maps: [HashMap; 2], button_state: HashMap, axis_state: [(Instant, [i16; 2]); 2], } impl Default for TermiosInputComponent { fn default() -> Self { let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { for k in std::io::stdin().keys().map(|kr| kr.unwrap()) { sender.send((Instant::now(), k)).unwrap(); if k == Key::Esc { break } } }); TermiosInputComponent { //reader: termion::async_stdin(), receiver, want_quit: false, preferred_pad: None, button_map: [ (Key::Up, InputDeviceId::Joypad(JoypadButton::Up)), (Key::Down, InputDeviceId::Joypad(JoypadButton::Down)), (Key::Left, InputDeviceId::Joypad(JoypadButton::Left)), (Key::Right, InputDeviceId::Joypad(JoypadButton::Right)), (Key::Char('x'), InputDeviceId::Joypad(JoypadButton::A)), (Key::Char('z'), InputDeviceId::Joypad(JoypadButton::B)), (Key::Char('s'), InputDeviceId::Joypad(JoypadButton::X)), (Key::Char('a'), InputDeviceId::Joypad(JoypadButton::Y)), (Key::Ctrl('x'), InputDeviceId::Joypad(JoypadButton::A)), (Key::Ctrl('z'), InputDeviceId::Joypad(JoypadButton::B)), (Key::Ctrl('s'), InputDeviceId::Joypad(JoypadButton::X)), (Key::Ctrl('a'), InputDeviceId::Joypad(JoypadButton::Y)), (Key::Alt('x'), InputDeviceId::Joypad(JoypadButton::A)), (Key::Alt('z'), InputDeviceId::Joypad(JoypadButton::B)), (Key::Alt('s'), InputDeviceId::Joypad(JoypadButton::X)), (Key::Alt('a'), InputDeviceId::Joypad(JoypadButton::Y)), (Key::Char('q'), InputDeviceId::Joypad(JoypadButton::L)), (Key::Char('w'), InputDeviceId::Joypad(JoypadButton::R)), (Key::Ctrl('q'), InputDeviceId::Joypad(JoypadButton::L2)), (Key::Ctrl('w'), InputDeviceId::Joypad(JoypadButton::R2)), (Key::Alt('q'), InputDeviceId::Joypad(JoypadButton::L3)), (Key::Alt('w'), InputDeviceId::Joypad(JoypadButton::R3)), (Key::Char('\n'), InputDeviceId::Joypad(JoypadButton::Start)), (Key::Backspace, InputDeviceId::Joypad(JoypadButton::Select)), (Key::Char('\t'), InputDeviceId::Joypad(JoypadButton::Select)), ].into_iter().collect(), axis_maps: [ [ (Key::Char('1'), (i16::MIN, i16::MAX)), (Key::Char('2'), (0, i16::MAX)), (Key::Char('3'), (i16::MAX, i16::MAX)), (Key::Char('4'), (i16::MIN, 0)), (Key::Char('5'), (0, 0)), (Key::Char('6'), (i16::MAX, 0)), (Key::Char('7'), (i16::MIN + 1, i16::MIN + 1)), // ??? (Key::Char('8'), (0, i16::MIN)), (Key::Char('9'), (i16::MAX, i16::MIN)), ].into_iter().collect(), [ (Key::Char('r'), (0, i16::MIN)), (Key::Char('d'), (0, i16::MAX)), (Key::Char('e'), (i16::MIN, 0)), (Key::Char('f'), (i16::MAX, 0)), ].into_iter().collect(), ], button_state: Default::default(), axis_state: [(Instant::now(), [0, 0]); 2], } } } impl RetroCallbacks for TermiosInputComponent { fn input_poll(&mut self) { while let Ok((now, k)) = self.receiver.try_recv() { if k == Key::Esc { self.want_quit = true; } if let Some(mapping) = self.button_map.get(&k) { self.button_state.insert(mapping.to_owned(), (now, 1)); } for (axis_map, axis_state) in self.axis_maps.iter().zip(self.axis_state.iter_mut()) { let (mut sum_x, mut sum_y) = (0, 0); let mut count = 0; if let Some((x, y)) = axis_map.get(&k) { sum_x += *x as i32; sum_y += *y as i32; count += 1; } if count != 0 { let average_x = (sum_x / count) as i16; let average_y = (sum_y / count) as i16; *axis_state = (now, [average_x, average_y]); } } } } fn input_state(&mut self, port: u32, device: InputDeviceId, index: InputIndex) -> i16 { if port != 0 { return 0; } if let Some((inst, val)) = self.button_state.get(&device) { // TODO: consult kbdrate.c (and X11?) for durations (can't detect key-up/held, only repeat) if Instant::now().duration_since(*inst) < Duration::from_millis(300) { return *val; } } match device { InputDeviceId::Analog(axis_id) => { let (inst, axes) = self.axis_state[index as u32 as usize]; let since = Instant::now().duration_since(inst); let ratio = if since < Duration::from_millis(100) { 1.0 } else if since < Duration::from_millis(300) { (0.3 - since.as_secs_f32()) * 5.0 } else { 0.0 }; (axes[axis_id as u32 as usize] as f32 * ratio) as i16 } // TODO: mouse? _ => 0, } } fn get_variable(&mut self, key: &str) -> Option { match key { "beetle_saturn_analog_stick_deadzone" => Some("15%".to_string()), "parallel-n64-astick-deadzone" => Some("15%".to_string()), _ => None, } } fn get_input_device_capabilities(&mut self) -> Option { let bits = (1 << (DeviceType::Joypad as u32)) | (1 << (DeviceType::Analog as u32)); Some(bits as u64) } fn set_controller_info(&mut self, controller_info: &[ControllerDescription2]) -> Option { for ci in controller_info { // so we can have analog support in beetle/mednafen saturn if ci.name.as_str() == "3D Control Pad" { self.preferred_pad = Some(ci.device_id()); break; } } Some(true) } } impl RetroComponent for TermiosInputComponent { fn post_run(&mut self, _: &mut LibretroWrapper) -> ControlFlow { if self.want_quit { ControlFlow::Break } else { ControlFlow::Continue } } fn post_load_game(&mut self, retro: &mut LibretroWrapper, _rom: &Path) -> ferretro_components::base::Result<()> { if let Some(device) = self.preferred_pad { retro.set_controller_port_device(0, device); } Ok(()) } } fn main() -> Result<(), Box> { let opt: Opt = Opt::from_args(); let mut emu = RetroComponentBase::new(&opt.core); emu.register_component(AnsiVideoComponent::default())?; emu.register_component(TermiosInputComponent::default())?; emu.register_component(StatefulInputComponent::default())?; 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()), })?; let mut vars_comp = VariableStoreComponent::default(); vars_comp.insert("mgba_skip_bios", "ON"); vars_comp.insert("mgba_sgb_borders", "OFF"); emu.register_component(vars_comp)?; emu.register_component(SleepFramerateLimitComponent::default())?; emu.init()?; emu.load_game(&opt.rom)?; while let ControlFlow::Continue = emu.run() { } Ok(()) }