mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
402 lines
17 KiB
Rust
402 lines
17 KiB
Rust
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<vimputti::DeviceConfig> {
|
|
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<Self> {
|
|
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<vimputti::client::VimputtiClient>,
|
|
cmd_tx: mpsc::Sender<Payload>,
|
|
rumble_tx: mpsc::Sender<(u32, u16, u16, u16, String)>, // (slot, strong, weak, duration_ms, session_id)
|
|
attach_tx: mpsc::Sender<ProtoControllerAttach>,
|
|
}
|
|
impl ControllerManager {
|
|
pub fn new(
|
|
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
|
) -> Result<(
|
|
Self,
|
|
mpsc::Receiver<(u32, u16, u16, u16, String)>,
|
|
mpsc::Receiver<ProtoControllerAttach>,
|
|
)> {
|
|
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<u32, ControllerSlot>) -> Option<u32> {
|
|
for slot in 0..17 {
|
|
if !controllers.contains_key(&slot) {
|
|
return Some(slot);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
async fn command_loop(
|
|
mut cmd_rx: mpsc::Receiver<Payload>,
|
|
vimputti_client: Arc<vimputti::client::VimputtiClient>,
|
|
rumble_tx: mpsc::Sender<(u32, u16, u16, u16, String)>,
|
|
attach_tx: mpsc::Sender<ProtoControllerAttach>,
|
|
) {
|
|
let mut controllers: HashMap<u32, ControllerSlot> = 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
|
|
}
|
|
}
|
|
}
|
|
}
|