use crate::proto::proto::ProtoControllerAttach; use crate::proto::proto::proto_message::Payload; use anyhow::Result; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::mpsc; fn controller_string_to_type(controller_type: &str) -> Result { match controller_type.to_lowercase().as_str() { "ps4" => Ok(vimputti::ControllerTemplates::ps4()), "ps5" => Ok(vimputti::ControllerTemplates::ps5()), "xbox360" => Ok(vimputti::ControllerTemplates::xbox360()), "xboxone" => Ok(vimputti::ControllerTemplates::xbox_one()), "switchpro" => Ok(vimputti::ControllerTemplates::switch_pro()), _ => Err(anyhow::anyhow!( "Unsupported controller type: {}", controller_type )), } } pub struct ControllerInput { config: vimputti::DeviceConfig, device: vimputti::client::VirtualController, } impl ControllerInput { pub async fn new( controller_type: String, client: &vimputti::client::VimputtiClient, ) -> Result { let config = controller_string_to_type(&controller_type)?; let device = client.create_device(config.clone()).await?; Ok(Self { config, device }) } pub fn device_mut(&mut self) -> &mut vimputti::client::VirtualController { &mut self.device } pub fn device(&self) -> &vimputti::client::VirtualController { &self.device } } pub struct ControllerManager { vimputti_client: Arc, cmd_tx: mpsc::Sender, rumble_tx: mpsc::Sender<(u32, u16, u16, u16, String)>, // (slot, strong, weak, duration_ms, session_id) attach_tx: mpsc::Sender, } impl ControllerManager { pub fn new( vimputti_client: Arc, ) -> Result<( Self, mpsc::Receiver<(u32, u16, u16, u16, String)>, mpsc::Receiver, )> { let (cmd_tx, cmd_rx) = mpsc::channel(512); let (rumble_tx, rumble_rx) = mpsc::channel(256); let (attach_tx, attach_rx) = mpsc::channel(64); tokio::spawn(command_loop( cmd_rx, vimputti_client.clone(), rumble_tx.clone(), attach_tx.clone(), )); Ok(( Self { vimputti_client, cmd_tx, rumble_tx, attach_tx, }, rumble_rx, attach_rx, )) } pub async fn send_command(&self, payload: Payload) -> Result<()> { self.cmd_tx.send(payload).await?; Ok(()) } } struct ControllerSlot { controller: ControllerInput, session_id: String, session_slot: u32, } // Returns first free controller slot from 0-16 fn get_free_slot(controllers: &HashMap) -> Option { for slot in 0..17 { if !controllers.contains_key(&slot) { return Some(slot); } } None } async fn command_loop( mut cmd_rx: mpsc::Receiver, vimputti_client: Arc, rumble_tx: mpsc::Sender<(u32, u16, u16, u16, String)>, attach_tx: mpsc::Sender, ) { let mut controllers: HashMap = HashMap::new(); while let Some(payload) = cmd_rx.recv().await { match payload { Payload::ControllerAttach(data) => { let session_id = data.session_id.clone(); let session_slot = data.session_slot.clone(); // Check if this session already has a slot (reconnection) let existing_slot = controllers .iter() .find(|(_, slot)| { slot.session_id == session_id && slot.session_slot == session_slot as u32 }) .map(|(slot_num, _)| *slot_num); if let Some(existing_slot) = existing_slot { if let Some(controller_slot) = controllers.get_mut(&existing_slot) { let rumble_tx = rumble_tx.clone(); let attach_tx = attach_tx.clone(); controller_slot .controller .device_mut() .on_rumble(move |strong, weak, duration_ms| { let _ = rumble_tx.try_send(( existing_slot, strong, weak, duration_ms, data.session_id.clone(), )); }) .await .map_err(|e| { tracing::warn!( "Failed to register rumble callback for slot {}: {}", existing_slot, e ); }) .ok(); // Return to attach_tx what slot was assigned let attach_info = ProtoControllerAttach { id: data.id.clone(), session_slot: existing_slot as i32, session_id: session_id.clone(), }; match attach_tx.send(attach_info).await { Ok(_) => { tracing::info!( "Controller {} re-attached to slot {} (session: {})", data.id, existing_slot, session_id ); } Err(e) => { tracing::error!( "Failed to send re-attach info for slot {}: {}", existing_slot, e ); } } } } else if let Some(slot) = get_free_slot(&controllers) { if let Ok(mut controller) = ControllerInput::new(data.id.clone(), &vimputti_client).await { let rumble_tx = rumble_tx.clone(); let attach_tx = attach_tx.clone(); controller .device_mut() .on_rumble(move |strong, weak, duration_ms| { let _ = rumble_tx.try_send(( slot, strong, weak, duration_ms, data.session_id.clone(), )); }) .await .map_err(|e| { tracing::warn!( "Failed to register rumble callback for slot {}: {}", slot, e ); }) .ok(); // Return to attach_tx what slot was assigned let attach_info = ProtoControllerAttach { id: data.id.clone(), session_slot: slot as i32, session_id: session_id.clone(), }; match attach_tx.send(attach_info).await { Ok(_) => { controllers.insert( slot, ControllerSlot { controller, session_id: session_id.clone(), session_slot: session_slot.clone() as u32, }, ); tracing::info!( "Controller {} attached to slot {} (session: {})", data.id, slot, session_id ); } Err(e) => { tracing::error!( "Failed to send attach info for slot {}: {}", slot, e ); } } } else { tracing::error!( "Failed to create controller of type {} for slot {}", data.id, slot ); } } } Payload::ControllerDetach(data) => { if controllers.remove(&(data.session_slot as u32)).is_some() { tracing::info!("Controller detached from slot {}", data.session_slot); } else { tracing::warn!( "No controller found in slot {} to detach", data.session_slot ); } } Payload::ClientDisconnected(data) => { tracing::info!( "Client disconnected, cleaning up controller slots: {:?} (client session: {})", data.controller_slots, data.session_id ); // Remove all controllers for the disconnected slots for slot in &data.controller_slots { if controllers.remove(&(*slot as u32)).is_some() { tracing::info!( "Removed controller from slot {} (client session: {})", slot, data.session_id ); } else { tracing::warn!( "No controller found in slot {} to cleanup (client session: {})", slot, data.session_id ); } } } Payload::ControllerStateBatch(data) => { if let Some(controller) = controllers.get(&(data.session_slot as u32)) { let device = controller.controller.device(); // Handle inputs based on update type if data.update_type == 0 { // FULL_STATE: Update all values let _ = device.sync().await; for (btn_code, pressed) in data.button_changed_mask { if let Some(button) = vimputti::Button::from_ev_code(btn_code as u16) { let _ = device.button(button, pressed).await; let _ = device.sync().await; } } if let Some(x) = data.left_stick_x { let _ = device.axis(vimputti::Axis::LeftStickX, x).await; let _ = device.sync().await; } if let Some(y) = data.left_stick_y { let _ = device.axis(vimputti::Axis::LeftStickY, y).await; let _ = device.sync().await; } if let Some(x) = data.right_stick_x { let _ = device.axis(vimputti::Axis::RightStickX, x).await; let _ = device.sync().await; } if let Some(y) = data.right_stick_y { let _ = device.axis(vimputti::Axis::RightStickY, y).await; let _ = device.sync().await; } if let Some(value) = data.left_trigger { let _ = device.axis(vimputti::Axis::LowerLeftTrigger, value).await; let _ = device.sync().await; } if let Some(value) = data.right_trigger { let _ = device.axis(vimputti::Axis::LowerRightTrigger, value).await; let _ = device.sync().await; } if let Some(x) = data.dpad_x { let _ = device.axis(vimputti::Axis::DPadX, x).await; let _ = device.sync().await; } if let Some(y) = data.dpad_y { let _ = device.axis(vimputti::Axis::DPadY, y).await; let _ = device.sync().await; } } else { // DELTA: Only update changed values if let Some(changed_fields) = data.changed_fields { let _ = device.sync().await; if (changed_fields & (1 << 0)) != 0 { for (btn_code, pressed) in data.button_changed_mask { if let Some(button) = vimputti::Button::from_ev_code(btn_code as u16) { let _ = device.button(button, pressed).await; let _ = device.sync().await; } } } if (changed_fields & (1 << 1)) != 0 { if let Some(x) = data.left_stick_x { let _ = device.axis(vimputti::Axis::LeftStickX, x).await; let _ = device.sync().await; } } if (changed_fields & (1 << 2)) != 0 { if let Some(y) = data.left_stick_y { let _ = device.axis(vimputti::Axis::LeftStickY, y).await; let _ = device.sync().await; } } if (changed_fields & (1 << 3)) != 0 { if let Some(x) = data.right_stick_x { let _ = device.axis(vimputti::Axis::RightStickX, x).await; let _ = device.sync().await; } } if (changed_fields & (1 << 4)) != 0 { if let Some(y) = data.right_stick_y { let _ = device.axis(vimputti::Axis::RightStickY, y).await; let _ = device.sync().await; } } if (changed_fields & (1 << 5)) != 0 { if let Some(value) = data.left_trigger { let _ = device.axis(vimputti::Axis::LowerLeftTrigger, value).await; let _ = device.sync().await; } } if (changed_fields & (1 << 6)) != 0 { if let Some(value) = data.right_trigger { let _ = device.axis(vimputti::Axis::LowerRightTrigger, value).await; let _ = device.sync().await; } } if (changed_fields & (1 << 7)) != 0 { if let Some(x) = data.dpad_x { let _ = device.axis(vimputti::Axis::DPadX, x).await; let _ = device.sync().await; } } if (changed_fields & (1 << 8)) != 0 { if let Some(y) = data.dpad_y { let _ = device.axis(vimputti::Axis::DPadY, y).await; let _ = device.sync().await; } } } } } else { tracing::warn!( "Controller slot {} not found for state batch event", data.session_slot ); } } _ => { //no-op } } } }