break components out into modules
This commit is contained in:
parent
d8a403110f
commit
3f88e61a0b
170
src/ansi_video/mod.rs
Normal file
170
src/ansi_video/mod.rs
Normal file
|
@ -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<RawTerminal<Stdout>>,
|
||||||
|
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<String> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
354
src/main.rs
354
src/main.rs
|
@ -1,8 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::path::PathBuf;
|
||||||
use std::io::{Stdout, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::mpsc::Receiver;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
|
@ -10,21 +6,8 @@ use ferretro_components::base::ControlFlow;
|
||||||
use ferretro_components::prelude::*;
|
use ferretro_components::prelude::*;
|
||||||
use ferretro_components::provided::stdlib::*;
|
use ferretro_components::provided::stdlib::*;
|
||||||
|
|
||||||
use termion::color::DetectColors;
|
mod ansi_video;
|
||||||
use termion::event::Key;
|
mod term_input;
|
||||||
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)]
|
#[derive(StructOpt)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
|
@ -39,338 +22,11 @@ struct Opt {
|
||||||
system: Option<PathBuf>,
|
system: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AnsiVideoComponent {
|
|
||||||
terminal_width: u32,
|
|
||||||
terminal_height: u32,
|
|
||||||
color_mode: ColorMode,
|
|
||||||
screen: AlternateScreen<RawTerminal<Stdout>>,
|
|
||||||
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<String> {
|
|
||||||
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<u32>,
|
|
||||||
button_map: HashMap<Key, InputDeviceId>,
|
|
||||||
axis_maps: [HashMap<Key, (i16, i16)>; 2],
|
|
||||||
button_state: HashMap<InputDeviceId, (Instant, i16)>,
|
|
||||||
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<String> {
|
|
||||||
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<u64> {
|
|
||||||
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<bool> {
|
|
||||||
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<dyn std::error::Error + Send + Sync>> {
|
fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let opt: Opt = Opt::from_args();
|
let opt: Opt = Opt::from_args();
|
||||||
let mut emu = RetroComponentBase::new(&opt.core);
|
let mut emu = RetroComponentBase::new(&opt.core);
|
||||||
emu.register_component(AnsiVideoComponent::default())?;
|
emu.register_component(ansi_video::AnsiVideoComponent::default())?;
|
||||||
emu.register_component(TermiosInputComponent::default())?;
|
emu.register_component(term_input::TermiosInputComponent::default())?;
|
||||||
emu.register_component(StatefulInputComponent::default())?;
|
emu.register_component(StatefulInputComponent::default())?;
|
||||||
emu.register_component(PathBufComponent {
|
emu.register_component(PathBufComponent {
|
||||||
sys_path: opt.system.clone(),
|
sys_path: opt.system.clone(),
|
||||||
|
|
187
src/term_input/mod.rs
Normal file
187
src/term_input/mod.rs
Normal file
|
@ -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<u32>,
|
||||||
|
button_map: HashMap<Key, InputDeviceId>,
|
||||||
|
axis_maps: [HashMap<Key, (i16, i16)>; 2],
|
||||||
|
button_state: HashMap<InputDeviceId, (Instant, i16)>,
|
||||||
|
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<String> {
|
||||||
|
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<u64> {
|
||||||
|
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<bool> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue