├── .cargo └── config ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .rustfmt.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE_APACHE_2_0.txt ├── LICENSE_BOOST_1_0.txt ├── LICENSE_MIT.txt ├── README.md ├── gamepad.svg ├── sdb ├── LICENSE ├── README.md └── linux │ ├── 03002509E8030101.toml │ ├── 03004C0568021081.toml │ ├── 03004F0404041101.toml │ ├── 03005E048E021001.toml │ ├── 03005E04D1020101.toml │ ├── 03006D0416C21101.toml │ ├── 03006F0E01050001.toml │ ├── 0300790044181001.toml │ ├── 0300B40412241101.toml │ ├── 0300B50716031001.toml │ └── 05005E04E0020309.toml ├── stick ├── Cargo.toml ├── README.md ├── examples │ └── haptic.rs ├── remap_linux.sdb ├── sdlgc_linux.sdb └── src │ ├── ctlr.rs │ ├── event.rs │ ├── focus.rs │ ├── gamepad.rs │ ├── lib.rs │ ├── listener.rs │ ├── raw.rs │ └── raw │ ├── android.rs │ ├── ardaku.rs │ ├── bsd.rs │ ├── dom.rs │ ├── fuchsia.rs │ ├── ios.rs │ ├── linux.rs │ ├── macos.rs │ ├── redox.rs │ ├── wasi.rs │ └── windows.rs └── xtask ├── Cargo.toml └── src ├── main.rs └── sdb.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: tests 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | tc: [stable, beta, nightly] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: ${{ matrix.tc }} 18 | override: true 19 | - uses: actions-rs/cargo@v1 20 | with: 21 | command: test 22 | args: --all --all-features 23 | cross-compile: 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | os: [ubuntu-latest] 28 | tc: [stable, beta, nightly] 29 | cc: 30 | - aarch64-linux-android 31 | - i686-pc-windows-gnu 32 | - i686-unknown-freebsd 33 | - i686-unknown-linux-gnu 34 | - wasm32-unknown-unknown 35 | - x86_64-apple-darwin 36 | - x86_64-unknown-redox 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: ${{ matrix.tc }} 43 | target: ${{ matrix.cc }} 44 | override: true 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: build 48 | args: --all-features --target=${{ matrix.cc }} --lib 49 | cross-compile-ios: 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | matrix: 53 | os: [macos-latest] 54 | tc: [stable, beta, nightly] 55 | cc: [aarch64-apple-ios] 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | profile: minimal 61 | toolchain: ${{ matrix.tc }} 62 | target: ${{ matrix.cc }} 63 | override: true 64 | - uses: actions-rs/cargo@v1 65 | with: 66 | command: build 67 | args: --all-features --target=${{ matrix.cc }} --lib 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.cargo-ok 3 | /Cargo.lock 4 | /**/*.rs.bk 5 | /**/.idea 6 | /**/*~ 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gcdb"] 2 | path = gcdb 3 | url = https://github.com/gabomdq/SDL_GameControllerDB.git 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Don't deviate too much, just reduce columns and stricter enforcement 2 | edition = "2021" 3 | unstable_features = true 4 | max_width = 80 5 | reorder_impl_items = true 6 | group_imports = "StdExternalCrate" 7 | imports_granularity = "Crate" 8 | newline_style = "Unix" 9 | normalize_doc_attributes = true 10 | wrap_comments = true 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to `stick` will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://github.com/AldaronLau/semver). 6 | 7 | ## [0.13.0] - 2023-03-03 8 | ### Changed 9 | - `Controller` is now `Send` 10 | - `Listener` is now `Send` 11 | 12 | ## [0.12.4] - 2022-01-29 13 | ### Added 14 | - Mappings for Flydigi Apex 2 (thanks to 15 | [zeddidragon](https://github.com/zeddidragon)!) 16 | - Generic `gamepad` controller type (thanks to 17 | [zeddidragon](https://github.com/zeddidragon)!) 18 | - `Event::ActionWheelX` and `Event::ActionWheelY` (thanks to 19 | [zeddidragon](https://github.com/zeddidragon)!) 20 | 21 | ## [0.12.3] - 2022-01-27 22 | ### Added 23 | - Support for buttons on Flydigi Apex 2 (thanks to 24 | [zeddidragon](https://github.com/zeddidragon)!) 25 | 26 | ## [0.12.2] - 2021-08-03 27 | ### Added 28 | - Support for KEY\_MENU event on Linux. 29 | - A joystick mapping for wireless XBox controller 30 | 31 | ### Fixed 32 | - A bug in xtask bytecode generation for some events. 33 | 34 | ## [0.12.1] - 2021-08-02 35 | ### Added 36 | - Support for KEY\_BACK and KEY\_FORWARD events on Linux. 37 | 38 | ## [0.12.0] - 2021-07-07 39 | ### Added 40 | - You can now import your own mappings at runtime with `Remap` 41 | - Default evdev id guessing 42 | - Windows support (thanks to [dparnell](https://github.com/dparnell)!) 43 | - Support adjusting left and right rumble separately. 44 | - `focus()` for enabling events (enabled by default). 45 | - `unfocus()` - for disabling events when window is not in focus. 46 | - New variants to `Event` 47 | 48 | ### Changed 49 | - Database now stored as data rather than control flow in resulting binary 50 | (lowering bloat) 51 | - Offload serde dependency to xtask pattern to improve compile times 52 | - Database TOML is simplified 53 | - Some `Event` names 54 | 55 | ### Removed 56 | - Controllers from sdl controller db (it assumes everything is a gamepad, which 57 | not all controllers are - stick will only include semantically correct 58 | mappings from now on by default). You may add them back with the new "gcdb" 59 | feature. 60 | 61 | ### Fixed 62 | - Cleaned up database (fixing inconsistencies). 63 | - Inconsistent freezing issues on Linux 64 | 65 | ## [0.11.1] - 2020-12-13 66 | ### Fixed 67 | - Dummy implementation not compiling (started using GitHub Actions so hopefully 68 | this won't happen again). 69 | 70 | ## [0.11.0] - 2020-11-25 71 | ### Added 72 | - `Controller::listener()` to replace `Hub`. 73 | 74 | ### Changed 75 | - `Pad` renamed to `Controller`. 76 | 77 | ### Removed 78 | - `Hub` type, use `Controller::listener()` instead. 79 | 80 | ### Fixed 81 | - The controller database is now sorted (thanks to 82 | [iRaiko](https://github.com/iRaiko)!) 83 | 84 | ## [0.10.2] - 2020-09-20 85 | ### Fixed 86 | - Incorrect mapping for Xbox One wired controller 87 | 88 | ### Contributors 89 | Thanks to everyone who contributed to make this version of stick possible! 90 | 91 | - [AldaronLau](https://github.com/AldaronLau) 92 | - [jamesmcm](https://github.com/jamesmcm) 93 | 94 | ## [0.10.1] - 2020-06-26 95 | ### Added 96 | - Support for XBox and Steam controller Grip Buttons / Paddles: 97 | - `Event::PaddleRight(bool)` 98 | - `Event::PaddleLeft(bool)` 99 | - `Event::PaddleRightPinky(bool)` 100 | - `Event::PaddleLeftPinky(bool)` 101 | 102 | ## [0.10.0] - 2020-06-09 103 | ### Added 104 | - Support for all of the gamepads and joysticks in SDL\_gamecontrollerdb by 105 | creating a database of gamepads (using a TOML format), as well as others that 106 | weren't in the database (pads stick supported before, plus the Thrustmaster 107 | Warthog). 108 | - `Event` variants: 109 | - `ActionC(bool)` 110 | - `JoyZ(f64)` 111 | - `CamZ(f64)` 112 | - `AutopilotToggle(bool)` 113 | - `LandingGearSilence(bool)` 114 | - `PovUp(bool)` 115 | - `PovDown(bool)` 116 | - `PovLeft(bool)` 117 | - `PovRight(bool)` 118 | - `MicUp(bool)` 119 | - `MicDown(bool)` 120 | - `MicLeft(bool)` 121 | - `MicRight(bool)` 122 | - `MicPush(bool)` 123 | - `Slew(f64)` 124 | - `Throttle(f64)` 125 | - `ThrottleL(f64)` 126 | - `ThrottleR(f64)` 127 | - `ThrottleButtonL(bool)` 128 | - `EngineFuelFlowL(bool)` 129 | - `EngineFuelFlowR(bool)` 130 | - `Eac(bool)` 131 | - `RadarAltimeter(bool)` 132 | - `Apu(bool)` 133 | - `AutopilotPath(bool)` 134 | - `AutopilotAlt(bool)` 135 | - `FlapsUp(bool)` 136 | - `FlapsDown(bool)` 137 | - `EngineLIgnition(bool)` 138 | - `EngineLMotor(bool)` 139 | - `EngineRIgnition(bool)` 140 | - `EngineRMotor(bool)` 141 | - `PinkyForward(bool)` 142 | - `PinkyBackward(bool)` 143 | - `SpeedbrakeForward(bool)` 144 | - `SpeedbrakeBackward(bool)` 145 | - `BoatForward(bool)` 146 | - `BoatBackward(bool)` 147 | - `ChinaForward(bool)` 148 | - `ChinaBackward(bool)` 149 | - `Dpi(bool)` 150 | - `MouseX(f64)` 151 | - `MouseY(f64)` 152 | - `MousePush(bool)` 153 | - `WheelX(f64)` 154 | - `WheelY(f64)` 155 | - `WheelPush(bool)` 156 | 157 | ### Changed 158 | - Renamed `Port` to `Hub` 159 | - Renamed `Gamepad` to `Pad` 160 | - `id()` now returns a `[u16; 4]` instead of a `u32` for gamepad ID. 161 | - Renamed `Event` variants: 162 | - `Accept(bool)` -> `ActionA(bool)` 163 | - `Cancel(bool)` -> `ActionB(bool)` 164 | - `Common(bool)` -> `ActionH(bool)` 165 | - `Action(bool)` -> `ActionV(bool)` 166 | - `Up(bool)` -> `DpadUp(bool)` 167 | - `Down(bool)` -> `DpadDown(bool)` 168 | - `Left(bool)` -> `DpadLeft(bool)` 169 | - `Right(bool)` -> `DpadRight(bool)` 170 | - `Back(bool)` -> `Prev(bool)` 171 | - `Forward(bool)` -> `Next(bool)` 172 | - `L(bool)` -> `BumperL(bool)` 173 | - `R(bool)` -> `BumperR(bool)` 174 | - `Lt(f32)` -> `TriggerL(f64)` 175 | - `Rt(f32)` -> `TriggerR(f64)` 176 | - `MotionH(f32)` -> `JoyX(f64)` 177 | - `MotionV(f32)` -> `JoyY(f64)` 178 | - `CameraH(f32)` -> `CamX(f64)` 179 | - `CameraV(f32)` -> `CamY(f64)` 180 | - `MotionButton(bool)` -> `JoyPush(bool)` 181 | - `CameraButton(bool)` -> `CamPush(bool)` 182 | - `ExtL(u8, bool)` -> `Action(u16, bool)` 183 | - `ExtR(u8, bool)` -> `Action(u16, bool)` 184 | - `Quit` -> `Home(bool)` 185 | - `Event` is now marked `#[non_exhaustive]`, so you will alway have to match 186 | for `_`. 187 | 188 | ### Fixed 189 | - Randomly crashing 190 | - All clippy lints 191 | - No longer does `dbg!()` prints constantly. 192 | 193 | ### Contributors 194 | Thanks to everyone who contributed to make this version of stick possible! 195 | 196 | - [AldaronLau](https://github.com/AldaronLau) 197 | - [theunkn0wn1](https://github.com/theunkn0wn1) 198 | 199 | ## [0.9.0] - 2020-05-18 200 | ### Added 201 | - Support for a lot of different kinds of joysticks. 202 | - `ExtL` and `ExtR` variants of `Event` enum for buttons that aren't on the W3C 203 | standard gamepad. 204 | 205 | ### Changed 206 | - Update documentation/examples to use pasts 0.4 207 | - Remove unneeded `(&mut )` in example/documentation. 208 | - `Lz` and `Rz` variants on `Event` are renamed to `Lt` and `Rt` 209 | 210 | ### Contributions 211 | Thanks to everyone who contributed to make this version of stick possible! 212 | 213 | - [AldaronLau](https://github.com/AldaronLau) 214 | - [Chronophylos](https://github.com/Chronophylos) 215 | 216 | - Thanks to everyone who helped test joysticks at 217 | [https://github.com/ardaku/stick/issues/5](https://github.com/ardaku/stick/issues/5)! 218 | 219 | ## [0.8.0] - 2020-05-03 220 | ### Added 221 | - Async/await support 222 | - Haptic support with `.rumble()` on `Gamepad`. 223 | - Linux implementation of `.name()` on `Gamepad`. 224 | - `Event` enum. 225 | 226 | ### Changed 227 | - Renamed `Device` to `Gamepad`. 228 | - Back to an event-based API with `Event`. 229 | 230 | ### Removed 231 | - `Btn` enum, use `Event` instead. 232 | 233 | ### Fixed 234 | - Panic on drop (joystick disconnected) 235 | 236 | ### Contributors 237 | Thanks to everyone who contributed to make this version of stick possible! 238 | 239 | - [AldaronLau](https://github.com/AldaronLau) 240 | - [jannic](https://github.com/jannic) 241 | 242 | ## [0.7.1] - 2019-07-18 243 | ### Fixed 244 | - Not compiling for 32-bit architecture. 245 | 246 | ## [0.7.0] - 2019-05-22 247 | ### Added 248 | - `poll()` method to block until an event is received from any controller. 249 | - Asynchronous support. 250 | - Multi-threaded support. 251 | - `count()` to get the number of plugged in controllers. 252 | - Implementation of default for `Port`. 253 | - `CONTROLLER_MAX` constant, set to 64 controllers. 254 | 255 | ### Removed 256 | - Pan separate from camera Y. 257 | - Mods 258 | 259 | ### Changed 260 | - `update()` is now renamed to `poll()` and is asynchronous. It's now recommended to put your input on it's own thread and call `poll` which blocks. 261 | - There's now a limit of 64 joysticks, because it makes multi-threaded joystick support easier and faster. 262 | - Joystick Ids are now `u8` instead of `u16`. 263 | 264 | ### Fixed 265 | - L & R triggers without buttons requiring mods to be treated as buttons. 266 | 267 | ## [0.6.0] - 2019-05-13 268 | ### Added 269 | - `Device.lrt()` function to get left & right trigger values. 270 | 271 | ### Fixed 272 | - Can only extract `Device.joy()` values once. 273 | 274 | ## [0.5.0] - 2019-05-12 275 | ### Added 276 | - Full support for 4 joysticks 277 | - New API with `Port`, `Device` and `Btn` 278 | - API to detect whether or not joystick features are supported (not complete). 279 | 280 | ### Removed 281 | - `ControllerManager` & `Input` 282 | 283 | ### Changed 284 | - Input is now received through function calls like `joy()` instead of the `Input` Enum 285 | 286 | ## [0.4.1] - 2018-08-05 287 | ### Fixed 288 | - Crash on specific hardware running Linux. 289 | 290 | ## [0.4.0] - 2018-05-23 291 | ### Added 292 | - Fake Windows support. 293 | 294 | ### Removed 295 | - `Button` - Now incorporated into `Input`. 296 | 297 | ## [0.3.0] - 2018-02-03 298 | ### Added 299 | - Added `ControllerManager` to simplify API 300 | 301 | ### Removed 302 | - Removed `Throttle` struct and `Joystick` struct 303 | 304 | ## [0.2.0] - 2018-01-27 305 | ### Added 306 | - Remapping 307 | 308 | ### Changed 309 | - Use evdev instead of js0 310 | 311 | ## [0.1.0] - 2018-01-01 312 | ### Added 313 | - Linux Support 314 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at jeronlau@plopgrizzly.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Unless you explicitly state otherwise, any contribution intentionally submitted 3 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 4 | dual licensed under the Apache License, Version 2.0 and the Boost License, 5 | Version 1.0, without any additional terms or conditions. 6 | 7 | Anyone is more than welcome to contribute! Don't be shy about getting involved, 8 | whether with a question, idea, bug report, bug fix, feature request, feature 9 | implementation, or other enhancement. Other projects have strict contributing 10 | guidelines, but this project accepts any and all formats for pull requests and 11 | issues. For ongoing code contributions, if you wish to ensure your code is 12 | used, open a draft PR so that I know not to write the same code. If a feature 13 | needs to be bumped in importance, I may merge an unfinished draft PR into it's 14 | own branch and finish it (after a week's deadline for the person who openned 15 | it). Contributors will always be notified in this situation, and given a choice 16 | to merge early. 17 | 18 | All pull request contributors will have their username added in the contributors 19 | section of the release notes of the next version after the merge, with a message 20 | thanking them. I always make time to fix bugs, so usually a patched version of 21 | the library will be out a few days after a report. Features requests will not 22 | complete as fast. If you have any questions, design critques, or want me to 23 | find you something to work on based on your skill level, you can email me at 24 | [jeronlau@plopgrizzly.com](mailto:jeronlau@plopgrizzly.com). Otherwise, 25 | [here's a link to the issues on GitHub](https://github.com/ardaku/stick/issues), 26 | and, as always, make sure to read and follow the 27 | [Code of Conduct](https://github.com/ardaku/stick/blob/stable/CODE_OF_CONDUCT.md). 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | # The Stick Crate 4 | "stick", 5 | # Generate Mappings 6 | "xtask", 7 | ] 8 | -------------------------------------------------------------------------------- /LICENSE_APACHE_2_0.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE_BOOST_1_0.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /LICENSE_MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stick 2 | Folders in this repository: 3 | - `stick/`: The Stick crate. 4 | - `sdb/`: The Stick Public Domain database of controllers (not limited 5 | to game controllers). 6 | - `gcdb/`: Git Submodule to grab optional SDL mappings from. 7 | - `xtask/`: Scripts run with `cargo-xtask`. 8 | 9 | ## Xtask 10 | Stick uses the [xtask](https://github.com/matklad/cargo-xtask) repository model 11 | to have various scripts written in Rust process controller data, also allowing 12 | serde to not be a direct dependency. 13 | 14 | ### Available Commands 15 | - `cargo xtask`, `cargo xtask --help` - Print help 16 | - `cargo xtask sdb` - Generate the embeddable bytecode databases. 17 | 18 | ## TOML Format 19 | File names are 64-bit hexadecimal values with leading zeros followed by `.toml` 20 | within a folder referring to the platform. 21 | 22 | ```toml 23 | name = "My Controller Name" 24 | type = "xbox" 25 | 26 | [remap] 27 | TriggerR = {} # Ignore events 28 | TriggerL = {} 29 | HatUp = "Up" # Map hat to dpad when you're not using a flightstick 30 | HatDown = "Down" 31 | HatLeft = "Left" 32 | HatRight = "Right" 33 | CamX = { event = "CamX", deadzone = 0.075 } # Adjust deadzones 34 | CamY = { event = "CamY", deadzone = 0.075 } 35 | JoyZ = { event = "TriggerR", max = 1024 } # Set higher-precision axis ranges (usually 255) 36 | CamZ = { event = "TriggerL", max = 1024 } 37 | ``` 38 | 39 | ### `type` 40 | Type can be any of the following: 41 | - `xbox` - An Xbox Gamepad (W3 Standard Gamepad Compliant) 42 | - `flight` - A Flightstick 43 | - `playstation` - A PlayStation Gamepad (W3 Standard Gamepad Compliant) 44 | - `nintendo` - A Nintendo Gamepad (W3 Standard Gamepad Compliant) 45 | - `gamepad` - A generic Gamepad (W3 Standard Gamepad Compliant) 46 | 47 | ## License 48 | Copyright © 2017-2023 The Stick Contributors. 49 | 50 | Licensed under any of 51 | - Apache License, Version 2.0, ([LICENSE_APACHE_2_0.txt][7] 52 | or [https://www.apache.org/licenses/LICENSE-2.0][8]) 53 | - Boost Software License, Version 1.0, ([LICENSE_BOOST_1_0.txt][11] 54 | or [https://www.boost.org/LICENSE_1_0.txt][12]) 55 | - MIT License, ([LICENSE_MIT.txt][9] or [https://mit-license.org/][10]) 56 | 57 | at your option. 58 | 59 | ### Contribution 60 | Unless you explicitly state otherwise, any contribution intentionally submitted 61 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 62 | licensed as described above, without any additional terms or conditions. 63 | 64 | ## Help 65 | If you want help using or contributing to this library, feel free to send me an 66 | email at [aldaronlau@gmail.com][13]. 67 | 68 | [7]: https://github.com/ardaku/stick/blob/stable/LICENSE_APACHE_2_0.txt 69 | [8]: https://www.apache.org/licenses/LICENSE-2.0 70 | [9]: https://github.com/ardaku/stick/blob/stable/LICENSE_MIT.txt 71 | [10]: https://mit-license.org/ 72 | [11]: https://github.com/ardaku/stick/blob/stable/LICENSE_BOOST_1_0.txt 73 | [12]: https://www.boost.org/LICENSE_1_0.txt 74 | [13]: mailto:aldaronlau@gmail.com 75 | -------------------------------------------------------------------------------- /sdb/LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /sdb/README.md: -------------------------------------------------------------------------------- 1 | # Controller Database 2 | This folder contains a database of all gamepads / joysticks currently supported 3 | by stick (This crate intends to support as many gamepads as possible). 4 | Controller interfaces are different between operating systems, so they are 5 | organized in folders by OS. Each mapping uses TOML to specify how devices are 6 | to be mapped. 7 | 8 | ## Example TOML Mapping File 9 | ```toml 10 | # TODO 11 | ``` 12 | 13 | ## License 14 |

15 | 17 | CC0 18 | 19 |
20 | To the extent possible under law, 21 | 23 | Jeron Aldaron Lau 24 | has waived all copyright and related or neighboring rights to 25 | SynthFall SoundFont. 26 |

