├── loader
├── .gitignore
├── src
│ └── main.rs
└── Cargo.toml
├── mouseplay
├── .gitignore
├── src
│ ├── controller
│ │ ├── mod.rs
│ │ └── ds4.rs
│ ├── input
│ │ ├── mod.rs
│ │ └── raw_input.rs
│ ├── lib.rs
│ ├── console.rs
│ ├── mapper.rs
│ └── hooks.rs
└── Cargo.toml
├── .cargo
└── config
├── .gitignore
├── mouseplay.logo.png
├── Cargo.toml
├── mappings
└── overwatch.json
├── README.md
└── LICENSE
/loader/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/mouseplay/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 |
--------------------------------------------------------------------------------
/mouseplay/src/controller/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod ds4;
2 |
--------------------------------------------------------------------------------
/mouseplay/src/input/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod raw_input;
2 |
--------------------------------------------------------------------------------
/.cargo/config:
--------------------------------------------------------------------------------
1 | [build]
2 | target = "stable-i686-pc-windows-msvc"
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | *.swp
4 | .vscode
5 | Cargo.lock
--------------------------------------------------------------------------------
/loader/src/main.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | println!("Hello, world!");
3 | }
4 |
--------------------------------------------------------------------------------
/mouseplay.logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ko1N/mouseplay/HEAD/mouseplay.logo.png
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "loader",
4 | "mouseplay",
5 | ]
6 | default-members = [
7 | "loader",
8 | "mouseplay",
9 | ]
10 |
--------------------------------------------------------------------------------
/loader/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "loader"
3 | version = "0.1.0"
4 | authors = ["John Smith"]
5 | edition = "2018"
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 |
--------------------------------------------------------------------------------
/mouseplay/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mouseplay"
3 | version = "0.1.0"
4 | authors = ["John Smith"]
5 | edition = "2018"
6 |
7 | [lib]
8 | crate-type = ["cdylib"]
9 |
10 | [dependencies]
11 | winapi = { version = "0.3.9", features = ["windef", "winuser", "winbase", "winnt", "windowsx", "consoleapi", "wincon", "processenv", "libloaderapi", "memoryapi", "libloaderapi", "impl-default"] }
12 | log = "0.4.8"
13 | simple_logger = "1.0"
14 | libc = "0.2"
15 | paste = "0.1"
16 | lazy_static = "1.4"
17 | serde = { version = "1.0", features = ["derive"] }
18 | serde_json = "1.0"
19 |
--------------------------------------------------------------------------------
/mouseplay/src/lib.rs:
--------------------------------------------------------------------------------
1 | mod console;
2 | mod controller;
3 | mod hooks;
4 | mod input;
5 | mod mapper;
6 |
7 | use winapi::um::winnt::{DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH};
8 |
9 | #[no_mangle]
10 | extern "system" fn DllMain(_hinst: *const u8, reason: u32, _reserved: *const u8) -> u32 {
11 | match reason {
12 | DLL_PROCESS_ATTACH => {
13 | std::thread::spawn(|| {
14 | console::init();
15 | hooks::setup();
16 | mapper::load("mappings.json").unwrap();
17 | });
18 | }
19 | DLL_PROCESS_DETACH => {
20 | // ...
21 | }
22 | _ => {}
23 | }
24 | 1
25 | }
26 |
--------------------------------------------------------------------------------
/mappings/overwatch.json:
--------------------------------------------------------------------------------
1 | [
2 | { "type": "Button", "input": "f1", "output": "share" },
3 | { "type": "Button", "input": "f2", "output": "options" },
4 | { "type": "Button", "input": "f3", "output": "ps" },
5 |
6 | { "type": "Axis", "input": "w", "output": "ly", "value": -1 },
7 | { "type": "Axis", "input": "a", "output": "lx", "value": -1 },
8 | { "type": "Axis", "input": "s", "output": "ly", "value": 1 },
9 | { "type": "Axis", "input": "d", "output": "lx", "value": 1 },
10 |
11 | { "type": "Button", "input": "mouse1", "output": "r2" },
12 | { "type": "Axis", "input": "mouse1", "output": "r2", "value": 1 },
13 | { "type": "Button", "input": "mouse2", "output": "l2" },
14 | { "type": "Axis", "input": "mouse2", "output": "l2", "value": 1 },
15 |
16 | { "type": "Button", "input": "space", "output": "cross" },
17 | { "type": "Button", "input": "ctrl", "output": "circle" },
18 |
19 | { "type": "Button", "input": "q", "output": "triangle" },
20 | { "type": "Button", "input": "r", "output": "square" },
21 | { "type": "Button", "input": "shift", "output": "l1" },
22 | { "type": "Button", "input": "e", "output": "r1" },
23 |
24 | { "type": "Button", "input": "f", "output": "l3" },
25 | { "type": "Button", "input": "v", "output": "r3" },
26 |
27 | { "type": "Button", "input": "tab", "output": "touch" },
28 | { "type": "Button", "input": "escape", "output": "options" },
29 |
30 | {
31 | "type": "Mouse",
32 |
33 | "output_x": "rx",
34 | "multiplier_x": 0.3,
35 | "dead_zone_x": 13,
36 |
37 | "output_y": "ry",
38 | "multiplier_y": 0.3,
39 | "dead_zone_y": 13,
40 |
41 | "sensitivity": 3,
42 | "exponent": 1,
43 |
44 | "shape": "circle"
45 | }
46 | ]
47 |
--------------------------------------------------------------------------------
/mouseplay/src/console.rs:
--------------------------------------------------------------------------------
1 | use std::ffi::CString;
2 | use std::fs::OpenOptions;
3 | use std::os::windows::io::AsRawHandle;
4 |
5 | use log::{info, Level};
6 |
7 | use winapi::um::consoleapi::AllocConsole;
8 | use winapi::um::processenv::SetStdHandle;
9 | use winapi::um::winbase::{STD_ERROR_HANDLE, STD_OUTPUT_HANDLE};
10 | use winapi::um::wincon::SetConsoleTitleA;
11 |
12 | pub fn init() {
13 | if unsafe { AllocConsole() } != 0 {
14 | // console title
15 | let console_title = CString::new("mouseplay").unwrap();
16 | unsafe { SetConsoleTitleA(console_title.as_ptr()) };
17 |
18 | // output redirection
19 | let file = OpenOptions::new()
20 | .write(true)
21 | .read(true)
22 | .open("CONOUT$")
23 | .unwrap();
24 | unsafe {
25 | SetStdHandle(
26 | STD_OUTPUT_HANDLE,
27 | file.as_raw_handle() as *mut winapi::ctypes::c_void,
28 | );
29 | SetStdHandle(
30 | STD_ERROR_HANDLE,
31 | file.as_raw_handle() as *mut winapi::ctypes::c_void,
32 | );
33 | }
34 | std::mem::forget(file);
35 |
36 | // setup logging
37 | simple_logger::SimpleLogger::new()
38 | .with_level(Level::Debug.to_level_filter())
39 | .init()
40 | .unwrap();
41 |
42 | // print header
43 | println!(
44 | " __
45 | ____ ___ ____ __ __________ ____ / /___ ___ __
46 | / __ `__ \\/ __ \\/ / / / ___/ _ \\/ __ \\/ / __ `/ / / /
47 | / / / / / / /_/ / /_/ (__ ) __/ /_/ / / /_/ / /_/ /
48 | /_/ /_/ /_/\\____/\\__,_/____/\\___/ .___/_/\\__,_/\\__, /
49 | /_/ /____/"
50 | );
51 | println!("");
52 | info!("console initialized");
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # mouseplay
6 | [](LICENSE)
7 | [](https://discord.gg/afsEtMR)
8 |
9 | mouseplay enables you to use a regular mouse & keyboard with the PS Remote Play Tool. The mouse and keyboard buttons are being translated to controller inputs.
10 |
11 | ## This project is still work-in-progress and does not yet have a release version.
12 |
13 | ### Mapping files
14 |
15 | Mapping files are simple plain JSon files. You can find example mappings in the `mappings` subfolder.
16 |
17 | ### Building it yourself
18 | Make sure to install rust (preferably via https://rustup.rs) and the Microsoft Compiler (via Visual Studio).
19 |
20 | It is important to compile the project for a 32bit target (as PS Remote Play in itself is also just a 32bit program). To do this with rustup just run:
21 | ```
22 | rustup target add i686-pc-windows-msvc
23 | ```
24 |
25 | To compile the project you can just run
26 | ```
27 | cargo build --target=i686-pc-windows-msvc --release
28 | ```
29 | or execute the provided `build.bat` file.
30 |
31 | ### Known limitations
32 |
33 | - Controller spoofing is not yet implemented so you have to connect a PlayStation controller to the PC.
34 | - Mouse cursor and toolbar is not hidden when mouse lock is active
35 | - Not all cases of window transitions (windows opening and closing) are handled well yet
36 |
37 | ### Demo
38 |
39 | [](http://www.youtube.com/watch?v=0QhSsvRO_Y8 "mouseplay demo")
40 |
41 | ## License
42 |
43 | Licensed under GPL-3.0 License, see [LICENSE](LICENSE).
44 |
45 | ### Contribution
46 |
47 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, shall be licensed as above, without any additional terms or conditions.
48 |
--------------------------------------------------------------------------------
/mouseplay/src/controller/ds4.rs:
--------------------------------------------------------------------------------
1 | macro_rules! input_axis {
2 | ($name:ident, $byte:expr) => {
3 | paste::item! {
4 | pub fn [](&self) -> u8 {
5 | self.buffer[$byte]
6 | }
7 |
8 | pub fn [](&mut self, value: u8) {
9 | self.buffer[$byte] = value;
10 | }
11 | }
12 | };
13 | }
14 |
15 | macro_rules! input_button {
16 | ($name:ident, $byte:expr, $bit:expr) => {
17 | paste::item! {
18 | pub fn [](&self) -> bool {
19 | (self.buffer[$byte] & $bit) != 0
20 | }
21 |
22 | pub fn [](&mut self, down: bool) {
23 | if down {
24 | self.buffer[$byte] |= $bit;
25 | } else {
26 | self.buffer[$byte] &= !$bit;
27 | }
28 | }
29 | }
30 | };
31 | }
32 |
33 | const TRIANGLE: u8 = 1 << 7;
34 | const CIRCLE: u8 = 1 << 6;
35 | const CROSS: u8 = 1 << 5;
36 | const SQUARE: u8 = 1 << 4;
37 |
38 | const L1: u8 = 1 << 0;
39 | const R1: u8 = 1 << 1;
40 |
41 | const L2: u8 = 1 << 2;
42 | const R2: u8 = 1 << 3;
43 |
44 | const L3: u8 = 1 << 6;
45 | const R3: u8 = 1 << 7;
46 |
47 | const SHARE: u8 = 1 << 4;
48 | const OPTIONS: u8 = 1 << 5;
49 | const PSBUTTON: u8 = 1 << 0;
50 | const TOUCHBUTTON: u8 = 1 << 2 - 1;
51 |
52 | pub struct DS4 {
53 | buffer: Vec,
54 | }
55 |
56 | #[allow(unused)]
57 | impl DS4 {
58 | pub fn new(buffer: &[u8]) -> Result {
59 | // report _should_ be 64 bytes long
60 | if buffer.len() == 64 {
61 | Ok(Self {
62 | buffer: buffer.to_vec(),
63 | })
64 | } else {
65 | Err("not a hid response")
66 | }
67 | }
68 |
69 | pub fn to_raw(&self) -> Vec {
70 | self.buffer.clone()
71 | }
72 |
73 | input_axis!(lx, 1);
74 | input_axis!(ly, 2);
75 | input_axis!(rx, 3);
76 | input_axis!(ry, 4);
77 |
78 | input_button!(triangle, 5, TRIANGLE);
79 | input_button!(circle, 5, CIRCLE);
80 | input_button!(cross, 5, CROSS);
81 | input_button!(square, 5, SQUARE);
82 |
83 | // dpad is in hat format, 0x8 is released state
84 |
85 | input_button!(l1, 6, L1);
86 | input_button!(r1, 6, R1);
87 |
88 | input_axis!(l2, 8);
89 | input_axis!(r2, 9);
90 | input_button!(l2, 6, L2);
91 | input_button!(r2, 6, R2);
92 |
93 | input_button!(l3, 6, L3);
94 | input_button!(r3, 6, R3);
95 |
96 | input_button!(share, 6, SHARE);
97 | input_button!(options, 6, OPTIONS);
98 | input_button!(ps, 7, PSBUTTON);
99 | input_button!(touch, 7, TOUCHBUTTON);
100 |
101 | pub fn set_btn(&mut self, button: &str, down: bool) {
102 | match button {
103 | "triangle" => self.set_btn_triangle(down),
104 | "circle" => self.set_btn_circle(down),
105 | "cross" => self.set_btn_cross(down),
106 | "square" => self.set_btn_square(down),
107 |
108 | "l1" => self.set_btn_l1(down),
109 | "r1" => self.set_btn_r1(down),
110 |
111 | "l2" => self.set_btn_l2(down),
112 | "r2" => self.set_btn_r2(down),
113 |
114 | "l3" => self.set_btn_l3(down),
115 | "r3" => self.set_btn_r3(down),
116 |
117 | "share" => self.set_btn_share(down),
118 | "options" => self.set_btn_options(down),
119 | "ps" => self.set_btn_ps(down),
120 | "touch" => self.set_btn_touch(down),
121 |
122 | _ => (),
123 | }
124 | }
125 |
126 | pub fn set_axis(&mut self, axis: &str, value: u8) {
127 | match axis {
128 | "lx" => self.set_axis_lx(value),
129 | "ly" => self.set_axis_ly(value),
130 | "rx" => self.set_axis_rx(value),
131 | "ry" => self.set_axis_ry(value),
132 |
133 | "l2" => self.set_axis_l2(value),
134 | "r2" => self.set_axis_r2(value),
135 |
136 | _ => (),
137 | }
138 | }
139 |
140 | pub fn frame_count(&self) -> u8 {
141 | self.buffer[7] >> 2
142 | }
143 |
144 | pub fn battery(&self) -> u8 {
145 | (self.buffer[30] & 0xF) * 10
146 | }
147 |
148 | pub fn is_charging(&self) -> bool {
149 | (self.buffer[30] & 0x10) != 0
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/mouseplay/src/mapper.rs:
--------------------------------------------------------------------------------
1 | use std::ffi::CStr;
2 | use std::path::{Path, PathBuf};
3 | use std::sync::RwLock;
4 |
5 | use log::info;
6 |
7 | use lazy_static::lazy_static;
8 | use serde::{Deserialize, Serialize};
9 | use winapi::{
10 | shared::minwindef::MAX_PATH,
11 | um::{libloaderapi::GetModuleFileNameA, winnt::IMAGE_DOS_HEADER},
12 | };
13 |
14 | use crate::{controller::ds4::DS4, input::raw_input::RawInput};
15 |
16 | extern "C" {
17 | pub static __ImageBase: u8;
18 | }
19 |
20 | lazy_static! {
21 | // thread safe storage for the global Mapper
22 | pub static ref MAPPER: RwLock