diff --git a/src/ansi_video/mod.rs b/src/ansi_video/mod.rs new file mode 100644 index 0000000..1dfc6c7 --- /dev/null +++ b/src/ansi_video/mod.rs @@ -0,0 +1,170 @@ +use std::io::{Stdout, Write}; +use std::path::Path; +use std::time::Instant; + +use ferretro_components::base::ControlFlow; +use ferretro_components::prelude::*; + +use termion::color::DetectColors; +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; + +pub 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 Drop for AnsiVideoComponent { + fn drop(&mut self) { + write!(self.screen, "{}", termion::cursor::Show).unwrap(); + } +} + +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, termion::color::Fg(termion::color::Black)).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(()) + } +} diff --git a/src/main.rs b/src/main.rs index 490ec5a..1ef134b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,4 @@ -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 std::path::PathBuf; use structopt::StructOpt; @@ -10,21 +6,8 @@ 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; +mod ansi_video; +mod term_input; #[derive(StructOpt)] struct Opt { @@ -39,338 +22,11 @@ struct Opt { 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 Drop for AnsiVideoComponent { - fn drop(&mut self) { - write!(self.screen, "{}", termion::cursor::Show).unwrap(); - } -} - -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, termion::color::Fg(termion::color::Black)).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(ansi_video::AnsiVideoComponent::default())?; + emu.register_component(term_input::TermiosInputComponent::default())?; emu.register_component(StatefulInputComponent::default())?; emu.register_component(PathBufComponent { sys_path: opt.system.clone(), diff --git a/src/term_input/mod.rs b/src/term_input/mod.rs new file mode 100644 index 0000000..fdcaf1f --- /dev/null +++ b/src/term_input/mod.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; +use std::path::Path; +use std::sync::mpsc::Receiver; +use std::time::{Duration, Instant}; + +use ferretro_components::base::ControlFlow; +use ferretro_components::prelude::*; + +use termion::event::Key; +use termion::input::TermRead; + +pub 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(()) + } +}