27 | -------------------------------------------------------------------------------- /sdb/linux/03002509E8030101.toml: -------------------------------------------------------------------------------- 1 | name = "Wii Remote - Mayflash Adapter" 2 | type = "nintendo" 3 | 4 | [remap] 5 | PovUp = "Up" 6 | PovDown = "Down" 7 | PovLeft = "Left" 8 | PovRight = "Right" 9 | Trigger = "ActionB" 10 | ActionM = "ActionA" 11 | Bumper = "ActionV" 12 | ActionR = "ActionH" 13 | 0 = "BumperL" 14 | 1 = "BumperR" 15 | 2 = "MenuL" 16 | 3 = "MenuR" 17 | 4 = "Exit" 18 | JoyZ = "CamX" 19 | CamX = "CamY" 20 | CamY = { event = "TriggerL" } 21 | CamZ = { event = "TriggerR" } 22 | -------------------------------------------------------------------------------- /sdb/linux/03004C0568021081.toml: -------------------------------------------------------------------------------- 1 | name = "Shanwan PlayStation3 Gamepad" 2 | type = "playstation" 3 | 4 | [remap] 5 | ActionA = "ActionB" 6 | ActionB = "ActionA" 7 | TriggerL = {} 8 | TriggerR = {} 9 | JoyZ = { event = "TriggerL" } 10 | CamZ = { event = "TriggerR" } 11 | -------------------------------------------------------------------------------- /sdb/linux/03004F0404041101.toml: -------------------------------------------------------------------------------- 1 | name = "Thrustmaster Warthog Throttle" 2 | type = "flight" 3 | 4 | [remap] 5 | Throttle = "Slew" 6 | Trigger = "Mouse" 7 | ActionM = "MicPush" 8 | Bumper = "MicUp" 9 | ActionR = "MicLeft" 10 | ActionL = "MicDown" 11 | Pinky = "MicRight" 12 | 1 = "SpeedbrakeForward" 13 | 2 = "SpeedbrakeBackward" 14 | 3 = "BoatForward" 15 | 4 = "BoatBackward" 16 | 5 = "ChinaForward" 17 | 6 = "ChinaBackward" 18 | 7 = "PinkyForward" 19 | 8 = "PinkyBackward" 20 | 9 = "ThrottleButton" 21 | 10 = "EngineFuelFlowL" 22 | 11 = "EngineFuelFlowR" 23 | 12 = "EngineMotorL" 24 | 13 = "EngineMotorR" 25 | 14 = "Apu" 26 | 15 = "LandingGearSilence" 27 | 16 = "FlapsUp" 28 | 17 = "FlapsDown" 29 | 18 = "Eac" 30 | 19 = "RadarAltimeter" 31 | 20 = "AutopilotToggle" 32 | 21 = "AutopilotPath" 33 | 22 = "AutopilotAlt" 34 | 25 = "EngineIgnitionL" 35 | 26 = "EngineIgnitionR" 36 | JoyX = { event = "MouseX", max = 1024 } 37 | JoyY = { event = "MouseY", max = 1024 } 38 | JoyZ = { event = "ThrottleR", max = 16339 } 39 | CamZ = { event = "ThrottleL", max = 16339 } 40 | -------------------------------------------------------------------------------- /sdb/linux/03005E048E021001.toml: -------------------------------------------------------------------------------- 1 | name = "X360 Controller" 2 | type = "xbox" 3 | 4 | [remap] 5 | Trigger = "ActionA" 6 | ActionM = "ActionB" 7 | Bumper = "ActionH" 8 | ActionR = "ActionV" 9 | ActionL = "BumperL" 10 | Pinky = "BumperR" 11 | 1 = "MenuL" 12 | 2 = "MenuR" 13 | 3 = "Exit" 14 | 4 = "Joy" 15 | 5 = "Cam" 16 | ActionA = "ActionB" 17 | ActionB = "ActionA" 18 | ActionV = "ActionH" 19 | ActionH = "ActionV" 20 | PovUp = "Up" 21 | PovDown = "Down" 22 | PovLeft = "Left" 23 | PovRight = "Right" 24 | JoyZ = { event = "TriggerL" } 25 | CamZ = { event = "TriggerR" } 26 | JoyX = { event = "JoyX", scale = 0.992 } 27 | JoyY = { event = "JoyY", scale = 0.992 } 28 | CamX = { event = "CamX", scale = 0.992 } 29 | CamY = { event = "CamY", scale = 0.992 } 30 | -------------------------------------------------------------------------------- /sdb/linux/03005E04D1020101.toml: -------------------------------------------------------------------------------- 1 | name = "Microsoft Xbox One Controller" 2 | type = "xbox" 3 | 4 | [remap] 5 | PovUp = "Up" 6 | PovDown = "Down" 7 | PovLeft = "Left" 8 | PovRight = "Right" 9 | JoyZ = { event = "TriggerL" } 10 | CamZ = { event = "TriggerR" } 11 | 1 = "MenuL" 12 | 4 = "Joy" 13 | 5 = "Cam" 14 | MenuL = "Exit" 15 | -------------------------------------------------------------------------------- /sdb/linux/03006D0416C21101.toml: -------------------------------------------------------------------------------- 1 | name = "Logitech Dual Action PlayStation Gamepad" 2 | type = "playstation" 3 | 4 | [remap] 5 | Trigger = "ActionH" 6 | ActionM = "ActionA" 7 | Bumper = "ActionB" 8 | ActionR = "ActionV" 9 | ActionL = "BumperL" 10 | Pinky = "BumperR" 11 | 0 = "TriggerL" 12 | 1 = "TriggerR" 13 | 2 = "MenuL" 14 | 3 = "MenuR" 15 | 4 = "Joy" 16 | 5 = "Cam" 17 | ActionA = "ActionH" 18 | ActionB = "ActionA" 19 | MenuL = "Joy" 20 | MenuR = "Cam" 21 | JoyZ = "CamX" 22 | CamX = "CamY" 23 | CamZ = { event = "CamX", scale = 0.67 } 24 | PovUp = "Up" 25 | PovDown = "Down" 26 | PovLeft = "Left" 27 | PovRight = "Right" 28 | -------------------------------------------------------------------------------- /sdb/linux/03006F0E01050001.toml: -------------------------------------------------------------------------------- 1 | name = "PDP Wired Xbox 360 Gamepad" 2 | type = "xbox" 3 | 4 | [remap] 5 | PovUp = "Up" 6 | PovDown = "Down" 7 | PovLeft = "Left" 8 | PovRight = "Right" 9 | ActionV = "ActionH" 10 | ActionH = "ActionV" 11 | TriggerL = {} 12 | TriggerR = {} 13 | JoyX = { event = "JoyX", deadzone = 0.075 } 14 | JoyY = { event = "JoyY", deadzone = 0.075 } 15 | CamX = { event = "CamX", deadzone = 0.075 } 16 | CamY = { event = "CamY", deadzone = 0.075 } 17 | JoyZ = { event = "TriggerL" } 18 | CamZ = { event = "TriggerR" } 19 | -------------------------------------------------------------------------------- /sdb/linux/0300790044181001.toml: -------------------------------------------------------------------------------- 1 | name = "GameCube Controller - Mayflash Adapter" 2 | type = "nintendo" 3 | 4 | [remap] 5 | PovUp = "Up" 6 | PovDown = "Down" 7 | PovLeft = "Left" 8 | PovRight = "Right" 9 | Trigger = "ActionH" 10 | ActionM = "ActionA" 11 | Bumper = "ActionB" 12 | ActionR = "ActionV" 13 | ActionL = {} # "TriggerL" duplicate 14 | Pinky = {} # "TriggerR" duplicate 15 | 1 = "BumperR" 16 | 3 = "MenuR" 17 | 7 = "Up" 18 | 8 = "Right" 19 | 9 = "Down" 20 | 10 = "Left" 21 | JoyX = { event = "JoyX", scale = 0.67 } 22 | JoyY = { event = "JoyY", scale = 0.67 } 23 | JoyZ = { event = "CamY", scale = 0.67 } 24 | CamX = { event = "TriggerL", deadzone = 0.125 } 25 | CamY = { event = "TriggerR", deadzone = 0.125 } 26 | CamZ = { event = "CamX", scale = 0.67 } 27 | -------------------------------------------------------------------------------- /sdb/linux/0300B40412241101.toml: -------------------------------------------------------------------------------- 1 | name = "Flydigi Apex 2 (Wired)" 2 | type = "gamepad" 3 | 4 | [remap] 5 | # Sticks 6 | JoyZ = "CamX" 7 | CamZ = "CamY" 8 | 9 | # Wheel 10 | Brake = "ActionWheelX" 11 | Gas = "ActionWheelY" 12 | 13 | # Face buttons flipped in function 14 | ActionV = "ActionH" 15 | ActionH = "ActionV" 16 | 17 | # Rear buttons 18 | 15 = "ActionL" # Left Nub, diagonally between and bumpers 19 | 14 = "ActionR" # Right Nub 20 | 21 | # Underside 22 | 11 = "PinkyLeft" # Right Under 23 | # Right Grip already dubbed PinkyRight 24 | 13 = "PaddleLeft" # Left Under 25 | 12 = "PaddleRight" # Left Grip 26 | -------------------------------------------------------------------------------- /sdb/linux/0300B50716031001.toml: -------------------------------------------------------------------------------- 1 | name = "Thrustmaster Flightstick" 2 | type = "flight" 3 | 4 | [remap] 5 | JoyX = { event = "JoyX", deadzone = 0.125 } 6 | JoyY = { event = "JoyY", deadzone = 0.125 } 7 | JoyZ = { event = "Throttle", max = 127, min = -128 } 8 | -------------------------------------------------------------------------------- /sdb/linux/05005E04E0020309.toml: -------------------------------------------------------------------------------- 1 | name = "Microsoft Xbox Wireless Controller" 2 | type = "xbox" 3 | 4 | [remap] 5 | ActionH = "BumperL" 6 | ActionD = "BumperR" 7 | BumperL = "MenuL" 8 | BumperR = "MenuR" 9 | JoyZ = "TriggerL" 10 | CamZ = "TriggerR" 11 | ActionC = "ActionH" 12 | TriggerL = "Joy" 13 | TriggerR = "Cam" 14 | Context = "Exit" 15 | -------------------------------------------------------------------------------- /stick/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stick" 3 | version = "0.13.0" 4 | license = "Apache-2.0 OR BSL-1.0 OR MIT" 5 | 6 | description = """ 7 | Platform-agnostic asynchronous gamepad, joystick, and flightstick library 8 | """ 9 | repository = "https://github.com/ardaku/stick" 10 | documentation = "https://docs.rs/stick" 11 | homepage = "https://github.com/ardaku/stick/blob/stable/CHANGELOG.md" 12 | include = ["Cargo.toml", "README.md", "src/", "*.sdb"] 13 | categories = ["hardware-support", "asynchronous", "game-development"] 14 | keywords = ["ux", "joystick", "gamepad", "platform-agnostic", "controller"] 15 | readme = "README.md" 16 | edition = "2021" 17 | 18 | [target.'cfg(all(not(target_arch="wasm32"),target_os="linux"))'.dependencies] 19 | smelling_salts = "0.4" 20 | 21 | [target.'cfg(windows)'.dependencies] 22 | winapi = { version = "0.3", features = ["libloaderapi", "xinput", "winerror"] } 23 | log = { version = "0.4", default-features = false } 24 | 25 | [dev-dependencies] 26 | pasts = "0.8" 27 | 28 | [package.metadata.docs.rs] 29 | all-features = true 30 | default-target = "x86_64-unknown-linux-gnu" 31 | 32 | [features] 33 | # By default, just include the stick controller database. 34 | default = ["sdb"] 35 | # Include the SDL game controller database (button/axis remappings). 36 | gcdb = [] 37 | # Include the stick controller database (button/axis remappings). 38 | sdb = [] 39 | # Include futures::stream::Stream impl. 40 | stream = ["dep:futures"] 41 | 42 | [dependencies] 43 | futures = { version = "0.3.30", optional = true } 44 | -------------------------------------------------------------------------------- /stick/README.md: -------------------------------------------------------------------------------- 1 | # Stick 2 | 3 | #### Platform-agnostic asynchronous gamepad, joystick and flightstick library 4 | 5 | [![tests](https://github.com/ardaku/stick/workflows/tests/badge.svg)](https://github.com/ardaku/stick/actions?query=workflow%3Atests) 6 | [![Docs](https://docs.rs/stick/badge.svg)](https://docs.rs/stick) 7 | [![crates.io](https://img.shields.io/crates/v/stick.svg)](https://crates.io/crates/stick) 8 | 9 | Stick supports getting controller input from a large variety of gamepads, 10 | joysticks, flightsticks, and other controllers. Stick also supports left/right 11 | rumble haptic effects. 12 | 13 | ### Why Does Stick Exist? 14 | The main reason is that I hadn't heard of gilrs when I started stick back in 15 | 2017 when gilrs was only a year old and had less than 500 all-time downloads. 16 | Now, I think there are many other reasons for stick to exist despite gilrs: 17 | 18 | - Executor-agnostic `async/.await` for gamepads, joysticks, etc (I recommend 19 | using the `pasts` crate for a simple single-threaded executor). 20 | - Low-level hotplugging support (you assign the gamepad ID's) 21 | - Meaningful Event Names (`ActionA` and `ActionB` instead of `South` and 22 | `East`) 23 | - Minimal dependencies 24 | - Dual licensed with the Boost license (permission to use without attribution 25 | in the binary's UI) - making it great for game development. 26 | - Not game-specific, doesn't depend on a "standard gamepad" model (which 27 | doesn't work due to the variety of controllers in existence) - therefore can 28 | also be used in robotics, control centers, advanced flight simulations, etc. 29 | - Support more types of gamepads/joysticks than gilrs, and (WIP) unified 30 | support across platforms. 31 | 32 | ### Platform Support 33 | - Linux 34 | - Windows 35 | 36 | ### Planned Platform Support 37 | - MacOS 38 | - BSD 39 | - Redox 40 | - Fuchsia 41 | - Android 42 | - iOS 43 | - Web Assembly 44 | - Nintendo Switch (And other game consoles) 45 | - Others 46 | 47 | ## Table of Contents 48 | - [Getting Started](#getting-started) 49 | - [Example](#example) 50 | - [API](#api) 51 | - [Features](#features) 52 | - [Upgrade](#upgrade) 53 | - [License](#license) 54 | - [Contribution](#contribution) 55 | 56 | ### API 57 | API documentation can be found on [docs.rs](https://docs.rs/stick). 58 | 59 | ### Features 60 | You may enable the following features 61 | - **sdb**: Enabled by default, the Stick database controller remappings 62 | - **gcdb**: The SDL game controller database remappings 63 | 64 | ## Upgrade 65 | You can use the [changelog][3] to facilitate upgrading this crate as a dependency. 66 | 67 | ## License 68 | Copyright © 2017-2023 The Stick Contributors. 69 | 70 | Licensed under any of 71 | - Apache License, Version 2.0, ([LICENSE_APACHE_2_0.txt][7] 72 | or [https://www.apache.org/licenses/LICENSE-2.0][8]) 73 | - Boost Software License, Version 1.0, ([LICENSE_BOOST_1_0.txt][11] 74 | or [https://www.boost.org/LICENSE_1_0.txt][12]) 75 | - MIT License, ([LICENSE_MIT.txt][9] or [https://mit-license.org/][10]) 76 | 77 | at your option. 78 | 79 | ### Contribution 80 | Unless you explicitly state otherwise, any contribution intentionally submitted 81 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 82 | licensed as described above, without any additional terms or conditions. 83 | 84 | ## Help 85 | If you want help using or contributing to this library, feel free to send me an 86 | email at [aldaronlau@gmail.com][13]. 87 | 88 | [0]: https://docs.rs/stick 89 | [1]: https://crates.io/crates/stick 90 | [2]: https://github.com/ardaku/stick/actions?query=workflow%3Atests 91 | [3]: https://github.com/ardaku/stick/blob/stable/CHANGELOG.md 92 | [4]: https://github.com/ardaku/stick/ 93 | [5]: https://docs.rs/stick#getting-started 94 | [6]: https://aldaronlau.com/ 95 | [7]: https://github.com/ardaku/stick/blob/stable/LICENSE_APACHE_2_0.txt 96 | [8]: https://www.apache.org/licenses/LICENSE-2.0 97 | [9]: https://github.com/ardaku/stick/blob/stable/LICENSE_MIT.txt 98 | [10]: https://mit-license.org/ 99 | [11]: https://github.com/ardaku/stick/blob/stable/LICENSE_BOOST_1_0.txt 100 | [12]: https://www.boost.org/LICENSE_1_0.txt 101 | [13]: mailto:aldaronlau@gmail.com 102 | -------------------------------------------------------------------------------- /stick/examples/haptic.rs: -------------------------------------------------------------------------------- 1 | //! This is the example from the lib.rs documentation. 2 | 3 | use std::task::Poll::{self, Pending, Ready}; 4 | 5 | use pasts::Loop; 6 | use stick::{Controller, Event, Listener}; 7 | 8 | type Exit = usize; 9 | 10 | struct State { 11 | listener: Listener, 12 | controllers: Vec, 13 | rumble: (f32, f32), 14 | } 15 | 16 | impl State { 17 | fn connect(&mut self, controller: Controller) -> Poll { 18 | println!( 19 | "Connected p{}, id: {:016X}, name: {}", 20 | self.controllers.len() + 1, 21 | controller.id(), 22 | controller.name(), 23 | ); 24 | self.controllers.push(controller); 25 | Pending 26 | } 27 | 28 | fn event(&mut self, id: usize, event: Event) -> Poll { 29 | let player = id + 1; 30 | println!("p{}: {}", player, event); 31 | match event { 32 | Event::Disconnect => { 33 | self.controllers.swap_remove(id); 34 | } 35 | Event::MenuR(true) => return Ready(player), 36 | Event::ActionA(pressed) => { 37 | self.controllers[id].rumble(f32::from(u8::from(pressed))); 38 | } 39 | Event::ActionB(pressed) => { 40 | self.controllers[id].rumble(0.5 * f32::from(u8::from(pressed))); 41 | } 42 | Event::BumperL(pressed) => { 43 | self.rumble.0 = f32::from(u8::from(pressed)); 44 | self.controllers[id].rumble(self.rumble); 45 | } 46 | Event::BumperR(pressed) => { 47 | self.rumble.1 = f32::from(u8::from(pressed)); 48 | self.controllers[id].rumble(self.rumble); 49 | } 50 | _ => {} 51 | } 52 | Pending 53 | } 54 | } 55 | 56 | async fn event_loop() { 57 | let mut state = State { 58 | listener: Listener::default(), 59 | controllers: Vec::new(), 60 | rumble: (0.0, 0.0), 61 | }; 62 | 63 | let player_id = Loop::new(&mut state) 64 | .when(|s| &mut s.listener, State::connect) 65 | .poll(|s| &mut s.controllers, State::event) 66 | .await; 67 | 68 | println!("p{} ended the session", player_id); 69 | } 70 | 71 | fn main() { 72 | pasts::block_on(event_loop()); 73 | } 74 | -------------------------------------------------------------------------------- /stick/remap_linux.sdb: -------------------------------------------------------------------------------- 1 | 03002509E8030101Wii Remote - Mayflash Adapter n800C;810D;8208;8309;8401;3302;3405;3106;2324;240E;250F;2223;1D11;1E12;1F13;1C10;3003 2 | 03004C0568021081Shanwan PlayStation3 Gamepad p0203;0302;250F;220E;0E00;0F00 3 | 03004F0404041101Thrustmaster Warthog Throttle f8145;8A40;8B41;8C3E;8D3F;8E48;8F4A;9038;9139;924B;9349;8244;944C;953C;963D;9942;9A43;833A;843B;8547;8646;8736;8837;894D;3219;332F;341A;3118;2528a16339;204Ea1024;214Fa1024;2229a16339;351B;2726;3050 4 | 03005E048E021001X360 Controller x8108;8209;8301;840A;850B;0203;0302;0506;320C;3303;3406;0605;3105;2323s0.992;2424s0.992;250F;2020s0.992;2121s0.992;220E;350D;1D11;1E12;1F13;1C10;3002 5 | 03005E04D1020101Microsoft Xbox One Controller x8108;840A;850B;250F;220E;0801;1D11;1E12;1F13;1C10 6 | 03006D0416C21101Logitech Dual Action PlayStation Gamepad p800E;810F;8208;8309;840A;850B;0205;0302;320C;3302;3406;3103;2324;2523s0.67;2223;080A;090B;350D;1D11;1E12;1F13;1C10;3005 7 | 03006F0E01050001PDP Wired Xbox 360 Gamepad x0506;0605;2323d0.075;2424d0.075;250F;2020d0.075;2121d0.075;220E;1D11;1E12;1F13;1C10;0E00;0F00 8 | 0300790044181001GameCube Controller - Mayflash Adapter n810D;8A12;8309;8710;8813;8911;3200;3302;3406;3103;230Ed0.125;240Fd0.125;2523s0.67;2020s0.67;2121s0.67;2224s0.67;3500;1D11;1E12;1F13;1C10;3005 9 | 0300B50716031001Thrustmaster Flightstick f2020d0.125;2121d0.125;2227a127i-128 10 | 05005E04E0020309Microsoft Xbox Wireless Controller x0405;070D;050C;0C08;0D09;250F;5501;220E;0E0A;0F0B 11 | 0300B40412241101Flydigi Apex 2 (Wired) g8B53;8C52;8D51;8E34;8F32;0506;0605;2E5E;2524;2D5F;2223 -------------------------------------------------------------------------------- /stick/src/ctlr.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::Debug, 4 | future::Future, 5 | pin::Pin, 6 | sync::Arc, 7 | task::{Context, Poll}, 8 | }; 9 | 10 | use crate::Event; 11 | 12 | #[repr(i8)] 13 | enum Btn { 14 | Exit = 0, 15 | MenuL = 1, 16 | MenuR = 2, 17 | ActionA = 3, 18 | ActionB = 4, 19 | ActionC = 5, 20 | ActionH = 6, 21 | ActionV = 7, 22 | ActionD = 8, 23 | Up = 9, 24 | Down = 10, 25 | Right = 11, 26 | Left = 12, 27 | BumperL = 13, 28 | BumperR = 14, 29 | Joy = 15, 30 | Cam = 16, 31 | PaddleLeft = 17, 32 | PaddleRight = 18, 33 | PinkyLeft = 19, 34 | PinkyRight = 20, 35 | Trigger = 21, 36 | HatUp = 22, 37 | HatDown = 23, 38 | HatRight = 24, 39 | HatLeft = 25, 40 | MicUp = 26, 41 | MicDown = 27, 42 | MicRight = 28, 43 | MicLeft = 29, 44 | PovUp = 30, 45 | PovDown = 31, 46 | PovRight = 32, 47 | PovLeft = 33, 48 | MicPush = 34, 49 | ActionL = 35, 50 | ActionR = 36, 51 | Bumper = 37, 52 | ActionM = 38, 53 | Pinky = 39, 54 | PinkyForward = 40, 55 | PinkyBackward = 41, 56 | FlapsUp = 42, 57 | FlapsDown = 43, 58 | BoatForward = 44, 59 | BoatBackward = 45, 60 | AutopilotPath = 46, 61 | AutopilotAlt = 47, 62 | EngineMotorL = 48, 63 | EngineMotorR = 49, 64 | EngineFuelFlowL = 50, 65 | EngineFuelFlowR = 51, 66 | EngineIgnitionL = 52, 67 | EngineIgnitionR = 53, 68 | SpeedbrakeBackward = 54, 69 | SpeedbrakeForward = 55, 70 | ChinaBackward = 56, 71 | ChinaForward = 57, 72 | Apu = 58, 73 | RadarAltimeter = 59, 74 | LandingGearSilence = 60, 75 | Eac = 61, 76 | AutopilotToggle = 62, 77 | ThrottleButton = 63, 78 | Mouse = 64, 79 | Scroll = 65, 80 | Context = 66, 81 | Dpi = 67, 82 | TrimUp = 68, 83 | TrimDown = 69, 84 | TrimRight = 70, 85 | TrimLeft = 71, 86 | } 87 | 88 | #[repr(i8)] 89 | enum Axs { 90 | TriggerL = 0, 91 | TriggerR = 1, 92 | JoyX = 2, 93 | JoyY = 3, 94 | JoyZ = 4, 95 | CamX = 5, 96 | CamY = 6, 97 | CamZ = 7, 98 | Wheel = 8, 99 | Brake = 9, 100 | Gas = 10, 101 | Rudder = 11, 102 | Slew = 12, 103 | Throttle = 13, 104 | ThrottleL = 14, 105 | ThrottleR = 15, 106 | Volume = 16, 107 | MouseX = 17, 108 | MouseY = 18, 109 | ScrollX = 19, 110 | ScrollY = 20, 111 | ActionWheelX = 21, 112 | ActionWheelY = 22, 113 | Count, // Inferred correctly as long as it's last 114 | } 115 | 116 | #[derive(Debug)] 117 | struct Map { 118 | deadzone: f64, 119 | #[allow(dead_code)] // FIXME 120 | scale: f64, 121 | max: i32, 122 | min: i32, 123 | out: u8, 124 | } 125 | 126 | #[derive(Debug)] 127 | struct Info { 128 | #[allow(dead_code)] // FIXME 129 | name: String, 130 | maps: HashMap, 131 | #[allow(dead_code)] // FIXME 132 | type_: char, 133 | } 134 | 135 | impl Default for Info { 136 | fn default() -> Self { 137 | Self { 138 | name: "Unknown".to_string(), 139 | maps: HashMap::new(), 140 | type_: 'w', 141 | } 142 | } 143 | } 144 | 145 | /// Controller remapping information 146 | #[derive(Debug)] 147 | pub struct Remap(HashMap>); 148 | 149 | impl Default for Remap { 150 | fn default() -> Self { 151 | Self::new() 152 | } 153 | } 154 | 155 | impl Remap { 156 | /// Create new remapper. 157 | #[allow(unused_mut, clippy::let_and_return)] 158 | pub fn new() -> Self { 159 | let mut remapper = Remap(HashMap::new()); 160 | #[cfg(all(feature = "gcdb", target_os = "linux"))] 161 | { 162 | let data = include_str!("../sdlgc_linux.sdb"); 163 | remapper = remapper.load(data).unwrap(); 164 | } 165 | #[cfg(all(feature = "sdb", target_os = "linux"))] 166 | { 167 | let data = include_str!("../remap_linux.sdb"); 168 | remapper = remapper.load(data).unwrap(); 169 | } 170 | remapper 171 | } 172 | 173 | /// Load a custom re-mapping. 174 | pub fn load(mut self, data: &str) -> Option { 175 | // Controllers 176 | for line in data.lines() { 177 | let id = u64::from_str_radix(&line[..16], 16).ok()?; 178 | let tab = line.find('\t')?; 179 | let name = line[16..tab].to_string(); 180 | let type_ = line.get(tab + 1..tab + 2)?.chars().next()?; 181 | let mut maps = HashMap::new(); 182 | 183 | // Events 184 | for event in line.get(tab + 2..)?.split(';') { 185 | let in_ = u8::from_str_radix(event.get(0..2)?, 16).ok()?; 186 | let out = u8::from_str_radix(event.get(2..4)?, 16).ok()?; 187 | 188 | // Tweaks 189 | let mut cursor = 4; 190 | let mut deadzone = f64::NAN; 191 | let mut scale = f64::NAN; 192 | let mut max: i32 = 0; 193 | let mut min: i32 = 0; 194 | while let Some(tweak) = event.get(cursor..)?.chars().next() { 195 | match tweak { 196 | 'd' => { 197 | let end = event 198 | .get(cursor + 1..)? 199 | .find(char::is_lowercase) 200 | .unwrap_or(event.get(cursor + 1..)?.len()); 201 | deadzone = event 202 | .get(cursor + 1..cursor + 1 + end)? 203 | .parse::() 204 | .ok()?; 205 | cursor += end + 1; 206 | } 207 | 's' => { 208 | let end = event 209 | .get(cursor + 1..)? 210 | .find(char::is_lowercase) 211 | .unwrap_or(event.get(cursor + 1..)?.len()); 212 | scale = event 213 | .get(cursor + 1..cursor + 1 + end)? 214 | .parse::() 215 | .ok()? 216 | .recip(); 217 | cursor += end + 1; 218 | } 219 | 'a' => { 220 | let end = event 221 | .get(cursor + 1..)? 222 | .find(char::is_lowercase) 223 | .unwrap_or(event.get(cursor + 1..)?.len()); 224 | max = event 225 | .get(cursor + 1..cursor + 1 + end)? 226 | .parse::() 227 | .ok()?; 228 | cursor += end + 1; 229 | } 230 | 'i' => { 231 | let end = event 232 | .get(cursor + 1..)? 233 | .find(char::is_lowercase) 234 | .unwrap_or(event.get(cursor + 1..)?.len()); 235 | min = event 236 | .get(cursor + 1..cursor + 1 + end)? 237 | .parse::() 238 | .ok()?; 239 | cursor += end + 1; 240 | } 241 | _ => return None, 242 | } 243 | } 244 | 245 | maps.insert( 246 | in_, 247 | Map { 248 | deadzone, 249 | scale, 250 | max, 251 | min, 252 | out, 253 | }, 254 | ); 255 | } 256 | 257 | self.0.insert(id, Arc::new(Info { name, maps, type_ })); 258 | } 259 | 260 | Some(self) 261 | } 262 | } 263 | 264 | /// A gamepad, flightstick, or other controller. 265 | pub struct Controller { 266 | // Shared remapping. 267 | remap: Arc, 268 | // 269 | raw: Box, 270 | // Button states 271 | btns: u128, 272 | // Number button states 273 | nums: u128, 274 | // Axis states: 275 | axis: [f64; Axs::Count as usize], 276 | } 277 | 278 | impl Debug for Controller { 279 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 280 | write!(f, "Controller(\"{}\")", self.name()) 281 | } 282 | } 283 | 284 | impl Controller { 285 | #[allow(unused)] 286 | pub(crate) fn new( 287 | raw: Box, 288 | remap: &Remap, 289 | ) -> Self { 290 | let btns = 0; 291 | let nums = 0; 292 | let axis = [0.0; Axs::Count as usize]; 293 | let remap = remap.0.get(&raw.id()).cloned().unwrap_or_default(); 294 | Self { 295 | remap, 296 | raw, 297 | btns, 298 | nums, 299 | axis, 300 | } 301 | } 302 | 303 | /// Get a unique identifier for the specific model of gamepad. 304 | pub fn id(&self) -> u64 { 305 | self.raw.id() 306 | } 307 | 308 | /// Get the name of this Pad. 309 | pub fn name(&self) -> &str { 310 | self.raw.name() 311 | } 312 | 313 | /// Turn on/off haptic force feedback. 314 | /// 315 | /// Takes either an `f32` for mono power or `(f32, f32)` for directional 316 | /// power. Power will be clamped between 0.0 (off) and 1.0 (maximum power). 317 | /// 318 | /// The first `f32` in directional power is typically low frequency and is 319 | /// located on the left, and the second is typically high frequency and is 320 | /// located on the right (controllers may vary). 321 | pub fn rumble(&mut self, power: R) { 322 | self.raw.rumble(power.left(), power.right()); 323 | } 324 | 325 | fn button(&mut self, b: Btn, f: fn(bool) -> Event, p: bool) -> Poll { 326 | let b = 1u128 << b as i8; 327 | if (self.btns & b != 0) == p { 328 | Poll::Pending 329 | } else { 330 | self.btns ^= b; 331 | Poll::Ready(f(p)) 332 | } 333 | } 334 | 335 | fn number( 336 | &mut self, 337 | n: i8, 338 | f: fn(i8, bool) -> Event, 339 | p: bool, 340 | ) -> Poll { 341 | let b = 1u128 << n; 342 | if (self.nums & b != 0) == p { 343 | Poll::Pending 344 | } else { 345 | self.nums ^= b; 346 | Poll::Ready(f(n, p)) 347 | } 348 | } 349 | 350 | #[allow(clippy::float_cmp)] // imprecision should be consistent 351 | fn axis( 352 | &mut self, 353 | ev: u8, 354 | a: Axs, 355 | f: fn(f64) -> Event, 356 | v: f64, 357 | ) -> Poll { 358 | let map = self.remap.maps.get(&ev); 359 | let v = if let Some(map) = map { 360 | let v = if map.min != 0 || map.max != 0 { 361 | (((v - f64::from(map.min)) / f64::from(map.max - map.min)) 362 | * 2.0 363 | - 1.0) 364 | .clamp(-1.0, 1.0) 365 | } else { 366 | self.raw.axis(v).clamp(-1.0, 1.0) 367 | }; 368 | if !map.deadzone.is_nan() && v.abs() <= map.deadzone { 369 | 0.0 370 | } else { 371 | v 372 | } 373 | } else { 374 | self.raw.axis(v).clamp(-1.0, 1.0) 375 | }; 376 | let axis = a as usize; 377 | if self.axis[axis] == v { 378 | Poll::Pending 379 | } else { 380 | self.axis[axis] = v; 381 | Poll::Ready(f(v)) 382 | } 383 | } 384 | 385 | #[allow(clippy::float_cmp)] // imprecision should be consistent 386 | fn pressure( 387 | &mut self, 388 | ev: u8, 389 | a: Axs, 390 | f: fn(f64) -> Event, 391 | v: f64, 392 | ) -> Poll { 393 | let map = self.remap.maps.get(&ev); 394 | let v = if let Some(map) = map { 395 | let v = if map.min != 0 || map.max != 0 { 396 | ((v - f64::from(map.min)) / f64::from(map.max - map.min)) 397 | .clamp(0.0, 1.0) 398 | } else { 399 | self.raw.pressure(v).clamp(0.0, 1.0) 400 | }; 401 | if !map.deadzone.is_nan() && v <= map.deadzone { 402 | 0.0 403 | } else { 404 | v 405 | } 406 | } else { 407 | self.raw.pressure(v).clamp(0.0, 1.0) 408 | }; 409 | let axis = a as usize; 410 | if self.axis[axis] == v { 411 | Poll::Pending 412 | } else { 413 | self.axis[axis] = v; 414 | Poll::Ready(f(v)) 415 | } 416 | } 417 | 418 | fn process(&mut self, event: Event) -> Poll { 419 | // Do remapping step first. 420 | let ev = event.to_id().0; 421 | let event = if let Some(new_id) = self.remap.maps.get(&ev) { 422 | let event = event.remap(new_id.out); 423 | if matches!(event, Disconnect) { 424 | return Poll::Pending; 425 | } 426 | event 427 | } else { 428 | event 429 | }; 430 | // 431 | use Event::*; 432 | match event { 433 | Disconnect => Poll::Ready(Disconnect), 434 | Exit(p) => self.button(Btn::Exit, Exit, p), 435 | MenuL(p) => self.button(Btn::MenuL, MenuL, p), 436 | MenuR(p) => self.button(Btn::MenuR, MenuR, p), 437 | ActionA(p) => self.button(Btn::ActionA, ActionA, p), 438 | ActionB(p) => self.button(Btn::ActionB, ActionB, p), 439 | ActionC(p) => self.button(Btn::ActionC, ActionC, p), 440 | ActionH(p) => self.button(Btn::ActionH, ActionH, p), 441 | ActionV(p) => self.button(Btn::ActionV, ActionV, p), 442 | ActionD(p) => self.button(Btn::ActionD, ActionD, p), 443 | Up(p) => self.button(Btn::Up, Up, p), 444 | Down(p) => self.button(Btn::Down, Down, p), 445 | Right(p) => self.button(Btn::Right, Right, p), 446 | Left(p) => self.button(Btn::Left, Left, p), 447 | BumperL(p) => self.button(Btn::BumperL, BumperL, p), 448 | BumperR(p) => self.button(Btn::BumperR, BumperR, p), 449 | TriggerL(v) => self.pressure(ev, Axs::TriggerL, TriggerL, v), 450 | TriggerR(v) => self.pressure(ev, Axs::TriggerR, TriggerR, v), 451 | Joy(p) => self.button(Btn::Joy, Joy, p), 452 | Cam(p) => self.button(Btn::Cam, Cam, p), 453 | JoyX(v) => self.axis(ev, Axs::JoyX, JoyX, v), 454 | JoyY(v) => self.axis(ev, Axs::JoyY, JoyY, v), 455 | JoyZ(v) => self.axis(ev, Axs::JoyZ, JoyZ, v), 456 | CamX(v) => self.axis(ev, Axs::CamX, CamX, v), 457 | CamY(v) => self.axis(ev, Axs::CamY, CamY, v), 458 | CamZ(v) => self.axis(ev, Axs::CamZ, CamZ, v), 459 | PaddleLeft(p) => self.button(Btn::PaddleLeft, PaddleLeft, p), 460 | PaddleRight(p) => self.button(Btn::PaddleRight, PaddleRight, p), 461 | PinkyLeft(p) => self.button(Btn::PinkyLeft, PinkyLeft, p), 462 | PinkyRight(p) => self.button(Btn::PinkyRight, PinkyRight, p), 463 | Number(n, p) => self.number(n, Number, p), 464 | HatUp(p) => self.button(Btn::HatUp, HatUp, p), 465 | HatDown(p) => self.button(Btn::HatDown, HatDown, p), 466 | HatRight(p) => self.button(Btn::HatRight, HatRight, p), 467 | HatLeft(p) => self.button(Btn::HatLeft, HatLeft, p), 468 | Trigger(p) => self.button(Btn::Trigger, Trigger, p), 469 | MicUp(p) => self.button(Btn::MicUp, MicUp, p), 470 | MicDown(p) => self.button(Btn::MicDown, MicDown, p), 471 | MicRight(p) => self.button(Btn::MicRight, MicRight, p), 472 | MicLeft(p) => self.button(Btn::MicLeft, MicLeft, p), 473 | PovUp(p) => self.button(Btn::PovUp, PovUp, p), 474 | PovDown(p) => self.button(Btn::PovDown, PovDown, p), 475 | PovRight(p) => self.button(Btn::PovRight, PovRight, p), 476 | PovLeft(p) => self.button(Btn::PovLeft, PovLeft, p), 477 | Slew(v) => self.pressure(ev, Axs::Slew, Slew, v), 478 | Throttle(v) => self.pressure(ev, Axs::Throttle, Throttle, v), 479 | ThrottleL(v) => self.pressure(ev, Axs::ThrottleL, ThrottleL, v), 480 | ThrottleR(v) => self.pressure(ev, Axs::ThrottleR, ThrottleR, v), 481 | Volume(v) => self.pressure(ev, Axs::Volume, Volume, v), 482 | Wheel(v) => self.pressure(ev, Axs::Wheel, Wheel, v), 483 | Rudder(v) => self.pressure(ev, Axs::Rudder, Rudder, v), 484 | Gas(v) => self.pressure(ev, Axs::Gas, Gas, v), 485 | Brake(v) => self.pressure(ev, Axs::Brake, Brake, v), 486 | MicPush(p) => self.button(Btn::MicPush, MicPush, p), 487 | ActionL(p) => self.button(Btn::ActionL, ActionL, p), 488 | ActionM(p) => self.button(Btn::ActionM, ActionM, p), 489 | ActionR(p) => self.button(Btn::ActionR, ActionR, p), 490 | Bumper(p) => self.button(Btn::Bumper, Bumper, p), 491 | Pinky(p) => self.button(Btn::Pinky, Pinky, p), 492 | PinkyForward(p) => self.button(Btn::PinkyForward, PinkyForward, p), 493 | PinkyBackward(p) => { 494 | self.button(Btn::PinkyBackward, PinkyBackward, p) 495 | } 496 | FlapsUp(p) => self.button(Btn::FlapsUp, FlapsUp, p), 497 | FlapsDown(p) => self.button(Btn::FlapsDown, FlapsDown, p), 498 | BoatForward(p) => self.button(Btn::BoatForward, BoatForward, p), 499 | BoatBackward(p) => self.button(Btn::BoatBackward, BoatBackward, p), 500 | AutopilotPath(p) => { 501 | self.button(Btn::AutopilotPath, AutopilotPath, p) 502 | } 503 | AutopilotAlt(p) => self.button(Btn::AutopilotAlt, AutopilotAlt, p), 504 | EngineMotorL(p) => self.button(Btn::EngineMotorL, EngineMotorL, p), 505 | EngineMotorR(p) => self.button(Btn::EngineMotorR, EngineMotorR, p), 506 | EngineFuelFlowL(p) => { 507 | self.button(Btn::EngineFuelFlowL, EngineFuelFlowL, p) 508 | } 509 | EngineFuelFlowR(p) => { 510 | self.button(Btn::EngineFuelFlowR, EngineFuelFlowR, p) 511 | } 512 | EngineIgnitionL(p) => { 513 | self.button(Btn::EngineIgnitionL, EngineIgnitionL, p) 514 | } 515 | EngineIgnitionR(p) => { 516 | self.button(Btn::EngineIgnitionR, EngineIgnitionR, p) 517 | } 518 | SpeedbrakeBackward(p) => { 519 | self.button(Btn::SpeedbrakeBackward, SpeedbrakeBackward, p) 520 | } 521 | SpeedbrakeForward(p) => { 522 | self.button(Btn::SpeedbrakeForward, SpeedbrakeForward, p) 523 | } 524 | ChinaBackward(p) => { 525 | self.button(Btn::ChinaBackward, ChinaBackward, p) 526 | } 527 | ChinaForward(p) => self.button(Btn::ChinaForward, ChinaForward, p), 528 | Apu(p) => self.button(Btn::Apu, Apu, p), 529 | RadarAltimeter(p) => { 530 | self.button(Btn::RadarAltimeter, RadarAltimeter, p) 531 | } 532 | LandingGearSilence(p) => { 533 | self.button(Btn::LandingGearSilence, LandingGearSilence, p) 534 | } 535 | Eac(p) => self.button(Btn::Eac, Eac, p), 536 | AutopilotToggle(p) => { 537 | self.button(Btn::AutopilotToggle, AutopilotToggle, p) 538 | } 539 | ThrottleButton(p) => { 540 | self.button(Btn::ThrottleButton, ThrottleButton, p) 541 | } 542 | MouseX(v) => self.axis(ev, Axs::MouseX, MouseX, v), 543 | MouseY(v) => self.axis(ev, Axs::MouseY, MouseY, v), 544 | ScrollX(v) => self.axis(ev, Axs::ScrollX, ScrollX, v), 545 | ScrollY(v) => self.axis(ev, Axs::ScrollY, ScrollY, v), 546 | Mouse(p) => self.button(Btn::Mouse, Mouse, p), 547 | Scroll(p) => self.button(Btn::Scroll, Scroll, p), 548 | Context(p) => self.button(Btn::Context, Context, p), 549 | Dpi(p) => self.button(Btn::Dpi, Dpi, p), 550 | TrimUp(p) => self.button(Btn::TrimUp, TrimUp, p), 551 | TrimDown(p) => self.button(Btn::TrimDown, TrimDown, p), 552 | TrimLeft(p) => self.button(Btn::TrimLeft, TrimLeft, p), 553 | TrimRight(p) => self.button(Btn::TrimRight, TrimRight, p), 554 | ActionWheelX(v) => { 555 | self.axis(ev, Axs::ActionWheelX, ActionWheelX, v) 556 | } 557 | ActionWheelY(v) => { 558 | self.axis(ev, Axs::ActionWheelY, ActionWheelY, v) 559 | } 560 | } 561 | } 562 | } 563 | 564 | impl Future for Controller { 565 | type Output = Event; 566 | 567 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 568 | let mut this = self.as_mut(); 569 | 570 | if let Poll::Ready(event) = this.raw.poll(cx) { 571 | let out = Self::process(&mut this, event); 572 | if out.is_pending() { 573 | Self::poll(self, cx) 574 | } else { 575 | out 576 | } 577 | } else { 578 | Poll::Pending 579 | } 580 | } 581 | } 582 | 583 | #[cfg(feature = "stream")] 584 | impl futures::stream::Stream for Controller { 585 | type Item = Event; 586 | fn poll_next( 587 | self: Pin<&mut Self>, 588 | cx: &mut Context<'_>, 589 | ) -> Poll> { 590 | match self.poll(cx) { 591 | Poll::Pending => Poll::Pending, 592 | Poll::Ready(e) => match e { 593 | Event::Disconnect => Poll::Ready(None), 594 | e => Poll::Ready(Some(e)), 595 | }, 596 | } 597 | } 598 | } 599 | 600 | pub trait Rumble { 601 | fn left(&self) -> f32; 602 | fn right(&self) -> f32; 603 | } 604 | 605 | impl Rumble for f32 { 606 | #[inline(always)] 607 | fn left(&self) -> f32 { 608 | self.clamp(0.0, 1.0) 609 | } 610 | 611 | #[inline(always)] 612 | fn right(&self) -> f32 { 613 | self.clamp(0.0, 1.0) 614 | } 615 | } 616 | 617 | impl Rumble for (f32, f32) { 618 | #[inline(always)] 619 | fn left(&self) -> f32 { 620 | self.0.clamp(0.0, 1.0) 621 | } 622 | 623 | #[inline(always)] 624 | fn right(&self) -> f32 { 625 | self.1.clamp(0.0, 1.0) 626 | } 627 | } 628 | -------------------------------------------------------------------------------- /stick/src/event.rs: -------------------------------------------------------------------------------- 1 | /// An event from a [`Controller`](crate::Controller). 2 | #[derive(Debug, Copy, Clone)] 3 | #[non_exhaustive] 4 | pub enum Event { 5 | /// Controller unplugged. 6 | Disconnect, 7 | /// Exit / Main / Home / Mode 8 | Exit(bool), 9 | /// A / 1 / 4 / Circle. Action A (Primary action). 10 | ActionA(bool), 11 | /// B / 2 / 3 / Cross. Action B (Secondary action). 12 | ActionB(bool), 13 | /// C. Action C (Tertiary action). 14 | ActionC(bool), 15 | /// Y / X / Square. Action H (Horizontal action). 16 | ActionH(bool), 17 | /// X / Y / Triangle. Action V (Vertical action). 18 | ActionV(bool), 19 | /// Z (in 6-button layout). Action D. 20 | ActionD(bool), 21 | /// Left Menu / Back / Select / Minus / Stop 22 | MenuL(bool), 23 | /// Right Menu / Forward / Start / Plus / Play 24 | MenuR(bool), 25 | /// Thumb Push Button On Main / Left Joystick 26 | Joy(bool), 27 | /// Thumb Push Button On Camera / Right Joystick 28 | Cam(bool), 29 | /// Left shoulder button (near button if no trigger) 30 | BumperL(bool), 31 | /// Right shoulder button (near button if no trigger) 32 | BumperR(bool), 33 | /// Left Bumper Trigger (far button if no trigger) - between 0.0 and 1.0 34 | TriggerL(f64), 35 | /// Right Bumper Trigger (far button if no trigger) - between 0.0 and 1.0 36 | TriggerR(f64), 37 | /// D-Pad Up 38 | Up(bool), 39 | /// D-Pad Down 40 | Down(bool), 41 | /// D-Pad Left 42 | Left(bool), 43 | /// D-Pad Right 44 | Right(bool), 45 | /// POV/Main Hat Left 46 | PovUp(bool), 47 | /// POV/Main Hat Down 48 | PovDown(bool), 49 | /// POV/Main Hat Left 50 | PovLeft(bool), 51 | /// POV/Main Hat Right 52 | PovRight(bool), 53 | /// Extra Hat Up 54 | HatUp(bool), 55 | /// Extra Hat Down 56 | HatDown(bool), 57 | /// Extra Hat Left 58 | HatLeft(bool), 59 | /// Extra Hat Right 60 | HatRight(bool), 61 | /// Trim Hat Up 62 | TrimUp(bool), 63 | /// Trim Hat Down 64 | TrimDown(bool), 65 | /// Trim Hat Left 66 | TrimLeft(bool), 67 | /// Trim Hat Right 68 | TrimRight(bool), 69 | /// Mic Hat Up 70 | MicUp(bool), 71 | /// Mic Hat Down 72 | MicDown(bool), 73 | /// Mic Hat Left 74 | MicLeft(bool), 75 | /// Mic Hat Right 76 | MicRight(bool), 77 | /// Main stick horizontal axis (A / D) - between -1.0 and 1.0 78 | JoyX(f64), 79 | /// Main stick vertical / depth axis (W / S) - between -1.0 and 1.0 80 | JoyY(f64), 81 | /// Main stick rotation / yaw axis - between -1.0 and 1.0 82 | JoyZ(f64), 83 | /// Secondary stick X axis (Mouse X Position) - between -1.0 and 1.0 84 | CamX(f64), 85 | /// Secondary stick Y axis (Mouse Y Position) - between -1.0 and 1.0 86 | CamY(f64), 87 | /// Secondary stick Z axis - between -1.0 and 1.0 88 | CamZ(f64), 89 | /// Slew Control - between 0.0 and 1.0 90 | Slew(f64), 91 | /// Stationary throttle (0.0 is forward, 1.0 is backward) 92 | Throttle(f64), 93 | /// Left stationary throttle (0.0 is forward, 1.0 is backward) 94 | ThrottleL(f64), 95 | /// Right stationary throttle (0.0 is forward, 1.0 is backward) 96 | ThrottleR(f64), 97 | /// Volume axis (0.0 is off, 1.0 is full volume) 98 | Volume(f64), 99 | /// Steering wheel - between 0.0 and 1.0 100 | Wheel(f64), 101 | /// Ship rudder - between 0.0 and 1.0 102 | Rudder(f64), 103 | /// Gas Pedal - between 0.0 and 1.0 104 | Gas(f64), 105 | /// Brake Pedal - between 0.0 and 1.0 106 | Brake(f64), 107 | /// Mic Hat Push Button 108 | MicPush(bool), 109 | /// Flightstick trigger button on the back. 110 | Trigger(bool), 111 | /// Flightstick Side Bumper Button 112 | Bumper(bool), 113 | /// Flightstick Top Middle Action Button 114 | ActionM(bool), 115 | /// Flightstick Top Left Action Button 116 | ActionL(bool), 117 | /// Flightstick Top Right Action Button 118 | ActionR(bool), 119 | /// Pinky Button 120 | Pinky(bool), 121 | /// Pinky three-way switch Forward. 122 | PinkyForward(bool), 123 | /// Pinky three-way switch Backward. 124 | PinkyBackward(bool), 125 | /// Flaps three-way switch Forward. 126 | /// - `true` - Forward (Up) 127 | /// - `false` - Neutral (Maneuver) 128 | FlapsUp(bool), 129 | /// Flaps three-way switch Backward. 130 | /// - `true` - Backward (Down) 131 | /// - `false` - Neutral (Maneuver) 132 | FlapsDown(bool), 133 | /// Boat three-way switch Forward. 134 | BoatForward(bool), 135 | /// Boat three-way switch Backward. 136 | BoatBackward(bool), 137 | /// Autopilot three-way switch Forward. 138 | /// - `true` - Forward (Path) 139 | /// - `false` - Neutral (Altitude / Heading) 140 | AutopilotPath(bool), 141 | /// Autopilot three-way switch Backward. 142 | /// - `true` - Backward (Alt) 143 | /// - `false` - Neutral (Altitude / Heading) 144 | AutopilotAlt(bool), 145 | /// Left Engine Operate three-way switch Backward. 146 | /// - `true` - Backward (Motor) 147 | /// - `false` - Neutral (Normal) 148 | EngineMotorL(bool), 149 | /// Right Engine Operate three-way switch Backward. 150 | /// - `true` - Backward (Motor) 151 | /// - `false` - Neutral (Normal) 152 | EngineMotorR(bool), 153 | /// Engine Fuel Flow Left two-way switch 154 | /// - `true` - Normal 155 | /// - `false` - Override 156 | EngineFuelFlowL(bool), 157 | /// Engine Fuel Flow Right two-way switch 158 | /// - `true` - Normal 159 | /// - `false` - Override 160 | EngineFuelFlowR(bool), 161 | /// Left Engine Operate three-way switch Forward. 162 | /// - `true` - Forward (Ignition) 163 | /// - `false` - Neutral (Normal) 164 | EngineIgnitionL(bool), 165 | /// Right Engine Operate three-way switch Forward. 166 | /// - `true` - Forward (Ignition) 167 | /// - `false` - Neutral (Normal) 168 | EngineIgnitionR(bool), 169 | /// Speedbrake three-way switch Backward. 170 | SpeedbrakeBackward(bool), 171 | /// Speedbrake three-way switch Forward. 172 | SpeedbrakeForward(bool), 173 | /// China hat three-way switch Backward. 174 | ChinaBackward(bool), 175 | /// China hat three-way switch Forward. 176 | ChinaForward(bool), 177 | /// APU (Auxiliary Power Unit) two-way switch 178 | /// - `true` - Start 179 | /// - `false` - Off 180 | Apu(bool), 181 | /// Radar Altimeter two-way switch (Altitude measurements) 182 | /// - `true` - Normal 183 | /// - `false` - Disabled 184 | RadarAltimeter(bool), 185 | /// Landing Gear Horn Silence Button 186 | LandingGearSilence(bool), 187 | /// EAC (Enhanced Attitude Control - Autopilot) two-way switch 188 | /// - `true` - Arm 189 | /// - `false` - Off 190 | Eac(bool), 191 | /// Autopilot Toggle Button 192 | AutopilotToggle(bool), 193 | /// Throttle button (Left) 194 | ThrottleButton(bool), 195 | /// Mouse delta position horizontal - between -1.0 and 1.0 196 | MouseX(f64), 197 | /// Mouse delta position vertical - between -1.0 and 1.0 198 | MouseY(f64), 199 | /// Mouse primary button 200 | Mouse(bool), 201 | /// Numbered or unlabeled programmable action buttons (If unlabelled, 202 | /// prefer numbering from left to right, upper to lower) 203 | Number(i8, bool), 204 | /// Back left grip button (upper if there are two) 205 | PaddleLeft(bool), 206 | /// Back right grip button (upper if there are two) 207 | PaddleRight(bool), 208 | /// Left Pinky Button / Back lower right grip button 209 | PinkyLeft(bool), 210 | /// Right Pinky Button / Back lower left grip button 211 | PinkyRight(bool), 212 | /// Context Menu Button on a mouse (Right Click) 213 | Context(bool), 214 | /// DPI Button on a mouse 215 | Dpi(bool), 216 | /// Scroll Wheel X on a mouse - between -1.0 and 1.0 217 | ScrollX(f64), 218 | /// Scroll Wheel Y on a mouse - between -1.0 and 1.0 219 | ScrollY(f64), 220 | /// Scroll Button on a mouse 221 | Scroll(bool), 222 | /// Horizontal axis under the action buttons - between -1.0 and 1.0 223 | ActionWheelX(f64), 224 | /// Vertical axis under the action buttons - between -1.0 and 1.0 225 | ActionWheelY(f64), 226 | } 227 | 228 | impl Event { 229 | #[inline(always)] 230 | pub(crate) fn remap(self, new_id: u8) -> Self { 231 | Self::from_id(new_id, self.to_id().1) 232 | } 233 | 234 | #[inline(always)] 235 | fn from_id(id: u8, value: f64) -> Self { 236 | match id { 237 | 0x00 => Event::Disconnect, 238 | 0x01 => Event::Exit(value != 0.0), 239 | 0x02 => Event::ActionA(value != 0.0), 240 | 0x03 => Event::ActionB(value != 0.0), 241 | 0x04 => Event::ActionC(value != 0.0), 242 | 0x05 => Event::ActionH(value != 0.0), 243 | 0x06 => Event::ActionV(value != 0.0), 244 | 0x07 => Event::ActionD(value != 0.0), 245 | 0x08 => Event::MenuL(value != 0.0), 246 | 0x09 => Event::MenuR(value != 0.0), 247 | 0x0A => Event::Joy(value != 0.0), 248 | 0x0B => Event::Cam(value != 0.0), 249 | 0x0C => Event::BumperL(value != 0.0), 250 | 0x0D => Event::BumperR(value != 0.0), 251 | 0x0E => Event::TriggerL(value), 252 | 0x0F => Event::TriggerR(value), 253 | 0x10 => Event::Up(value != 0.0), 254 | 0x11 => Event::Down(value != 0.0), 255 | 0x12 => Event::Left(value != 0.0), 256 | 0x13 => Event::Right(value != 0.0), 257 | 0x14 => Event::HatUp(value != 0.0), 258 | 0x15 => Event::HatDown(value != 0.0), 259 | 0x16 => Event::HatLeft(value != 0.0), 260 | 0x17 => Event::HatRight(value != 0.0), 261 | 0x18 => Event::MicUp(value != 0.0), 262 | 0x19 => Event::MicDown(value != 0.0), 263 | 0x1A => Event::MicLeft(value != 0.0), 264 | 0x1B => Event::MicRight(value != 0.0), 265 | 0x1C => Event::PovUp(value != 0.0), 266 | 0x1D => Event::PovDown(value != 0.0), 267 | 0x1E => Event::PovLeft(value != 0.0), 268 | 0x1F => Event::PovRight(value != 0.0), 269 | 0x20 => Event::JoyX(value), 270 | 0x21 => Event::JoyY(value), 271 | 0x22 => Event::JoyZ(value), 272 | 0x23 => Event::CamX(value), 273 | 0x24 => Event::CamY(value), 274 | 0x25 => Event::CamZ(value), 275 | 0x26 => Event::Slew(value), 276 | 0x27 => Event::Throttle(value), 277 | 0x28 => Event::ThrottleL(value), 278 | 0x29 => Event::ThrottleR(value), 279 | 0x2A => Event::Volume(value), 280 | 0x2B => Event::Wheel(value), 281 | 0x2C => Event::Rudder(value), 282 | 0x2D => Event::Gas(value), 283 | 0x2E => Event::Brake(value), 284 | 0x2F => Event::MicPush(value != 0.0), 285 | 0x30 => Event::Trigger(value != 0.0), 286 | 0x31 => Event::Bumper(value != 0.0), 287 | 0x32 => Event::ActionL(value != 0.0), 288 | 0x33 => Event::ActionM(value != 0.0), 289 | 0x34 => Event::ActionR(value != 0.0), 290 | 0x35 => Event::Pinky(value != 0.0), 291 | 0x36 => Event::PinkyForward(value != 0.0), 292 | 0x37 => Event::PinkyBackward(value != 0.0), 293 | 0x38 => Event::FlapsUp(value != 0.0), 294 | 0x39 => Event::FlapsDown(value != 0.0), 295 | 0x3A => Event::BoatForward(value != 0.0), 296 | 0x3B => Event::BoatBackward(value != 0.0), 297 | 0x3C => Event::AutopilotPath(value != 0.0), 298 | 0x3D => Event::AutopilotAlt(value != 0.0), 299 | 0x3E => Event::EngineMotorL(value != 0.0), 300 | 0x3F => Event::EngineMotorR(value != 0.0), 301 | 0x40 => Event::EngineFuelFlowL(value != 0.0), 302 | 0x41 => Event::EngineFuelFlowR(value != 0.0), 303 | 0x42 => Event::EngineIgnitionL(value != 0.0), 304 | 0x43 => Event::EngineIgnitionR(value != 0.0), 305 | 0x44 => Event::SpeedbrakeBackward(value != 0.0), 306 | 0x45 => Event::SpeedbrakeForward(value != 0.0), 307 | 0x46 => Event::ChinaBackward(value != 0.0), 308 | 0x47 => Event::ChinaForward(value != 0.0), 309 | 0x48 => Event::Apu(value != 0.0), 310 | 0x49 => Event::RadarAltimeter(value != 0.0), 311 | 0x4A => Event::LandingGearSilence(value != 0.0), 312 | 0x4B => Event::Eac(value != 0.0), 313 | 0x4C => Event::AutopilotToggle(value != 0.0), 314 | 0x4D => Event::ThrottleButton(value != 0.0), 315 | 0x4E => Event::MouseX(value), 316 | 0x4F => Event::MouseY(value), 317 | 0x50 => Event::Mouse(value != 0.0), 318 | 0x51 => Event::PaddleLeft(value != 0.0), 319 | 0x52 => Event::PaddleRight(value != 0.0), 320 | 0x53 => Event::PinkyLeft(value != 0.0), 321 | 0x54 => Event::PinkyRight(value != 0.0), 322 | 0x55 => Event::Context(value != 0.0), 323 | 0x56 => Event::Dpi(value != 0.0), 324 | 0x57 => Event::ScrollX(value), 325 | 0x58 => Event::ScrollY(value), 326 | 0x59 => Event::Scroll(value != 0.0), 327 | 0x5A => Event::TrimUp(value != 0.0), 328 | 0x5B => Event::TrimDown(value != 0.0), 329 | 0x5C => Event::TrimLeft(value != 0.0), 330 | 0x5D => Event::TrimRight(value != 0.0), 331 | 0x5E => Event::ActionWheelX(value), 332 | 0x5F => Event::ActionWheelY(value), 333 | n => Event::Number((n & !0x80) as i8, value != 0.0), 334 | } 335 | } 336 | 337 | #[inline(always)] 338 | pub(crate) fn to_id(self) -> (u8, f64) { 339 | use Event::*; 340 | match self { 341 | Disconnect => (0x00, f64::NAN), 342 | Exit(p) => (0x01, f64::from(u8::from(p))), 343 | ActionA(p) => (0x02, f64::from(u8::from(p))), 344 | ActionB(p) => (0x03, f64::from(u8::from(p))), 345 | ActionC(p) => (0x04, f64::from(u8::from(p))), 346 | ActionH(p) => (0x05, f64::from(u8::from(p))), 347 | ActionV(p) => (0x06, f64::from(u8::from(p))), 348 | ActionD(p) => (0x07, f64::from(u8::from(p))), 349 | MenuL(p) => (0x08, f64::from(u8::from(p))), 350 | MenuR(p) => (0x09, f64::from(u8::from(p))), 351 | Joy(p) => (0x0A, f64::from(u8::from(p))), 352 | Cam(p) => (0x0B, f64::from(u8::from(p))), 353 | BumperL(p) => (0x0C, f64::from(u8::from(p))), 354 | BumperR(p) => (0x0D, f64::from(u8::from(p))), 355 | TriggerL(t) => (0x0E, t), 356 | TriggerR(t) => (0x0F, t), 357 | Up(p) => (0x10, f64::from(u8::from(p))), 358 | Down(p) => (0x11, f64::from(u8::from(p))), 359 | Left(p) => (0x12, f64::from(u8::from(p))), 360 | Right(p) => (0x13, f64::from(u8::from(p))), 361 | HatUp(p) => (0x14, f64::from(u8::from(p))), 362 | HatDown(p) => (0x15, f64::from(u8::from(p))), 363 | HatLeft(p) => (0x16, f64::from(u8::from(p))), 364 | HatRight(p) => (0x17, f64::from(u8::from(p))), 365 | MicUp(p) => (0x18, f64::from(u8::from(p))), 366 | MicDown(p) => (0x19, f64::from(u8::from(p))), 367 | MicLeft(p) => (0x1A, f64::from(u8::from(p))), 368 | MicRight(p) => (0x1B, f64::from(u8::from(p))), 369 | PovUp(p) => (0x1C, f64::from(u8::from(p))), 370 | PovDown(p) => (0x1D, f64::from(u8::from(p))), 371 | PovLeft(p) => (0x1E, f64::from(u8::from(p))), 372 | PovRight(p) => (0x1F, f64::from(u8::from(p))), 373 | JoyX(v) => (0x20, v), 374 | JoyY(v) => (0x21, v), 375 | JoyZ(v) => (0x22, v), 376 | CamX(v) => (0x23, v), 377 | CamY(v) => (0x24, v), 378 | CamZ(v) => (0x25, v), 379 | Slew(t) => (0x26, t), 380 | Throttle(t) => (0x27, t), 381 | ThrottleL(t) => (0x28, t), 382 | ThrottleR(t) => (0x29, t), 383 | Volume(t) => (0x2A, t), 384 | Wheel(t) => (0x2B, t), 385 | Rudder(t) => (0x2C, t), 386 | Gas(t) => (0x2D, t), 387 | Brake(t) => (0x2E, t), 388 | MicPush(p) => (0x2F, f64::from(u8::from(p))), 389 | Trigger(p) => (0x30, f64::from(u8::from(p))), 390 | Bumper(p) => (0x31, f64::from(u8::from(p))), 391 | ActionL(p) => (0x32, f64::from(u8::from(p))), 392 | ActionM(p) => (0x33, f64::from(u8::from(p))), 393 | ActionR(p) => (0x34, f64::from(u8::from(p))), 394 | Pinky(p) => (0x35, f64::from(u8::from(p))), 395 | PinkyForward(p) => (0x36, f64::from(u8::from(p))), 396 | PinkyBackward(p) => (0x37, f64::from(u8::from(p))), 397 | FlapsUp(p) => (0x38, f64::from(u8::from(p))), 398 | FlapsDown(p) => (0x39, f64::from(u8::from(p))), 399 | BoatForward(p) => (0x3A, f64::from(u8::from(p))), 400 | BoatBackward(p) => (0x3B, f64::from(u8::from(p))), 401 | AutopilotPath(p) => (0x3C, f64::from(u8::from(p))), 402 | AutopilotAlt(p) => (0x3D, f64::from(u8::from(p))), 403 | EngineMotorL(p) => (0x3E, f64::from(u8::from(p))), 404 | EngineMotorR(p) => (0x3F, f64::from(u8::from(p))), 405 | EngineFuelFlowL(p) => (0x40, f64::from(u8::from(p))), 406 | EngineFuelFlowR(p) => (0x41, f64::from(u8::from(p))), 407 | EngineIgnitionL(p) => (0x42, f64::from(u8::from(p))), 408 | EngineIgnitionR(p) => (0x43, f64::from(u8::from(p))), 409 | SpeedbrakeBackward(p) => (0x44, f64::from(u8::from(p))), 410 | SpeedbrakeForward(p) => (0x45, f64::from(u8::from(p))), 411 | ChinaBackward(p) => (0x46, f64::from(u8::from(p))), 412 | ChinaForward(p) => (0x47, f64::from(u8::from(p))), 413 | Apu(p) => (0x48, f64::from(u8::from(p))), 414 | RadarAltimeter(p) => (0x49, f64::from(u8::from(p))), 415 | LandingGearSilence(p) => (0x4A, f64::from(u8::from(p))), 416 | Eac(p) => (0x4B, f64::from(u8::from(p))), 417 | AutopilotToggle(p) => (0x4C, f64::from(u8::from(p))), 418 | ThrottleButton(p) => (0x4D, f64::from(u8::from(p))), 419 | MouseX(v) => (0x4E, v), 420 | MouseY(v) => (0x4F, v), 421 | Mouse(p) => (0x50, f64::from(u8::from(p))), 422 | Number(n, p) => (n as u8 | 0x80, f64::from(u8::from(p))), 423 | PaddleLeft(p) => (0x51, f64::from(u8::from(p))), 424 | PaddleRight(p) => (0x52, f64::from(u8::from(p))), 425 | PinkyLeft(p) => (0x53, f64::from(u8::from(p))), 426 | PinkyRight(p) => (0x54, f64::from(u8::from(p))), 427 | Context(p) => (0x55, f64::from(u8::from(p))), 428 | Dpi(p) => (0x56, f64::from(u8::from(p))), 429 | ScrollX(v) => (0x57, v), 430 | ScrollY(v) => (0x58, v), 431 | Scroll(p) => (0x59, f64::from(u8::from(p))), 432 | TrimUp(p) => (0x5A, f64::from(u8::from(p))), 433 | TrimDown(p) => (0x5B, f64::from(u8::from(p))), 434 | TrimLeft(p) => (0x5C, f64::from(u8::from(p))), 435 | TrimRight(p) => (0x5D, f64::from(u8::from(p))), 436 | ActionWheelX(v) => (0x5E, v), 437 | ActionWheelY(v) => (0x5F, v), 438 | } 439 | } 440 | } 441 | 442 | impl std::fmt::Display for Event { 443 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 444 | use Event::*; 445 | 446 | let pushed = |pushd: &bool| if *pushd { "Pushed" } else { "Released" }; 447 | let two = |two: &bool| match two { 448 | true => "Forward", 449 | false => "Backward", 450 | }; 451 | let sw = |three: &bool| match three { 452 | true => "Enter", 453 | false => "Leave", 454 | }; 455 | 456 | match self { 457 | Disconnect => write!(f, "Controller Disconnected"), 458 | Exit(p) => write!(f, "Exit {}", pushed(p)), 459 | MenuL(p) => write!(f, "MenuL {}", pushed(p)), 460 | MenuR(p) => write!(f, "MenuR {}", pushed(p)), 461 | ActionA(p) => write!(f, "ActionA {}", pushed(p)), 462 | ActionB(p) => write!(f, "ActionB {}", pushed(p)), 463 | ActionC(p) => write!(f, "ActionC {}", pushed(p)), 464 | ActionH(p) => write!(f, "ActionH {}", pushed(p)), 465 | ActionV(p) => write!(f, "ActionV {}", pushed(p)), 466 | ActionD(p) => write!(f, "ActionD {}", pushed(p)), 467 | Up(p) => write!(f, "Up {}", pushed(p)), 468 | Down(p) => write!(f, "Down {}", pushed(p)), 469 | Right(p) => write!(f, "Right {}", pushed(p)), 470 | Left(p) => write!(f, "Left {}", pushed(p)), 471 | BumperL(p) => write!(f, "BumperL {}", pushed(p)), 472 | BumperR(p) => write!(f, "BumperR {}", pushed(p)), 473 | TriggerL(v) => write!(f, "TriggerL {}", v), 474 | TriggerR(v) => write!(f, "TriggerR {}", v), 475 | Joy(p) => write!(f, "Joy {}", pushed(p)), 476 | Cam(p) => write!(f, "Cam {}", pushed(p)), 477 | JoyX(v) => write!(f, "JoyX {}", v), 478 | JoyY(v) => write!(f, "JoyY {}", v), 479 | JoyZ(v) => write!(f, "JoyZ {}", v), 480 | CamX(v) => write!(f, "CamX {}", v), 481 | CamY(v) => write!(f, "CamY {}", v), 482 | CamZ(v) => write!(f, "CamZ {}", v), 483 | PaddleLeft(p) => write!(f, "PaddleLeft {}", pushed(p)), 484 | PaddleRight(p) => write!(f, "PaddleRight {}", pushed(p)), 485 | PinkyLeft(p) => write!(f, "PinkyLeft {}", pushed(p)), 486 | PinkyRight(p) => write!(f, "PinkyRight {}", pushed(p)), 487 | Number(n, p) => write!(f, "Number({}) {}", n, pushed(p)), 488 | Wheel(v) => write!(f, "Wheel {}", v), 489 | Brake(v) => write!(f, "Brake {}", v), 490 | Gas(v) => write!(f, "Gas {}", v), 491 | Rudder(v) => write!(f, "Rudder {}", v), 492 | Trigger(p) => write!(f, "Trigger {}", pushed(p)), 493 | HatUp(p) => write!(f, "HatUp {}", pushed(p)), 494 | HatDown(p) => write!(f, "HatDown {}", pushed(p)), 495 | HatLeft(p) => write!(f, "HatLeft {}", pushed(p)), 496 | HatRight(p) => write!(f, "HatRight {}", pushed(p)), 497 | AutopilotToggle(p) => write!(f, "AutopilotToggle {}", pushed(p)), 498 | LandingGearSilence(p) => { 499 | write!(f, "LandingGearSilence {}", pushed(p)) 500 | } 501 | TrimUp(p) => write!(f, "TrimUp {}", pushed(p)), 502 | TrimDown(p) => write!(f, "TrimDown {}", pushed(p)), 503 | TrimLeft(p) => write!(f, "TrimLeft {}", pushed(p)), 504 | TrimRight(p) => write!(f, "TrimRight {}", pushed(p)), 505 | PovUp(p) => write!(f, "PovUp {}", pushed(p)), 506 | PovDown(p) => write!(f, "PovDown {}", pushed(p)), 507 | PovLeft(p) => write!(f, "PovLeft {}", pushed(p)), 508 | PovRight(p) => write!(f, "PovRight {}", pushed(p)), 509 | MicUp(p) => write!(f, "MicUp {}", pushed(p)), 510 | MicDown(p) => write!(f, "MicDown {}", pushed(p)), 511 | MicLeft(p) => write!(f, "MicLeft {}", pushed(p)), 512 | MicRight(p) => write!(f, "MicRight {}", pushed(p)), 513 | MicPush(p) => write!(f, "MicPush {}", pushed(p)), 514 | Slew(v) => write!(f, "Slew {}", v), 515 | Throttle(v) => write!(f, "Throttle {}", v), 516 | ThrottleL(v) => write!(f, "ThrottleL {}", v), 517 | ThrottleR(v) => write!(f, "ThrottleR {}", v), 518 | ThrottleButton(p) => write!(f, "ThrottleButton {}", pushed(p)), 519 | EngineFuelFlowL(t) => write!(f, "EngineFuelFlowL {}", two(t)), 520 | EngineFuelFlowR(t) => write!(f, "EngineFuelFlowR {}", two(t)), 521 | Eac(t) => write!(f, "Eac {}", two(t)), 522 | RadarAltimeter(t) => write!(f, "RadarAltimeter {}", two(t)), 523 | Apu(t) => write!(f, "Apu {}", two(t)), 524 | AutopilotPath(p) => write!(f, "AutopilotPath {}", sw(p)), 525 | AutopilotAlt(p) => write!(f, "AutopilotAlt {}", sw(p)), 526 | FlapsUp(p) => write!(f, "FlapsUp {}", sw(p)), 527 | FlapsDown(p) => write!(f, "FlapsDown {}", sw(p)), 528 | EngineIgnitionL(p) => write!(f, "EngineIgnitionL {}", sw(p)), 529 | EngineMotorL(p) => write!(f, "EngineMotorL {}", sw(p)), 530 | EngineIgnitionR(p) => write!(f, "EngineIgnitionR {}", sw(p)), 531 | EngineMotorR(p) => write!(f, "EngineMotorR {}", sw(p)), 532 | PinkyForward(p) => write!(f, "PinkyForward {}", sw(p)), 533 | PinkyBackward(p) => write!(f, "PinkyBackward {}", sw(p)), 534 | SpeedbrakeForward(p) => write!(f, "SpeedbrakeForward {}", sw(p)), 535 | SpeedbrakeBackward(p) => write!(f, "SpeedbrakeBackward {}", sw(p)), 536 | BoatForward(p) => write!(f, "BoatForward {}", sw(p)), 537 | BoatBackward(p) => write!(f, "BoatBackward {}", sw(p)), 538 | ChinaForward(p) => write!(f, "ChinaForward {}", sw(p)), 539 | ChinaBackward(p) => write!(f, "ChinaBackward {}", sw(p)), 540 | Dpi(p) => write!(f, "Dpi {}", pushed(p)), 541 | MouseX(v) => write!(f, "MouseX {}", v), 542 | MouseY(v) => write!(f, "MouseY {}", v), 543 | Mouse(p) => write!(f, "Mouse {}", pushed(p)), 544 | Context(p) => write!(f, "Context {}", pushed(p)), 545 | ScrollX(v) => write!(f, "ScrollX {}", v), 546 | ScrollY(v) => write!(f, "ScrollY {}", v), 547 | Scroll(p) => write!(f, "Scroll {}", pushed(p)), 548 | Volume(v) => write!(f, "Volume {}", v), 549 | Bumper(p) => write!(f, "Bumper {}", pushed(p)), 550 | ActionM(p) => write!(f, "ActionM {}", pushed(p)), 551 | ActionL(p) => write!(f, "ActionL {}", pushed(p)), 552 | ActionR(p) => write!(f, "ActionR {}", pushed(p)), 553 | Pinky(p) => write!(f, "Pinky {}", pushed(p)), 554 | ActionWheelX(v) => write!(f, "ActionWheelX {}", v), 555 | ActionWheelY(v) => write!(f, "ActionWheelY {}", v), 556 | } 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /stick/src/focus.rs: -------------------------------------------------------------------------------- 1 | /// Window grab focus, re-enable events if they were disabled. 2 | pub fn focus() { 3 | crate::raw::GLOBAL.with(|g| g.enable()); 4 | } 5 | 6 | /// Window ungrab focus, disable events. 7 | pub fn unfocus() { 8 | crate::raw::GLOBAL.with(|g| g.disable()); 9 | } 10 | -------------------------------------------------------------------------------- /stick/src/gamepad.rs: -------------------------------------------------------------------------------- 1 | // FIXME: Use in crate. 2 | 3 | /// An event from a `Gamepad`. 4 | enum GamepadEvent { 5 | /// Main / Home / Mode / XBox Button / PS3 Button 6 | Home(bool), 7 | /// Select / Back / Minus / Menu Button 8 | Menu(bool), 9 | /// Start / Forward / Plus / Play Button 10 | Play(bool), 11 | /// D-Pad Up 12 | Up(bool), 13 | /// D-Pad Down 14 | Down(bool), 15 | /// D-Pad Left 16 | Left(bool), 17 | /// D-Pad Right 18 | Right(bool), 19 | /// The primary face action button (Circle, A, 1) 20 | A(bool), 21 | /// The secondary face action button (Cross, B, 2) 22 | B(bool), 23 | /// The topmost face action button (Triangle, may be either X or Y, 3 or 4) 24 | Top(bool), 25 | /// The remaining face action button (Square, may be either X or Y, 3 or 4) 26 | Use(bool), 27 | /// Left bumper button 28 | BumperL(bool), 29 | /// Right bumper button 30 | BumperR(bool), 31 | /// The camera joystick push button 32 | Cam(bool), 33 | /// The direction joystick push button 34 | Dir(bool), 35 | /// Camera joystick X 36 | CamX(f32), 37 | /// Camera joystick Y 38 | CamY(f32), 39 | /// Direction joystick X 40 | DirX(f32), 41 | /// Direction joystick Y 42 | DirY(f32), 43 | /// Left trigger 44 | TriggerL(f32), 45 | /// Right trigger 46 | TriggerR(f32), 47 | /// Extended Gamepad: Top grip button on the left 48 | PaddleL(bool), 49 | /// Extended Gamepad: Top grip button on the right 50 | PaddleR(bool), 51 | /// Extended Gamepad: Lower grip button on the left 52 | GripL(bool), 53 | /// Extended Gamepad: Lower grip button on the right 54 | GripR(bool), 55 | } 56 | -------------------------------------------------------------------------------- /stick/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ## Getting Started 2 | //! Add the following to your *Cargo.toml*: 3 | //! 4 | //! ```toml 5 | //! [dependencies] 6 | //! pasts = "0.8" 7 | //! stick = "0.12" 8 | //! ``` 9 | //! 10 | //! ### Example 11 | //! This example demonstrates getting joystick input and sending haptic 12 | //! feedback (copied from *examples/haptic.rs*): 13 | //! 14 | //! ```rust,no_run 15 | //! use pasts::Loop; 16 | //! use std::task::Poll::{self, Pending, Ready}; 17 | //! use stick::{Controller, Event, Listener}; 18 | //! 19 | //! type Exit = usize; 20 | //! 21 | //! struct State { 22 | //! listener: Listener, 23 | //! controllers: Vec, 24 | //! rumble: (f32, f32), 25 | //! } 26 | //! 27 | //! impl State { 28 | //! fn connect(&mut self, controller: Controller) -> Poll { 29 | //! println!( 30 | //! "Connected p{}, id: {:016X}, name: {}", 31 | //! self.controllers.len() + 1, 32 | //! controller.id(), 33 | //! controller.name(), 34 | //! ); 35 | //! self.controllers.push(controller); 36 | //! Pending 37 | //! } 38 | //! 39 | //! fn event(&mut self, id: usize, event: Event) -> Poll { 40 | //! let player = id + 1; 41 | //! println!("p{}: {}", player, event); 42 | //! match event { 43 | //! Event::Disconnect => { 44 | //! self.controllers.swap_remove(id); 45 | //! } 46 | //! Event::MenuR(true) => return Ready(player), 47 | //! Event::ActionA(pressed) => { 48 | //! self.controllers[id].rumble(f32::from(u8::from(pressed))); 49 | //! } 50 | //! Event::ActionB(pressed) => { 51 | //! self.controllers[id].rumble(0.5 * f32::from(u8::from(pressed))); 52 | //! } 53 | //! Event::BumperL(pressed) => { 54 | //! self.rumble.0 = f32::from(u8::from(pressed)); 55 | //! self.controllers[id].rumble(self.rumble); 56 | //! } 57 | //! Event::BumperR(pressed) => { 58 | //! self.rumble.1 = f32::from(u8::from(pressed)); 59 | //! self.controllers[id].rumble(self.rumble); 60 | //! } 61 | //! _ => {} 62 | //! } 63 | //! Pending 64 | //! } 65 | //! } 66 | //! 67 | //! async fn event_loop() { 68 | //! let mut state = State { 69 | //! listener: Listener::default(), 70 | //! controllers: Vec::new(), 71 | //! rumble: (0.0, 0.0), 72 | //! }; 73 | //! 74 | //! let player_id = Loop::new(&mut state) 75 | //! .when(|s| &mut s.listener, State::connect) 76 | //! .poll(|s| &mut s.controllers, State::event) 77 | //! .await; 78 | //! 79 | //! println!("p{} ended the session", player_id); 80 | //! } 81 | //! 82 | //! fn main() { 83 | //! pasts::block_on(event_loop()); 84 | //! } 85 | //! ``` 86 | 87 | #![doc( 88 | html_logo_url = "https://ardaku.github.io/mm/logo.svg", 89 | html_favicon_url = "https://ardaku.github.io/mm/icon.svg", 90 | html_root_url = "https://docs.rs/stick" 91 | )] 92 | #![deny(unsafe_code)] 93 | #![warn( 94 | anonymous_parameters, 95 | missing_copy_implementations, 96 | missing_debug_implementations, 97 | missing_docs, 98 | nonstandard_style, 99 | rust_2018_idioms, 100 | single_use_lifetimes, 101 | trivial_casts, 102 | trivial_numeric_casts, 103 | unreachable_pub, 104 | unused_extern_crates, 105 | unused_qualifications, 106 | variant_size_differences 107 | )] 108 | 109 | #[cfg(target_os = "windows")] 110 | #[macro_use] 111 | extern crate log; 112 | 113 | mod ctlr; 114 | mod event; 115 | mod focus; 116 | mod listener; 117 | mod raw; 118 | 119 | pub use ctlr::{Controller, Remap}; 120 | pub use event::Event; 121 | pub use focus::{focus, unfocus}; 122 | pub use listener::Listener; 123 | -------------------------------------------------------------------------------- /stick/src/listener.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Debug, 3 | future::Future, 4 | pin::Pin, 5 | task::{Context, Poll}, 6 | }; 7 | 8 | use crate::Remap; 9 | 10 | /// Listener for when new controllers are plugged in. 11 | pub struct Listener(Box); 12 | 13 | impl Debug for Listener { 14 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 15 | write!(f, "Listener") 16 | } 17 | } 18 | 19 | impl Default for Listener { 20 | fn default() -> Self { 21 | Self::new(Remap::default()) 22 | } 23 | } 24 | 25 | impl Listener { 26 | /// Create a new listener for when new controllers are plugged in. 27 | pub fn new(remap: Remap) -> Self { 28 | Self(crate::raw::GLOBAL.with(|g| g.listener(remap))) 29 | } 30 | } 31 | 32 | impl Future for Listener { 33 | type Output = crate::Controller; 34 | 35 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 36 | Pin::new(&mut self.get_mut().0).poll(cx) 37 | } 38 | } 39 | #[cfg(feature = "stream")] 40 | impl futures::stream::Stream for Listener { 41 | type Item = crate::Controller; 42 | fn poll_next( 43 | self: Pin<&mut Self>, 44 | cx: &mut Context<'_>, 45 | ) -> Poll> { 46 | match self.poll(cx) { 47 | Poll::Ready(c) => Poll::Ready(Some(c)), 48 | Poll::Pending => Poll::Pending, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /stick/src/raw.rs: -------------------------------------------------------------------------------- 1 | #![allow(unsafe_code)] 2 | 3 | use std::task::{Context, Poll}; 4 | 5 | use crate::{Event, Remap}; 6 | 7 | #[cfg_attr( 8 | any(target_arch = "wasm32", target_arch = "asmjs"), 9 | cfg_attr(target_os = "wasi", path = "raw/wasi.rs"), 10 | cfg_attr(target_os = "ardaku", path = "raw/ardaku.rs"), 11 | cfg_attr( 12 | any(target_os = "unknown", target_os = "emscripten"), 13 | path = "raw/dom.rs" 14 | ) 15 | )] 16 | #[cfg_attr( 17 | not(any(target_arch = "wasm32", target_arch = "asmjs")), 18 | cfg_attr(target_os = "linux", path = "raw/linux.rs"), 19 | cfg_attr(target_os = "android", path = "raw/android.rs"), 20 | cfg_attr(target_os = "macos", path = "raw/macos.rs"), 21 | cfg_attr(target_os = "ios", path = "raw/ios.rs"), 22 | cfg_attr(target_os = "windows", path = "raw/windows.rs"), 23 | cfg_attr(target_os = "fuchsia", path = "raw/fuchsia.rs"), 24 | cfg_attr(target_os = "redox", path = "raw/redox.rs"), 25 | cfg_attr( 26 | any( 27 | target_os = "freebsd", 28 | target_os = "dragonfly", 29 | target_os = "bitrig", 30 | target_os = "openbsd", 31 | target_os = "netbsd" 32 | ), 33 | path = "raw/bsd.rs", 34 | ) 35 | )] 36 | mod ffi; 37 | 38 | /// Global state for when the system implementation can fail. 39 | struct FakeGlobal; 40 | 41 | impl Global for FakeGlobal {} 42 | 43 | /// A Listener that never returns any controllers for unsupported platforms. 44 | struct FakeListener; 45 | 46 | impl Listener for FakeListener { 47 | fn poll(&mut self, _cx: &mut Context<'_>) -> Poll { 48 | Poll::Pending 49 | } 50 | } 51 | 52 | /// Controller Listener Implementation 53 | pub(crate) trait Listener: Send { 54 | /// Poll for controllers. 55 | fn poll(&mut self, cx: &mut Context<'_>) -> Poll; 56 | } 57 | 58 | /// Controller Implementation 59 | pub(crate) trait Controller: Send { 60 | /// The hardware identifier for this controller. 61 | fn id(&self) -> u64 { 62 | 0 63 | } 64 | /// Poll for events. 65 | fn poll(&mut self, _cx: &mut Context<'_>) -> Poll { 66 | Poll::Pending 67 | } 68 | /// Stereo rumble effect (left is low frequency, right is high frequency). 69 | fn rumble(&mut self, _left: f32, _right: f32) {} 70 | /// Get the name of this controller. 71 | fn name(&self) -> &str { 72 | "Unknown" 73 | } 74 | /// Floating Point Translation for pressure axis/buttons. 75 | fn pressure(&self, input: f64) -> f64 { 76 | input 77 | } 78 | /// Floating Point Translation for full axis values. 79 | fn axis(&self, input: f64) -> f64 { 80 | input 81 | } 82 | } 83 | 84 | /// Thread local global state implementation. 85 | pub(crate) trait Global: std::any::Any { 86 | /// Enable all events (when window comes in focus). 87 | fn enable(&self) {} 88 | /// Disable all events (when window leaves focus). 89 | fn disable(&self) {} 90 | /// Create a new listener. 91 | fn listener(&self, _remap: Remap) -> Box { 92 | Box::new(FakeListener) 93 | } 94 | } 95 | 96 | thread_local! { 97 | pub(crate) static GLOBAL: Box = ffi::global(); 98 | } 99 | -------------------------------------------------------------------------------- /stick/src/raw/android.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/ardaku.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/bsd.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/dom.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/fuchsia.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/ios.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/linux.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp::Ordering, 3 | fs::read_dir, 4 | mem::{size_of, MaybeUninit}, 5 | os::{ 6 | raw::{c_char, c_int, c_long, c_uint, c_ulong, c_ushort, c_void}, 7 | unix::io::RawFd, 8 | }, 9 | task::{Context, Poll}, 10 | }; 11 | 12 | use smelling_salts::{Device, Watcher}; 13 | 14 | use crate::{Event, Remap}; 15 | 16 | // Event codes taken from 17 | // https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h 18 | 19 | // Convert Linux BTN press to stick Event. 20 | fn linux_btn_to_stick_event( 21 | pending: &mut Vec, 22 | btn: c_ushort, 23 | pushed: bool, 24 | ) { 25 | pending.push(match btn { 26 | 0x08B /* KEY_MENU */ => Event::Context(pushed), 27 | 28 | 0x09E /* KEY_BACK */ => Event::PaddleLeft(pushed), 29 | 0x09F /* KEY_FORWARD */ => Event::PaddleRight(pushed), 30 | 31 | 0x120 /* BTN_TRIGGER */ => Event::Trigger(pushed), 32 | 0x121 /* BTN_THUMB */ => Event::ActionM(pushed), 33 | 0x122 /* BTN_THUMB2 */ => Event::Bumper(pushed), 34 | 0x123 /* BTN_TOP */ => Event::ActionR(pushed), 35 | 0x124 /* BTN_TOP2 */ => Event::ActionL(pushed), 36 | 0x125 /* BTN_PINKIE */ => Event::Pinky(pushed), 37 | 0x126 /* BTN_BASE1 */ => Event::Number(1, pushed), 38 | 0x127 /* BTN_BASE2 */ => Event::Number(2, pushed), 39 | 0x128 /* BTN_BASE3 */ => Event::Number(3, pushed), 40 | 0x129 /* BTN_BASE4 */ => Event::Number(4, pushed), 41 | 0x12A /* BTN_BASE5 */ => Event::Number(5, pushed), 42 | 0x12B /* BTN_BASE6 */ => Event::Number(6, pushed), 43 | 0x12C /* BTN_BASE7 */ => Event::Number(7, pushed), 44 | 0x12D /* BTN_BASE8 */ => Event::Number(8, pushed), 45 | 0x12E /* BTN_BASE9 */ => Event::Number(9, pushed), 46 | 0x12F /* BTN_BASE10 */ => Event::Number(10, pushed), 47 | 48 | 0x130 /* BTN_A / BTN_SOUTH */ => Event::ActionA(pushed), 49 | 0x131 /* BTN_B / BTN_EAST */ => Event::ActionB(pushed), 50 | 0x132 /* BTN_C */ => Event::ActionC(pushed), 51 | 0x133 /* BTN_X / BTN_NORTH */ => Event::ActionV(pushed), 52 | 0x134 /* BTN_Y / BTN_WEST */ => Event::ActionH(pushed), 53 | 0x135 /* BTN_Z */ => Event::ActionD(pushed), 54 | 0x136 /* BTN_TL */ => Event::BumperL(pushed), 55 | 0x137 /* BTN_TR */ => Event::BumperR(pushed), 56 | 0x138 /* BTN_TL2 */ => Event::TriggerL(f64::from(u8::from(pushed)) * 255.0), 57 | 0x139 /* BTN_TR2 */ => Event::TriggerR(f64::from(u8::from(pushed)) * 255.0), 58 | 0x13A /* BTN_SELECT */ => Event::MenuL(pushed), 59 | 0x13B /* BTN_START */ => Event::MenuR(pushed), 60 | 0x13C /* BTN_MODE */ => Event::Exit(pushed), 61 | 0x13D /* BTN_THUMBL */ => Event::Joy(pushed), 62 | 0x13E /* BTN_THUMBR */ => Event::Cam(pushed), 63 | 0x13F /* BTN_PINKYR */ => Event::PinkyRight(pushed), 64 | 0x140 /* BTN_PINKYL */ => Event::PinkyLeft(pushed), 65 | 66 | 0x220 /* BTN_DPAD_UP */ => Event::Up(pushed), 67 | 0x221 /* BTN_DPAD_DOWN */ => Event::Down(pushed), 68 | 0x222 /* BTN_DPAD_LEFT */ => Event::Left(pushed), 69 | 0x223 /* BTN_DPAD_RIGHT */ => Event::Right(pushed), 70 | 71 | 0x2C0 /* BTN_TRIGGER_HAPPY1 */ => Event::Number(11, pushed), 72 | 0x2C1 /* BTN_TRIGGER_HAPPY2 */ => Event::Number(12, pushed), 73 | 0x2C2 /* BTN_TRIGGER_HAPPY3 */ => Event::Number(13, pushed), 74 | 0x2C3 /* BTN_TRIGGER_HAPPY4 */ => Event::Number(14, pushed), 75 | 0x2C4 /* BTN_TRIGGER_HAPPY5 */ => Event::Number(15, pushed), 76 | 0x2C5 /* BTN_TRIGGER_HAPPY6 */ => Event::Number(16, pushed), 77 | 0x2C6 /* BTN_TRIGGER_HAPPY7 */ => Event::Number(17, pushed), 78 | 0x2C7 /* BTN_TRIGGER_HAPPY8 */ => Event::Number(18, pushed), 79 | 0x2C8 /* BTN_TRIGGER_HAPPY9 */ => Event::Number(19, pushed), 80 | 0x2C9 /* BTN_TRIGGER_HAPPY10 */ => Event::Number(20, pushed), 81 | 0x2CA /* BTN_TRIGGER_HAPPY11 */ => Event::Number(21, pushed), 82 | 0x2CB /* BTN_TRIGGER_HAPPY12 */ => Event::Number(22, pushed), 83 | 0x2CC /* BTN_TRIGGER_HAPPY13 */ => Event::Number(23, pushed), 84 | 0x2CD /* BTN_TRIGGER_HAPPY14 */ => Event::Number(24, pushed), 85 | 0x2CE /* BTN_TRIGGER_HAPPY15 */ => Event::Number(25, pushed), 86 | 0x2CF /* BTN_TRIGGER_HAPPY16 */ => Event::Number(26, pushed), 87 | 0x2D0 /* BTN_TRIGGER_HAPPY17 */ => Event::Number(27, pushed), 88 | 0x2D1 /* BTN_TRIGGER_HAPPY18 */ => Event::Number(28, pushed), 89 | 0x2D2 /* BTN_TRIGGER_HAPPY19 */ => Event::Number(29, pushed), 90 | 0x2D3 /* BTN_TRIGGER_HAPPY20 */ => Event::Number(30, pushed), 91 | 0x2D4 /* BTN_TRIGGER_HAPPY21 */ => Event::Number(31, pushed), 92 | 0x2D5 /* BTN_TRIGGER_HAPPY22 */ => Event::Number(32, pushed), 93 | 0x2D6 /* BTN_TRIGGER_HAPPY23 */ => Event::Number(33, pushed), 94 | 0x2D7 /* BTN_TRIGGER_HAPPY24 */ => Event::Number(34, pushed), 95 | 0x2D8 /* BTN_TRIGGER_HAPPY25 */ => Event::Number(35, pushed), 96 | 0x2D9 /* BTN_TRIGGER_HAPPY26 */ => Event::Number(36, pushed), 97 | 0x2DA /* BTN_TRIGGER_HAPPY27 */ => Event::Number(37, pushed), 98 | 0x2DB /* BTN_TRIGGER_HAPPY28 */ => Event::Number(38, pushed), 99 | 0x2DC /* BTN_TRIGGER_HAPPY29 */ => Event::Number(39, pushed), 100 | 0x2DD /* BTN_TRIGGER_HAPPY30 */ => Event::Number(40, pushed), 101 | 0x2DE /* BTN_TRIGGER_HAPPY31 */ => Event::Number(41, pushed), 102 | 0x2DF /* BTN_TRIGGER_HAPPY32 */ => Event::Number(42, pushed), 103 | 0x2E0 /* BTN_TRIGGER_HAPPY33 */ => Event::Number(43, pushed), 104 | 0x2E1 /* BTN_TRIGGER_HAPPY34 */ => Event::Number(44, pushed), 105 | 0x2E2 /* BTN_TRIGGER_HAPPY35 */ => Event::Number(45, pushed), 106 | 0x2E3 /* BTN_TRIGGER_HAPPY36 */ => Event::Number(46, pushed), 107 | 0x2E4 /* BTN_TRIGGER_HAPPY37 */ => Event::Number(47, pushed), 108 | 0x2E5 /* BTN_TRIGGER_HAPPY38 */ => Event::Number(48, pushed), 109 | 0x2E6 /* BTN_TRIGGER_HAPPY39 */ => Event::Number(49, pushed), 110 | 0x2E7 /* BTN_TRIGGER_HAPPY40 */ => Event::Number(50, pushed), 111 | 112 | _unknown => { 113 | eprintln!("Unknown Linux Button {}", _unknown); 114 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 115 | return; 116 | } 117 | }) 118 | } 119 | 120 | // Convert Linux REL axis to stick Event. 121 | fn linux_rel_to_stick_event( 122 | pending: &mut Vec, 123 | axis: c_ushort, 124 | value: c_int, 125 | ) { 126 | match axis { 127 | 0x00 /* REL_X */ => pending.push(Event::MouseX(value as f64)), 128 | 0x01 /* REL_Y */ => pending.push(Event::MouseY(value as f64)), 129 | 0x02 /* REL_Z */ => { 130 | eprintln!("FIXME: REL_Z"); 131 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 132 | } 133 | 0x03 /* REL_RX */ => { 134 | eprintln!("FIXME: REL_RX"); 135 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 136 | } 137 | 0x04 /* REL_RY */ => { 138 | eprintln!("FIXME: REL_RY"); 139 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 140 | } 141 | 0x05 /* REL_RZ */ => { 142 | eprintln!("FIXME: REL_RZ"); 143 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 144 | } 145 | 0x06 /* REL_HWHEEL */ => { 146 | eprintln!("FIXME: REL_HWHEEL"); 147 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 148 | } 149 | 0x07 /* REL_DIAL */ => { 150 | eprintln!("FIXME: REL_DIAL"); 151 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 152 | } 153 | 0x08 /* REL_WHEEL */ => { 154 | eprintln!("FIXME: REL_WHEEL"); 155 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 156 | } 157 | 0x09 /* REL_MISC */ => { 158 | eprintln!("FIXME: REL_MISC"); 159 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 160 | } 161 | _unknown => { 162 | eprintln!("Unknown Linux Axis {}", _unknown); 163 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 164 | } 165 | } 166 | } 167 | 168 | // Convert Linux ABS axis to stick Event. 169 | fn linux_abs_to_stick_event( 170 | pending: &mut Vec, 171 | axis: c_ushort, 172 | value: c_int, 173 | ) { 174 | match axis { 175 | 0x00 /* ABS_X */ => pending.push(Event::JoyX(value as f64)), 176 | 0x01 /* ABS_Y */ => pending.push(Event::JoyY(value as f64)), 177 | 0x02 /* ABS_Z */ => pending.push(Event::JoyZ(value as f64)), 178 | 0x03 /* ABS_RX */ => pending.push(Event::CamX(value as f64)), 179 | 0x04 /* ABS_RY */ => pending.push(Event::CamY(value as f64)), 180 | 0x05 /* ABS_RZ */ => pending.push(Event::CamZ(value as f64)), 181 | 0x06 /* ABS_THROTTLE */ => pending.push(Event::Throttle(value as f64)), 182 | 0x07 /* ABS_RUDDER */ => pending.push(Event::Rudder(value as f64)), 183 | 0x08 /* ABS_WHEEL */ => pending.push(Event::Wheel(value as f64)), 184 | 0x09 /* ABS_GAS */ => pending.push(Event::Gas(value as f64)), 185 | 0x0A /* ABS_BRAKE */ => pending.push(Event::Brake(value as f64)), 186 | 0x0B /* ABS_UNKNOWN0 */ => pending.push(Event::Slew(value as f64)), 187 | 0x0C /* ABS_UNKNOWN1 */ => pending.push(Event::ThrottleL(value as f64)), 188 | 0x0D /* ABS_UNKNOWN2 */ => pending.push(Event::ThrottleR(value as f64)), 189 | 0x0E /* ABS_UNKNOWN3 */ => pending.push(Event::ScrollX(value as f64)), 190 | 0x0F /* ABS_UNKNOWN4 */ => pending.push(Event::ScrollY(value as f64)), 191 | 0x10 /* ABS_HAT0X */ => match value.cmp(&0) { 192 | Ordering::Greater => pending.push(Event::PovRight(true)), 193 | Ordering::Less => pending.push(Event::PovLeft(true)), 194 | Ordering::Equal => { 195 | pending.push(Event::PovRight(false)); 196 | pending.push(Event::PovLeft(false)); 197 | } 198 | }, 199 | 0x11 /* ABS_HAT0Y */ => match value.cmp(&0) { 200 | Ordering::Greater => pending.push(Event::PovDown(true)), 201 | Ordering::Less => pending.push(Event::PovUp(true)), 202 | Ordering::Equal => { 203 | pending.push(Event::PovUp(false)); 204 | pending.push(Event::PovDown(false)); 205 | } 206 | }, 207 | 0x12 /* ABS_HAT1X */ => match value.cmp(&0) { 208 | Ordering::Greater => pending.push(Event::HatRight(true)), 209 | Ordering::Less => pending.push(Event::HatLeft(true)), 210 | Ordering::Equal => { 211 | pending.push(Event::HatRight(false)); 212 | pending.push(Event::HatLeft(false)); 213 | } 214 | }, 215 | 0x13 /* ABS_HAT1Y */ => match value.cmp(&0) { 216 | Ordering::Greater => pending.push(Event::HatDown(true)), 217 | Ordering::Less => pending.push(Event::HatUp(true)), 218 | Ordering::Equal => { 219 | pending.push(Event::HatUp(false)); 220 | pending.push(Event::HatDown(false)); 221 | } 222 | }, 223 | 0x14 /* ABS_HAT2X */ => match value.cmp(&0) { 224 | Ordering::Greater => pending.push(Event::TrimRight(true)), 225 | Ordering::Less => pending.push(Event::TrimLeft(true)), 226 | Ordering::Equal => { 227 | pending.push(Event::TrimRight(false)); 228 | pending.push(Event::TrimLeft(false)); 229 | } 230 | }, 231 | 0x15 /* ABS_HAT2Y */ => match value.cmp(&0) { 232 | Ordering::Greater => pending.push(Event::TrimDown(true)), 233 | Ordering::Less => pending.push(Event::TrimUp(true)), 234 | Ordering::Equal => { 235 | pending.push(Event::TrimUp(false)); 236 | pending.push(Event::TrimDown(false)); 237 | } 238 | }, 239 | 0x16 /* ABS_HAT3X */ => match value.cmp(&0) { 240 | Ordering::Greater => pending.push(Event::MicRight(true)), 241 | Ordering::Less => pending.push(Event::MicLeft(true)), 242 | Ordering::Equal => { 243 | pending.push(Event::MicRight(false)); 244 | pending.push(Event::MicLeft(false)); 245 | } 246 | }, 247 | 0x17 /* ABS_HAT3Y */ => match value.cmp(&0) { 248 | Ordering::Greater => pending.push(Event::MicDown(true)), 249 | Ordering::Less => pending.push(Event::MicUp(true)), 250 | Ordering::Equal => { 251 | pending.push(Event::MicUp(false)); 252 | pending.push(Event::MicDown(false)); 253 | } 254 | }, 255 | 0x18 /* ABS_PRESSURE */ => { 256 | eprintln!("Unknown Event: ABS_PRESSURE"); 257 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 258 | } 259 | 0x19 /* ABS_DISTANCE */ => { 260 | eprintln!("Unknown Event: ABS_DISTANCE"); 261 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 262 | } 263 | 0x1a /* ABS_TILT_X */ => { 264 | eprintln!("Unknown Event: ABS_TILT_X"); 265 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 266 | } 267 | 0x1b /* ABS_TILT_Y */ => { 268 | eprintln!("Unknown Event: ABS_TILT_Y"); 269 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 270 | } 271 | 0x1c /* ABS_TOOL_WIDTH */ => { 272 | eprintln!("Unknown Event: ABS_TOOL_WIDTH"); 273 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 274 | } 275 | 0x20 /* ABS_VOLUME */ => { 276 | eprintln!("Unknown Event: ABS_VOLUME"); 277 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 278 | } 279 | 0x28 /* ABS_MISC */ => { 280 | eprintln!("Unknown Event: ABS_MISC"); 281 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 282 | } 283 | _unknown => { 284 | eprintln!("Unknown Linux Axis {}", _unknown); 285 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 286 | } 287 | } 288 | } 289 | 290 | fn linux_evdev_to_stick_event(pending: &mut Vec, e: EvdevEv) { 291 | match e.ev_type { 292 | 0x00 /* SYN */ => {}, // Ignore Syn Input Events 293 | 0x01 /* BTN */ => linux_btn_to_stick_event(pending, e.ev_code, e.ev_value != 0), 294 | 0x02 /* REL */ => linux_rel_to_stick_event(pending, e.ev_code, e.ev_value), 295 | 0x03 /* ABS */ => linux_abs_to_stick_event(pending, e.ev_code, e.ev_value), 296 | 0x04 /* MSC */ => { 297 | if e.ev_code != 4 { // Ignore Misc./Scan Events 298 | let (code, val) = (e.ev_code, e.ev_value); 299 | eprintln!("Unknown Linux Misc Code: {}, Value: {}", code, val); 300 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 301 | } 302 | } 303 | 0x15 /* FF */ => {}, // Ignore Force Feedback Input Events 304 | _unknown => { 305 | eprintln!("Unknown Linux Event Type: {}", _unknown); 306 | eprintln!("Report at https://github.com/ardaku/stick/issues"); 307 | } 308 | } 309 | } 310 | 311 | #[repr(C)] 312 | struct InotifyEv { 313 | // struct inotify_event, from C. 314 | wd: c_int, /* Watch descriptor */ 315 | mask: u32, /* Mask describing event */ 316 | cookie: u32, /* Unique cookie associating related 317 | events (for rename(2)) */ 318 | len: u32, /* Size of name field */ 319 | name: [u8; 256], /* Optional null-terminated name */ 320 | } 321 | 322 | #[repr(C)] 323 | struct TimeVal { 324 | // struct timeval, from C. 325 | tv_sec: c_long, 326 | tv_usec: c_long, 327 | } 328 | 329 | #[repr(C)] 330 | struct EvdevEv { 331 | // struct input_event, from C. 332 | ev_time: TimeVal, 333 | ev_type: c_ushort, 334 | ev_code: c_ushort, 335 | // Though in the C header it's defined as uint, define as int because 336 | // that's how it's meant to be interpreted. 337 | ev_value: c_int, 338 | } 339 | 340 | #[repr(C)] 341 | struct AbsInfo { 342 | // struct input_absinfo, from C. 343 | value: i32, 344 | // Though in the C header it's defined as uint32, define as int32 because 345 | // that's how it's meant to be interpreted. 346 | minimum: i32, 347 | // Though in the C header it's defined as uint32, define as int32 because 348 | // that's how it's meant to be interpreted. 349 | maximum: i32, 350 | fuzz: i32, 351 | flat: i32, 352 | resolution: i32, 353 | } 354 | 355 | extern "C" { 356 | fn strlen(s: *const u8) -> usize; 357 | 358 | fn open(pathname: *const u8, flags: c_int) -> c_int; 359 | fn read(fd: RawFd, buf: *mut c_void, count: usize) -> isize; 360 | fn write(fd: RawFd, buf: *const c_void, count: usize) -> isize; 361 | fn close(fd: RawFd) -> c_int; 362 | fn fcntl(fd: RawFd, cmd: c_int, v: c_int) -> c_int; 363 | fn ioctl(fd: RawFd, request: c_ulong, v: *mut c_void) -> c_int; 364 | 365 | fn inotify_init1(flags: c_int) -> c_int; 366 | fn inotify_add_watch(fd: RawFd, path: *const u8, mask: u32) -> c_int; 367 | 368 | fn __errno_location() -> *mut c_int; 369 | } 370 | 371 | // From: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input.h 372 | 373 | #[repr(C)] 374 | struct FfTrigger { 375 | button: u16, 376 | interval: u16, 377 | } 378 | 379 | #[repr(C)] 380 | struct FfReplay { 381 | length: u16, 382 | delay: u16, 383 | } 384 | 385 | #[repr(C)] 386 | #[derive(Copy, Clone)] 387 | struct FfEnvelope { 388 | attack_length: u16, 389 | attack_level: u16, 390 | fade_length: u16, 391 | fade_level: u16, 392 | } 393 | 394 | #[repr(C)] 395 | #[derive(Copy, Clone)] 396 | struct FfConstantEffect { 397 | level: i16, 398 | envelope: FfEnvelope, 399 | } 400 | 401 | #[repr(C)] 402 | #[derive(Copy, Clone)] 403 | struct FfRampEffect { 404 | start_level: i16, 405 | end_level: i16, 406 | envelope: FfEnvelope, 407 | } 408 | 409 | #[repr(C)] 410 | #[derive(Copy, Clone)] 411 | struct FfPeriodicEffect { 412 | waveform: u16, 413 | period: u16, 414 | magnitude: i16, 415 | offset: i16, 416 | phase: u16, 417 | 418 | envelope: FfEnvelope, 419 | 420 | custom_len: u32, 421 | custom_data: *mut i16, 422 | } 423 | 424 | #[repr(C)] 425 | #[derive(Copy, Clone)] 426 | struct FfConditionEffect { 427 | right_saturation: u16, 428 | left_saturation: u16, 429 | 430 | right_coeff: i16, 431 | left_coeff: i16, 432 | 433 | deadband: u16, 434 | center: i16, 435 | } 436 | 437 | #[repr(C)] 438 | #[derive(Copy, Clone)] 439 | struct FfRumbleEffect { 440 | strong_magnitude: u16, 441 | weak_magnitude: u16, 442 | } 443 | 444 | #[repr(C)] 445 | union FfUnion { 446 | constant: FfConstantEffect, // Not supported. 447 | ramp: FfRampEffect, 448 | periodic: FfPeriodicEffect, 449 | condition: [FfConditionEffect; 2], /* One for each axis */ 450 | rumble: FfRumbleEffect, // Not supported 451 | } 452 | 453 | #[repr(C)] 454 | struct FfEffect { 455 | stype: u16, 456 | id: i16, 457 | direction: u16, 458 | 459 | trigger: FfTrigger, 460 | replay: FfReplay, 461 | 462 | u: FfUnion, 463 | } 464 | 465 | fn joystick_ff(fd: RawFd, code: i16, strong: f32, weak: f32) { 466 | // Update haptic effect `code`. 467 | if strong != 0.0 || weak != 0.0 { 468 | joystick_haptic(fd, code, strong, weak); 469 | } 470 | // 471 | let ev_code = code.try_into().unwrap(); 472 | 473 | let play = &EvdevEv { 474 | ev_time: TimeVal { 475 | tv_sec: 0, 476 | tv_usec: 0, 477 | }, 478 | ev_type: 0x15, /* EV_FF */ 479 | ev_code, 480 | ev_value: (strong > 0.0 || weak > 0.0) as _, 481 | }; 482 | let play: *const _ = play; 483 | unsafe { 484 | if write(fd, play.cast(), size_of::()) 485 | != size_of::() as isize 486 | { 487 | let errno = *__errno_location(); 488 | if errno != 19 && errno != 9 { 489 | // 19 = device unplugged, ignore 490 | // 9 = device openned read-only, ignore 491 | panic!("Write exited with {}", *__errno_location()); 492 | } 493 | } 494 | } 495 | } 496 | 497 | // Get ID's for rumble and vibrate, if they're supported (otherwise, -1). 498 | fn joystick_haptic(fd: RawFd, id: i16, strong: f32, weak: f32) -> i16 { 499 | let a = &mut FfEffect { 500 | stype: 0x50, 501 | id, /* allocate new effect */ 502 | direction: 0, 503 | trigger: FfTrigger { 504 | button: 0, 505 | interval: 0, 506 | }, 507 | replay: FfReplay { 508 | length: 0, 509 | delay: 0, 510 | }, 511 | u: FfUnion { 512 | rumble: FfRumbleEffect { 513 | strong_magnitude: (u16::MAX as f32 * strong) as u16, 514 | weak_magnitude: (u16::MAX as f32 * weak) as u16, 515 | }, 516 | }, 517 | }; 518 | let b: *mut _ = a; 519 | if unsafe { ioctl(fd, 0x40304580, b.cast()) } == -1 { 520 | -1 521 | } else { 522 | a.id 523 | } 524 | } 525 | 526 | //////////////////////////////////////////////////////////////////////////////// 527 | 528 | /// Gamepad / Other HID 529 | struct Controller { 530 | // Async device handle 531 | device: Device, 532 | // Hexadecimal controller type ID 533 | id: u64, 534 | // Rumble effect id. 535 | rumble: i16, 536 | /// Signed axis multiplier 537 | norm: f64, 538 | /// Signed axis zero 539 | zero: f64, 540 | /// Don't process near 0 541 | flat: f64, 542 | /// 543 | pending_events: Vec, 544 | /// 545 | name: String, 546 | } 547 | 548 | impl Controller { 549 | fn new(fd: c_int) -> Self { 550 | // Enable evdev async. 551 | assert_ne!(unsafe { fcntl(fd, 0x4, 0x800) }, -1); 552 | 553 | // Get the hardware id of this controller. 554 | let mut id = MaybeUninit::::uninit(); 555 | assert_ne!( 556 | unsafe { ioctl(fd, 0x_8008_4502, id.as_mut_ptr().cast()) }, 557 | -1 558 | ); 559 | let id = unsafe { id.assume_init() }.to_be(); 560 | 561 | // Get the min and max absolute values for axis. 562 | let mut a = MaybeUninit::::uninit(); 563 | assert_ne!( 564 | unsafe { ioctl(fd, 0x_8018_4540, a.as_mut_ptr().cast()) }, 565 | -1 566 | ); 567 | let a = unsafe { a.assume_init() }; 568 | let norm = (a.maximum as f64 - a.minimum as f64) * 0.5; 569 | let zero = a.minimum as f64 + norm; 570 | // Invert so multiplication can be used instead of division 571 | let norm = norm.recip(); 572 | let flat = a.flat as f64 * norm; 573 | 574 | // Query the controller for haptic support. 575 | let rumble = joystick_haptic(fd, -1, 0.0, 0.0); 576 | // Construct device from fd, looking for input events. 577 | let device = Device::new(fd, Watcher::new().input()); 578 | // 579 | let pending_events = Vec::new(); 580 | 581 | // Get Name 582 | let fd = device.raw(); 583 | let mut a = MaybeUninit::<[c_char; 256]>::uninit(); 584 | assert_ne!( 585 | unsafe { ioctl(fd, 0x80FF_4506, a.as_mut_ptr().cast()) }, 586 | -1 587 | ); 588 | let a = unsafe { a.assume_init() }; 589 | let name = unsafe { std::ffi::CStr::from_ptr(a.as_ptr()) }; 590 | let name = name.to_string_lossy().to_string(); 591 | 592 | // Return 593 | Self { 594 | device, 595 | id, 596 | rumble, 597 | norm, 598 | zero, 599 | flat, 600 | pending_events, 601 | name, 602 | } 603 | } 604 | } 605 | 606 | impl super::Controller for Controller { 607 | fn id(&self) -> u64 { 608 | self.id 609 | } 610 | 611 | fn poll(&mut self, cx: &mut Context<'_>) -> Poll { 612 | // Queue 613 | if let Some(e) = self.pending_events.pop() { 614 | return Poll::Ready(e); 615 | } 616 | 617 | // Early return if a different device woke the executor. 618 | if self.device.pending() { 619 | return self.device.sleep(cx); 620 | } 621 | 622 | // Read an event. 623 | let mut ev = MaybeUninit::::uninit(); 624 | let ev = { 625 | let bytes = unsafe { 626 | read( 627 | self.device.raw(), 628 | ev.as_mut_ptr().cast(), 629 | size_of::(), 630 | ) 631 | }; 632 | if bytes <= 0 { 633 | let errno = unsafe { *__errno_location() }; 634 | if errno == 19 { 635 | return Poll::Ready(Event::Disconnect); 636 | } 637 | assert_eq!(errno, 11); 638 | // If no new controllers found, return pending. 639 | return self.device.sleep(cx); 640 | } 641 | assert_eq!(size_of::() as isize, bytes); 642 | unsafe { ev.assume_init() } 643 | }; 644 | 645 | // Convert the event (may produce multiple stick events). 646 | linux_evdev_to_stick_event(&mut self.pending_events, ev); 647 | 648 | // Check if events should be dropped. 649 | if !ENABLED.load(std::sync::atomic::Ordering::Relaxed) { 650 | self.pending_events.clear(); 651 | } 652 | 653 | // Tail call recursion! 654 | self.poll(cx) 655 | } 656 | 657 | fn name(&self) -> &str { 658 | &self.name 659 | } 660 | 661 | fn rumble(&mut self, left: f32, right: f32) { 662 | if self.rumble >= 0 { 663 | joystick_ff(self.device.raw(), self.rumble, left, right); 664 | } 665 | } 666 | 667 | /// Use default unsigned axis range 668 | fn pressure(&self, input: f64) -> f64 { 669 | input * (1.0 / 255.0) 670 | } 671 | 672 | /// Use full joystick axis range. 673 | fn axis(&self, input: f64) -> f64 { 674 | let input = (input - self.zero) * self.norm; 675 | if input.abs() <= self.flat { 676 | 0.0 677 | } else { 678 | input 679 | } 680 | } 681 | } 682 | 683 | impl Drop for Controller { 684 | fn drop(&mut self) { 685 | assert_ne!(unsafe { close(self.device.stop()) }, -1); 686 | } 687 | } 688 | 689 | struct Listener { 690 | device: Device, 691 | read_dir: Option>, 692 | remap: Remap, 693 | } 694 | 695 | impl Listener { 696 | fn new(remap: Remap) -> Self { 697 | const CLOEXEC: c_int = 0o2000000; 698 | const NONBLOCK: c_int = 0o0004000; 699 | const ATTRIB: c_uint = 0x00000004; 700 | const DIR: &[u8] = b"/dev/input/\0"; 701 | 702 | // Create an inotify. 703 | let listen = unsafe { inotify_init1(NONBLOCK | CLOEXEC) }; 704 | if listen == -1 { 705 | panic!("Couldn't create inotify!"); 706 | } 707 | 708 | // Start watching the controller directory. 709 | if unsafe { inotify_add_watch(listen, DIR.as_ptr(), ATTRIB) } == -1 { 710 | panic!("Couldn't add inotify watch!"); 711 | } 712 | 713 | Self { 714 | // Create watcher, and register with fd as a "device". 715 | device: Device::new(listen, Watcher::new().input()), 716 | // 717 | read_dir: Some(Box::new(read_dir("/dev/input/").unwrap())), 718 | // 719 | remap, 720 | } 721 | } 722 | 723 | fn controller( 724 | remap: &Remap, 725 | mut filename: String, 726 | ) -> Poll { 727 | if filename.contains("event") { 728 | filename.push('\0'); 729 | // Try read & write first 730 | let mut fd = unsafe { open(filename.as_ptr(), 2) }; 731 | // Try readonly second (bluetooth controller - input device) 732 | if fd == -1 { 733 | fd = unsafe { open(filename.as_ptr(), 0) }; 734 | } 735 | // Try writeonly third (bluetooth haptic device) 736 | if fd == -1 { 737 | fd = unsafe { open(filename.as_ptr(), 1) }; 738 | } 739 | // If one succeeded, return that controller. 740 | if fd != -1 { 741 | return Poll::Ready(crate::Controller::new( 742 | Box::new(Controller::new(fd)), 743 | remap, 744 | )); 745 | } 746 | } 747 | Poll::Pending 748 | } 749 | } 750 | 751 | impl super::Listener for Listener { 752 | fn poll(&mut self, cx: &mut Context<'_>) -> Poll { 753 | // Read the directory for ctrls if initialization hasn't completed yet. 754 | if let Some(ref mut read_dir) = &mut self.read_dir { 755 | for dir_entry in read_dir.flatten() { 756 | let file = dir_entry.path(); 757 | let path = file.as_path().to_string_lossy().to_string(); 758 | if let Poll::Ready(controller) = 759 | Self::controller(&self.remap, path) 760 | { 761 | return Poll::Ready(controller); 762 | } 763 | } 764 | self.read_dir = None; 765 | } 766 | 767 | // Read the Inotify Event. 768 | let mut ev = MaybeUninit::::zeroed(); 769 | let read = unsafe { 770 | read( 771 | self.device.raw(), 772 | ev.as_mut_ptr().cast(), 773 | size_of::(), 774 | ) 775 | }; 776 | if read > 0 { 777 | let ev = unsafe { ev.assume_init() }; 778 | let len = unsafe { strlen(&ev.name[0]) }; 779 | let filename = String::from_utf8_lossy(&ev.name[..len]); 780 | let path = format!("/dev/input/{}", filename); 781 | if let Poll::Ready(controller) = Self::controller(&self.remap, path) 782 | { 783 | return Poll::Ready(controller); 784 | } 785 | } 786 | 787 | // Register waker & go to sleep for this device 788 | self.device.sleep(cx) 789 | } 790 | } 791 | 792 | impl Drop for Listener { 793 | fn drop(&mut self) { 794 | assert_eq!(unsafe { close(self.device.stop()) }, 0); 795 | } 796 | } 797 | 798 | static ENABLED: std::sync::atomic::AtomicBool = 799 | std::sync::atomic::AtomicBool::new(true); 800 | 801 | struct Global; 802 | 803 | impl super::Global for Global { 804 | /// Enable all events (when window comes in focus). 805 | fn enable(&self) { 806 | ENABLED.store(true, std::sync::atomic::Ordering::Relaxed); 807 | } 808 | 809 | /// Disable all events (when window leaves focus). 810 | fn disable(&self) { 811 | ENABLED.store(false, std::sync::atomic::Ordering::Relaxed); 812 | } 813 | 814 | /// Create a new listener. 815 | fn listener(&self, remap: Remap) -> Box { 816 | Box::new(Listener::new(remap)) 817 | } 818 | } 819 | 820 | pub(super) fn global() -> Box { 821 | Box::new(Global) 822 | } 823 | -------------------------------------------------------------------------------- /stick/src/raw/macos.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/redox.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/wasi.rs: -------------------------------------------------------------------------------- 1 | pub(super) fn global() -> Box { 2 | Box::new(super::FakeGlobal) 3 | } 4 | -------------------------------------------------------------------------------- /stick/src/raw/windows.rs: -------------------------------------------------------------------------------- 1 | //! This file's code is based on https://github.com/Lokathor/rusty-xinput 2 | 3 | use std::{ 4 | fmt::{self, Debug, Formatter}, 5 | sync::Arc, 6 | task::{Context, Poll, Waker}, 7 | }; 8 | 9 | use winapi::{ 10 | shared::{ 11 | guiddef::GUID, 12 | minwindef::{BOOL, BYTE, DWORD, HMODULE, UINT}, 13 | ntdef::LPWSTR, 14 | winerror::{ERROR_DEVICE_NOT_CONNECTED, ERROR_EMPTY, ERROR_SUCCESS}, 15 | }, 16 | um::{ 17 | libloaderapi::{FreeLibrary, GetProcAddress, LoadLibraryW}, 18 | xinput, 19 | }, 20 | }; 21 | 22 | use crate::{Event, Remap}; 23 | 24 | type XInputEnableFunc = unsafe extern "system" fn(BOOL); 25 | type XInputGetStateFunc = 26 | unsafe extern "system" fn(DWORD, *mut xinput::XINPUT_STATE) -> DWORD; 27 | type XInputSetStateFunc = 28 | unsafe extern "system" fn(DWORD, *mut xinput::XINPUT_VIBRATION) -> DWORD; 29 | type XInputGetCapabilitiesFunc = unsafe extern "system" fn( 30 | DWORD, 31 | DWORD, 32 | *mut xinput::XINPUT_CAPABILITIES, 33 | ) -> DWORD; 34 | 35 | // Removed in xinput1_4.dll. 36 | type XInputGetDSoundAudioDeviceGuidsFunc = 37 | unsafe extern "system" fn(DWORD, *mut GUID, *mut GUID) -> DWORD; 38 | 39 | // Added in xinput1_3.dll. 40 | type XInputGetKeystrokeFunc = 41 | unsafe extern "system" fn(DWORD, DWORD, xinput::PXINPUT_KEYSTROKE) -> DWORD; 42 | type XInputGetBatteryInformationFunc = unsafe extern "system" fn( 43 | DWORD, 44 | BYTE, 45 | *mut xinput::XINPUT_BATTERY_INFORMATION, 46 | ) -> DWORD; 47 | 48 | // Added in xinput1_4.dll. 49 | type XInputGetAudioDeviceIdsFunc = unsafe extern "system" fn( 50 | DWORD, 51 | LPWSTR, 52 | *mut UINT, 53 | LPWSTR, 54 | *mut UINT, 55 | ) -> DWORD; 56 | 57 | struct ScopedHMODULE(HMODULE); 58 | impl Drop for ScopedHMODULE { 59 | fn drop(&mut self) { 60 | unsafe { FreeLibrary(self.0) }; 61 | } 62 | } 63 | 64 | /// A handle to a loaded XInput DLL. 65 | #[derive(Clone)] 66 | struct XInputHandle { 67 | handle: Arc, 68 | xinput_enable: XInputEnableFunc, 69 | xinput_get_state: XInputGetStateFunc, 70 | xinput_set_state: XInputSetStateFunc, 71 | xinput_get_capabilities: XInputGetCapabilitiesFunc, 72 | opt_xinput_get_keystroke: Option, 73 | opt_xinput_get_battery_information: Option, 74 | opt_xinput_get_audio_device_ids: Option, 75 | opt_xinput_get_dsound_audio_device_guids: 76 | Option, 77 | } 78 | 79 | impl Debug for XInputHandle { 80 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { 81 | write!(f, "XInputHandle(handle = {:?})", self.handle.0) 82 | } 83 | } 84 | 85 | /// Quick and dirty wrapper to let us format log messages easier. 86 | struct WideNullU16<'a>(&'a [u16; ::winapi::shared::minwindef::MAX_PATH]); 87 | impl ::std::fmt::Debug for WideNullU16<'_> { 88 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 89 | for &u in self.0.iter() { 90 | if u == 0 { 91 | break; 92 | } else { 93 | write!(f, "{}", u as u8 as char)? 94 | } 95 | } 96 | Ok(()) 97 | } 98 | } 99 | 100 | /// Converts a rusty string into a win32 string. 101 | fn wide_null>( 102 | s: S, 103 | ) -> [u16; ::winapi::shared::minwindef::MAX_PATH] { 104 | let mut output: [u16; ::winapi::shared::minwindef::MAX_PATH] = 105 | [0; ::winapi::shared::minwindef::MAX_PATH]; 106 | let mut i = 0; 107 | for u in s.as_ref().encode_utf16() { 108 | if i == output.len() - 1 { 109 | break; 110 | } else { 111 | output[i] = u; 112 | } 113 | i += 1; 114 | } 115 | output[i] = 0; 116 | output 117 | } 118 | 119 | unsafe impl Send for XInputHandle {} 120 | unsafe impl Sync for XInputHandle {} 121 | 122 | /// The ways that a dynamic load of XInput can fail. 123 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] 124 | enum XInputLoadingFailure { 125 | /// No DLL for XInput could be found. This places the system back into an 126 | /// "uninitialized" status, and you could potentially try again later if 127 | /// the user fiddles with the program's DLL path or whatever. 128 | NoDLL, 129 | /// A DLL was found that matches one of the expected XInput DLL names, but 130 | /// it didn't contain both of the expected functions. This is probably 131 | /// a weird situation to find. Either way, the xinput status is set to 132 | /// "uninitialized" and as with the NoDLL error you could potentially 133 | /// try again. 134 | NoPointers, 135 | } 136 | 137 | impl XInputHandle { 138 | /// Attempts to dynamically load an XInput DLL and get the function 139 | /// pointers. 140 | /// 141 | /// # Failure 142 | /// 143 | /// This can fail in a few ways, as explained in the `XInputLoadingFailure` 144 | /// type. The most likely failure case is that the user's system won't have 145 | /// the required DLL, in which case you should probably allow them to 146 | /// play with just a keyboard/mouse instead. 147 | /// 148 | /// # Current DLL Names 149 | /// 150 | /// Currently the following DLL names are searched for in this order: 151 | /// 152 | /// * `xinput1_4.dll` 153 | /// * `xinput1_3.dll` 154 | /// * `xinput1_2.dll` 155 | /// * `xinput1_1.dll` 156 | /// * `xinput9_1_0.dll` 157 | pub(crate) fn load_default( 158 | ) -> Result, XInputLoadingFailure> { 159 | let xinput14 = "xinput1_4.dll"; 160 | let xinput13 = "xinput1_3.dll"; 161 | let xinput12 = "xinput1_2.dll"; 162 | let xinput11 = "xinput1_1.dll"; 163 | let xinput91 = "xinput9_1_0.dll"; 164 | 165 | for lib_name in 166 | [xinput14, xinput13, xinput12, xinput11, xinput91].iter() 167 | { 168 | if let Ok(handle) = XInputHandle::load(lib_name) { 169 | return Ok(Arc::new(handle)); 170 | } 171 | } 172 | 173 | debug!("Failure: XInput could not be loaded."); 174 | Err(XInputLoadingFailure::NoDLL) 175 | } 176 | 177 | /// Attempt to load a specific XInput DLL and get the function pointers. 178 | pub(crate) fn load>( 179 | s: S, 180 | ) -> Result { 181 | let lib_name = wide_null(s); 182 | trace!( 183 | "Attempting to load XInput DLL: {:?}", 184 | WideNullU16(&lib_name) 185 | ); 186 | // It's always safe to call `LoadLibraryW`, the worst that can happen is 187 | // that we get a null pointer back. 188 | let xinput_handle = unsafe { LoadLibraryW(lib_name.as_ptr()) }; 189 | if !xinput_handle.is_null() { 190 | debug!("Success: XInput Loaded: {:?}", WideNullU16(&lib_name)); 191 | } 192 | 193 | let xinput_handle = ScopedHMODULE(xinput_handle); 194 | 195 | let enable_name = b"XInputEnable\0"; 196 | let get_state_name = b"XInputGetState\0"; 197 | let set_state_name = b"XInputSetState\0"; 198 | let get_capabilities_name = b"XInputGetCapabilities\0"; 199 | let get_keystroke_name = b"XInputGetKeystroke\0"; 200 | let get_battery_information_name = b"XInputGetBatteryInformation\0"; 201 | let get_audio_device_ids_name = b"XInputGetAudioDeviceIds\0"; 202 | let get_dsound_audio_device_guids_name = 203 | b"XInputGetDSoundAudioDeviceGuids\0"; 204 | 205 | let mut opt_xinput_enable = None; 206 | let mut opt_xinput_get_state = None; 207 | let mut opt_xinput_set_state = None; 208 | let mut opt_xinput_get_capabilities = None; 209 | let mut opt_xinput_get_keystroke = None; 210 | let mut opt_xinput_get_battery_information = None; 211 | let mut opt_xinput_get_audio_device_ids = None; 212 | let mut opt_xinput_get_dsound_audio_device_guids = None; 213 | 214 | // using transmute is so dodgy we'll put that in its own unsafe block. 215 | unsafe { 216 | let enable_ptr = GetProcAddress( 217 | xinput_handle.0, 218 | enable_name.as_ptr() as *mut i8, 219 | ); 220 | if !enable_ptr.is_null() { 221 | trace!("Found XInputEnable."); 222 | opt_xinput_enable = Some(::std::mem::transmute(enable_ptr)); 223 | } else { 224 | trace!("Could not find XInputEnable."); 225 | } 226 | } 227 | 228 | // using transmute is so dodgy we'll put that in its own unsafe block. 229 | unsafe { 230 | let get_state_ptr = GetProcAddress( 231 | xinput_handle.0, 232 | get_state_name.as_ptr() as *mut i8, 233 | ); 234 | if !get_state_ptr.is_null() { 235 | trace!("Found XInputGetState."); 236 | opt_xinput_get_state = 237 | Some(::std::mem::transmute(get_state_ptr)); 238 | } else { 239 | trace!("Could not find XInputGetState."); 240 | } 241 | } 242 | 243 | // using transmute is so dodgy we'll put that in its own unsafe block. 244 | unsafe { 245 | let set_state_ptr = GetProcAddress( 246 | xinput_handle.0, 247 | set_state_name.as_ptr() as *mut i8, 248 | ); 249 | if !set_state_ptr.is_null() { 250 | trace!("Found XInputSetState."); 251 | opt_xinput_set_state = 252 | Some(::std::mem::transmute(set_state_ptr)); 253 | } else { 254 | trace!("Could not find XInputSetState."); 255 | } 256 | } 257 | 258 | // using transmute is so dodgy we'll put that in its own unsafe block. 259 | unsafe { 260 | let get_capabilities_ptr = GetProcAddress( 261 | xinput_handle.0, 262 | get_capabilities_name.as_ptr() as *mut i8, 263 | ); 264 | if !get_capabilities_ptr.is_null() { 265 | trace!("Found XInputGetCapabilities."); 266 | opt_xinput_get_capabilities = 267 | Some(::std::mem::transmute(get_capabilities_ptr)); 268 | } else { 269 | trace!("Could not find XInputGetCapabilities."); 270 | } 271 | } 272 | 273 | // using transmute is so dodgy we'll put that in its own unsafe block. 274 | unsafe { 275 | let get_keystroke_ptr = GetProcAddress( 276 | xinput_handle.0, 277 | get_keystroke_name.as_ptr() as *mut i8, 278 | ); 279 | if !get_keystroke_ptr.is_null() { 280 | trace!("Found XInputGetKeystroke."); 281 | opt_xinput_get_keystroke = 282 | Some(::std::mem::transmute(get_keystroke_ptr)); 283 | } else { 284 | trace!("Could not find XInputGetKeystroke."); 285 | } 286 | } 287 | 288 | // using transmute is so dodgy we'll put that in its own unsafe block. 289 | unsafe { 290 | let get_battery_information_ptr = GetProcAddress( 291 | xinput_handle.0, 292 | get_battery_information_name.as_ptr() as *mut i8, 293 | ); 294 | if !get_battery_information_ptr.is_null() { 295 | trace!("Found XInputGetBatteryInformation."); 296 | opt_xinput_get_battery_information = 297 | Some(::std::mem::transmute(get_battery_information_ptr)); 298 | } else { 299 | trace!("Could not find XInputGetBatteryInformation."); 300 | } 301 | } 302 | 303 | // using transmute is so dodgy we'll put that in its own unsafe block. 304 | unsafe { 305 | let get_dsound_audio_device_guids_ptr = GetProcAddress( 306 | xinput_handle.0, 307 | get_dsound_audio_device_guids_name.as_ptr() as *mut i8, 308 | ); 309 | if !get_dsound_audio_device_guids_ptr.is_null() { 310 | trace!("Found XInputGetDSoundAudioDeviceGuids."); 311 | opt_xinput_get_dsound_audio_device_guids = Some( 312 | ::std::mem::transmute(get_dsound_audio_device_guids_ptr), 313 | ); 314 | } else { 315 | trace!("Could not find XInputGetDSoundAudioDeviceGuids."); 316 | } 317 | } 318 | 319 | // using transmute is so dodgy we'll put that in its own unsafe block. 320 | unsafe { 321 | let get_audio_device_ids_ptr = GetProcAddress( 322 | xinput_handle.0, 323 | get_audio_device_ids_name.as_ptr() as *mut i8, 324 | ); 325 | if !get_audio_device_ids_ptr.is_null() { 326 | trace!("Found XInputGetAudioDeviceIds."); 327 | opt_xinput_get_audio_device_ids = 328 | Some(::std::mem::transmute(get_audio_device_ids_ptr)); 329 | } else { 330 | trace!("Could not find XInputGetAudioDeviceIds."); 331 | } 332 | } 333 | 334 | // this is safe because no other code can be loading xinput at the same 335 | // time as us. 336 | if let ( 337 | Some(xinput_enable), 338 | Some(xinput_get_state), 339 | Some(xinput_set_state), 340 | Some(xinput_get_capabilities), 341 | ) = ( 342 | opt_xinput_enable, 343 | opt_xinput_get_state, 344 | opt_xinput_set_state, 345 | opt_xinput_get_capabilities, 346 | ) { 347 | debug!("All function pointers loaded successfully."); 348 | Ok(XInputHandle { 349 | handle: Arc::new(xinput_handle), 350 | xinput_enable, 351 | xinput_get_state, 352 | xinput_set_state, 353 | xinput_get_capabilities, 354 | opt_xinput_get_keystroke, 355 | opt_xinput_get_battery_information, 356 | opt_xinput_get_dsound_audio_device_guids, 357 | opt_xinput_get_audio_device_ids, 358 | }) 359 | } else { 360 | debug!("Could not load the function pointers."); 361 | Err(XInputLoadingFailure::NoPointers) 362 | } 363 | } 364 | } 365 | 366 | /// These are all the sorts of problems that can come up when you're using the 367 | /// xinput system. 368 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] 369 | enum XInputUsageError { 370 | /// The controller ID you gave was 4 or more. 371 | InvalidControllerID, 372 | /// Not really an error, this controller is just missing. 373 | DeviceNotConnected, 374 | /// There was some sort of unexpected error happened, this is the error 375 | /// code windows returned. 376 | UnknownError(u32), 377 | } 378 | 379 | /// Error that can be returned by functions that are not guaranteed to be 380 | /// present in earlier XInput versions. 381 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] 382 | enum XInputOptionalFnUsageError { 383 | /// The controller ID you gave was 4 or more. 384 | InvalidControllerID, 385 | /// Not really an error, this controller is just missing. 386 | DeviceNotConnected, 387 | /// Function is not present in loaded DLL 388 | FunctionNotLoaded, 389 | /// There was some sort of unexpected error happened, this is the error 390 | /// code windows returned. 391 | UnknownError(u32), 392 | } 393 | 394 | /// This wraps an `XINPUT_STATE` value and provides a more rusty (read-only) 395 | /// interface to the data it contains. 396 | /// 397 | /// All three major game companies use different names for most of the buttons, 398 | /// so the docs for each button method list out what each of the major companies 399 | /// call that button. To the driver it's all the same, it's just however you 400 | /// want to think of them. 401 | /// 402 | /// If sequential calls to `xinput_get_state` for a given controller slot have 403 | /// the same packet number then the controller state has not changed since the 404 | /// last call. The `PartialEq` and `Eq` implementations for this wrapper type 405 | /// reflect that. The exact value of the packet number is unimportant. 406 | /// 407 | /// If you want to do something that the rust wrapper doesn't support, just use 408 | /// the raw field to get at the inner value. 409 | struct XInputState { 410 | /// The raw value we're wrapping. 411 | pub(crate) raw: xinput::XINPUT_STATE, 412 | } 413 | 414 | impl ::std::cmp::PartialEq for XInputState { 415 | /// Equality for `XInputState` values is based _only_ on the 416 | /// `dwPacketNumber` of the wrapped `XINPUT_STATE` value. This is entirely 417 | /// correct for values obtained from the xinput system, but if you make your 418 | /// own `XInputState` values for some reason you can confuse it. 419 | fn eq(&self, other: &XInputState) -> bool { 420 | self.raw.dwPacketNumber == other.raw.dwPacketNumber 421 | } 422 | } 423 | 424 | impl ::std::cmp::Eq for XInputState {} 425 | 426 | impl ::std::fmt::Debug for XInputState { 427 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 428 | write!(f, "XInputState (_)") 429 | } 430 | } 431 | 432 | impl XInputState { 433 | /// This helper normalizes a raw stick value using the given deadzone. 434 | #[inline] 435 | fn normalize_raw_stick_value( 436 | (x, y): (i16, i16), 437 | deadzone: i16, 438 | ) -> (f64, f64) { 439 | // Clamp the deadzone value to make the normalization work properly 440 | let deadzone = deadzone.clamp(0, 32_766); 441 | // Convert x and y to range -1 to 1, invert Y axis 442 | let x = (x as f64 + 0.5) * (1.0 / 32_767.5); 443 | let y = (y as f64 + 0.5) * -(1.0 / 32_767.5); 444 | // Convert deadzone to range 0 to 1 445 | let deadzone = deadzone as f64 * (1.0 / 32_767.5); 446 | // Calculate distance from (0, 0) 447 | let distance = (x * x + y * y).sqrt(); 448 | // Return 0 unless distance is far enough away from deadzone 449 | if distance > deadzone { 450 | (x, y) 451 | } else { 452 | (0.0, 0.0) 453 | } 454 | } 455 | } 456 | 457 | impl XInputHandle { 458 | /// Polls the controller port given for the current controller state. 459 | /// 460 | /// # Notes 461 | /// 462 | /// It is a persistent problem (since ~2007?) with xinput that polling for 463 | /// the data of a controller that isn't connected will cause a long 464 | /// delay. In the area of 500_000 cpu cycles. That's like 2_000 cache 465 | /// misses in a row. 466 | /// 467 | /// Once a controller is detected as not being plugged in you are strongly 468 | /// advised to not poll for its data again next frame. Instead, you should 469 | /// probably only poll for one known-missing controller per frame at most. 470 | /// 471 | /// Alternately, you can register for your app to get plug and play events 472 | /// and then wait for one of them to come in before you ever poll for a 473 | /// missing controller a second time. That's up to you. 474 | /// 475 | /// # Errors 476 | /// 477 | /// A few things can cause an `Err` value to come back, as explained by the 478 | /// `XInputUsageError` type. 479 | /// 480 | /// Most commonly, a controller will simply not be connected. Most people 481 | /// don't have all four slots plugged in all the time. 482 | pub(crate) fn get_state( 483 | &self, 484 | user_index: u32, 485 | ) -> Result { 486 | if user_index >= 4 { 487 | Err(XInputUsageError::InvalidControllerID) 488 | } else { 489 | let mut output: xinput::XINPUT_STATE = 490 | unsafe { ::std::mem::zeroed() }; 491 | let return_status = 492 | unsafe { (self.xinput_get_state)(user_index, &mut output) }; 493 | match return_status { 494 | ERROR_SUCCESS => Ok(XInputState { raw: output }), 495 | ERROR_DEVICE_NOT_CONNECTED => { 496 | Err(XInputUsageError::DeviceNotConnected) 497 | } 498 | s => { 499 | trace!("Unexpected error code: {}", s); 500 | Err(XInputUsageError::UnknownError(s)) 501 | } 502 | } 503 | } 504 | } 505 | 506 | /// Allows you to set the rumble speeds of the left and right motors. 507 | /// 508 | /// Valid motor speeds are across the whole `u16` range, and the number is 509 | /// the scale of the motor intensity. In other words, 0 is 0%, and 510 | /// 65,535 is 100%. 511 | /// 512 | /// On a 360 controller the left motor is low-frequency and the right motor 513 | /// is high-frequency. On other controllers running through xinput this 514 | /// might be the case, or the controller might not even have rumble 515 | /// ability at all. If rumble is missing from the device you'll still 516 | /// get `Ok` return values, so treat rumble as an extra, not an 517 | /// essential. 518 | /// 519 | /// # Errors 520 | /// 521 | /// A few things can cause an `Err` value to come back, as explained by the 522 | /// `XInputUsageError` type. 523 | /// 524 | /// Most commonly, a controller will simply not be connected. Most people 525 | /// don't have all four slots plugged in all the time. 526 | pub(crate) fn set_state( 527 | &self, 528 | user_index: u32, 529 | left_motor_speed: u16, 530 | right_motor_speed: u16, 531 | ) -> Result<(), XInputUsageError> { 532 | if user_index >= 4 { 533 | Err(XInputUsageError::InvalidControllerID) 534 | } else { 535 | let mut input = xinput::XINPUT_VIBRATION { 536 | wLeftMotorSpeed: left_motor_speed, 537 | wRightMotorSpeed: right_motor_speed, 538 | }; 539 | let return_status = 540 | unsafe { (self.xinput_set_state)(user_index, &mut input) }; 541 | match return_status { 542 | ERROR_SUCCESS => Ok(()), 543 | ERROR_DEVICE_NOT_CONNECTED => { 544 | Err(XInputUsageError::DeviceNotConnected) 545 | } 546 | s => { 547 | trace!("Unexpected error code: {}", s); 548 | Err(XInputUsageError::UnknownError(s)) 549 | } 550 | } 551 | } 552 | } 553 | 554 | /// Retrieve a gamepad input event. 555 | /// 556 | /// See the [MSDN documentation for XInputGetKeystroke](https://docs.microsoft.com/en-us/windows/desktop/api/xinput/nf-xinput-xinputgetkeystroke). 557 | pub(crate) fn get_keystroke( 558 | &self, 559 | user_index: u32, 560 | ) -> Result, XInputOptionalFnUsageError> 561 | { 562 | if user_index >= 4 { 563 | Err(XInputOptionalFnUsageError::InvalidControllerID) 564 | } else if let Some(func) = self.opt_xinput_get_keystroke { 565 | unsafe { 566 | let mut keystroke = 567 | std::mem::MaybeUninit::::uninit(); 568 | let return_status = 569 | (func)(user_index, 0, keystroke.as_mut_ptr()); 570 | match return_status { 571 | ERROR_SUCCESS => Ok(Some(keystroke.assume_init())), 572 | ERROR_EMPTY => Ok(None), 573 | ERROR_DEVICE_NOT_CONNECTED => { 574 | Err(XInputOptionalFnUsageError::DeviceNotConnected) 575 | } 576 | s => { 577 | trace!("Unexpected error code: {}", s); 578 | Err(XInputOptionalFnUsageError::UnknownError(s)) 579 | } 580 | } 581 | } 582 | } else { 583 | Err(XInputOptionalFnUsageError::FunctionNotLoaded) 584 | } 585 | } 586 | } 587 | 588 | type LpTimeCallback = extern "C" fn( 589 | timer_id: u32, 590 | msg: u32, 591 | dw_user: usize, 592 | dw1: usize, 593 | dw2: usize, 594 | ); 595 | 596 | const TIME_ONESHOT: u32 = 0; 597 | 598 | // Link to the windows multimedia library. 599 | #[link(name = "winmm")] 600 | extern "system" { 601 | // set a callback to be triggered after a given ammount of time has passed 602 | fn timeSetEvent( 603 | delay: u32, 604 | resolution: u32, 605 | lpTimeProc: LpTimeCallback, 606 | dw_user: usize, 607 | fu_event: u32, 608 | ) -> u32; 609 | } 610 | 611 | extern "C" fn waker_callback( 612 | _timer_id: u32, 613 | _msg: u32, 614 | dw_user: usize, 615 | _dw1: usize, 616 | _dw2: usize, 617 | ) { 618 | unsafe { 619 | let waker = std::mem::transmute::(dw_user); 620 | 621 | waker.wake_by_ref(); 622 | } 623 | } 624 | 625 | fn register_wake_timeout(delay: u32, waker: &Waker) { 626 | unsafe { 627 | let waker = std::mem::transmute::<&Waker, usize>(waker); 628 | 629 | timeSetEvent(delay, 0, waker_callback, waker, TIME_ONESHOT); 630 | } 631 | } 632 | 633 | //////////////////////////////////////////////////////////////////////////////// 634 | 635 | pub(crate) struct Controller { 636 | xinput: Arc, 637 | device_id: u8, 638 | pending_events: Vec, 639 | last_packet: DWORD, 640 | } 641 | 642 | impl Controller { 643 | #[allow(unused)] 644 | fn new(device_id: u8, xinput: Arc) -> Self { 645 | Self { 646 | xinput, 647 | device_id, 648 | pending_events: Vec::new(), 649 | last_packet: 0, 650 | } 651 | } 652 | } 653 | 654 | impl super::Controller for Controller { 655 | fn id(&self) -> u64 { 656 | 0 // FIXME 657 | } 658 | 659 | /// Poll for events. 660 | fn poll(&mut self, cx: &mut Context<'_>) -> Poll { 661 | if let Some(e) = self.pending_events.pop() { 662 | return Poll::Ready(e); 663 | } 664 | 665 | if let Ok(state) = self.xinput.get_state(self.device_id as u32) { 666 | if state.raw.dwPacketNumber != self.last_packet { 667 | // we have a new packet from the controller 668 | self.last_packet = state.raw.dwPacketNumber; 669 | 670 | let (nx, ny) = XInputState::normalize_raw_stick_value( 671 | (state.raw.Gamepad.sThumbRX, state.raw.Gamepad.sThumbRY), 672 | xinput::XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE, 673 | ); 674 | 675 | self.pending_events.push(Event::CamX(nx)); 676 | self.pending_events.push(Event::CamY(ny)); 677 | 678 | let (nx, ny) = XInputState::normalize_raw_stick_value( 679 | (state.raw.Gamepad.sThumbLX, state.raw.Gamepad.sThumbLY), 680 | xinput::XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, 681 | ); 682 | 683 | self.pending_events.push(Event::JoyX(nx)); 684 | self.pending_events.push(Event::JoyY(ny)); 685 | 686 | let t = if state.raw.Gamepad.bLeftTrigger 687 | > xinput::XINPUT_GAMEPAD_TRIGGER_THRESHOLD 688 | { 689 | state.raw.Gamepad.bLeftTrigger 690 | } else { 691 | 0 692 | }; 693 | 694 | self.pending_events.push(Event::TriggerL(t as f64 / 255.0)); 695 | 696 | let t = if state.raw.Gamepad.bRightTrigger 697 | > xinput::XINPUT_GAMEPAD_TRIGGER_THRESHOLD 698 | { 699 | state.raw.Gamepad.bRightTrigger 700 | } else { 701 | 0 702 | }; 703 | 704 | self.pending_events.push(Event::TriggerR(t as f64 / 255.0)); 705 | 706 | while let Ok(Some(keystroke)) = 707 | self.xinput.get_keystroke(self.device_id as u32) 708 | { 709 | // Ignore key repeat events 710 | if keystroke.Flags & xinput::XINPUT_KEYSTROKE_REPEAT != 0 { 711 | continue; 712 | } 713 | 714 | let held = 715 | keystroke.Flags & xinput::XINPUT_KEYSTROKE_KEYDOWN != 0; 716 | 717 | match keystroke.VirtualKey { 718 | xinput::VK_PAD_START => { 719 | self.pending_events.push(Event::MenuR(held)) 720 | } 721 | xinput::VK_PAD_BACK => { 722 | self.pending_events.push(Event::MenuL(held)) 723 | } 724 | xinput::VK_PAD_A => { 725 | self.pending_events.push(Event::ActionA(held)) 726 | } 727 | xinput::VK_PAD_B => { 728 | self.pending_events.push(Event::ActionB(held)) 729 | } 730 | xinput::VK_PAD_X => { 731 | self.pending_events.push(Event::ActionH(held)) 732 | } 733 | xinput::VK_PAD_Y => { 734 | self.pending_events.push(Event::ActionV(held)) 735 | } 736 | xinput::VK_PAD_LSHOULDER => { 737 | self.pending_events.push(Event::BumperL(held)) 738 | } 739 | xinput::VK_PAD_RSHOULDER => { 740 | self.pending_events.push(Event::BumperR(held)) 741 | } 742 | xinput::VK_PAD_LTHUMB_PRESS => { 743 | self.pending_events.push(Event::Joy(held)) 744 | } 745 | xinput::VK_PAD_RTHUMB_PRESS => { 746 | self.pending_events.push(Event::Cam(held)) 747 | } 748 | xinput::VK_PAD_DPAD_UP => { 749 | self.pending_events.push(Event::Up(held)) 750 | } 751 | xinput::VK_PAD_DPAD_DOWN => { 752 | self.pending_events.push(Event::Down(held)) 753 | } 754 | xinput::VK_PAD_DPAD_LEFT => { 755 | self.pending_events.push(Event::Left(held)) 756 | } 757 | xinput::VK_PAD_DPAD_RIGHT => { 758 | self.pending_events.push(Event::Right(held)) 759 | } 760 | _ => (), 761 | } 762 | } 763 | 764 | if let Some(event) = self.pending_events.pop() { 765 | return Poll::Ready(event); 766 | } 767 | } 768 | } else { 769 | // the device has gone 770 | return Poll::Ready(Event::Disconnect); 771 | } 772 | 773 | register_wake_timeout(10, cx.waker()); 774 | Poll::Pending 775 | } 776 | 777 | /// Stereo rumble effect (left is low frequency, right is high frequency). 778 | fn rumble(&mut self, left: f32, right: f32) { 779 | self.xinput 780 | .set_state( 781 | self.device_id as u32, 782 | (u16::MAX as f32 * left) as u16, 783 | (u16::MAX as f32 * right) as u16, 784 | ) 785 | .unwrap() 786 | } 787 | 788 | /// Get the name of this controller. 789 | fn name(&self) -> &str { 790 | "XInput Controller" 791 | } 792 | } 793 | 794 | pub(crate) struct Listener { 795 | xinput: Arc, 796 | connected: u64, 797 | to_check: u8, 798 | remap: Remap, 799 | } 800 | 801 | impl Listener { 802 | fn new(remap: Remap, xinput: Arc) -> Self { 803 | Self { 804 | xinput, 805 | connected: 0, 806 | to_check: 0, 807 | remap, 808 | } 809 | } 810 | } 811 | 812 | impl super::Listener for Listener { 813 | fn poll(&mut self, cx: &mut Context<'_>) -> Poll { 814 | let id = self.to_check; 815 | let mask = 1 << id; 816 | self.to_check += 1; 817 | // direct input only allows for 4 controllers 818 | if self.to_check > 3 { 819 | self.to_check = 0; 820 | } 821 | let was_connected = (self.connected & mask) != 0; 822 | 823 | if self.xinput.get_state(id as u32).is_ok() { 824 | if !was_connected { 825 | // we have a new device! 826 | self.connected |= mask; 827 | 828 | return Poll::Ready(crate::Controller::new( 829 | Box::new(Controller::new(id, self.xinput.clone())), 830 | &self.remap, 831 | )); 832 | } 833 | } else if was_connected { 834 | // a device has been unplugged 835 | self.connected &= !mask; 836 | } 837 | 838 | register_wake_timeout(100, cx.waker()); 839 | 840 | Poll::Pending 841 | } 842 | } 843 | 844 | struct Global { 845 | xinput: Arc, 846 | } 847 | 848 | impl super::Global for Global { 849 | /// Enable all events (when window comes in focus). 850 | fn enable(&self) { 851 | unsafe { (self.xinput.xinput_enable)(true as _) }; 852 | } 853 | 854 | /// Disable all events (when window leaves focus). 855 | fn disable(&self) { 856 | unsafe { (self.xinput.xinput_enable)(false as _) }; 857 | } 858 | 859 | /// Create a new listener. 860 | fn listener(&self, remap: Remap) -> Box { 861 | Box::new(Listener::new(remap, self.xinput.clone())) 862 | } 863 | } 864 | 865 | pub(super) fn global() -> Box { 866 | // Windows implementation may fail. 867 | if let Ok(xinput) = XInputHandle::load_default() { 868 | Box::new(Global { xinput }) 869 | } else { 870 | Box::new(super::FakeGlobal) 871 | } 872 | } 873 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = "1.0" 8 | serde_derive = "1.0" 9 | toml = "0.5" 10 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | mod sdb; 4 | 5 | fn print_help() { 6 | eprintln!("Tasks:"); 7 | eprintln!(); 8 | eprintln!("--help Print this help text"); 9 | eprintln!("sdb Generate stick & gcdb bytecode databases"); 10 | } 11 | 12 | fn print_unknown(x: &str) { 13 | eprintln!("cargo xtask {} is an invalid command.", x); 14 | eprintln!(); 15 | eprintln!("Run `cargo xtask` for help page."); 16 | } 17 | 18 | fn sdb() { 19 | sdb::main() 20 | } 21 | 22 | fn main() { 23 | let task = env::args().nth(1); 24 | match task.as_deref() { 25 | Some("sdb") => sdb(), 26 | None | Some("--help") => print_help(), 27 | Some(x) => print_unknown(x), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /xtask/src/sdb.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt::Write}; 2 | 3 | use serde_derive::Deserialize; 4 | 5 | const LINUX: &str = "./sdb/linux/"; 6 | const _MACOS: &str = "./sdb/macos/"; 7 | const _WINDOWS: &str = "./sdb/windows/"; 8 | const _WEB: &str = "./sdb/web/"; 9 | const _USB: &str = "./sdb/usb/"; 10 | 11 | const SDL: &str = "./gcdb/gamecontrollerdb.txt"; 12 | 13 | #[derive(Deserialize)] 14 | struct Map { 15 | name: String, 16 | r#type: String, 17 | remap: HashMap, 18 | } 19 | 20 | fn name_to_hex(name: &str) -> &str { 21 | match name { 22 | "None" => "00", 23 | "Exit" => "01", 24 | "ActionA" => "02", 25 | "ActionB" => "03", 26 | "ActionC" => "04", 27 | "ActionH" => "05", 28 | "ActionV" => "06", 29 | "ActionD" => "07", 30 | "MenuL" => "08", 31 | "MenuR" => "09", 32 | "Joy" => "0A", 33 | "Cam" => "0B", 34 | "BumperL" => "0C", 35 | "BumperR" => "0D", 36 | "TriggerL" => "0E", 37 | "TriggerR" => "0F", 38 | "Up" => "10", 39 | "Down" => "11", 40 | "Left" => "12", 41 | "Right" => "13", 42 | "HatUp" => "14", 43 | "HatDown" => "15", 44 | "HatLeft" => "16", 45 | "HatRight" => "17", 46 | "MicUp" => "18", 47 | "MicDown" => "19", 48 | "MicLeft" => "1A", 49 | "MicRight" => "1B", 50 | "PovUp" => "1C", 51 | "PovDown" => "1D", 52 | "PovLeft" => "1E", 53 | "PovRight" => "1F", 54 | "JoyX" => "20", 55 | "JoyY" => "21", 56 | "JoyZ" => "22", 57 | "CamX" => "23", 58 | "CamY" => "24", 59 | "CamZ" => "25", 60 | "Slew" => "26", 61 | "Throttle" => "27", 62 | "ThrottleL" => "28", 63 | "ThrottleR" => "29", 64 | "Volume" => "2A", 65 | "Wheel" => "2B", 66 | "Rudder" => "2C", 67 | "Gas" => "2D", 68 | "Brake" => "2E", 69 | "MicPush" => "2F", 70 | "Trigger" => "30", 71 | "Bumper" => "31", 72 | "ActionL" => "32", 73 | "ActionM" => "33", 74 | "ActionR" => "34", 75 | "Pinky" => "35", 76 | "PinkyForward" => "36", 77 | "PinkyBackward" => "37", 78 | "FlapsUp" => "38", 79 | "FlapsDown" => "39", 80 | "BoatForward" => "3A", 81 | "BoatBackward" => "3B", 82 | "AutopilotPath" => "3C", 83 | "AutopilotAlt" => "3D", 84 | "EngineMotorL" => "3E", 85 | "EngineMotorR" => "3F", 86 | "EngineFuelFlowL" => "40", 87 | "EngineFuelFlowR" => "41", 88 | "EngineIgnitionL" => "42", 89 | "EngineIgnitionR" => "43", 90 | "SpeedbrakeBackward" => "44", 91 | "SpeedbrakeForward" => "45", 92 | "ChinaBackward" => "46", 93 | "ChinaForward" => "47", 94 | "Apu" => "48", 95 | "RadarAltimeter" => "49", 96 | "LandingGearSilence" => "4A", 97 | "Eac" => "4B", 98 | "AutopilotToggle" => "4C", 99 | "ThrottleButton" => "4D", 100 | "MouseX" => "4E", 101 | "MouseY" => "4F", 102 | "Mouse" => "50", 103 | "PaddleLeft" => "51", 104 | "PaddleRight" => "52", 105 | "PinkyLeft" => "53", 106 | "PinkyRight" => "54", 107 | "Context" => "55", 108 | "Dpi" => "56", 109 | "ScrollX" => "57", 110 | "ScrollY" => "58", 111 | "Scroll" => "59", 112 | "TrimUp" => "5A", 113 | "TrimDown" => "5B", 114 | "TrimLeft" => "5C", 115 | "TrimRight" => "5D", 116 | "ActionWheelX" => "5E", 117 | "ActionWheelY" => "5F", 118 | _unknown => panic!("Unknown: {}", _unknown), 119 | } 120 | } 121 | 122 | pub(super) fn main() { 123 | let mut out = String::new(); 124 | 125 | println!("Loading Linux TOML Controller Mappings…"); 126 | for file in std::fs::read_dir(LINUX) 127 | .expect("Missing database") 128 | .flatten() 129 | { 130 | let path = file.path(); 131 | let file = std::fs::read_to_string(&path).expect("Open file failed"); 132 | let file: Map = toml::from_str(&file).unwrap(); 133 | 134 | // ID of Controller 135 | out.push_str( 136 | &path.as_path().file_name().unwrap().to_str().unwrap()[..16], 137 | ); 138 | 139 | // Name of Controller. 140 | out.push_str(&file.name); 141 | out.push('\t'); 142 | 143 | // Type of controller 144 | let ctlr_type = match file.r#type.as_str() { 145 | "xbox" => 'x', 146 | "playstation" => 'p', 147 | "nintendo" => 'n', 148 | "gamepad" => 'g', 149 | "flight" => 'f', 150 | _type => panic!("Unknown type: {}", _type), 151 | }; 152 | out.push(ctlr_type); 153 | 154 | // Add remappings 155 | let mut kv = Vec::new(); 156 | for (key, value) in file.remap { 157 | kv.push((key, value)); 158 | } 159 | kv.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); 160 | for (key, value) in kv { 161 | if let Ok(number) = key.parse::() { 162 | write!(&mut out, "{:02X}", number | 0x80).unwrap(); 163 | } else { 164 | out.push_str(name_to_hex(key.as_str())); 165 | } 166 | match value { 167 | toml::value::Value::String(event) => { 168 | out.push_str(name_to_hex(event.as_str())); 169 | out.push(';'); 170 | } 171 | toml::value::Value::Table(table) => { 172 | if let Some(event) = table.get("event") { 173 | out.push_str(name_to_hex(event.as_str().unwrap())); 174 | } else { 175 | out.push_str(name_to_hex("None")); 176 | } 177 | if let Some(max) = table.get("max") { 178 | let max = max.as_integer().unwrap(); 179 | out.push('a'); 180 | write!(&mut out, "{}", max).unwrap(); 181 | } 182 | if let Some(min) = table.get("min") { 183 | let min = min.as_integer().unwrap(); 184 | out.push('i'); 185 | write!(&mut out, "{}", min).unwrap(); 186 | } 187 | if let Some(scale) = table.get("scale") { 188 | let scale = scale.as_float().unwrap(); 189 | out.push('s'); 190 | write!(&mut out, "{}", scale).unwrap(); 191 | } 192 | if let Some(deadzone) = table.get("deadzone") { 193 | let deadzone = deadzone.as_float().unwrap(); 194 | out.push('d'); 195 | write!(&mut out, "{}", deadzone).unwrap(); 196 | } 197 | out.push(';'); 198 | } 199 | _map => panic!("invalid mapping: {:?}", _map), 200 | } 201 | } 202 | out.pop(); 203 | 204 | // Newline to separate controllers. 205 | out.push('\n'); 206 | } 207 | out.pop(); 208 | 209 | std::fs::write("./stick/remap_linux.sdb", &out).unwrap(); 210 | 211 | println!("Loading Linux (SDL) TOML Controller Mappings…"); 212 | 213 | out.clear(); 214 | 215 | for line in std::fs::read_to_string(SDL) 216 | .expect("Missing database") 217 | .lines() 218 | { 219 | if line.is_empty() || line.starts_with('#') { 220 | continue; 221 | } 222 | 223 | // ID of Controller 224 | let guid = line.get(0..32).unwrap(); 225 | // Skip over emulated joysticks. 226 | if guid.get(2..8) != Some("000000") 227 | || guid.get(12..16) != Some("0000") 228 | || guid.get(20..24) != Some("0000") 229 | || guid.get(28..32) != Some("0000") 230 | || !line.contains("platform:Linux") 231 | { 232 | continue; 233 | } 234 | 235 | out.push_str(&guid.get(0..4).unwrap().to_uppercase()); 236 | out.push_str(&guid.get(8..12).unwrap().to_uppercase()); 237 | out.push_str(&guid.get(16..24).unwrap().to_uppercase()); 238 | out.push_str(&guid.get(24..28).unwrap().to_uppercase()); 239 | 240 | // Name of Controller. 241 | let mut iter = line[33..].split(','); 242 | let name = iter.next().expect("No name"); 243 | out.push_str(name); 244 | out.push('\t'); 245 | 246 | // Type of controller 247 | out.push('w'); 248 | 249 | // Add remappings 250 | for mapping in iter { 251 | if mapping.is_empty() { 252 | continue; 253 | } 254 | 255 | let mut mapping = mapping.split(':'); 256 | let js_out = mapping.next().unwrap(); 257 | let js_in = mapping.next().unwrap(); 258 | 259 | let js_in = match js_in { 260 | "b0" => name_to_hex("Trigger"), 261 | "b1" => name_to_hex("ActionM"), 262 | "b2" => name_to_hex("Bumper"), 263 | "b3" => name_to_hex("ActionR"), 264 | "b4" => name_to_hex("ActionL"), 265 | "b5" => name_to_hex("Pinky"), 266 | "b6" => "80", 267 | "b7" => "81", 268 | "b8" => "82", 269 | "b9" => "83", 270 | "b10" => "84", 271 | "b11" => "85", 272 | "b12" => "86", 273 | "b13" => "87", 274 | "b14" => "88", 275 | "b15" => "89", 276 | "b16" => name_to_hex("ActionA"), 277 | "b17" => name_to_hex("ActionB"), 278 | "b18" => name_to_hex("ActionC"), 279 | "b19" => name_to_hex("ActionV"), 280 | "b20" => name_to_hex("ActionH"), 281 | "b21" => name_to_hex("ActionD"), 282 | "b22" => name_to_hex("BumperL"), 283 | "b23" => continue, // FIXME 284 | "b24" => continue, // FIXME 285 | "b25" => continue, // FIXME 286 | "b26" => continue, // FIXME 287 | "b30" => continue, // Not a gamepad? 288 | "b32" => continue, // Not a gamepad? 289 | "b33" => continue, // Not a gamepad? 290 | "b36" => continue, // Not a gamepad? 291 | "b52" => continue, // Not a gamepad? 292 | "b53" => continue, // Not a gamepad? 293 | "h0.1" => name_to_hex("PovUp"), 294 | "h0.2" => name_to_hex("PovRight"), 295 | "h0.4" => name_to_hex("PovDown"), 296 | "h0.8" => name_to_hex("PovLeft"), 297 | "a0" | "a0~" => name_to_hex("JoyX"), 298 | "a1" | "a1~" => name_to_hex("JoyY"), 299 | "a2" | "a2~" => name_to_hex("JoyZ"), 300 | "a3" | "a3~" => name_to_hex("CamX"), 301 | "a4" | "a4~" => name_to_hex("CamY"), 302 | "a5" | "a5~" => name_to_hex("CamZ"), 303 | "a6" | "a6~" => name_to_hex("Throttle"), 304 | "a7" | "a7~" => name_to_hex("Rudder"), 305 | "a8" | "a8~" => name_to_hex("Wheel"), 306 | "a9" | "a9~" => name_to_hex("Gas"), 307 | "a10" | "a10~" => name_to_hex("Brake"), 308 | "a11" | "a11~" => name_to_hex("Slew"), 309 | "a12" => name_to_hex("ThrottleL"), 310 | "a13" => name_to_hex("ThrottleR"), 311 | "a14" => name_to_hex("ScrollX"), 312 | "+a0" | "+a1" | "+a2" | "+a3" | "+a4" | "+a5" | "-a0" 313 | | "-a1" | "-a2" | "-a3" | "-a4" | "-a5" => continue, 314 | "Linux" => continue, 315 | // ? 316 | "b122" => name_to_hex("Down"), 317 | "b119" => name_to_hex("Left"), 318 | "b120" => name_to_hex("Right"), 319 | "b117" => name_to_hex("Up"), 320 | "b161" => "8B", 321 | "b136" => continue, 322 | _in => panic!("Unknown input {}", _in), 323 | }; 324 | 325 | let js_out = match js_out { 326 | "a" => "02", 327 | "b" => "03", 328 | "x" => "05", 329 | "y" => "06", 330 | "back" => "08", 331 | "start" => "09", 332 | "guide" => "01", 333 | "leftshoulder" => "0C", 334 | "lefttrigger" => "0E", 335 | "leftx" => "20", 336 | "lefty" => "21", 337 | "rightx" => "23", 338 | "righty" => "24", 339 | "rightshoulder" => "0D", 340 | "righttrigger" => "0F", 341 | "leftstick" => "0A", 342 | "rightstick" => "0B", 343 | "dpleft" => "12", 344 | "dpright" => "13", 345 | "dpup" => "10", 346 | "dpdown" => "11", 347 | "misc1" => "81", 348 | "+leftx" | "-leftx" | "+lefty" | "-lefty" => continue, 349 | "+rightx" | "-rightx" | "+righty" | "-righty" => continue, 350 | "touchpad" => continue, 351 | "paddle1" => "51", 352 | "paddle2" => "52", 353 | "paddle3" => "53", 354 | "paddle4" => "54", 355 | _out => panic!("Unknown output {}", _out), 356 | }; 357 | 358 | out.push_str(js_in); 359 | out.push_str(js_out); 360 | // FIXME: Tweaks 361 | out.push(';'); 362 | } 363 | out.pop(); 364 | out.push('\n'); 365 | } 366 | out.pop(); 367 | 368 | std::fs::write("./stick/sdlgc_linux.sdb", out).unwrap(); 369 | } 370 | --------------------------------------------------------------------------------