Save input mappings to disk
This commit is contained in:
+3
-1
@@ -17,6 +17,7 @@ use crate::{
|
||||
controller::ControllerManager,
|
||||
emulator::{EmulatorClient, EmulatorCommand, SimId},
|
||||
input::MappingProvider,
|
||||
persistence::Persistence,
|
||||
window::{AppWindow, GameWindow, InputWindow},
|
||||
};
|
||||
|
||||
@@ -44,7 +45,8 @@ pub struct Application {
|
||||
impl Application {
|
||||
pub fn new(client: EmulatorClient, proxy: EventLoopProxy<UserEvent>) -> Self {
|
||||
let icon = load_icon().ok().map(Arc::new);
|
||||
let mappings = MappingProvider::new();
|
||||
let persistence = Persistence::new();
|
||||
let mappings = MappingProvider::new(persistence);
|
||||
let controllers = ControllerManager::new(client.clone(), &mappings);
|
||||
{
|
||||
let mappings = mappings.clone();
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{ffi::c_void, ptr, slice};
|
||||
use anyhow::{anyhow, Result};
|
||||
use bitflags::bitflags;
|
||||
use num_derive::{FromPrimitive, ToPrimitive};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[repr(C)]
|
||||
struct VB {
|
||||
@@ -33,7 +34,7 @@ enum VBOption {
|
||||
|
||||
bitflags! {
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct VBKey: u16 {
|
||||
const PWR = 0x0001;
|
||||
const SGN = 0x0002;
|
||||
|
||||
+125
-13
@@ -1,16 +1,40 @@
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap},
|
||||
fmt::Display,
|
||||
str::FromStr,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use gilrs::{ev::Code, Axis, Button, Gamepad, GamepadId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
|
||||
use crate::emulator::{SimId, VBKey};
|
||||
use crate::{
|
||||
emulator::{SimId, VBKey},
|
||||
persistence::Persistence,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
struct DeviceId(u16, u16);
|
||||
|
||||
impl FromStr for DeviceId {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut ids = s.split("-");
|
||||
let vendor_id: u16 = ids.next().ok_or(anyhow!("missing vendor id"))?.parse()?;
|
||||
let product_id: u16 = ids.next().ok_or(anyhow!("missing product id"))?.parse()?;
|
||||
Ok(Self(vendor_id, product_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DeviceId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}-{}", self.0, self.1))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GamepadInfo {
|
||||
pub id: GamepadId,
|
||||
@@ -26,6 +50,7 @@ pub trait Mappings {
|
||||
fn use_default_mappings(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GamepadMapping {
|
||||
buttons: HashMap<Code, VBKey>,
|
||||
axes: HashMap<Code, (VBKey, VBKey)>,
|
||||
@@ -89,6 +114,30 @@ impl GamepadMapping {
|
||||
.or_insert((VBKey::empty(), VBKey::empty()));
|
||||
entry.1 = entry.1.union(key);
|
||||
}
|
||||
|
||||
fn save_mappings(&self) -> PersistedGamepadMapping {
|
||||
fn flatten<V: Copy>(values: &HashMap<Code, V>) -> Vec<(Code, V)> {
|
||||
values.iter().map(|(k, v)| (*k, *v)).collect()
|
||||
}
|
||||
PersistedGamepadMapping {
|
||||
buttons: flatten(&self.buttons),
|
||||
axes: flatten(&self.axes),
|
||||
default_buttons: flatten(&self.default_buttons),
|
||||
default_axes: flatten(&self.default_axes),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_mappings(mappings: &PersistedGamepadMapping) -> Self {
|
||||
fn unflatten<V: Copy>(values: &[(Code, V)]) -> HashMap<Code, V> {
|
||||
values.iter().map(|(k, v)| (*k, *v)).collect()
|
||||
}
|
||||
Self {
|
||||
buttons: unflatten(&mappings.buttons),
|
||||
axes: unflatten(&mappings.axes),
|
||||
default_buttons: unflatten(&mappings.default_buttons),
|
||||
default_axes: unflatten(&mappings.default_axes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mappings for GamepadMapping {
|
||||
@@ -158,6 +207,16 @@ impl InputMapping {
|
||||
let entry = self.keys.entry(keyboard_key).or_insert(VBKey::empty());
|
||||
*entry = entry.union(key);
|
||||
}
|
||||
|
||||
fn save_mappings(&self) -> PersistedKeyboardMapping {
|
||||
PersistedKeyboardMapping {
|
||||
keys: self.keys.iter().map(|(k, v)| (*k, *v)).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_mappings(&mut self, persisted: &PersistedKeyboardMapping) {
|
||||
self.keys = persisted.keys.iter().map(|(k, v)| (*k, *v)).collect();
|
||||
}
|
||||
}
|
||||
|
||||
impl Mappings for InputMapping {
|
||||
@@ -210,25 +269,42 @@ impl Mappings for InputMapping {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MappingProvider {
|
||||
persistence: Persistence,
|
||||
device_mappings: Arc<RwLock<HashMap<DeviceId, Arc<RwLock<GamepadMapping>>>>>,
|
||||
sim_mappings: HashMap<SimId, Arc<RwLock<InputMapping>>>,
|
||||
gamepad_info: Arc<RwLock<HashMap<GamepadId, GamepadInfo>>>,
|
||||
}
|
||||
|
||||
impl MappingProvider {
|
||||
pub fn new() -> Self {
|
||||
let mut mappings = HashMap::new();
|
||||
pub fn new(persistence: Persistence) -> Self {
|
||||
let mut sim_mappings = HashMap::new();
|
||||
let mut device_mappings = HashMap::new();
|
||||
|
||||
let mut p1_mappings = InputMapping::default();
|
||||
p1_mappings.use_default_mappings();
|
||||
let p2_mappings = InputMapping::default();
|
||||
let mut p2_mappings = InputMapping::default();
|
||||
|
||||
mappings.insert(SimId::Player1, Arc::new(RwLock::new(p1_mappings)));
|
||||
mappings.insert(SimId::Player2, Arc::new(RwLock::new(p2_mappings)));
|
||||
if let Ok(persisted) = persistence.load_config::<PersistedInputMappings>("mappings") {
|
||||
p1_mappings.restore_mappings(&persisted.p1_keyboard);
|
||||
p2_mappings.restore_mappings(&persisted.p2_keyboard);
|
||||
|
||||
for (device_id, mappings) in persisted.gamepads {
|
||||
let Ok(device_id) = device_id.parse::<DeviceId>() else {
|
||||
continue;
|
||||
};
|
||||
let gamepad = GamepadMapping::from_mappings(&mappings);
|
||||
device_mappings.insert(device_id, Arc::new(RwLock::new(gamepad)));
|
||||
}
|
||||
} else {
|
||||
p1_mappings.use_default_mappings();
|
||||
}
|
||||
|
||||
sim_mappings.insert(SimId::Player1, Arc::new(RwLock::new(p1_mappings)));
|
||||
sim_mappings.insert(SimId::Player2, Arc::new(RwLock::new(p2_mappings)));
|
||||
Self {
|
||||
device_mappings: Arc::new(RwLock::new(HashMap::new())),
|
||||
persistence,
|
||||
device_mappings: Arc::new(RwLock::new(device_mappings)),
|
||||
gamepad_info: Arc::new(RwLock::new(HashMap::new())),
|
||||
sim_mappings: mappings,
|
||||
sim_mappings,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,7 +314,7 @@ impl MappingProvider {
|
||||
|
||||
pub fn for_gamepad(&self, gamepad_id: GamepadId) -> Option<Arc<RwLock<GamepadMapping>>> {
|
||||
let lock = self.gamepad_info.read().unwrap();
|
||||
let device_id = lock.get(&gamepad_id)?.device_id;
|
||||
let device_id = lock.get(&gamepad_id)?.device_id.clone();
|
||||
drop(lock);
|
||||
let lock = self.device_mappings.read().unwrap();
|
||||
lock.get(&device_id).cloned()
|
||||
@@ -250,7 +326,7 @@ impl MappingProvider {
|
||||
gamepad.product_id().unwrap_or_default(),
|
||||
);
|
||||
let mut lock = self.device_mappings.write().unwrap();
|
||||
let mappings = match lock.entry(device_id) {
|
||||
let mappings = match lock.entry(device_id.clone()) {
|
||||
Entry::Vacant(entry) => {
|
||||
let mappings = GamepadMapping::for_gamepad(gamepad);
|
||||
entry.insert(Arc::new(RwLock::new(mappings)))
|
||||
@@ -303,7 +379,7 @@ impl MappingProvider {
|
||||
return;
|
||||
};
|
||||
info.bound_to = Some(sim_id);
|
||||
let device_id = info.device_id;
|
||||
let device_id = info.device_id.clone();
|
||||
drop(lock);
|
||||
let Some(device_mappings) = self
|
||||
.device_mappings
|
||||
@@ -341,4 +417,40 @@ impl MappingProvider {
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn save(&self) {
|
||||
let p1_keyboard = self.for_sim(SimId::Player1).read().unwrap().save_mappings();
|
||||
let p2_keyboard = self.for_sim(SimId::Player2).read().unwrap().save_mappings();
|
||||
let mut gamepads = HashMap::new();
|
||||
for (device_id, gamepad) in self.device_mappings.read().unwrap().iter() {
|
||||
let mapping = gamepad.read().unwrap().save_mappings();
|
||||
gamepads.insert(device_id.to_string(), mapping);
|
||||
}
|
||||
let persisted = PersistedInputMappings {
|
||||
p1_keyboard,
|
||||
p2_keyboard,
|
||||
gamepads,
|
||||
};
|
||||
let _ = self.persistence.save_config("mappings", &persisted);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PersistedInputMappings {
|
||||
p1_keyboard: PersistedKeyboardMapping,
|
||||
p2_keyboard: PersistedKeyboardMapping,
|
||||
gamepads: HashMap<String, PersistedGamepadMapping>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PersistedKeyboardMapping {
|
||||
keys: Vec<(PhysicalKey, VBKey)>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PersistedGamepadMapping {
|
||||
buttons: Vec<(Code, VBKey)>,
|
||||
axes: Vec<(Code, (VBKey, VBKey))>,
|
||||
default_buttons: Vec<(Code, VBKey)>,
|
||||
default_axes: Vec<(Code, (VBKey, VBKey))>,
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ mod controller;
|
||||
mod emulator;
|
||||
mod graphics;
|
||||
mod input;
|
||||
mod persistence;
|
||||
mod window;
|
||||
|
||||
#[derive(Parser)]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Persistence {
|
||||
dirs: Option<Dirs>,
|
||||
}
|
||||
|
||||
impl Persistence {
|
||||
pub fn new() -> Self {
|
||||
Self { dirs: init_dirs() }
|
||||
}
|
||||
|
||||
pub fn save_config<T: Serialize>(&self, file: &str, data: &T) -> Result<()> {
|
||||
if let Some(dirs) = self.dirs.as_ref() {
|
||||
let bytes = serde_json::to_vec_pretty(data)?;
|
||||
let filename = dirs.config_dir.join(file).with_extension("json");
|
||||
fs::write(&filename, bytes)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_config<T: for<'a> Deserialize<'a>>(&self, file: &str) -> Result<T> {
|
||||
let Some(dirs) = self.dirs.as_ref() else {
|
||||
bail!("config directory not found");
|
||||
};
|
||||
let filename = dirs.config_dir.join(file).with_extension("json");
|
||||
let bytes = fs::read(filename)?;
|
||||
Ok(serde_json::from_slice(&bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Dirs {
|
||||
config_dir: PathBuf,
|
||||
}
|
||||
|
||||
fn init_dirs() -> Option<Dirs> {
|
||||
let dirs = ProjectDirs::from("com", "virtual-boy", "Lemur")?;
|
||||
let config_dir = dirs.config_dir().to_path_buf();
|
||||
fs::create_dir_all(&config_dir).ok()?;
|
||||
Some(Dirs { config_dir })
|
||||
}
|
||||
@@ -53,10 +53,12 @@ impl InputWindow {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Use defaults").clicked() {
|
||||
mappings.write().unwrap().use_default_mappings();
|
||||
self.mappings.save();
|
||||
self.now_binding = None;
|
||||
}
|
||||
if ui.button("Clear all").clicked() {
|
||||
mappings.write().unwrap().clear_all_mappings();
|
||||
self.mappings.save();
|
||||
self.now_binding = None;
|
||||
}
|
||||
});
|
||||
@@ -99,6 +101,8 @@ impl InputWindow {
|
||||
{
|
||||
let mut mapping = mappings.write().unwrap();
|
||||
mapping.clear_mappings(*key);
|
||||
drop(mapping);
|
||||
self.mappings.save();
|
||||
self.now_binding = None;
|
||||
}
|
||||
});
|
||||
@@ -210,6 +214,8 @@ impl AppWindow for InputWindow {
|
||||
};
|
||||
let mut mappings = self.mappings.for_sim(sim_id).write().unwrap();
|
||||
mappings.add_keyboard_mapping(vb, event.physical_key);
|
||||
drop(mappings);
|
||||
self.mappings.save();
|
||||
}
|
||||
|
||||
fn handle_gamepad_event(&mut self, event: &gilrs::Event) {
|
||||
|
||||
Reference in New Issue
Block a user