├── .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 |
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 | [](https://github.com/ardaku/stick/actions?query=workflow%3Atests)
6 | [](https://docs.rs/stick)
7 | [](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 |
--------------------------------------------------------------------------------