Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion docs/scripting/contexts/input_modules/joycons.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
| `set_rumble` | [`set_rumble`](#set_rumble) |
| `set_indicator` | [`set_indicator`](#set_indicator) |
| `set_indicator_slot` | [`set_indicator_slot`](#set_indicator_slot) |
| `ensure_calibration` | [`ensure_calibration`](#ensure_calibration) |

## Overview

Expand All @@ -25,7 +26,11 @@ Tiernan DeFranco, lead developer of Perro, built the first version as a standalo

This code comes from reading Bluetooth HID and BLE GATT raw bytes from Joy-Con devices on PC, then mapping those bytes into Perro controls.

Public open source projects, including JoyconPython, helped explain control reads and mappings.
Public open source projects, including JoyconPython and joycon2cpp, helped explain control reads, mappings, player LEDs, and Joy-Con 2 rumble writes.

joycon2cpp documents Joy-Con 2 BLE notification offsets for buttons, sticks, mouse data, battery, temperature, accel, gyro, and analog triggers, plus observed pairing cooldown behavior.

Perro does not claim Joy-Con 2 decryption work here. The current PC backend reads BLE reports after normal OS pairing and uses observed public report layouts and command packets.

This code does not use Nintendo SDK code, private Nintendo internals, or NDA material. Tiernan does not have access to those materials at the time this PC backend was written.

Expand Down Expand Up @@ -96,6 +101,17 @@ Nintendo Switch or Switch 2 game builds will use a separate private implementati
| Use when | Use when gameplay must change engine state or queue an action this frame. |
| Fails when / edge behavior | Returns the documented empty value when backing runtime data is missing, stale, or the target type does not match. |

### `ensure_calibration`

| Field | Detail |
| --- | --- |
| Access | `ctx.ipt.JoyCons()` |
| Signature | `pub fn ensure_calibration(&self, index: usize) -> bool` |
| Params | `&self, index: usize` |
| Returns | `bool` |
| Use when | Use when script code wants Perro to queue calibration only if the indexed Joy-Con needs it. |
| Fails when / edge behavior | Missing slots return `false`. Backend maps index to the connected serial and stores calibration in the global Perro calibration folder. |

### `joycon_accel`

| Field | Detail |
Expand Down Expand Up @@ -206,6 +222,17 @@ Nintendo Switch or Switch 2 game builds will use a separate private implementati
| Use when | Use when code needs current input device data without storing platform input state itself. |
| Fails when / edge behavior | Missing device slots return `None`, `false`, or a zero vector depending on the macro return type. Command macros queue work when an input command buffer exists. |

### `joycon_ensure_calibration`

| Field | Detail |
| --- | --- |
| Access | `ctx.ipt` |
| Signature | `joycon_ensure_calibration!(ctx.ipt, 0)` |
| Params | `ctx.ipt, 0` |
| Returns | `bool` |
| Use when | Use when code wants Perro to queue calibration only if the indexed Joy-Con needs it. |
| Fails when / edge behavior | Missing device slots return `false`. Backend maps index to serial and stores calibration for all Perro projects. |

### `joycon_pressed`

| Field | Detail |
Expand Down Expand Up @@ -283,3 +310,14 @@ Nintendo Switch or Switch 2 game builds will use a separate private implementati
| Use when | Use when code needs current input device data without storing platform input state itself. |
| Fails when / edge behavior | Missing device slots return `None`, `false`, or a zero vector depending on the macro return type. Command macros queue work when an input command buffer exists. |

### `joycon_mouse_sensor`

| Field | Detail |
| --- | --- |
| Access | `ctx.ipt` |
| Signature | `joycon_mouse_sensor!(ctx.ipt, 0)` |
| Params | `ctx.ipt, 0` |
| Returns | `JoyConMouseSensor` |
| Use when | Use when code needs Joy-Con 2 mouse sensor delta, extra axis, and distance data. |
| Fails when / edge behavior | Missing device slots return zeroed sensor data. Joy-Con 1 slots also stay zero. |

20 changes: 17 additions & 3 deletions perro_source/api_modules/perro_input_api/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
GamepadAxis, GamepadButton, InputSnapshot, JoyConButton, JoyConSide, KeyCode, MouseButton,
MouseMode, PlayerBinding,
};
use perro_structs::SignedUnitVector2;
use std::collections::VecDeque;

#[derive(Clone, Debug, PartialEq)]
Expand Down Expand Up @@ -61,8 +62,7 @@ pub enum InputEvent {
},
JoyConStick {
index: usize,
x: f32,
y: f32,
stick: SignedUnitVector2,
},
JoyConSide {
index: usize,
Expand Down Expand Up @@ -98,6 +98,13 @@ pub enum InputEvent {
y: f32,
z: f32,
},
JoyConMouseSensor {
index: usize,
x: f32,
y: f32,
extra: f32,
distance: f32,
},
BindPlayer {
index: usize,
binding: PlayerBinding,
Expand Down Expand Up @@ -214,7 +221,7 @@ fn apply_event(snapshot: &mut InputSnapshot, event: &InputEvent) {
button,
is_down,
} => snapshot.set_joycon_button_state(*index, *button, *is_down),
InputEvent::JoyConStick { index, x, y } => snapshot.set_joycon_stick(*index, *x, *y),
InputEvent::JoyConStick { index, stick } => snapshot.set_joycon_stick_unit(*index, *stick),
InputEvent::JoyConSide { index, side } => snapshot.set_joycon_side(*index, *side),
InputEvent::JoyConConnected { index, connected } => {
snapshot.set_joycon_connected(*index, *connected)
Expand All @@ -230,6 +237,13 @@ fn apply_event(snapshot: &mut InputSnapshot, event: &InputEvent) {
}
InputEvent::JoyConGyro { index, x, y, z } => snapshot.set_joycon_gyro(*index, *x, *y, *z),
InputEvent::JoyConAccel { index, x, y, z } => snapshot.set_joycon_accel(*index, *x, *y, *z),
InputEvent::JoyConMouseSensor {
index,
x,
y,
extra,
distance,
} => snapshot.set_joycon_mouse_sensor(*index, *x, *y, *extra, *distance),
InputEvent::BindPlayer { index, binding } => snapshot.bind_player(*index, *binding),
}
}
Expand Down
68 changes: 58 additions & 10 deletions perro_source/api_modules/perro_input_api/src/joycon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//! project in Summer 2025, then moved it into Rust and Perro. The public PC path
//! comes from reading Bluetooth HID and BLE GATT raw bytes from Joy-Con devices,
//! then mapping those bytes into Perro controls. Public open source projects,
//! including JoyconPython, helped explain control reads and mappings.
//! including JoyconPython and joycon2cpp, helped explain control reads,
//! mappings, player LEDs, and Joy-Con 2 rumble.
//!
//! This code does not use Nintendo SDK code, private Nintendo internals, or NDA
//! material; Tiernan does not have access to those materials at the time this PC
Expand Down Expand Up @@ -71,6 +72,14 @@ impl JoyConButton {
}
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct JoyConMouseSensor {
pub x: f32,
pub y: f32,
pub extra: f32,
pub distance: f32,
}

#[derive(Clone, Debug)]
pub struct JoyConState {
side: JoyConSide,
Expand All @@ -81,9 +90,9 @@ pub struct JoyConState {
calibrated: bool,
calibration_in_progress: bool,
calibration_requested: bool,
stick_x: f32,
stick_y: f32,
stick: perro_structs::SignedUnitVector2,
calibration_bias: perro_structs::Vector3,
mouse_sensor: JoyConMouseSensor,
gyro: perro_structs::Vector3,
accel: perro_structs::Vector3,
}
Expand All @@ -101,9 +110,9 @@ impl JoyConState {
calibrated: false,
calibration_in_progress: false,
calibration_requested: false,
stick_x: 0.0,
stick_y: 0.0,
stick: perro_structs::SignedUnitVector2::ZERO,
calibration_bias: perro_structs::Vector3::new(0.0, 0.0, 0.0),
mouse_sensor: JoyConMouseSensor::default(),
gyro: perro_structs::Vector3::new(0.0, 0.0, 0.0),
accel: perro_structs::Vector3::new(0.0, 0.0, 0.0),
}
Expand Down Expand Up @@ -156,6 +165,16 @@ impl JoyConState {
self.calibration_bias = perro_structs::Vector3::new(x, y, z);
}

#[inline(always)]
pub fn set_mouse_sensor(&mut self, x: f32, y: f32, extra: f32, distance: f32) {
self.mouse_sensor = JoyConMouseSensor {
x,
y,
extra,
distance,
};
}

#[inline(always)]
pub fn set_button_state(&mut self, button: JoyConButton, is_down: bool) {
let idx = button.as_index();
Expand All @@ -176,8 +195,12 @@ impl JoyConState {

#[inline(always)]
pub fn set_stick(&mut self, x: f32, y: f32) {
self.stick_x = x;
self.stick_y = y;
self.set_stick_unit(perro_structs::SignedUnitVector2::new(x, y));
}

#[inline(always)]
pub fn set_stick_unit(&mut self, stick: perro_structs::SignedUnitVector2) {
self.stick = stick;
}

#[inline(always)]
Expand All @@ -192,17 +215,22 @@ impl JoyConState {

#[inline(always)]
pub fn stick_x(&self) -> f32 {
self.stick_x
self.stick.x.to_f32()
}

#[inline(always)]
pub fn stick_y(&self) -> f32 {
self.stick_y
self.stick.y.to_f32()
}

#[inline(always)]
pub fn stick_unit(&self) -> perro_structs::SignedUnitVector2 {
self.stick
}

#[inline(always)]
pub fn stick(&self) -> perro_structs::Vector2 {
perro_structs::Vector2::new(self.stick_x, self.stick_y)
self.stick.as_vector2()
}

#[inline(always)]
Expand Down Expand Up @@ -245,6 +273,11 @@ impl JoyConState {
self.calibration_bias
}

#[inline(always)]
pub fn mouse_sensor(&self) -> JoyConMouseSensor {
self.mouse_sensor
}

#[inline(always)]
pub fn is_button_down(&self, button: JoyConButton) -> bool {
self.test(&self.buttons_down, button)
Expand Down Expand Up @@ -380,6 +413,21 @@ macro_rules! joycon_stick {
}};
}

#[macro_export]
/// Signature:
/// - `joycon_mouse_sensor!(&InputWindow<_>, JoyConIndex) -> JoyConMouseSensor`
///
/// Usage:
/// - `joycon_mouse_sensor!(ipt, index) -> JoyConMouseSensor`
macro_rules! joycon_mouse_sensor {
($ipt:expr, $index:expr) => {{
let jc = $ipt.JoyCons();
jc.get($index)
.map(|jc| jc.mouse_sensor())
.unwrap_or_default()
}};
}

#[macro_export]
/// Signature:
/// - `joycon_gyro!(&InputWindow<_>, JoyConIndex) -> Vector3`
Expand Down
29 changes: 15 additions & 14 deletions perro_source/api_modules/perro_input_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ mod window;
pub use frame::*;
pub use gamepad::{GamepadAxis, GamepadButton, GamepadState};
pub use input_map::{InputAction, InputBinding, InputMap, action_hash};
pub use joycon::{JoyConButton, JoyConSide, JoyConState};
pub use joycon::{JoyConButton, JoyConMouseSensor, JoyConSide, JoyConState};
pub use keycode::KeyCode;
pub use mouse_button::MouseButton;
use perro_structs::Vector2;
pub use perro_structs::{SignedUnit, SignedUnitVector2};
pub use player::{PlayerBinding, PlayerModule, PlayerState};
pub use snapshot::*;
pub use state::*;
Expand All @@ -43,19 +44,19 @@ pub mod prelude {
pub use crate::{
ActionModule, GamepadAxis, GamepadButton, GamepadIndex, GamepadModule, GamepadState,
InputAPI, InputAction, InputBinding, InputMap, InputSnapshot, InputWindow, JoyConButton,
JoyConIndex, JoyConModule, JoyConSide, JoyConState, KeyCode, KeyModule, KeyboardModule,
KeyboardState, MouseButton, MouseMode, MouseModule, MouseState, MouseStateModule,
PlayerBinding, PlayerIndicatorSlot, PlayerModule, PlayerState, RumbleIntensity,
action_down, action_hash, action_pressed, action_released, gamepad_accel, gamepad_down,
gamepad_get, gamepad_gyro, gamepad_left_stick, gamepad_list, gamepad_pressed,
JoyConIndex, JoyConModule, JoyConMouseSensor, JoyConSide, JoyConState, KeyCode, KeyModule,
KeyboardModule, KeyboardState, MouseButton, MouseMode, MouseModule, MouseState,
MouseStateModule, PlayerBinding, PlayerIndicatorSlot, PlayerModule, PlayerState,
RumbleIntensity, action_down, action_hash, action_pressed, action_released, gamepad_accel,
gamepad_down, gamepad_get, gamepad_gyro, gamepad_left_stick, gamepad_list, gamepad_pressed,
gamepad_released, gamepad_right_stick, gamepad_set_rumble, joycon_accel, joycon_calibrated,
joycon_calibrating, joycon_calibration_bias, joycon_connected, joycon_down, joycon_get,
joycon_gyro, joycon_list, joycon_needs_calibration, joycon_pressed, joycon_released,
joycon_request_calibration, joycon_set_indicator, joycon_set_rumble, joycon_side,
joycon_stick, key_down, key_pressed, key_released, mouse_capture, mouse_confine,
mouse_confine_hidden, mouse_delta, mouse_down, mouse_hide, mouse_mode, mouse_position,
mouse_pressed, mouse_released, mouse_set_mode, mouse_show, mouse_wheel, player_bind,
player_get, player_list, viewport_size,
joycon_calibrating, joycon_calibration_bias, joycon_connected, joycon_down,
joycon_ensure_calibration, joycon_get, joycon_gyro, joycon_list, joycon_mouse_sensor,
joycon_needs_calibration, joycon_pressed, joycon_released, joycon_request_calibration,
joycon_set_indicator, joycon_set_rumble, joycon_side, joycon_stick, key_down, key_pressed,
key_released, mouse_capture, mouse_confine, mouse_confine_hidden, mouse_delta, mouse_down,
mouse_hide, mouse_mode, mouse_position, mouse_pressed, mouse_released, mouse_set_mode,
mouse_show, mouse_wheel, player_bind, player_get, player_list, viewport_size,
};
pub use perro_structs::Vector2;
pub use perro_structs::{SignedUnit, SignedUnitVector2, Unit, UnitVector2, Vector2};
}
7 changes: 7 additions & 0 deletions perro_source/api_modules/perro_input_api/src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ macro_rules! joycon_request_calibration {
($ipt:expr, $index:expr) => {{ $ipt.request_joycon_calibration($index) }};
}

#[macro_export]
/// Signature:
/// - `joycon_ensure_calibration!(&InputWindow<_>, JoyConIndex) -> bool`
macro_rules! joycon_ensure_calibration {
($ipt:expr, $index:expr) => {{ $ipt.ensure_joycon_calibration($index) }};
}

#[macro_export]
macro_rules! gamepad_set_rumble {
($ipt:expr, $index:expr, $low:expr, $high:expr) => {{ $ipt.Gamepads().set_rumble($index, $low, $high) }};
Expand Down
Loading
Loading