├── .yarnrc.yml ├── rustfmt.toml ├── examples ├── images │ ├── baboon.png │ └── parts_of_baboon.png ├── keyboard1.ts ├── keyboard2.ts ├── window2.ts ├── imageSearch1.ts ├── imageSearch2.ts ├── keyboard3.ts ├── mouse1.ts ├── memory1.ts └── window1.ts ├── src ├── win │ ├── mod.rs │ ├── screen.rs │ ├── mouse.rs │ ├── window.rs │ ├── keyboard.rs │ └── memory.rs ├── lib.rs ├── geometry.rs ├── utils.rs └── screen.rs ├── npm └── win32-x64-msvc │ ├── README.md │ └── package.json ├── .npmignore ├── tsconfig.json ├── Cargo.toml ├── package.json ├── README.md ├── .gitignore ├── index.d.ts └── index.js /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | edition = "2021" 3 | -------------------------------------------------------------------------------- /examples/images/baboon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deeean/sophia/HEAD/examples/images/baboon.png -------------------------------------------------------------------------------- /src/win/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod keyboard; 2 | pub mod mouse; 3 | pub mod screen; 4 | pub mod window; 5 | pub mod memory; 6 | -------------------------------------------------------------------------------- /examples/images/parts_of_baboon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deeean/sophia/HEAD/examples/images/parts_of_baboon.png -------------------------------------------------------------------------------- /npm/win32-x64-msvc/README.md: -------------------------------------------------------------------------------- 1 | # `sophia-win32-x64-msvc` 2 | 3 | This is the **x86_64-pc-windows-msvc** binary for `sophia` 4 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(clippy::all)] 2 | 3 | pub mod geometry; 4 | pub mod screen; 5 | pub mod utils; 6 | 7 | #[cfg(target_os = "windows")] 8 | pub mod win; 9 | 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .cargo 4 | .github 5 | npm 6 | .eslintrc 7 | .prettierignore 8 | rustfmt.toml 9 | yarn.lock 10 | *.node 11 | .yarn 12 | __test__ 13 | renovate.json 14 | .idea -------------------------------------------------------------------------------- /examples/keyboard1.ts: -------------------------------------------------------------------------------- 1 | import { Keyboard } from "../index"; 2 | 3 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 4 | 5 | async function main() { 6 | await Keyboard.typing('Hello, World!'); 7 | } 8 | 9 | main(); -------------------------------------------------------------------------------- /examples/keyboard2.ts: -------------------------------------------------------------------------------- 1 | import { Keyboard, Key } from "../index"; 2 | 3 | async function main() { 4 | await Keyboard.press(Key.LeftWin); 5 | await Keyboard.click(Key.D); 6 | await Keyboard.release(Key.LeftWin); 7 | } 8 | 9 | 10 | main(); -------------------------------------------------------------------------------- /examples/window2.ts: -------------------------------------------------------------------------------- 1 | import { Window } from '../index'; 2 | 3 | const TAU = Math.PI * 2; 4 | 5 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 6 | 7 | async function main() { 8 | const chrome = await Window.findWindowByClassName("Chrome_WidgetWin_1"); 9 | console.log(await chrome?.getTitle()); 10 | } 11 | 12 | main(); -------------------------------------------------------------------------------- /npm/win32-x64-msvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deeean/sophia-win32-x64-msvc", 3 | "version": "0.2.1", 4 | "os": [ 5 | "win32" 6 | ], 7 | "cpu": [ 8 | "x64" 9 | ], 10 | "main": "sophia.win32-x64-msvc.node", 11 | "files": [ 12 | "sophia.win32-x64-msvc.node" 13 | ], 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">= 10" 17 | } 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "noUnusedLocals": false, 8 | "noUnusedParameters": false, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["."], 13 | "exclude": ["node_modules"] 14 | } -------------------------------------------------------------------------------- /examples/imageSearch1.ts: -------------------------------------------------------------------------------- 1 | import * as sophia from '../index'; 2 | 3 | async function main() { 4 | const [ 5 | baboon, 6 | partsOfBaboon, 7 | ] = await Promise.all([ 8 | sophia.readImageData('./examples/images/baboon.png'), 9 | sophia.readImageData('./examples/images/parts_of_baboon.png'), 10 | ]); 11 | 12 | const position = await sophia.imageSearch(baboon, partsOfBaboon); 13 | if (position) { 14 | console.log('Found at', position); 15 | } else { 16 | console.log('Not found'); 17 | } 18 | } 19 | 20 | main(); -------------------------------------------------------------------------------- /examples/imageSearch2.ts: -------------------------------------------------------------------------------- 1 | import * as sophia from '../index'; 2 | 3 | async function main() { 4 | const [ 5 | baboon, 6 | ] = await Promise.all([ 7 | sophia.readImageData('./examples/images/baboon.png'), 8 | ]); 9 | 10 | const screenSize = await sophia.getScreenSize(); 11 | const screenshot = await sophia.takeScreenshot(0, 0, screenSize.x, screenSize.y); 12 | 13 | const position = await sophia.imageSearch(screenshot, baboon); 14 | if (position) { 15 | console.log('Found at', position); 16 | } else { 17 | console.log('Not found'); 18 | } 19 | } 20 | 21 | main(); -------------------------------------------------------------------------------- /examples/keyboard3.ts: -------------------------------------------------------------------------------- 1 | import { Keyboard, Mouse, Modifiers, Key } from '../index'; 2 | 3 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 4 | 5 | async function main() { 6 | Keyboard.registerHotkey([Modifiers.Control], Key.A, async () => { 7 | await Mouse.move(100, 100); 8 | await sleep(100); 9 | await Mouse.move(200, 100); 10 | await sleep(100); 11 | await Mouse.move(200, 200); 12 | await sleep(100); 13 | await Mouse.move(100, 200); 14 | await sleep(100); 15 | await Mouse.move(100, 100); 16 | }); 17 | } 18 | 19 | 20 | main(); -------------------------------------------------------------------------------- /src/geometry.rs: -------------------------------------------------------------------------------- 1 | use napi_derive::napi; 2 | 3 | #[napi(object)] 4 | #[derive(Debug, Clone)] 5 | pub struct Point { 6 | pub x: i32, 7 | pub y: i32, 8 | } 9 | 10 | impl Point { 11 | pub fn new(x: i32, y: i32) -> Self { 12 | Self { x, y } 13 | } 14 | } 15 | 16 | #[napi(object)] 17 | #[derive(Debug, Clone)] 18 | pub struct Rect { 19 | pub left: i32, 20 | pub top: i32, 21 | pub right: i32, 22 | pub bottom: i32, 23 | } 24 | 25 | impl Rect { 26 | pub fn new(left: i32, top: i32, right: i32, bottom: i32) -> Self { 27 | Self { left, top, right, bottom } 28 | } 29 | } -------------------------------------------------------------------------------- /examples/mouse1.ts: -------------------------------------------------------------------------------- 1 | import { Mouse, getScreenSize } from "../index"; 2 | 3 | const TAU = Math.PI * 2; 4 | 5 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 6 | 7 | async function main() { 8 | const screenSize = await getScreenSize(); 9 | const smallest = Math.min(screenSize.x, screenSize.y); 10 | const radius = smallest / 2; 11 | const center = { x: screenSize.x / 2, y: screenSize.y / 2 }; 12 | const iteration = 200; 13 | 14 | for (let i = 0; i < iteration; i++) { 15 | const angle = i * TAU / iteration; 16 | const x = center.x + radius * Math.cos(angle); 17 | const y = center.y + radius * Math.sin(angle); 18 | await Mouse.move(x, y); 19 | await sleep(10); 20 | } 21 | } 22 | 23 | main(); -------------------------------------------------------------------------------- /examples/memory1.ts: -------------------------------------------------------------------------------- 1 | import { getProcesses, openProcess, ProcessAccess } from '../index'; 2 | 3 | const BASE_ADDRESS = BigInt(0x003264D0); 4 | const OFFSETS = [ 5 | BigInt(0x48), 6 | BigInt(0x0), 7 | BigInt(0xF8), 8 | BigInt(0x18), 9 | BigInt(0x408), 10 | BigInt(0x50), 11 | BigInt(0x7F8), 12 | ]; 13 | 14 | async function main() { 15 | const processes = await getProcesses(); 16 | const tutorial = processes.find(p => p.name === 'Tutorial-x86_64.exe'); 17 | if (!tutorial) { 18 | console.log('Tutorial-x86_64.exe not found'); 19 | return; 20 | } 21 | 22 | const openedProcess = await openProcess(ProcessAccess.AllAccess, tutorial.pid); 23 | 24 | const health = await openedProcess.readMemoryChainUint32(BASE_ADDRESS, OFFSETS); 25 | if (health < 1000n) { 26 | await openedProcess.writeMemoryChainUint32(BASE_ADDRESS, OFFSETS, 1000n); 27 | } 28 | } 29 | 30 | main(); -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "sophia" 4 | version = "0.1.0" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | napi = { version = "2.16.0", features = ["tokio_rt", "napi6"] } 11 | napi-derive = "2.16.0" 12 | image = "0.25.0" 13 | tokio = { version = "1.21.1", features = ["full"] } 14 | lazy_static = "1.4.0" 15 | crossbeam-channel = "0.5.12" 16 | 17 | [dependencies.windows] 18 | version = "0.54.0" 19 | features = [ 20 | "Win32_UI_Input_KeyboardAndMouse", 21 | "Win32_UI_WindowsAndMessaging", 22 | "Win32_Foundation", 23 | "Win32_Graphics_Gdi", 24 | "Win32_System_Threading", 25 | "Win32_System_LibraryLoader", 26 | "Win32_System_Diagnostics_Debug", 27 | "Win32_System_Diagnostics_ToolHelp", 28 | "Win32_System_ProcessStatus", 29 | ] 30 | 31 | [build-dependencies] 32 | napi-build = "2.1.2" 33 | 34 | [profile.release] 35 | lto = true 36 | -------------------------------------------------------------------------------- /examples/window1.ts: -------------------------------------------------------------------------------- 1 | import { Window, Mouse, MouseButton } from '../index'; 2 | 3 | const TAU = Math.PI * 2; 4 | 5 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 6 | 7 | async function main() { 8 | const mspaint = await Window.findWindowByTitle("Untitled - Paint"); 9 | if (mspaint) { 10 | await mspaint.foreground(); 11 | await mspaint.maximize(); 12 | } 13 | 14 | let x = 400; 15 | let y = 600; 16 | 17 | await Mouse.move(x, y); 18 | await Mouse.press(MouseButton.Left); 19 | 20 | const iterations = 100; 21 | 22 | for (let i = 0; i < iterations; i++) { 23 | x += Math.sin(i / iterations * TAU) * 10; 24 | y += Math.cos(i / iterations * TAU) * 10; 25 | await Mouse.move(x, y); 26 | 27 | if (i === 0) { 28 | await Mouse.press(MouseButton.Left); 29 | } 30 | 31 | await sleep(10); 32 | } 33 | 34 | await Mouse.release(MouseButton.Left); 35 | } 36 | 37 | main(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deeean/sophia", 3 | "version": "0.2.1", 4 | "main": "index.js", 5 | "types": "index.d.ts", 6 | "author": "deeean ", 7 | "license": "MIT", 8 | "keywords": [ 9 | "AutoHotkey", 10 | "RobotJS", 11 | "AutoIt", 12 | "Automation" 13 | ], 14 | "repository": "https://github.com/deeean/sophia.git", 15 | "bugs": { 16 | "url": "https://github.com/deeean/sophia/issues" 17 | }, 18 | "homepage": "https://github.com/deeean/sophia#readme", 19 | "watch": { 20 | "build-rs": { 21 | "patterns": [ 22 | "src" 23 | ], 24 | "extensions": "rs", 25 | "quiet": true 26 | }, 27 | "run-ts": { 28 | "patterns": [ 29 | "example", 30 | "*.node" 31 | ], 32 | "extensions": "ts", 33 | "quiet": true 34 | } 35 | }, 36 | "scripts": { 37 | "prepublishOnly": "napi prepublish -t npm", 38 | "artifacts": "napi artifacts", 39 | "build": "napi build --platform", 40 | "build:release": "napi build --release --platform", 41 | "test": "ava", 42 | "version": "napi version" 43 | }, 44 | "devDependencies": { 45 | "@napi-rs/cli": "^2.18.0", 46 | "ava": "^6.1.2", 47 | "npm-run-all": "^4.1.5", 48 | "npm-watch": "^0.11.0", 49 | "prettier": "^3.2.5", 50 | "tsx": "^4.7.1", 51 | "typescript": "^5.4.3" 52 | }, 53 | "engines": { 54 | "node": ">= 10" 55 | }, 56 | "napi": { 57 | "name": "sophia", 58 | "triples": { 59 | "defaults": false, 60 | "additional": [ 61 | "x86_64-pc-windows-msvc" 62 | ] 63 | } 64 | }, 65 | "prettier": { 66 | "printWidth": 120, 67 | "semi": false, 68 | "trailingComma": "all", 69 | "singleQuote": true, 70 | "arrowParens": "always" 71 | }, 72 | "optionalDependencies": { 73 | "@deeean/sophia-win32-x64-msvc": "0.2.1" 74 | } 75 | } -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use napi::{Error, Status, Result}; 2 | use napi::bindgen_prelude::BigInt; 3 | 4 | pub fn bigint_to_i8(bigint: BigInt) -> i8 { 5 | bigint_to_i64(bigint) as i8 6 | } 7 | 8 | pub fn bigint_to_u8(bigint: BigInt) -> u8 { 9 | bigint_to_u64(bigint) as u8 10 | } 11 | 12 | pub fn bigint_to_i16(bigint: BigInt) -> i16 { 13 | bigint_to_i64(bigint) as i16 14 | } 15 | 16 | pub fn bigint_to_u16(bigint: BigInt) -> u16 { 17 | bigint_to_u64(bigint) as u16 18 | } 19 | 20 | pub fn bigint_to_i32(bigint: BigInt) -> i32 { 21 | bigint_to_i64(bigint) as i32 22 | } 23 | 24 | pub fn bigint_to_u32(bigint: BigInt) -> u32 { 25 | bigint_to_u64(bigint) as u32 26 | } 27 | 28 | pub fn bigint_to_i64(bigint: BigInt) -> i64 { 29 | let (value, _) = bigint.get_i64(); 30 | value 31 | } 32 | 33 | pub fn bigint_to_u64(bigint: BigInt) -> u64 { 34 | let (_, value, _) = bigint.get_u64(); 35 | value 36 | } 37 | 38 | pub fn bigint_to_usize(bigint: BigInt) -> usize { 39 | bigint_to_u64(bigint) as usize 40 | } 41 | 42 | pub fn encode_wide>(string: S) -> Vec { 43 | std::os::windows::prelude::OsStrExt::encode_wide(string.as_ref()) 44 | .chain(std::iter::once(0)) 45 | .collect() 46 | } 47 | 48 | pub fn decode_wide(chars: &[u16]) -> String { 49 | String::from_utf16_lossy(chars) 50 | .trim_end_matches('\0') 51 | .to_string() 52 | } 53 | 54 | pub async fn handle_result(task: tokio::task::JoinHandle>) -> Result { 55 | match task.await { 56 | Ok(result) => match result { 57 | Ok(value) => Ok(value), 58 | Err(e) => Err(Error::new( 59 | Status::GenericFailure, 60 | format!("Operation failed: {:?}", e), 61 | )), 62 | }, 63 | Err(e) => Err(Error::new( 64 | Status::GenericFailure, 65 | format!("Task join failed: {:?}", e), 66 | )), 67 | } 68 | } -------------------------------------------------------------------------------- /src/win/screen.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | use windows::Win32::Graphics::Gdi::{BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits, ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, RGBQUAD, SRCCOPY}; 4 | use windows::Win32::UI::WindowsAndMessaging::{GetDesktopWindow, GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN}; 5 | use crate::screen::{ImageData}; 6 | use crate::geometry::Point; 7 | use crate::utils::handle_result; 8 | 9 | fn create_bitmap_info(width: i32, height: i32) -> BITMAPINFO { 10 | unsafe { 11 | let mut bmi = std::mem::zeroed::(); 12 | 13 | bmi.biSize = std::mem::size_of::() as u32; 14 | bmi.biWidth = width; 15 | bmi.biHeight = -height; 16 | bmi.biPlanes = 1; 17 | bmi.biBitCount = 32; 18 | bmi.biCompression = BI_RGB.0; 19 | bmi.biSizeImage = 0; 20 | bmi.biXPelsPerMeter = 0; 21 | bmi.biYPelsPerMeter = 0; 22 | bmi.biClrUsed = 0; 23 | bmi.biClrImportant = 0; 24 | 25 | BITMAPINFO { 26 | bmiHeader: bmi, 27 | bmiColors: [RGBQUAD::default(); 1], 28 | } 29 | } 30 | } 31 | 32 | 33 | #[napi] 34 | pub async fn get_screen_size() -> Result { 35 | let task = tokio::spawn(async move { 36 | unsafe { 37 | let width = GetSystemMetrics(SM_CXSCREEN); 38 | let height = GetSystemMetrics(SM_CYSCREEN); 39 | Ok(Point::new(width, height)) 40 | } 41 | }); 42 | 43 | handle_result(task).await 44 | } 45 | 46 | #[napi] 47 | pub async fn take_screenshot(x: i32, y: i32, width: i32, height: i32) -> Result { 48 | let task = tokio::spawn(async move { 49 | unsafe { 50 | let hwnd = GetDesktopWindow(); 51 | let h_window_dc = GetDC(hwnd); 52 | 53 | let h_dc = CreateCompatibleDC(h_window_dc); 54 | if h_dc.is_invalid() { 55 | return Err("CreateCompatibleDC failed".to_string()); 56 | } 57 | 58 | let h_bitmap = CreateCompatibleBitmap(h_window_dc, width, height); 59 | if h_bitmap.is_invalid() { 60 | return Err("CreateCompatibleBitmap failed".to_string()); 61 | } 62 | 63 | let res = SelectObject(h_dc, h_bitmap); 64 | if res.is_invalid() { 65 | return Err("SelectObject failed".to_string()); 66 | } 67 | 68 | let mut bitmap_info = create_bitmap_info(width, height); 69 | 70 | let size: usize = (width * height) as usize * 4; 71 | let mut buf: Vec = vec![0; size]; 72 | 73 | let res = BitBlt(h_dc, 0, 0, width, height, h_window_dc, x, y, SRCCOPY); 74 | if res.is_err() { 75 | return Err("BitBlt failed".to_string()); 76 | } 77 | 78 | GetDIBits(h_dc, h_bitmap, 0, height as u32, Some(buf.as_mut_ptr() as *mut _), &mut bitmap_info, DIB_RGB_COLORS, ); 79 | 80 | ReleaseDC(hwnd, h_window_dc); 81 | DeleteDC(h_dc); 82 | DeleteObject(h_bitmap); 83 | 84 | for i in (0..buf.len()).step_by(4) { 85 | let b = buf[i]; 86 | let r = buf[i + 2]; 87 | buf[i] = r; 88 | buf[i + 2] = b; 89 | } 90 | 91 | Ok(ImageData { 92 | data: buf, 93 | width: width as u32, 94 | height: height as u32, 95 | pixel_width: 4, 96 | }) 97 | } 98 | }); 99 | 100 | handle_result(task).await 101 | } -------------------------------------------------------------------------------- /src/win/mouse.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | use windows::Win32::UI::Input::KeyboardAndMouse::{MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP}; 4 | use windows::Win32::UI::WindowsAndMessaging::{GetCursorPos, GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN}; 5 | use crate::geometry::Point; 6 | use crate::utils::handle_result; 7 | 8 | #[napi] 9 | pub enum MouseButton { 10 | Left, 11 | Right, 12 | Middle, 13 | } 14 | 15 | #[napi] 16 | pub struct Mouse { 17 | 18 | } 19 | 20 | #[napi] 21 | impl Mouse { 22 | #[napi(js_name = "move")] 23 | pub async fn mouse_move(x: i32, y: i32) -> Result<()> { 24 | let task = tokio::spawn(async move { 25 | mouse_move_inner(x, y); 26 | 27 | Ok(()) 28 | }); 29 | 30 | handle_result(task).await 31 | } 32 | 33 | #[napi] 34 | pub async fn press(button: MouseButton) -> Result<()> { 35 | let task = tokio::spawn(async move { 36 | let down = match button { 37 | MouseButton::Left => MOUSEEVENTF_LEFTDOWN, 38 | MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, 39 | MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN, 40 | }; 41 | 42 | mouse_event(down, 0, 0, 0, 0); 43 | 44 | Ok(()) 45 | }); 46 | 47 | handle_result(task).await 48 | } 49 | 50 | #[napi] 51 | pub async fn release(button: MouseButton) -> Result<()> { 52 | let task = tokio::spawn(async move { 53 | let up = match button { 54 | MouseButton::Left => MOUSEEVENTF_LEFTUP, 55 | MouseButton::Right => MOUSEEVENTF_RIGHTUP, 56 | MouseButton::Middle => MOUSEEVENTF_MIDDLEUP, 57 | }; 58 | 59 | mouse_event(up, 0, 0, 0, 0); 60 | 61 | Ok(()) 62 | }); 63 | 64 | handle_result(task).await 65 | } 66 | 67 | #[napi] 68 | pub async fn click(button: MouseButton, x: i32, y: i32) -> Result<()> { 69 | let task = tokio::spawn(async move { 70 | let (down, up) = match button { 71 | MouseButton::Left => (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP), 72 | MouseButton::Right => (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP), 73 | MouseButton::Middle => (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP), 74 | }; 75 | 76 | mouse_move_inner(x, y); 77 | mouse_event(down, x, y, 0, 0); 78 | mouse_event(up, x, y, 0, 0); 79 | 80 | Ok(()) 81 | }); 82 | 83 | handle_result(task).await 84 | } 85 | 86 | #[napi] 87 | pub async fn get_position() -> Result { 88 | let task = tokio::spawn(async move { 89 | Ok(get_mouse_position_inner()) 90 | }); 91 | 92 | handle_result(task).await 93 | } 94 | } 95 | 96 | fn get_mouse_position_inner() -> Point { 97 | let mut position = windows::Win32::Foundation::POINT { x: 0, y: 0 }; 98 | unsafe { 99 | let _ = GetCursorPos(&mut position); 100 | } 101 | 102 | Point::new(position.x, position.y) 103 | } 104 | 105 | fn mouse_event(dw_flags: MOUSE_EVENT_FLAGS, dx: i32, dy: i32, dw_data: i32, dw_extra_info: usize) { 106 | unsafe { 107 | let x = dx * 65536 / GetSystemMetrics(SM_CXSCREEN); 108 | let y = dy * 65536 / GetSystemMetrics(SM_CYSCREEN); 109 | windows::Win32::UI::Input::KeyboardAndMouse::mouse_event(dw_flags, x, y, dw_data, dw_extra_info); 110 | } 111 | } 112 | 113 | fn mouse_move_inner(x: i32, y: i32) { 114 | mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, x, y, 0, 0); 115 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Sophia

3 |

4 | 🤖 A Node.js library for automating Windows applications. 5 |

6 | 7 | [![NPM Version](https://img.shields.io/npm/v/@deeean/sophia)](https://www.npmjs.com/package/@deeean/sophia) 8 | ![NPM License](https://img.shields.io/npm/l/@deeean/sophia) 9 |
10 | 11 | ## Features 12 | - Keyboard 13 | - Mouse 14 | - Screen 15 | - Window 16 | - Memory 17 | 18 | ## Installation 19 | ```bash 20 | npm install @deeean/sophia 21 | ``` 22 | 23 | ## Example 24 | Typing a string 25 | ```typescript 26 | import { Keyboard } from '@deeean/sophia'; 27 | 28 | async function main() { 29 | await Keyboard.typing('Hello, World!'); 30 | } 31 | 32 | main(); 33 | ``` 34 | 35 |
36 | 37 | Registering a hotkey for specific key combinations and handling events. 38 | ```typescript 39 | import { Keyboard, Modifiers, Key } from '@deeean/sophia'; 40 | 41 | Keyboard.registerHotkey([Modifiers.Control], Key.A, () => { 42 | console.log('Ctrl + A is pressed'); 43 | }); 44 | ``` 45 | 46 |
47 | 48 | Finding the location of one image within another 49 | ```typescript 50 | import { readImageData, imageSearch } from '@deeean/sophia'; 51 | 52 | async function main() { 53 | const [ 54 | baboon, 55 | partsOfBaboon, 56 | ] = await Promise.all([ 57 | readImageData('./examples/images/baboon.png'), 58 | readImageData('./examples/images/parts_of_baboon.png'), 59 | ]); 60 | 61 | const position = await imageSearch(baboon, partsOfBaboon); 62 | if (position) { 63 | console.log('Found at', position); 64 | } else { 65 | console.log('Not found'); 66 | } 67 | } 68 | 69 | main(); 70 | ``` 71 | 72 |
73 | 74 | Finding the location of one image within the screenshot 75 | ```typescript 76 | import * as sophia from '@deeean/sophia'; 77 | 78 | async function main() { 79 | const [ 80 | baboon, 81 | ] = await Promise.all([ 82 | sophia.readImageData('./examples/images/baboon.png'), 83 | ]); 84 | 85 | const screenSize = await sophia.getScreenSize(); 86 | const screenshot = await sophia.takeScreenshot(0, 0, screenSize.x, screenSize.y); 87 | 88 | const position = await sophia.imageSearch(screenshot, baboon); 89 | if (position) { 90 | console.log('Found at', position); 91 | } else { 92 | console.log('Not found'); 93 | } 94 | } 95 | 96 | main(); 97 | ``` 98 | 99 |
100 | 101 | Getting the list of processes and reading/writing memory 102 | ```typescript 103 | import { getProcesses, openProcess, ProcessAccess } from '@deeean/sophia'; 104 | 105 | const BASE_ADDRESS = BigInt(0x003264D0); 106 | const OFFSETS = [ 107 | BigInt(0x48), 108 | BigInt(0x0), 109 | BigInt(0xF8), 110 | BigInt(0x18), 111 | BigInt(0x408), 112 | BigInt(0x50), 113 | BigInt(0x7F8), 114 | ]; 115 | 116 | async function main() { 117 | const processes = await getProcesses(); 118 | const tutorial = processes.find(p => p.name === 'Tutorial-x86_64.exe'); 119 | if (!tutorial) { 120 | console.log('Tutorial-x86_64.exe not found'); 121 | return; 122 | } 123 | 124 | const openedProcess = await openProcess(ProcessAccess.AllAccess, tutorial.pid); 125 | 126 | const health = await openedProcess.readMemoryChainUint32(BASE_ADDRESS, OFFSETS); 127 | if (health < 1000n) { 128 | await openedProcess.writeMemoryChainUint32(BASE_ADDRESS, OFFSETS, 1000n); 129 | } 130 | } 131 | 132 | main(); 133 | ``` 134 | 135 | ## Supported Platforms 136 | Only support Windows x64 for now. 137 | 138 | ## Inspiration 139 | I'm a big fan of [AutoHotkey](https://www.autohotkey.com/), but I want to use it in Node.js. So I decided to create a library that can automate Windows applications. 140 | 141 | ## Related projects 142 | - [AutoHotkey](https://github.com/AutoHotkey/AutoHotkey) 143 | - [PyAutoGUI](https://github.com/asweigart/pyautogui) 144 | - [RobotJS](https://github.com/octalmage/robotjs) 145 | 146 | ## License 147 | Sophia is licensed under the MIT License. Feel free to use it in your projects, adhering to the license terms. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # End of https://www.toptal.com/developers/gitignore/api/node 114 | 115 | # Created by https://www.toptal.com/developers/gitignore/api/macos 116 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos 117 | 118 | ### macOS ### 119 | # General 120 | .DS_Store 121 | .AppleDouble 122 | .LSOverride 123 | 124 | # Icon must end with two 125 | Icon 126 | 127 | 128 | # Thumbnails 129 | ._* 130 | 131 | # Files that might appear in the root of a volume 132 | .DocumentRevisions-V100 133 | .fseventsd 134 | .Spotlight-V100 135 | .TemporaryItems 136 | .Trashes 137 | .VolumeIcon.icns 138 | .com.apple.timemachine.donotpresent 139 | 140 | # Directories potentially created on remote AFP share 141 | .AppleDB 142 | .AppleDesktop 143 | Network Trash Folder 144 | Temporary Items 145 | .apdisk 146 | 147 | ### macOS Patch ### 148 | # iCloud generated files 149 | *.icloud 150 | 151 | # End of https://www.toptal.com/developers/gitignore/api/macos 152 | 153 | # Created by https://www.toptal.com/developers/gitignore/api/windows 154 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows 155 | 156 | ### Windows ### 157 | # Windows thumbnail cache files 158 | Thumbs.db 159 | Thumbs.db:encryptable 160 | ehthumbs.db 161 | ehthumbs_vista.db 162 | 163 | # Dump file 164 | *.stackdump 165 | 166 | # Folder config file 167 | [Dd]esktop.ini 168 | 169 | # Recycle Bin used on file shares 170 | $RECYCLE.BIN/ 171 | 172 | # Windows Installer files 173 | *.cab 174 | *.msi 175 | *.msix 176 | *.msm 177 | *.msp 178 | 179 | # Windows shortcuts 180 | *.lnk 181 | 182 | # End of https://www.toptal.com/developers/gitignore/api/windows 183 | 184 | #Added by cargo 185 | 186 | /target 187 | Cargo.lock 188 | 189 | .pnp.* 190 | .yarn/* 191 | !.yarn/patches 192 | !.yarn/plugins 193 | !.yarn/releases 194 | !.yarn/sdks 195 | !.yarn/versions 196 | 197 | *.node 198 | 199 | # IntelliJ IDEA 200 | .idea -------------------------------------------------------------------------------- /src/win/window.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | use windows::core::PCWSTR; 4 | use windows::Win32::Foundation::HWND; 5 | use windows::Win32::UI::WindowsAndMessaging::{FindWindowW, GetForegroundWindow, GetWindowTextLengthW, GetWindowTextW, SET_WINDOW_POS_FLAGS, SetForegroundWindow, SetWindowPos, SHOW_WINDOW_CMD, ShowWindow, ShowWindowAsync, SW_MAXIMIZE, SW_MINIMIZE, SW_SHOWNORMAL, SWP_NOMOVE, SWP_NOSIZE}; 6 | use crate::geometry::Rect; 7 | use crate::utils::{decode_wide, encode_wide, handle_result}; 8 | 9 | #[napi] 10 | pub struct Window { 11 | hwnd: HWND 12 | } 13 | 14 | #[napi] 15 | impl Window { 16 | #[napi] 17 | pub async fn minimize(&self) -> Result<()> { 18 | self.show_window(SW_MINIMIZE).await 19 | } 20 | 21 | #[napi] 22 | pub async fn maximize(&self) -> Result<()> { 23 | self.show_window(SW_MAXIMIZE).await 24 | } 25 | 26 | #[napi] 27 | pub async fn get_title(&self) -> Result { 28 | let hwnd = self.hwnd; 29 | 30 | let task = tokio::spawn(async move { 31 | 32 | unsafe { 33 | let len = GetWindowTextLengthW(hwnd); 34 | let mut buffer = vec![0u16; len as usize + 1]; 35 | GetWindowTextW(hwnd, &mut buffer); 36 | Ok(decode_wide(&buffer)) 37 | } 38 | }); 39 | 40 | handle_result(task).await 41 | } 42 | 43 | #[napi] 44 | pub async fn get_window_rect(&self) -> Result { 45 | let hwnd = self.hwnd; 46 | 47 | let task = tokio::spawn(async move { 48 | let mut rect = windows::Win32::Foundation::RECT::default(); 49 | 50 | unsafe { 51 | let _ = windows::Win32::UI::WindowsAndMessaging::GetWindowRect(hwnd, &mut rect); 52 | } 53 | 54 | Ok(Rect { 55 | left: rect.left, 56 | top: rect.top, 57 | right: rect.right, 58 | bottom: rect.bottom, 59 | }) 60 | }); 61 | 62 | handle_result(task).await 63 | } 64 | 65 | async fn show_window(&self, state: SHOW_WINDOW_CMD) -> Result<()> { 66 | let hwnd = self.hwnd; 67 | 68 | let task = tokio::spawn(async move { 69 | unsafe { 70 | ShowWindow(hwnd, state); 71 | } 72 | 73 | Ok(()) 74 | }); 75 | 76 | handle_result(task).await 77 | } 78 | 79 | #[napi] 80 | pub async fn set_position(&self, x: i32, y: i32) -> Result<()> { 81 | self.set_window_pos(x, y, 0, 0, SWP_NOSIZE).await 82 | } 83 | 84 | #[napi] 85 | pub async fn set_size(&self, width: i32, height: i32) -> Result<()> { 86 | self.set_window_pos(0, 0, width, height, SWP_NOMOVE).await 87 | } 88 | 89 | #[napi] 90 | pub async fn foreground(&self) -> Result { 91 | let hwnd = self.hwnd; 92 | 93 | let task = tokio::spawn(async move { 94 | unsafe { 95 | let _ = ShowWindowAsync(hwnd, SW_SHOWNORMAL); 96 | }; 97 | 98 | let res = unsafe { 99 | SetForegroundWindow(hwnd) 100 | }; 101 | 102 | Ok(res.0 != 0) 103 | }); 104 | 105 | handle_result(task).await 106 | } 107 | 108 | async fn set_window_pos(&self, x: i32, y: i32, width: i32, height: i32, flags: SET_WINDOW_POS_FLAGS) -> Result<()> { 109 | let hwnd = self.hwnd; 110 | 111 | let task = tokio::spawn(async move { 112 | unsafe { 113 | let _ = SetWindowPos( 114 | hwnd, 115 | None, 116 | x, 117 | y, 118 | width, 119 | height, 120 | flags, 121 | ); 122 | } 123 | 124 | Ok(()) 125 | }); 126 | 127 | handle_result(task).await 128 | } 129 | 130 | 131 | #[napi] 132 | pub async fn get_foreground_window() -> Result> { 133 | let task = tokio::spawn(async move { 134 | let hwnd = unsafe { GetForegroundWindow() }; 135 | 136 | if hwnd.0 == 0 { 137 | Ok(None) 138 | } else { 139 | Ok(Some(Window { hwnd })) 140 | } 141 | }); 142 | 143 | handle_result(task).await 144 | } 145 | 146 | #[napi] 147 | pub async fn find_window_by_title(title: String) -> Result> { 148 | let task = tokio::spawn(async move { 149 | let hwnd = unsafe { 150 | FindWindowW(None, PCWSTR(encode_wide(title).as_ptr())) 151 | }; 152 | 153 | if hwnd.0 == 0 { 154 | Ok(None) 155 | } else { 156 | Ok(Some(Window { hwnd })) 157 | } 158 | }); 159 | 160 | handle_result(task).await 161 | } 162 | 163 | #[napi] 164 | pub async fn find_window_by_class_name(classname: String) -> Result> { 165 | let task = tokio::spawn(async move { 166 | let hwnd = unsafe { 167 | FindWindowW(PCWSTR(encode_wide(classname).as_ptr()), None) 168 | }; 169 | 170 | if hwnd.0 == 0 { 171 | Ok(None) 172 | } else { 173 | Ok(Some(Window { hwnd })) 174 | } 175 | }); 176 | 177 | handle_result(task).await 178 | } 179 | } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | 4 | /* auto-generated by NAPI-RS */ 5 | 6 | export interface Point { 7 | x: number 8 | y: number 9 | } 10 | export interface Rect { 11 | left: number 12 | top: number 13 | right: number 14 | bottom: number 15 | } 16 | export interface Color { 17 | r: number 18 | g: number 19 | b: number 20 | } 21 | export const MAGENTA: Color 22 | export function readImageData(path: string): Promise 23 | export function saveImageData(path: string, imageData: ImageData): Promise 24 | export function imageSearch(source: ImageData, target: ImageData, variant?: number | undefined | null, transColor?: Color | undefined | null): Promise 25 | export function multipleImageSearch(source: ImageData, target: ImageData, variant?: number | undefined | null, transColor?: Color | undefined | null): Promise> 26 | export const enum Modifiers { 27 | Alt = 1, 28 | AltGraph = 2, 29 | CapsLock = 4, 30 | Control = 8, 31 | Fn = 16, 32 | FnLock = 32, 33 | Meta = 64, 34 | NumLock = 128, 35 | ScrollLock = 256, 36 | Shift = 512, 37 | Symbol = 1024, 38 | SymbolLock = 2048, 39 | Hyper = 4096, 40 | Super = 8192 41 | } 42 | export const enum Key { 43 | None = 0, 44 | Back = 8, 45 | Tab = 9, 46 | LineFeed = 10, 47 | Clear = 12, 48 | Enter = 13, 49 | Shift = 16, 50 | Control = 17, 51 | Alt = 18, 52 | Pause = 19, 53 | CapsLock = 20, 54 | Esc = 27, 55 | Space = 32, 56 | PageUp = 33, 57 | PageDown = 34, 58 | End = 35, 59 | Home = 36, 60 | ArrowLeft = 37, 61 | ArrowUp = 38, 62 | ArrowRight = 39, 63 | ArrowDown = 40, 64 | Insert = 45, 65 | Delete = 46, 66 | D0 = 48, 67 | D1 = 49, 68 | D2 = 50, 69 | D3 = 51, 70 | D4 = 52, 71 | D5 = 53, 72 | D6 = 54, 73 | D7 = 55, 74 | D8 = 56, 75 | D9 = 57, 76 | A = 65, 77 | B = 66, 78 | C = 67, 79 | D = 68, 80 | E = 69, 81 | F = 70, 82 | G = 71, 83 | H = 72, 84 | I = 73, 85 | J = 74, 86 | K = 75, 87 | L = 76, 88 | M = 77, 89 | N = 78, 90 | O = 79, 91 | P = 80, 92 | Q = 81, 93 | R = 82, 94 | S = 83, 95 | T = 84, 96 | U = 85, 97 | V = 86, 98 | W = 87, 99 | X = 88, 100 | Y = 89, 101 | Z = 90, 102 | LeftWin = 91, 103 | RightWin = 92, 104 | Apps = 93, 105 | Sleep = 95, 106 | NumPad0 = 96, 107 | NumPad1 = 97, 108 | NumPad2 = 98, 109 | NumPad3 = 99, 110 | NumPad4 = 100, 111 | NumPad5 = 101, 112 | NumPad6 = 102, 113 | NumPad7 = 103, 114 | NumPad8 = 104, 115 | NumPad9 = 105, 116 | Multiply = 106, 117 | Add = 107, 118 | Separator = 108, 119 | Subtract = 109, 120 | Decimal = 110, 121 | Divide = 111, 122 | F1 = 112, 123 | F2 = 113, 124 | F3 = 114, 125 | F4 = 115, 126 | F5 = 116, 127 | F6 = 117, 128 | F7 = 118, 129 | F8 = 119, 130 | F9 = 120, 131 | F10 = 121, 132 | F11 = 122, 133 | F12 = 123, 134 | F13 = 124, 135 | F14 = 125, 136 | F15 = 126, 137 | F16 = 127, 138 | F17 = 128, 139 | F18 = 129, 140 | F19 = 130, 141 | F20 = 131, 142 | F21 = 132, 143 | F22 = 133, 144 | F23 = 134, 145 | F24 = 135, 146 | NumLock = 144, 147 | ScrollLock = 145, 148 | LeftShift = 160, 149 | RightShift = 161, 150 | LeftControl = 162, 151 | RightControl = 163, 152 | LeftAlt = 164, 153 | RightAlt = 165 154 | } 155 | export const enum MouseButton { 156 | Left = 0, 157 | Right = 1, 158 | Middle = 2 159 | } 160 | export function getScreenSize(): Promise 161 | export function takeScreenshot(x: number, y: number, width: number, height: number): Promise 162 | export interface Process { 163 | pid: number 164 | name: string 165 | } 166 | export const enum ProcessAccess { 167 | AllAccess = 0, 168 | CreateProcess = 1, 169 | CreateThread = 2, 170 | Delete = 3, 171 | DupHandle = 4, 172 | QueryInformation = 5, 173 | QueryLimitedInformation = 6, 174 | ReadControl = 7, 175 | SetInformation = 8, 176 | SetLimitedInformation = 9, 177 | SetQuota = 10, 178 | SetSessionId = 11, 179 | Synchronize = 12, 180 | Terminate = 13, 181 | VmOperation = 14, 182 | VmRead = 15, 183 | VmWrite = 16, 184 | WriteDac = 17, 185 | WriteOwner = 18 186 | } 187 | export function openProcess(access: ProcessAccess, pid: number): Promise 188 | export function getProcesses(): Promise> 189 | export class ImageData { 190 | data: Array 191 | width: number 192 | height: number 193 | pixelWidth: number 194 | } 195 | export class Keyboard { 196 | static press(key: Key): Promise 197 | static release(key: Key): Promise 198 | static click(key: Key): Promise 199 | static typing(text: string): Promise 200 | static registerHotkey(mods: Array, key: Key, callback: (...args: any[]) => any): number 201 | static unregisterHotkey(id: number): void 202 | } 203 | export class Mouse { 204 | static move(x: number, y: number): Promise 205 | static press(button: MouseButton): Promise 206 | static release(button: MouseButton): Promise 207 | static click(button: MouseButton, x: number, y: number): Promise 208 | static getPosition(): Promise 209 | } 210 | export class Window { 211 | minimize(): Promise 212 | maximize(): Promise 213 | getTitle(): Promise 214 | getWindowRect(): Promise 215 | setPosition(x: number, y: number): Promise 216 | setSize(width: number, height: number): Promise 217 | foreground(): Promise 218 | static getForegroundWindow(): Promise 219 | static findWindowByTitle(title: string): Promise 220 | static findWindowByClassName(classname: string): Promise 221 | } 222 | export class OpenedProcess { 223 | readMemoryBool(address: bigint): Promise 224 | readMemoryChainBool(baseAddress: bigint, offsets: Array): Promise 225 | readMemoryUint8(address: bigint): Promise 226 | readMemoryChainUint8(baseAddress: bigint, offsets: Array): Promise 227 | readMemoryInt8(address: bigint): Promise 228 | readMemoryChainInt8(baseAddress: bigint, offsets: Array): Promise 229 | readMemoryUint16(address: bigint): Promise 230 | readMemoryChainUint16(baseAddress: bigint, offsets: Array): Promise 231 | readMemoryInt16(address: bigint): Promise 232 | readMemoryChainInt16(baseAddress: bigint, offsets: Array): Promise 233 | readMemoryUint32(address: bigint): Promise 234 | readMemoryChainUint32(baseAddress: bigint, offsets: Array): Promise 235 | readMemoryInt32(address: bigint): Promise 236 | readMemoryChainInt32(baseAddress: bigint, offsets: Array): Promise 237 | readMemoryUint64(address: bigint): Promise 238 | readMemoryChainUint64(baseAddress: bigint, offsets: Array): Promise 239 | readMemoryInt64(address: bigint): Promise 240 | readMemoryChainInt64(baseAddress: bigint, offsets: Array): Promise 241 | readMemoryUsize(address: bigint): Promise 242 | readMemoryChainUsize(baseAddress: bigint, offsets: Array): Promise 243 | readMemoryFloat32(address: bigint): Promise 244 | readMemoryChainFloat32(baseAddress: bigint, offsets: Array): Promise 245 | readMemoryFloat64(address: bigint): Promise 246 | readMemoryChainFloat64(baseAddress: bigint, offsets: Array): Promise 247 | writeMemoryBool(address: bigint, value: boolean): Promise 248 | writeMemoryChainBool(baseAddress: bigint, offsets: Array, value: boolean): Promise 249 | writeMemoryUint8(address: bigint, value: bigint): Promise 250 | writeMemoryChainUint8(baseAddress: bigint, offsets: Array, value: bigint): Promise 251 | writeMemoryInt8(address: bigint, value: bigint): Promise 252 | writeMemoryChainInt8(baseAddress: bigint, offsets: Array, value: bigint): Promise 253 | writeMemoryUint16(address: bigint, value: bigint): Promise 254 | writeMemoryChainUint16(baseAddress: bigint, offsets: Array, value: bigint): Promise 255 | writeMemoryInt16(address: bigint, value: bigint): Promise 256 | writeMemoryChainInt16(baseAddress: bigint, offsets: Array, value: bigint): Promise 257 | writeMemoryUint32(address: bigint, value: bigint): Promise 258 | writeMemoryChainUint32(baseAddress: bigint, offsets: Array, value: bigint): Promise 259 | writeMemoryInt32(address: bigint, value: bigint): Promise 260 | writeMemoryChainInt32(baseAddress: bigint, offsets: Array, value: bigint): Promise 261 | writeMemoryUint64(address: bigint, value: bigint): Promise 262 | writeMemoryChainUint64(baseAddress: bigint, offsets: Array, value: bigint): Promise 263 | writeMemoryInt64(address: bigint, value: bigint): Promise 264 | writeMemoryChainInt64(baseAddress: bigint, offsets: Array, value: bigint): Promise 265 | writeMemoryUsize(address: bigint, value: bigint): Promise 266 | writeMemoryChainUsize(baseAddress: bigint, offsets: Array, value: bigint): Promise 267 | writeMemoryFloat32(address: bigint, value: number): Promise 268 | writeMemoryChainFloat32(baseAddress: bigint, offsets: Array, value: number): Promise 269 | writeMemoryFloat64(address: bigint, value: number): Promise 270 | writeMemoryChainFloat64(baseAddress: bigint, offsets: Array, value: number): Promise 271 | } 272 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /* prettier-ignore */ 4 | 5 | /* auto-generated by NAPI-RS */ 6 | 7 | const { existsSync, readFileSync } = require('fs') 8 | const { join } = require('path') 9 | 10 | const { platform, arch } = process 11 | 12 | let nativeBinding = null 13 | let localFileExisted = false 14 | let loadError = null 15 | 16 | function isMusl() { 17 | // For Node 10 18 | if (!process.report || typeof process.report.getReport !== 'function') { 19 | try { 20 | const lddPath = require('child_process').execSync('which ldd').toString().trim() 21 | return readFileSync(lddPath, 'utf8').includes('musl') 22 | } catch (e) { 23 | return true 24 | } 25 | } else { 26 | const { glibcVersionRuntime } = process.report.getReport().header 27 | return !glibcVersionRuntime 28 | } 29 | } 30 | 31 | switch (platform) { 32 | case 'android': 33 | switch (arch) { 34 | case 'arm64': 35 | localFileExisted = existsSync(join(__dirname, 'sophia.android-arm64.node')) 36 | try { 37 | if (localFileExisted) { 38 | nativeBinding = require('./sophia.android-arm64.node') 39 | } else { 40 | nativeBinding = require('@deeean/sophia-android-arm64') 41 | } 42 | } catch (e) { 43 | loadError = e 44 | } 45 | break 46 | case 'arm': 47 | localFileExisted = existsSync(join(__dirname, 'sophia.android-arm-eabi.node')) 48 | try { 49 | if (localFileExisted) { 50 | nativeBinding = require('./sophia.android-arm-eabi.node') 51 | } else { 52 | nativeBinding = require('@deeean/sophia-android-arm-eabi') 53 | } 54 | } catch (e) { 55 | loadError = e 56 | } 57 | break 58 | default: 59 | throw new Error(`Unsupported architecture on Android ${arch}`) 60 | } 61 | break 62 | case 'win32': 63 | switch (arch) { 64 | case 'x64': 65 | localFileExisted = existsSync( 66 | join(__dirname, 'sophia.win32-x64-msvc.node') 67 | ) 68 | try { 69 | if (localFileExisted) { 70 | nativeBinding = require('./sophia.win32-x64-msvc.node') 71 | } else { 72 | nativeBinding = require('@deeean/sophia-win32-x64-msvc') 73 | } 74 | } catch (e) { 75 | loadError = e 76 | } 77 | break 78 | case 'ia32': 79 | localFileExisted = existsSync( 80 | join(__dirname, 'sophia.win32-ia32-msvc.node') 81 | ) 82 | try { 83 | if (localFileExisted) { 84 | nativeBinding = require('./sophia.win32-ia32-msvc.node') 85 | } else { 86 | nativeBinding = require('@deeean/sophia-win32-ia32-msvc') 87 | } 88 | } catch (e) { 89 | loadError = e 90 | } 91 | break 92 | case 'arm64': 93 | localFileExisted = existsSync( 94 | join(__dirname, 'sophia.win32-arm64-msvc.node') 95 | ) 96 | try { 97 | if (localFileExisted) { 98 | nativeBinding = require('./sophia.win32-arm64-msvc.node') 99 | } else { 100 | nativeBinding = require('@deeean/sophia-win32-arm64-msvc') 101 | } 102 | } catch (e) { 103 | loadError = e 104 | } 105 | break 106 | default: 107 | throw new Error(`Unsupported architecture on Windows: ${arch}`) 108 | } 109 | break 110 | case 'darwin': 111 | localFileExisted = existsSync(join(__dirname, 'sophia.darwin-universal.node')) 112 | try { 113 | if (localFileExisted) { 114 | nativeBinding = require('./sophia.darwin-universal.node') 115 | } else { 116 | nativeBinding = require('@deeean/sophia-darwin-universal') 117 | } 118 | break 119 | } catch {} 120 | switch (arch) { 121 | case 'x64': 122 | localFileExisted = existsSync(join(__dirname, 'sophia.darwin-x64.node')) 123 | try { 124 | if (localFileExisted) { 125 | nativeBinding = require('./sophia.darwin-x64.node') 126 | } else { 127 | nativeBinding = require('@deeean/sophia-darwin-x64') 128 | } 129 | } catch (e) { 130 | loadError = e 131 | } 132 | break 133 | case 'arm64': 134 | localFileExisted = existsSync( 135 | join(__dirname, 'sophia.darwin-arm64.node') 136 | ) 137 | try { 138 | if (localFileExisted) { 139 | nativeBinding = require('./sophia.darwin-arm64.node') 140 | } else { 141 | nativeBinding = require('@deeean/sophia-darwin-arm64') 142 | } 143 | } catch (e) { 144 | loadError = e 145 | } 146 | break 147 | default: 148 | throw new Error(`Unsupported architecture on macOS: ${arch}`) 149 | } 150 | break 151 | case 'freebsd': 152 | if (arch !== 'x64') { 153 | throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) 154 | } 155 | localFileExisted = existsSync(join(__dirname, 'sophia.freebsd-x64.node')) 156 | try { 157 | if (localFileExisted) { 158 | nativeBinding = require('./sophia.freebsd-x64.node') 159 | } else { 160 | nativeBinding = require('@deeean/sophia-freebsd-x64') 161 | } 162 | } catch (e) { 163 | loadError = e 164 | } 165 | break 166 | case 'linux': 167 | switch (arch) { 168 | case 'x64': 169 | if (isMusl()) { 170 | localFileExisted = existsSync( 171 | join(__dirname, 'sophia.linux-x64-musl.node') 172 | ) 173 | try { 174 | if (localFileExisted) { 175 | nativeBinding = require('./sophia.linux-x64-musl.node') 176 | } else { 177 | nativeBinding = require('@deeean/sophia-linux-x64-musl') 178 | } 179 | } catch (e) { 180 | loadError = e 181 | } 182 | } else { 183 | localFileExisted = existsSync( 184 | join(__dirname, 'sophia.linux-x64-gnu.node') 185 | ) 186 | try { 187 | if (localFileExisted) { 188 | nativeBinding = require('./sophia.linux-x64-gnu.node') 189 | } else { 190 | nativeBinding = require('@deeean/sophia-linux-x64-gnu') 191 | } 192 | } catch (e) { 193 | loadError = e 194 | } 195 | } 196 | break 197 | case 'arm64': 198 | if (isMusl()) { 199 | localFileExisted = existsSync( 200 | join(__dirname, 'sophia.linux-arm64-musl.node') 201 | ) 202 | try { 203 | if (localFileExisted) { 204 | nativeBinding = require('./sophia.linux-arm64-musl.node') 205 | } else { 206 | nativeBinding = require('@deeean/sophia-linux-arm64-musl') 207 | } 208 | } catch (e) { 209 | loadError = e 210 | } 211 | } else { 212 | localFileExisted = existsSync( 213 | join(__dirname, 'sophia.linux-arm64-gnu.node') 214 | ) 215 | try { 216 | if (localFileExisted) { 217 | nativeBinding = require('./sophia.linux-arm64-gnu.node') 218 | } else { 219 | nativeBinding = require('@deeean/sophia-linux-arm64-gnu') 220 | } 221 | } catch (e) { 222 | loadError = e 223 | } 224 | } 225 | break 226 | case 'arm': 227 | localFileExisted = existsSync( 228 | join(__dirname, 'sophia.linux-arm-gnueabihf.node') 229 | ) 230 | try { 231 | if (localFileExisted) { 232 | nativeBinding = require('./sophia.linux-arm-gnueabihf.node') 233 | } else { 234 | nativeBinding = require('@deeean/sophia-linux-arm-gnueabihf') 235 | } 236 | } catch (e) { 237 | loadError = e 238 | } 239 | break 240 | case 'riscv64': 241 | if (isMusl()) { 242 | localFileExisted = existsSync( 243 | join(__dirname, 'sophia.linux-riscv64-musl.node') 244 | ) 245 | try { 246 | if (localFileExisted) { 247 | nativeBinding = require('./sophia.linux-riscv64-musl.node') 248 | } else { 249 | nativeBinding = require('@deeean/sophia-linux-riscv64-musl') 250 | } 251 | } catch (e) { 252 | loadError = e 253 | } 254 | } else { 255 | localFileExisted = existsSync( 256 | join(__dirname, 'sophia.linux-riscv64-gnu.node') 257 | ) 258 | try { 259 | if (localFileExisted) { 260 | nativeBinding = require('./sophia.linux-riscv64-gnu.node') 261 | } else { 262 | nativeBinding = require('@deeean/sophia-linux-riscv64-gnu') 263 | } 264 | } catch (e) { 265 | loadError = e 266 | } 267 | } 268 | break 269 | case 's390x': 270 | localFileExisted = existsSync( 271 | join(__dirname, 'sophia.linux-s390x-gnu.node') 272 | ) 273 | try { 274 | if (localFileExisted) { 275 | nativeBinding = require('./sophia.linux-s390x-gnu.node') 276 | } else { 277 | nativeBinding = require('@deeean/sophia-linux-s390x-gnu') 278 | } 279 | } catch (e) { 280 | loadError = e 281 | } 282 | break 283 | default: 284 | throw new Error(`Unsupported architecture on Linux: ${arch}`) 285 | } 286 | break 287 | default: 288 | throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) 289 | } 290 | 291 | if (!nativeBinding) { 292 | if (loadError) { 293 | throw loadError 294 | } 295 | throw new Error(`Failed to load native binding`) 296 | } 297 | 298 | const { ImageData, MAGENTA, readImageData, saveImageData, imageSearch, multipleImageSearch, Modifiers, Key, Keyboard, MouseButton, Mouse, getScreenSize, takeScreenshot, Window, ProcessAccess, OpenedProcess, openProcess, getProcesses } = nativeBinding 299 | 300 | module.exports.ImageData = ImageData 301 | module.exports.MAGENTA = MAGENTA 302 | module.exports.readImageData = readImageData 303 | module.exports.saveImageData = saveImageData 304 | module.exports.imageSearch = imageSearch 305 | module.exports.multipleImageSearch = multipleImageSearch 306 | module.exports.Modifiers = Modifiers 307 | module.exports.Key = Key 308 | module.exports.Keyboard = Keyboard 309 | module.exports.MouseButton = MouseButton 310 | module.exports.Mouse = Mouse 311 | module.exports.getScreenSize = getScreenSize 312 | module.exports.takeScreenshot = takeScreenshot 313 | module.exports.Window = Window 314 | module.exports.ProcessAccess = ProcessAccess 315 | module.exports.OpenedProcess = OpenedProcess 316 | module.exports.openProcess = openProcess 317 | module.exports.getProcesses = getProcesses 318 | -------------------------------------------------------------------------------- /src/win/keyboard.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Display; 3 | use std::hash::Hash; 4 | use std::sync::Mutex; 5 | use crossbeam_channel::{Receiver, Sender, unbounded}; 6 | use lazy_static::lazy_static; 7 | use napi::bindgen_prelude::*; 8 | use napi_derive::napi; 9 | use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode}; 10 | use windows::core::PCWSTR; 11 | use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}; 12 | use windows::Win32::System::LibraryLoader::GetModuleHandleW; 13 | use windows::Win32::UI::Input::KeyboardAndMouse::{KEYBD_EVENT_FLAGS, KEYEVENTF_UNICODE, SendInput, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, VIRTUAL_KEY, RegisterHotKey, MOD_NOREPEAT, MOD_SHIFT, MOD_ALT, MOD_CONTROL, UnregisterHotKey}; 14 | use windows::Win32::UI::WindowsAndMessaging::{CreateWindowExW, CW_USEDEFAULT, DefWindowProcW, DispatchMessageW, HMENU, PeekMessageW, PM_REMOVE, RegisterClassW, TranslateMessage, WM_HOTKEY, WM_QUIT, WNDCLASSW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED,}; 15 | use crate::utils::{encode_wide, handle_result}; 16 | 17 | #[napi] 18 | #[derive(Debug, PartialEq)] 19 | pub enum Modifiers { 20 | Alt = 0x01, 21 | AltGraph = 0x2, 22 | CapsLock = 0x4, 23 | Control = 0x8, 24 | Fn = 0x10, 25 | FnLock = 0x20, 26 | Meta = 0x40, 27 | NumLock = 0x80, 28 | ScrollLock = 0x100, 29 | Shift = 0x200, 30 | Symbol = 0x400, 31 | SymbolLock = 0x800, 32 | Hyper = 0x1000, 33 | Super = 0x2000, 34 | } 35 | 36 | 37 | #[napi] 38 | #[derive(Debug, PartialEq)] 39 | pub enum Key { 40 | None = 0, 41 | Back = 8, 42 | Tab = 9, 43 | LineFeed = 10, 44 | Clear = 12, 45 | Enter = 13, 46 | Shift = 16, 47 | Control = 17, 48 | Alt = 18, 49 | Pause = 19, 50 | CapsLock = 20, 51 | Esc = 27, 52 | Space = 32, 53 | PageUp = 33, 54 | PageDown = 34, 55 | End = 35, 56 | Home = 36, 57 | ArrowLeft = 37, 58 | ArrowUp = 38, 59 | ArrowRight = 39, 60 | ArrowDown = 40, 61 | Insert = 45, 62 | Delete = 46, 63 | D0 = 48, 64 | D1 = 49, 65 | D2 = 50, 66 | D3 = 51, 67 | D4 = 52, 68 | D5 = 53, 69 | D6 = 54, 70 | D7 = 55, 71 | D8 = 56, 72 | D9 = 57, 73 | A = 65, 74 | B = 66, 75 | C = 67, 76 | D = 68, 77 | E = 69, 78 | F = 70, 79 | G = 71, 80 | H = 72, 81 | I = 73, 82 | J = 74, 83 | K = 75, 84 | L = 76, 85 | M = 77, 86 | N = 78, 87 | O = 79, 88 | P = 80, 89 | Q = 81, 90 | R = 82, 91 | S = 83, 92 | T = 84, 93 | U = 85, 94 | V = 86, 95 | W = 87, 96 | X = 88, 97 | Y = 89, 98 | Z = 90, 99 | LeftWin = 91, 100 | RightWin = 92, 101 | Apps = 93, 102 | Sleep = 95, 103 | NumPad0 = 96, 104 | NumPad1 = 97, 105 | NumPad2 = 98, 106 | NumPad3 = 99, 107 | NumPad4 = 100, 108 | NumPad5 = 101, 109 | NumPad6 = 102, 110 | NumPad7 = 103, 111 | NumPad8 = 104, 112 | NumPad9 = 105, 113 | Multiply = 106, 114 | Add = 107, 115 | Separator = 108, 116 | Subtract = 109, 117 | Decimal = 110, 118 | Divide = 111, 119 | F1 = 112, 120 | F2 = 113, 121 | F3 = 114, 122 | F4 = 115, 123 | F5 = 116, 124 | F6 = 117, 125 | F7 = 118, 126 | F8 = 119, 127 | F9 = 120, 128 | F10 = 121, 129 | F11 = 122, 130 | F12 = 123, 131 | F13 = 124, 132 | F14 = 125, 133 | F15 = 126, 134 | F16 = 127, 135 | F17 = 128, 136 | F18 = 129, 137 | F19 = 130, 138 | F20 = 131, 139 | F21 = 132, 140 | F22 = 133, 141 | F23 = 134, 142 | F24 = 135, 143 | NumLock = 144, 144 | ScrollLock = 145, 145 | LeftShift = 160, 146 | RightShift = 161, 147 | LeftControl = 162, 148 | RightControl = 163, 149 | LeftAlt = 164, 150 | RightAlt = 165, 151 | } 152 | 153 | impl Display for Key { 154 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 155 | let str = match self { 156 | Key::None => "None".to_string(), 157 | Key::Back => "Back".to_string(), 158 | Key::Tab => "Tab".to_string(), 159 | Key::LineFeed => "LineFeed".to_string(), 160 | Key::Clear => "Clear".to_string(), 161 | Key::Enter => "Enter".to_string(), 162 | Key::Shift => "Shift".to_string(), 163 | Key::Control => "Control".to_string(), 164 | Key::Alt => "Alt".to_string(), 165 | Key::Pause => "Pause".to_string(), 166 | Key::CapsLock => "CapsLock".to_string(), 167 | Key::Esc => "Esc".to_string(), 168 | Key::Space => "Space".to_string(), 169 | Key::PageUp => "PageUp".to_string(), 170 | Key::PageDown => "PageDown".to_string(), 171 | Key::End => "End".to_string(), 172 | Key::Home => "Home".to_string(), 173 | Key::ArrowLeft => "ArrowLeft".to_string(), 174 | Key::ArrowUp => "ArrowUp".to_string(), 175 | Key::ArrowRight => "ArrowRight".to_string(), 176 | Key::ArrowDown => "ArrowDown".to_string(), 177 | Key::Insert => "Insert".to_string(), 178 | Key::Delete => "Delete".to_string(), 179 | Key::D0 => "D0".to_string(), 180 | Key::D1 => "D1".to_string(), 181 | Key::D2 => "D2".to_string(), 182 | Key::D3 => "D3".to_string(), 183 | Key::D4 => "D4".to_string(), 184 | Key::D5 => "D5".to_string(), 185 | Key::D6 => "D6".to_string(), 186 | Key::D7 => "D7".to_string(), 187 | Key::D8 => "D8".to_string(), 188 | Key::D9 => "D9".to_string(), 189 | Key::A => "A".to_string(), 190 | Key::B => "B".to_string(), 191 | Key::C => "C".to_string(), 192 | Key::D => "D".to_string(), 193 | Key::E => "E".to_string(), 194 | Key::F => "F".to_string(), 195 | Key::G => "G".to_string(), 196 | Key::H => "H".to_string(), 197 | Key::I => "I".to_string(), 198 | Key::J => "J".to_string(), 199 | Key::K => "K".to_string(), 200 | Key::L => "L".to_string(), 201 | Key::M => "M".to_string(), 202 | Key::N => "N".to_string(), 203 | Key::O => "O".to_string(), 204 | Key::P => "P".to_string(), 205 | Key::Q => "Q".to_string(), 206 | Key::R => "R".to_string(), 207 | Key::S => "S".to_string(), 208 | Key::T => "T".to_string(), 209 | Key::U => "U".to_string(), 210 | Key::V => "V".to_string(), 211 | Key::W => "W".to_string(), 212 | Key::X => "X".to_string(), 213 | Key::Y => "Y".to_string(), 214 | Key::Z => "Z".to_string(), 215 | Key::LeftWin => "LeftWin".to_string(), 216 | Key::RightWin => "RightWin".to_string(), 217 | Key::Apps => "Apps".to_string(), 218 | Key::Sleep => "Sleep".to_string(), 219 | Key::NumPad0 => "NumPad0".to_string(), 220 | Key::NumPad1 => "NumPad1".to_string(), 221 | Key::NumPad2 => "NumPad2".to_string(), 222 | Key::NumPad3 => "NumPad3".to_string(), 223 | Key::NumPad4 => "NumPad4".to_string(), 224 | Key::NumPad5 => "NumPad5".to_string(), 225 | Key::NumPad6 => "NumPad6".to_string(), 226 | Key::NumPad7 => "NumPad7".to_string(), 227 | Key::NumPad8 => "NumPad8".to_string(), 228 | Key::NumPad9 => "NumPad9".to_string(), 229 | Key::Multiply => "Multiply".to_string(), 230 | Key::Add => "Add".to_string(), 231 | Key::Separator => "Separator".to_string(), 232 | Key::Subtract => "Subtract".to_string(), 233 | Key::Decimal => "Decimal".to_string(), 234 | Key::Divide => "Divide".to_string(), 235 | Key::F1 => "F1".to_string(), 236 | Key::F2 => "F2".to_string(), 237 | Key::F3 => "F3".to_string(), 238 | Key::F4 => "F4".to_string(), 239 | Key::F5 => "F5".to_string(), 240 | Key::F6 => "F6".to_string(), 241 | Key::F7 => "F7".to_string(), 242 | Key::F8 => "F8".to_string(), 243 | Key::F9 => "F9".to_string(), 244 | Key::F10 => "F10".to_string(), 245 | Key::F11 => "F11".to_string(), 246 | Key::F12 => "F12".to_string(), 247 | Key::F13 => "F13".to_string(), 248 | Key::F14 => "F14".to_string(), 249 | Key::F15 => "F15".to_string(), 250 | Key::F16 => "F16".to_string(), 251 | Key::F17 => "F17".to_string(), 252 | Key::F18 => "F18".to_string(), 253 | Key::F19 => "F19".to_string(), 254 | Key::F20 => "F20".to_string(), 255 | Key::F21 => "F21".to_string(), 256 | Key::F22 => "F22".to_string(), 257 | Key::F23 => "F23".to_string(), 258 | Key::F24 => "F24".to_string(), 259 | Key::NumLock => "NumLock".to_string(), 260 | Key::ScrollLock => "ScrollLock".to_string(), 261 | Key::LeftShift => "LeftShift".to_string(), 262 | Key::RightShift => "RightShift".to_string(), 263 | Key::LeftControl => "LeftControl".to_string(), 264 | Key::RightControl => "RightControl".to_string(), 265 | Key::LeftAlt => "LeftAlt".to_string(), 266 | Key::RightAlt => "RightAlt".to_string(), 267 | }; 268 | write!(f, "{}", str) 269 | } 270 | } 271 | 272 | lazy_static! { 273 | static ref GLOBAL_HOTKEY_PREPARED: Mutex = Mutex::new(false); 274 | static ref GLOBAL_HOTKEY_CHANNEL: (Sender, Receiver) = unbounded(); 275 | static ref GLOBAL_HOTKEY_CALLBACKS: Mutex>>> = Mutex::new(HashMap::new()); 276 | } 277 | 278 | #[derive(Debug)] 279 | pub struct Hotkey { 280 | id: u32, 281 | key: Key, 282 | mods: Vec, 283 | } 284 | 285 | impl Hotkey { 286 | pub fn new(mods: Vec, key: Key) -> Self { 287 | let id = Hotkey::generate_hash(mods.clone(), key); 288 | 289 | Self { 290 | id, 291 | key, 292 | mods, 293 | } 294 | } 295 | 296 | pub fn generate_hash(mods: Vec, key: Key) -> u32 { 297 | let mut hotkey_str = String::new(); 298 | if mods.contains(&Modifiers::Shift) { 299 | hotkey_str.push_str("Shift+"); 300 | } 301 | 302 | if mods.contains(&Modifiers::Control) { 303 | hotkey_str.push_str("Control+"); 304 | } 305 | 306 | if mods.contains(&Modifiers::Alt) { 307 | hotkey_str.push_str("Alt+"); 308 | } 309 | 310 | if mods.contains(&Modifiers::Super) { 311 | hotkey_str.push_str("Super+"); 312 | } 313 | 314 | hotkey_str.push_str(&key.to_string()); 315 | 316 | let mut hasher = std::collections::hash_map::DefaultHasher::new(); 317 | hotkey_str.hash(&mut hasher); 318 | std::hash::Hasher::finish(&hasher) as u32 319 | } 320 | } 321 | 322 | enum GlobalHotkeyMessage { 323 | Register(Hotkey), 324 | Unregister(u32), 325 | } 326 | 327 | 328 | fn prepare_global_hotkey() { 329 | std::thread::spawn(move || { 330 | unsafe { 331 | let class_name = PCWSTR(encode_wide("global_hotkey_manager").as_ptr()); 332 | let hinstance = GetModuleHandleW(None).unwrap(); 333 | 334 | let mut wnd_class = WNDCLASSW::default(); 335 | wnd_class.lpfnWndProc = Some(global_hotkey_proc); 336 | wnd_class.lpszClassName = class_name; 337 | wnd_class.hInstance = HINSTANCE(hinstance.0 as _); 338 | 339 | RegisterClassW(&wnd_class); 340 | 341 | let hwnd = CreateWindowExW( 342 | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW, 343 | class_name, 344 | None, 345 | WS_OVERLAPPED, 346 | CW_USEDEFAULT, 347 | 0, 348 | CW_USEDEFAULT, 349 | 0, 350 | HWND::default(), 351 | HMENU::default(), 352 | hinstance, 353 | None 354 | ); 355 | 356 | let mut msg = std::mem::zeroed(); 357 | 358 | loop { 359 | while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).0 != 0 { 360 | if msg.message == WM_QUIT { 361 | break; 362 | } 363 | 364 | TranslateMessage(&msg); 365 | DispatchMessageW(&msg); 366 | } 367 | 368 | while let Ok(msg) = GLOBAL_HOTKEY_CHANNEL.1.try_recv() { 369 | match msg { 370 | GlobalHotkeyMessage::Register(hotkey) => { 371 | let mut mods = MOD_NOREPEAT; 372 | 373 | hotkey.mods.iter().for_each(|it| { 374 | match it { 375 | Modifiers::Shift => mods |= MOD_SHIFT, 376 | Modifiers::Super | Modifiers::Meta => mods |= MOD_CONTROL, 377 | Modifiers::Alt => mods |= MOD_ALT, 378 | Modifiers::Control => mods |= MOD_CONTROL, 379 | _ => {} 380 | } 381 | }); 382 | 383 | // @todo: error handling 384 | let _ = RegisterHotKey(hwnd, hotkey.id as i32, mods, hotkey.key as u32); 385 | } 386 | GlobalHotkeyMessage::Unregister(id) => { 387 | // @todo: error handling 388 | let _ = UnregisterHotKey(hwnd, id as i32); 389 | } 390 | } 391 | } 392 | } 393 | } 394 | }); 395 | } 396 | 397 | unsafe extern "system" fn global_hotkey_proc( 398 | hwnd: HWND, 399 | msg: u32, 400 | wparam: WPARAM, 401 | lparam: LPARAM, 402 | ) -> LRESULT { 403 | if msg == WM_HOTKEY { 404 | let id = wparam.0 as u32; 405 | 406 | let callbacks = GLOBAL_HOTKEY_CALLBACKS.lock().unwrap(); 407 | if let Some(vec) = callbacks.get(&id) { 408 | vec.iter().for_each(|it| { 409 | it.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking); 410 | }); 411 | } 412 | } 413 | 414 | DefWindowProcW(hwnd, msg, wparam, lparam) 415 | } 416 | 417 | #[napi] 418 | pub struct Keyboard { 419 | 420 | } 421 | 422 | #[napi] 423 | impl Keyboard { 424 | #[napi] 425 | pub async fn press(key: Key) -> Result<()> { 426 | let task = tokio::spawn(async move { 427 | unsafe { 428 | let mut input = INPUT::default(); 429 | input.r#type = INPUT_KEYBOARD; 430 | input.Anonymous.ki.wVk = VIRTUAL_KEY(key as u16); 431 | input.Anonymous.ki.dwFlags = KEYBD_EVENT_FLAGS::from(KEYEVENTF_UNICODE); 432 | input.Anonymous.ki.time = 0; 433 | SendInput(&[input], std::mem::size_of::() as i32); 434 | } 435 | 436 | Ok(()) 437 | }); 438 | 439 | handle_result(task).await 440 | } 441 | 442 | #[napi] 443 | pub async fn release(key: Key) -> Result<()> { 444 | let task = tokio::spawn(async move { 445 | unsafe { 446 | let mut input = INPUT::default(); 447 | input.r#type = INPUT_KEYBOARD; 448 | input.Anonymous.ki.wVk = VIRTUAL_KEY(key as u16); 449 | input.Anonymous.ki.dwFlags = KEYBD_EVENT_FLAGS::from(KEYEVENTF_UNICODE | KEYEVENTF_KEYUP); 450 | input.Anonymous.ki.time = 0; 451 | SendInput(&[input], std::mem::size_of::() as i32); 452 | } 453 | 454 | Ok(()) 455 | }); 456 | 457 | handle_result(task).await 458 | } 459 | 460 | #[napi] 461 | pub async fn click(key: Key) -> Result<()> { 462 | let task = tokio::spawn(async move { 463 | unsafe { 464 | let mut inputs = Vec::new(); 465 | 466 | let mut input = INPUT::default(); 467 | input.r#type = INPUT_KEYBOARD; 468 | input.Anonymous.ki.wVk = VIRTUAL_KEY(key as u16); 469 | input.Anonymous.ki.dwFlags = KEYBD_EVENT_FLAGS::from(KEYEVENTF_UNICODE); 470 | input.Anonymous.ki.time = 0; 471 | inputs.push(input); 472 | 473 | input.Anonymous.ki.dwFlags |= KEYEVENTF_KEYUP; 474 | inputs.push(input); 475 | 476 | SendInput(inputs.as_slice(), std::mem::size_of::() as i32); 477 | } 478 | 479 | Ok(()) 480 | }); 481 | 482 | handle_result(task).await 483 | } 484 | 485 | #[napi] 486 | pub async fn typing(text: String) -> Result<()> { 487 | let task = tokio::spawn(async move { 488 | unsafe { 489 | let text = text 490 | .encode_utf16() 491 | .collect::>(); 492 | 493 | let mut inputs = Vec::new(); 494 | 495 | for c in text { 496 | let mut input = INPUT::default(); 497 | input.r#type = INPUT_KEYBOARD; 498 | input.Anonymous.ki.dwFlags = KEYEVENTF_UNICODE; 499 | input.Anonymous.ki.wScan = c; 500 | input.Anonymous.ki.time = 0; 501 | inputs.push(input); 502 | 503 | input.Anonymous.ki.dwFlags |= KEYEVENTF_KEYUP; 504 | inputs.push(input); 505 | } 506 | 507 | SendInput(inputs.as_slice(), std::mem::size_of::() as i32); 508 | } 509 | 510 | Ok(()) 511 | }); 512 | 513 | handle_result(task).await 514 | } 515 | 516 | #[napi] 517 | pub fn register_hotkey(mods: Vec, key: Key, callback: JsFunction) -> u32 { 518 | if !*GLOBAL_HOTKEY_PREPARED.lock().unwrap() { 519 | prepare_global_hotkey(); 520 | *GLOBAL_HOTKEY_PREPARED.lock().unwrap() = true; 521 | } 522 | 523 | let tsfn: ThreadsafeFunction<()> = callback.create_threadsafe_function(0, |_ctx| { 524 | Ok(vec![0]) 525 | }).unwrap(); 526 | 527 | let hotkey = Hotkey::new(mods, key); 528 | let mut callbacks = GLOBAL_HOTKEY_CALLBACKS.lock().unwrap(); 529 | let id = hotkey.id; 530 | let vec: &mut Vec> = callbacks.entry(id).or_insert(Vec::new()); 531 | vec.push(tsfn); 532 | 533 | GLOBAL_HOTKEY_CHANNEL.0.send(GlobalHotkeyMessage::Register(hotkey)).unwrap(); 534 | 535 | id 536 | } 537 | 538 | #[napi] 539 | pub fn unregister_hotkey(id: u32) { 540 | let mut callbacks = GLOBAL_HOTKEY_CALLBACKS.lock().unwrap(); 541 | callbacks.remove(&id); 542 | GLOBAL_HOTKEY_CHANNEL.0.send(GlobalHotkeyMessage::Unregister(id)).unwrap(); 543 | } 544 | } -------------------------------------------------------------------------------- /src/screen.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | use crate::geometry::Point; 4 | use crate::utils::handle_result; 5 | 6 | #[napi] 7 | #[derive(Debug, Clone)] 8 | pub struct ImageData { 9 | pub data: Vec, 10 | pub width: u32, 11 | pub height: u32, 12 | pub pixel_width: u8, 13 | } 14 | 15 | #[napi(object)] 16 | #[derive(Debug, Clone)] 17 | pub struct Color { 18 | pub r: u8, 19 | pub g: u8, 20 | pub b: u8, 21 | } 22 | 23 | #[napi] 24 | pub const MAGENTA: Color = Color { 25 | r: 255, 26 | g: 0, 27 | b: 255, 28 | }; 29 | 30 | #[napi] 31 | pub async fn read_image_data(path: String) -> Result { 32 | let task = tokio::spawn(async move { 33 | let img = match image::open(path) { 34 | Ok(img) => img, 35 | Err(e) => return Err(format!("Error: {:?}", e)), 36 | }; 37 | 38 | let width = img.width(); 39 | let height = img.height(); 40 | let pixel_width = img.color().bytes_per_pixel(); 41 | let data = img.as_bytes().to_vec(); 42 | 43 | Ok(ImageData { 44 | data, 45 | width, 46 | height, 47 | pixel_width, 48 | }) 49 | }); 50 | 51 | handle_result(task).await 52 | } 53 | 54 | #[napi] 55 | pub async fn save_image_data(path: String, image_data: &ImageData) -> Result<()> { 56 | let path = path.clone(); 57 | let image_data = image_data.clone(); 58 | 59 | let task = tokio::spawn(async move { 60 | let image_buffer = 61 | match image::RgbaImage::from_raw(image_data.width, image_data.height, image_data.data) { 62 | Some(buffer) => buffer, 63 | None => return Err("Failed to create image buffer".to_string()), 64 | }; 65 | 66 | match image::DynamicImage::ImageRgba8(image_buffer).save(path) { 67 | Ok(_) => Ok(()), 68 | Err(e) => Err(format!("Failed to save image: {:?}", e)), 69 | } 70 | }); 71 | 72 | handle_result(task).await 73 | } 74 | 75 | #[napi] 76 | pub async fn image_search( 77 | source: &ImageData, 78 | target: &ImageData, 79 | variant: Option, 80 | trans_color: Option, 81 | ) -> Result> { 82 | let variant = variant.unwrap_or(0); 83 | let source = source.clone(); 84 | let target = target.clone(); 85 | 86 | let task = tokio::spawn(async move { 87 | Ok(if let Some(trans_color) = trans_color { 88 | image_search_trans_inner(&source, &target, variant, trans_color) 89 | } else { 90 | image_search_inner(&source, &target, variant) 91 | }) 92 | }); 93 | 94 | handle_result(task).await 95 | } 96 | 97 | #[napi] 98 | pub async fn multiple_image_search( 99 | source: &ImageData, 100 | target: &ImageData, 101 | variant: Option, 102 | trans_color: Option, 103 | ) -> Result> { 104 | let variant = variant.unwrap_or(0); 105 | let source = source.clone(); 106 | let target = target.clone(); 107 | 108 | let task = tokio::spawn(async move { 109 | Ok(if let Some(trans_color) = trans_color { 110 | multiple_image_search_trans_inner(&source, &target, variant, trans_color) 111 | } else { 112 | multiple_image_search_inner(&source, &target, variant) 113 | }) 114 | }); 115 | 116 | handle_result(task).await 117 | } 118 | 119 | fn multiple_image_search_inner( 120 | source: &ImageData, 121 | target: &ImageData, 122 | variant: i32, 123 | ) -> Vec { 124 | let source_pixels = source.data.as_slice(); 125 | let target_pixels = target.data.as_slice(); 126 | 127 | let source_width = source.width; 128 | let source_height = source.height; 129 | 130 | let target_width = target.width; 131 | let target_height = target.height; 132 | 133 | let source_pixel_width = source.pixel_width as u32; 134 | let target_pixel_width = target.pixel_width as u32; 135 | 136 | let source_pixel_count = source_width * source_height; 137 | let target_pixel_count = target_width * target_height; 138 | let mut points = Vec::new(); 139 | 140 | if variant == 0 { 141 | for i in 0..source_pixel_count { 142 | let sx = i % source_width; 143 | let sy = i / source_width; 144 | 145 | if sx + target_width > source_width || sy + target_height > source_height { 146 | continue; 147 | } 148 | 149 | let mut is_found = true; 150 | 151 | for j in 0..target_pixel_count { 152 | let tx = j % target_width; 153 | let ty = j / target_width; 154 | 155 | let x = sx + tx; 156 | let y = sy + ty; 157 | 158 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 159 | let source_red = source_pixels[source_index]; 160 | let source_green = source_pixels[source_index + 1]; 161 | let source_blue = source_pixels[source_index + 2]; 162 | 163 | let target_index = (j * target_pixel_width) as usize; 164 | 165 | let red = target_pixels[target_index]; 166 | let green = target_pixels[target_index + 1]; 167 | let blue = target_pixels[target_index + 2]; 168 | 169 | is_found = source_red == red && source_green == green && source_blue == blue; 170 | 171 | if !is_found { 172 | break; 173 | } 174 | } 175 | 176 | if is_found { 177 | points.push(Point { x: sx as i32, y: sy as i32 }); 178 | } 179 | } 180 | } else { 181 | for i in 0..source_pixel_count { 182 | let sx = i % source_width; 183 | let sy = i / source_width; 184 | 185 | if sx + target_width > source_width || sy + target_height > source_height { 186 | continue; 187 | } 188 | 189 | let mut is_found = true; 190 | 191 | for j in 0..target_pixel_count { 192 | let tx = j % target_width; 193 | let ty = j / target_width; 194 | 195 | let x = sx + tx; 196 | let y = sy + ty; 197 | 198 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 199 | let source_red = source_pixels[source_index] as i32; 200 | let source_green = source_pixels[source_index + 1] as i32; 201 | let source_blue = source_pixels[source_index + 2] as i32; 202 | 203 | let target_index = (j * target_pixel_width) as usize; 204 | 205 | let red = target_pixels[target_index] as i32; 206 | let green = target_pixels[target_index + 1] as i32; 207 | let blue = target_pixels[target_index + 2] as i32; 208 | 209 | let red_low = if source_red < variant { 210 | 0 211 | } else { 212 | source_red - variant 213 | }; 214 | let red_high = if source_red + variant > 255 { 215 | 255 216 | } else { 217 | source_red + variant 218 | }; 219 | 220 | let green_low = if source_green < variant { 221 | 0 222 | } else { 223 | source_green - variant 224 | }; 225 | let green_high = if source_green + variant > 255 { 226 | 255 227 | } else { 228 | source_green + variant 229 | }; 230 | 231 | let blue_low = if source_blue < variant { 232 | 0 233 | } else { 234 | source_blue - variant 235 | }; 236 | let blue_high = if source_blue + variant > 255 { 237 | 255 238 | } else { 239 | source_blue + variant 240 | }; 241 | 242 | is_found = red >= red_low 243 | && red <= red_high 244 | && green >= green_low 245 | && green <= green_high 246 | && blue >= blue_low 247 | && blue <= blue_high; 248 | 249 | if !is_found { 250 | break; 251 | } 252 | } 253 | 254 | if is_found { 255 | points.push(Point { 256 | x: sx as i32, 257 | y: sy as i32 258 | }); 259 | } 260 | } 261 | } 262 | 263 | points 264 | } 265 | 266 | fn image_search_inner( 267 | source: &ImageData, 268 | target: &ImageData, 269 | variant: i32, 270 | ) -> Option { 271 | let source_pixels = source.data.as_slice(); 272 | let target_pixels = target.data.as_slice(); 273 | 274 | let source_width = source.width; 275 | let source_height = source.height; 276 | 277 | let target_width = target.width; 278 | let target_height = target.height; 279 | 280 | let source_pixel_width = source.pixel_width as u32; 281 | let target_pixel_width = target.pixel_width as u32; 282 | 283 | let source_pixel_count = source_width * source_height; 284 | let target_pixel_count = target_width * target_height; 285 | 286 | if variant == 0 { 287 | for i in 0..source_pixel_count { 288 | let sx = i % source_width; 289 | let sy = i / source_width; 290 | 291 | if sx + target_width > source_width || sy + target_height > source_height { 292 | continue; 293 | } 294 | 295 | let mut is_found = true; 296 | 297 | for j in 0..target_pixel_count { 298 | let tx = j % target_width; 299 | let ty = j / target_width; 300 | 301 | let x = sx + tx; 302 | let y = sy + ty; 303 | 304 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 305 | let source_red = source_pixels[source_index]; 306 | let source_green = source_pixels[source_index + 1]; 307 | let source_blue = source_pixels[source_index + 2]; 308 | 309 | let target_index = (j * target_pixel_width) as usize; 310 | 311 | let red = target_pixels[target_index]; 312 | let green = target_pixels[target_index + 1]; 313 | let blue = target_pixels[target_index + 2]; 314 | 315 | is_found = source_red == red && source_green == green && source_blue == blue; 316 | 317 | if !is_found { 318 | break; 319 | } 320 | } 321 | 322 | if is_found { 323 | return Some(Point { x: sx as i32, y: sy as i32 }); 324 | } 325 | } 326 | } else { 327 | for i in 0..source_pixel_count { 328 | let sx = i % source_width; 329 | let sy = i / source_width; 330 | 331 | if sx + target_width > source_width || sy + target_height > source_height { 332 | continue; 333 | } 334 | 335 | let mut is_found = true; 336 | 337 | for j in 0..target_pixel_count { 338 | let tx = j % target_width; 339 | let ty = j / target_width; 340 | 341 | let x = sx + tx; 342 | let y = sy + ty; 343 | 344 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 345 | let source_red = source_pixels[source_index] as i32; 346 | let source_green = source_pixels[source_index + 1] as i32; 347 | let source_blue = source_pixels[source_index + 2] as i32; 348 | 349 | let target_index = (j * target_pixel_width) as usize; 350 | 351 | let red = target_pixels[target_index] as i32; 352 | let green = target_pixels[target_index + 1] as i32; 353 | let blue = target_pixels[target_index + 2] as i32; 354 | 355 | let red_low = if source_red < variant { 356 | 0 357 | } else { 358 | source_red - variant 359 | }; 360 | let red_high = if source_red + variant > 255 { 361 | 255 362 | } else { 363 | source_red + variant 364 | }; 365 | 366 | let green_low = if source_green < variant { 367 | 0 368 | } else { 369 | source_green - variant 370 | }; 371 | let green_high = if source_green + variant > 255 { 372 | 255 373 | } else { 374 | source_green + variant 375 | }; 376 | 377 | let blue_low = if source_blue < variant { 378 | 0 379 | } else { 380 | source_blue - variant 381 | }; 382 | let blue_high = if source_blue + variant > 255 { 383 | 255 384 | } else { 385 | source_blue + variant 386 | }; 387 | 388 | is_found = red >= red_low 389 | && red <= red_high 390 | && green >= green_low 391 | && green <= green_high 392 | && blue >= blue_low 393 | && blue <= blue_high; 394 | 395 | if !is_found { 396 | break; 397 | } 398 | } 399 | 400 | if is_found { 401 | return Some(Point { x: sx as i32, y: sy as i32 }); 402 | } 403 | } 404 | } 405 | 406 | None 407 | } 408 | 409 | fn image_search_trans_inner( 410 | source: &ImageData, 411 | target: &ImageData, 412 | variant: i32, 413 | trans_color: Color, 414 | ) -> Option { 415 | let source_pixels = source.data.as_slice(); 416 | let target_pixels = target.data.as_slice(); 417 | 418 | let source_width = source.width; 419 | let source_height = source.height; 420 | 421 | let target_width = target.width; 422 | let target_height = target.height; 423 | 424 | let source_pixel_width = source.pixel_width as u32; 425 | let target_pixel_width = target.pixel_width as u32; 426 | 427 | let source_pixel_count = source_width * source_height; 428 | let target_pixel_count = target_width * target_height; 429 | 430 | if variant == 0 { 431 | for i in 0..source_pixel_count { 432 | let sx = i % source_width; 433 | let sy = i / source_width; 434 | 435 | if sx + target_width > source_width || sy + target_height > source_height { 436 | continue; 437 | } 438 | 439 | let mut is_found = true; 440 | 441 | for j in 0..target_pixel_count { 442 | let tx = j % target_width; 443 | let ty = j / target_width; 444 | 445 | let x = sx + tx; 446 | let y = sy + ty; 447 | 448 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 449 | let source_red = source_pixels[source_index]; 450 | let source_green = source_pixels[source_index + 1]; 451 | let source_blue = source_pixels[source_index + 2]; 452 | 453 | let target_index = (j * target_pixel_width) as usize; 454 | 455 | let red = target_pixels[target_index]; 456 | let green = target_pixels[target_index + 1]; 457 | let blue = target_pixels[target_index + 2]; 458 | 459 | is_found = (trans_color.r == red && trans_color.g == green && trans_color.b == blue) 460 | || (source_red == red && source_green == green && source_blue == blue); 461 | 462 | if !is_found { 463 | break; 464 | } 465 | } 466 | 467 | if is_found { 468 | return Some(Point { x: sx as i32, y: sy as i32 }); 469 | } 470 | } 471 | } else { 472 | for i in 0..source_pixel_count { 473 | let sx = i % source_width; 474 | let sy = i / source_width; 475 | 476 | if sx + target_width > source_width || sy + target_height > source_height { 477 | continue; 478 | } 479 | 480 | let mut is_found = true; 481 | 482 | for j in 0..target_pixel_count { 483 | let tx = j % target_width; 484 | let ty = j / target_width; 485 | 486 | let x = sx + tx; 487 | let y = sy + ty; 488 | 489 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 490 | let source_red = source_pixels[source_index] as i32; 491 | let source_green = source_pixels[source_index + 1] as i32; 492 | let source_blue = source_pixels[source_index + 2] as i32; 493 | 494 | let target_index = (j * target_pixel_width) as usize; 495 | 496 | let red = target_pixels[target_index] as i32; 497 | let green = target_pixels[target_index + 1] as i32; 498 | let blue = target_pixels[target_index + 2] as i32; 499 | 500 | if trans_color.r == red as u8 && trans_color.g == green as u8 && trans_color.b == blue as u8 { 501 | continue; 502 | } 503 | 504 | let red_low = if source_red < variant { 505 | 0 506 | } else { 507 | source_red - variant 508 | }; 509 | let red_high = if source_red + variant > 255 { 510 | 255 511 | } else { 512 | source_red + variant 513 | }; 514 | 515 | let green_low = if source_green < variant { 516 | 0 517 | } else { 518 | source_green - variant 519 | }; 520 | let green_high = if source_green + variant > 255 { 521 | 255 522 | } else { 523 | source_green + variant 524 | }; 525 | 526 | let blue_low = if source_blue < variant { 527 | 0 528 | } else { 529 | source_blue - variant 530 | }; 531 | let blue_high = if source_blue + variant > 255 { 532 | 255 533 | } else { 534 | source_blue + variant 535 | }; 536 | 537 | is_found = red >= red_low 538 | && red <= red_high 539 | && green >= green_low 540 | && green <= green_high 541 | && blue >= blue_low 542 | && blue <= blue_high; 543 | 544 | if !is_found { 545 | break; 546 | } 547 | } 548 | 549 | if is_found { 550 | return Some(Point { x: sx as i32, y: sy as i32 }); 551 | } 552 | } 553 | } 554 | 555 | None 556 | } 557 | 558 | fn multiple_image_search_trans_inner( 559 | source: &ImageData, 560 | target: &ImageData, 561 | variant: i32, 562 | trans: Color, 563 | ) -> Vec { 564 | let source_pixels = source.data.as_slice(); 565 | let target_pixels = target.data.as_slice(); 566 | 567 | let source_width = source.width; 568 | let source_height = source.height; 569 | 570 | let target_width = target.width; 571 | let target_height = target.height; 572 | 573 | let source_pixel_width = source.pixel_width as u32; 574 | let target_pixel_width = target.pixel_width as u32; 575 | 576 | let source_pixel_count = source_width * source_height; 577 | let target_pixel_count = target_width * target_height; 578 | let mut points = Vec::new(); 579 | 580 | if variant == 0 { 581 | for i in 0..source_pixel_count { 582 | let sx = i % source_width; 583 | let sy = i / source_width; 584 | 585 | if sx + target_width > source_width || sy + target_height > source_height { 586 | continue; 587 | } 588 | 589 | let mut is_found = true; 590 | 591 | for j in 0..target_pixel_count { 592 | let tx = j % target_width; 593 | let ty = j / target_width; 594 | 595 | let x = sx + tx; 596 | let y = sy + ty; 597 | 598 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 599 | let source_red = source_pixels[source_index]; 600 | let source_green = source_pixels[source_index + 1]; 601 | let source_blue = source_pixels[source_index + 2]; 602 | 603 | let target_index = (j * target_pixel_width) as usize; 604 | 605 | let red = target_pixels[target_index]; 606 | let green = target_pixels[target_index + 1]; 607 | let blue = target_pixels[target_index + 2]; 608 | 609 | is_found = (trans.r == red && trans.g == green && trans.b == blue) 610 | || source_red == red && source_green == green && source_blue == blue; 611 | 612 | if !is_found { 613 | break; 614 | } 615 | } 616 | 617 | if is_found { 618 | points.push(Point { x: sx as i32, y: sy as i32 }); 619 | } 620 | } 621 | } else { 622 | for i in 0..source_pixel_count { 623 | let sx = i % source_width; 624 | let sy = i / source_width; 625 | 626 | if sx + target_width > source_width || sy + target_height > source_height { 627 | continue; 628 | } 629 | 630 | let mut is_found = true; 631 | 632 | for j in 0..target_pixel_count { 633 | let tx = j % target_width; 634 | let ty = j / target_width; 635 | 636 | let x = sx + tx; 637 | let y = sy + ty; 638 | 639 | let source_index = ((y * source_width + x) * source_pixel_width) as usize; 640 | let source_red = source_pixels[source_index] as i32; 641 | let source_green = source_pixels[source_index + 1] as i32; 642 | let source_blue = source_pixels[source_index + 2] as i32; 643 | 644 | let target_index = (j * target_pixel_width) as usize; 645 | 646 | let red = target_pixels[target_index] as i32; 647 | let green = target_pixels[target_index + 1] as i32; 648 | let blue = target_pixels[target_index + 2] as i32; 649 | 650 | if trans.r == red as u8 && trans.g == green as u8 && trans.b == blue as u8 { 651 | continue; 652 | } 653 | 654 | let red_low = if source_red < variant { 655 | 0 656 | } else { 657 | source_red - variant 658 | }; 659 | let red_high = if source_red + variant > 255 { 660 | 255 661 | } else { 662 | source_red + variant 663 | }; 664 | 665 | let green_low = if source_green < variant { 666 | 0 667 | } else { 668 | source_green - variant 669 | }; 670 | let green_high = if source_green + variant > 255 { 671 | 255 672 | } else { 673 | source_green + variant 674 | }; 675 | 676 | let blue_low = if source_blue < variant { 677 | 0 678 | } else { 679 | source_blue - variant 680 | }; 681 | let blue_high = if source_blue + variant > 255 { 682 | 255 683 | } else { 684 | source_blue + variant 685 | }; 686 | 687 | is_found = red >= red_low 688 | && red <= red_high 689 | && green >= green_low 690 | && green <= green_high 691 | && blue >= blue_low 692 | && blue <= blue_high; 693 | 694 | if !is_found { 695 | break; 696 | } 697 | } 698 | 699 | if is_found { 700 | points.push(Point { x: sx as i32, y: sy as i32 }); 701 | } 702 | } 703 | } 704 | 705 | points 706 | } -------------------------------------------------------------------------------- /src/win/memory.rs: -------------------------------------------------------------------------------- 1 | use napi::bindgen_prelude::*; 2 | use napi_derive::napi; 3 | use windows::Win32::Foundation::{CloseHandle, HANDLE}; 4 | use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory; 5 | use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS}; 6 | use windows::Win32::System::ProcessStatus::{EnumProcessModules, GetModuleFileNameExW}; 7 | use windows::Win32::System::Threading::{OpenProcess, PROCESS_ACCESS_RIGHTS, PROCESS_ALL_ACCESS, PROCESS_CREATE_PROCESS, PROCESS_CREATE_THREAD, PROCESS_DELETE, PROCESS_DUP_HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_READ_CONTROL, PROCESS_SET_INFORMATION, PROCESS_SET_LIMITED_INFORMATION, PROCESS_SET_QUOTA, PROCESS_SET_SESSIONID, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE, PROCESS_WRITE_DAC, PROCESS_WRITE_OWNER}; 8 | use crate::utils::{bigint_to_u8, bigint_to_i8, bigint_to_u16, bigint_to_i16, bigint_to_u32, bigint_to_u64, bigint_to_i32, decode_wide, handle_result, bigint_to_i64, bigint_to_usize}; 9 | 10 | #[napi(object)] 11 | #[derive(Debug)] 12 | pub struct Process { 13 | pub pid: u32, 14 | pub name: String, 15 | } 16 | 17 | #[napi] 18 | pub enum ProcessAccess { 19 | AllAccess, 20 | CreateProcess, 21 | CreateThread, 22 | Delete, 23 | DupHandle, 24 | QueryInformation, 25 | QueryLimitedInformation, 26 | ReadControl, 27 | SetInformation, 28 | SetLimitedInformation, 29 | SetQuota, 30 | SetSessionId, 31 | Synchronize, 32 | Terminate, 33 | VmOperation, 34 | VmRead, 35 | VmWrite, 36 | WriteDac, 37 | WriteOwner, 38 | } 39 | 40 | impl From for PROCESS_ACCESS_RIGHTS { 41 | fn from(access: ProcessAccess) -> Self { 42 | match access { 43 | ProcessAccess::AllAccess => PROCESS_ALL_ACCESS, 44 | ProcessAccess::CreateProcess => PROCESS_CREATE_PROCESS, 45 | ProcessAccess::CreateThread => PROCESS_CREATE_THREAD, 46 | ProcessAccess::Delete => PROCESS_DELETE, 47 | ProcessAccess::DupHandle => PROCESS_DUP_HANDLE, 48 | ProcessAccess::QueryInformation => PROCESS_QUERY_INFORMATION, 49 | ProcessAccess::QueryLimitedInformation => PROCESS_QUERY_LIMITED_INFORMATION, 50 | ProcessAccess::ReadControl => PROCESS_READ_CONTROL, 51 | ProcessAccess::SetInformation => PROCESS_SET_INFORMATION, 52 | ProcessAccess::SetLimitedInformation => PROCESS_SET_LIMITED_INFORMATION, 53 | ProcessAccess::SetQuota => PROCESS_SET_QUOTA, 54 | ProcessAccess::SetSessionId => PROCESS_SET_SESSIONID, 55 | ProcessAccess::Synchronize => PROCESS_SYNCHRONIZE, 56 | ProcessAccess::Terminate => PROCESS_TERMINATE, 57 | ProcessAccess::VmOperation => PROCESS_VM_OPERATION, 58 | ProcessAccess::VmRead => PROCESS_VM_READ, 59 | ProcessAccess::VmWrite => PROCESS_VM_WRITE, 60 | ProcessAccess::WriteDac => PROCESS_WRITE_DAC, 61 | ProcessAccess::WriteOwner => PROCESS_WRITE_OWNER, 62 | } 63 | } 64 | } 65 | #[napi] 66 | #[derive(Debug)] 67 | pub struct OpenedProcess { 68 | handle: HANDLE, 69 | base_address: usize, 70 | } 71 | 72 | pub fn get_base_address(handle: HANDLE) -> std::result::Result { 73 | let mut lph_module = windows::Win32::Foundation::HMODULE::default(); 74 | let mut cb_needed = 0u32; 75 | 76 | if let Err(_) = unsafe { 77 | EnumProcessModules( 78 | handle, 79 | &mut lph_module, 80 | std::mem::size_of_val(&lph_module) as u32, 81 | &mut cb_needed, 82 | ) 83 | } { 84 | return Err(std::io::Error::last_os_error()); 85 | } 86 | 87 | let mut sz_module_name = [0u16; 256]; 88 | 89 | unsafe { 90 | GetModuleFileNameExW(handle, lph_module, &mut sz_module_name); 91 | } 92 | 93 | Ok(lph_module.0 as usize) 94 | } 95 | 96 | fn read_memory_inner(handle: HANDLE, address: usize) -> std::result::Result { 97 | let mut result = T::default(); 98 | 99 | if let Err(_) = unsafe { 100 | ReadProcessMemory( 101 | handle, 102 | address as *const _, 103 | &mut result as *mut _ as *mut _, 104 | std::mem::size_of::(), 105 | None, 106 | ) 107 | } { 108 | return Err(std::io::Error::last_os_error()); 109 | } 110 | 111 | Ok(result) 112 | } 113 | 114 | fn read_memory_chain_inner( 115 | handle: HANDLE, 116 | base_address: usize, 117 | offsets: &[usize], 118 | ) -> std::result::Result { 119 | let mut address = read_memory_inner::(handle, base_address)?; 120 | let last_index = offsets.len() - 1; 121 | 122 | for i in 0..last_index { 123 | address = read_memory_inner::(handle, address + offsets[i])?; 124 | } 125 | 126 | Ok(read_memory_inner::(handle, address + offsets[last_index])?) 127 | } 128 | 129 | async fn read_memory( 130 | handle: HANDLE, 131 | address: u64, 132 | ) -> Result where T: Send + 'static, { 133 | let task = tokio::spawn(async move { 134 | match read_memory_inner::(handle, address as usize) { 135 | Ok(result) => Ok(result), 136 | Err(err) => Err(format!("Failed to read memory: {:?}", err)), 137 | } 138 | }); 139 | 140 | handle_result(task).await 141 | } 142 | 143 | async fn read_memory_chain( 144 | handle: HANDLE, 145 | base_address: u64, 146 | offsets: Vec, 147 | ) -> Result where T: Send + 'static, { 148 | let task = tokio::spawn(async move { 149 | let offsets: Vec = offsets.iter().map(|&x| x as usize).collect(); 150 | 151 | match read_memory_chain_inner::(handle, base_address as usize, &offsets) { 152 | Ok(result) => Ok(result), 153 | Err(err) => Err(format!("Failed to read memory chain: {:?}", err)), 154 | } 155 | }); 156 | 157 | handle_result(task).await 158 | } 159 | 160 | fn write_memory_inner( 161 | handle: HANDLE, 162 | address: usize, 163 | value: T, 164 | ) -> std::result::Result<(), std::io::Error> { 165 | if let Err(_) = unsafe { 166 | windows::Win32::System::Diagnostics::Debug::WriteProcessMemory( 167 | handle, 168 | address as *mut _, 169 | &value as *const _ as *const _, 170 | std::mem::size_of::(), 171 | None, 172 | ) 173 | } { 174 | return Err(std::io::Error::last_os_error()); 175 | } 176 | 177 | Ok(()) 178 | } 179 | 180 | fn write_memory_chain_inner( 181 | handle: HANDLE, 182 | base_address: usize, 183 | offsets: &[usize], 184 | value: T, 185 | ) -> std::result::Result<(), std::io::Error> { 186 | let mut address = read_memory_inner::(handle, base_address)?; 187 | let last_index = offsets.len() - 1; 188 | 189 | for i in 0..last_index { 190 | address = read_memory_inner::(handle, address + offsets[i])?; 191 | } 192 | 193 | write_memory_inner::(handle, address + offsets[last_index], value) 194 | } 195 | 196 | async fn write_memory( 197 | handle: HANDLE, 198 | address: u64, 199 | value: T, 200 | ) -> Result<()> where T: Send + 'static, { 201 | let task = tokio::spawn(async move { 202 | match write_memory_inner::(handle, address as usize, value) { 203 | Ok(_) => Ok(()), 204 | Err(err) => Err(format!("Failed to write memory: {:?}", err)), 205 | } 206 | }); 207 | 208 | handle_result(task).await 209 | } 210 | 211 | async fn write_memory_chain( 212 | handle: HANDLE, 213 | base_address: u64, 214 | offsets: Vec, 215 | value: T, 216 | ) -> Result<()> where T: Send + 'static, { 217 | let task = tokio::spawn(async move { 218 | let offsets: Vec = offsets.iter().map(|&x| x as usize).collect(); 219 | 220 | match write_memory_chain_inner::(handle, base_address as usize, &offsets, value) { 221 | Ok(_) => Ok(()), 222 | Err(err) => Err(format!("Failed to write memory chain: {:?}", err)), 223 | } 224 | }); 225 | 226 | handle_result(task).await 227 | } 228 | 229 | #[napi] 230 | impl OpenedProcess { 231 | #[napi] 232 | pub async fn read_memory_bool(&self, address: BigInt) -> Result { 233 | let address = bigint_to_u64(address); 234 | read_memory::(self.handle, address).await 235 | } 236 | 237 | #[napi] 238 | pub async fn read_memory_chain_bool(&self, base_address: BigInt, offsets: Vec) -> Result { 239 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 240 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 241 | read_memory_chain::(self.handle, base_address, offsets).await 242 | } 243 | 244 | #[napi] 245 | pub async fn read_memory_uint8(&self, address: BigInt) -> Result { 246 | let address = bigint_to_u64(address); 247 | let result = read_memory::(self.handle, address).await?; 248 | Ok(BigInt { 249 | sign_bit: false, 250 | words: vec![result as u64], 251 | }) 252 | } 253 | 254 | #[napi] 255 | pub async fn read_memory_chain_uint8(&self, base_address: BigInt, offsets: Vec) -> Result { 256 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 257 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 258 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 259 | Ok(BigInt { 260 | sign_bit: false, 261 | words: vec![result as u64], 262 | }) 263 | } 264 | 265 | #[napi] 266 | pub async fn read_memory_int8(&self, address: BigInt) -> Result { 267 | let address = bigint_to_u64(address); 268 | let result = read_memory::(self.handle, address).await?; 269 | Ok(BigInt { 270 | sign_bit: result < 0, 271 | words: vec![result as u64], 272 | }) 273 | } 274 | 275 | #[napi] 276 | pub async fn read_memory_chain_int8(&self, base_address: BigInt, offsets: Vec) -> Result { 277 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 278 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 279 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 280 | Ok(BigInt { 281 | sign_bit: result < 0, 282 | words: vec![result as u64], 283 | }) 284 | } 285 | 286 | #[napi] 287 | pub async fn read_memory_uint16(&self, address: BigInt) -> Result { 288 | let address = bigint_to_u64(address); 289 | let result = read_memory::(self.handle, address).await?; 290 | Ok(BigInt { 291 | sign_bit: false, 292 | words: vec![result as u64], 293 | }) 294 | } 295 | 296 | #[napi] 297 | pub async fn read_memory_chain_uint16(&self, base_address: BigInt, offsets: Vec) -> Result { 298 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 299 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 300 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 301 | Ok(BigInt { 302 | sign_bit: false, 303 | words: vec![result as u64], 304 | }) 305 | } 306 | 307 | #[napi] 308 | pub async fn read_memory_int16(&self, address: BigInt) -> Result { 309 | let address = bigint_to_u64(address); 310 | let result = read_memory::(self.handle, address).await?; 311 | Ok(BigInt { 312 | sign_bit: result < 0, 313 | words: vec![result as u64], 314 | }) 315 | } 316 | 317 | #[napi] 318 | pub async fn read_memory_chain_int16(&self, base_address: BigInt, offsets: Vec) -> Result { 319 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 320 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 321 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 322 | Ok(BigInt { 323 | sign_bit: result < 0, 324 | words: vec![result as u64], 325 | }) 326 | } 327 | 328 | #[napi] 329 | pub async fn read_memory_uint32(&self, address: BigInt) -> Result { 330 | let address = bigint_to_u64(address); 331 | let result = read_memory::(self.handle, address).await?; 332 | Ok(BigInt { 333 | sign_bit: false, 334 | words: vec![result as u64], 335 | }) 336 | } 337 | 338 | #[napi] 339 | pub async fn read_memory_chain_uint32(&self, base_address: BigInt, offsets: Vec) -> Result { 340 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 341 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 342 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 343 | Ok(BigInt { 344 | sign_bit: false, 345 | words: vec![result as u64], 346 | }) 347 | } 348 | 349 | #[napi] 350 | pub async fn read_memory_int32(&self, address: BigInt) -> Result { 351 | let address = bigint_to_u64(address); 352 | let result = read_memory::(self.handle, address).await?; 353 | Ok(BigInt { 354 | sign_bit: result < 0, 355 | words: vec![result as u64], 356 | }) 357 | } 358 | 359 | #[napi] 360 | pub async fn read_memory_chain_int32(&self, base_address: BigInt, offsets: Vec) -> Result { 361 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 362 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 363 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 364 | Ok(BigInt { 365 | sign_bit: result < 0, 366 | words: vec![result as u64], 367 | }) 368 | } 369 | 370 | #[napi] 371 | pub async fn read_memory_uint64(&self, address: BigInt) -> Result { 372 | let address = bigint_to_u64(address); 373 | let result = read_memory::(self.handle, address).await?; 374 | Ok(BigInt { 375 | sign_bit: false, 376 | words: vec![result], 377 | }) 378 | } 379 | 380 | #[napi] 381 | pub async fn read_memory_chain_uint64(&self, base_address: BigInt, offsets: Vec) -> Result { 382 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 383 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 384 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 385 | Ok(BigInt { 386 | sign_bit: false, 387 | words: vec![result], 388 | }) 389 | } 390 | 391 | #[napi] 392 | pub async fn read_memory_int64(&self, address: BigInt) -> Result { 393 | let address = bigint_to_u64(address); 394 | let result = read_memory::(self.handle, address).await?; 395 | Ok(BigInt { 396 | sign_bit: result < 0, 397 | words: vec![result as u64], 398 | }) 399 | } 400 | 401 | #[napi] 402 | pub async fn read_memory_chain_int64(&self, base_address: BigInt, offsets: Vec) -> Result { 403 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 404 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 405 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 406 | Ok(BigInt { 407 | sign_bit: result < 0, 408 | words: vec![result as u64], 409 | }) 410 | } 411 | 412 | #[napi] 413 | pub async fn read_memory_usize(&self, address: BigInt) -> Result { 414 | let address = bigint_to_u64(address); 415 | let result = read_memory::(self.handle, address).await?; 416 | Ok(BigInt { 417 | sign_bit: false, 418 | words: vec![result as u64], 419 | }) 420 | } 421 | 422 | #[napi] 423 | pub async fn read_memory_chain_usize(&self, base_address: BigInt, offsets: Vec) -> Result { 424 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 425 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 426 | let result = read_memory_chain::(self.handle, base_address, offsets).await?; 427 | Ok(BigInt { 428 | sign_bit: false, 429 | words: vec![result as u64], 430 | }) 431 | } 432 | 433 | #[napi] 434 | pub async fn read_memory_float32(&self, address: BigInt) -> Result { 435 | let address = bigint_to_u64(address); 436 | read_memory::(self.handle, address).await 437 | } 438 | 439 | #[napi] 440 | pub async fn read_memory_chain_float32(&self, base_address: BigInt, offsets: Vec) -> Result { 441 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 442 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 443 | read_memory_chain::(self.handle, base_address, offsets).await 444 | } 445 | 446 | #[napi] 447 | pub async fn read_memory_float64(&self, address: BigInt) -> Result { 448 | let address = bigint_to_u64(address); 449 | read_memory::(self.handle, address).await 450 | } 451 | 452 | #[napi] 453 | pub async fn read_memory_chain_float64(&self, base_address: BigInt, offsets: Vec) -> Result { 454 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 455 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 456 | read_memory_chain::(self.handle, base_address, offsets).await 457 | } 458 | 459 | #[napi] 460 | pub async fn write_memory_bool(&self, address: BigInt, value: bool) -> Result<()> { 461 | let address = bigint_to_u64(address); 462 | write_memory::(self.handle, address, value).await 463 | } 464 | 465 | #[napi] 466 | pub async fn write_memory_chain_bool(&self, base_address: BigInt, offsets: Vec, value: bool) -> Result<()> { 467 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 468 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 469 | write_memory_chain::(self.handle, base_address, offsets, value).await 470 | } 471 | 472 | #[napi] 473 | pub async fn write_memory_uint8(&self, address: BigInt, value: BigInt) -> Result<()> { 474 | let address = bigint_to_u64(address); 475 | let value = bigint_to_u8(value); 476 | write_memory::(self.handle, address, value).await 477 | } 478 | 479 | #[napi] 480 | pub async fn write_memory_chain_uint8(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 481 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 482 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 483 | let value = bigint_to_u8(value); 484 | write_memory_chain::(self.handle, base_address, offsets, value).await 485 | } 486 | 487 | #[napi] 488 | pub async fn write_memory_int8(&self, address: BigInt, value: BigInt) -> Result<()> { 489 | let address = bigint_to_u64(address); 490 | let value = bigint_to_i8(value); 491 | write_memory::(self.handle, address, value).await 492 | } 493 | 494 | #[napi] 495 | pub async fn write_memory_chain_int8(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 496 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 497 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 498 | let value = bigint_to_i8(value); 499 | write_memory_chain::(self.handle, base_address, offsets, value).await 500 | } 501 | 502 | #[napi] 503 | pub async fn write_memory_uint16(&self, address: BigInt, value: BigInt) -> Result<()> { 504 | let address = bigint_to_u64(address); 505 | let value = bigint_to_u16(value); 506 | write_memory::(self.handle, address, value).await 507 | } 508 | 509 | #[napi] 510 | pub async fn write_memory_chain_uint16(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 511 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 512 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 513 | let value = bigint_to_u16(value); 514 | write_memory_chain::(self.handle, base_address, offsets, value).await 515 | } 516 | 517 | #[napi] 518 | pub async fn write_memory_int16(&self, address: BigInt, value: BigInt) -> Result<()> { 519 | let address = bigint_to_u64(address); 520 | let value = bigint_to_i16(value); 521 | write_memory::(self.handle, address, value).await 522 | } 523 | 524 | #[napi] 525 | pub async fn write_memory_chain_int16(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 526 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 527 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 528 | let value = bigint_to_i16(value); 529 | write_memory_chain::(self.handle, base_address, offsets, value).await 530 | } 531 | 532 | #[napi] 533 | pub async fn write_memory_uint32(&self, address: BigInt, value: BigInt) -> Result<()> { 534 | let address = bigint_to_u64(address); 535 | let value = bigint_to_u32(value); 536 | write_memory::(self.handle, address, value).await 537 | } 538 | 539 | #[napi] 540 | pub async fn write_memory_chain_uint32(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 541 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 542 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 543 | let value = bigint_to_u32(value); 544 | write_memory_chain::(self.handle, base_address, offsets, value).await 545 | } 546 | 547 | #[napi] 548 | pub async fn write_memory_int32(&self, address: BigInt, value: BigInt) -> Result<()> { 549 | let address = bigint_to_u64(address); 550 | let value = bigint_to_i32(value); 551 | write_memory::(self.handle, address, value).await 552 | } 553 | 554 | #[napi] 555 | pub async fn write_memory_chain_int32(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 556 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 557 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 558 | let value = bigint_to_i32(value); 559 | write_memory_chain::(self.handle, base_address, offsets, value).await 560 | } 561 | 562 | #[napi] 563 | pub async fn write_memory_uint64(&self, address: BigInt, value: BigInt) -> Result<()> { 564 | let address = bigint_to_u64(address); 565 | let value = bigint_to_u64(value); 566 | write_memory::(self.handle, address, value).await 567 | } 568 | 569 | #[napi] 570 | pub async fn write_memory_chain_uint64(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 571 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 572 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 573 | let value = bigint_to_u64(value); 574 | write_memory_chain::(self.handle, base_address, offsets, value).await 575 | } 576 | 577 | #[napi] 578 | pub async fn write_memory_int64(&self, address: BigInt, value: BigInt) -> Result<()> { 579 | let address = bigint_to_u64(address); 580 | let value = bigint_to_i64(value); 581 | write_memory::(self.handle, address, value).await 582 | } 583 | 584 | #[napi] 585 | pub async fn write_memory_chain_int64(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 586 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 587 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 588 | let value = bigint_to_i64(value); 589 | write_memory_chain::(self.handle, base_address, offsets, value).await 590 | } 591 | 592 | #[napi] 593 | pub async fn write_memory_usize(&self, address: BigInt, value: BigInt) -> Result<()> { 594 | let address = bigint_to_u64(address); 595 | let value = bigint_to_usize(value); 596 | write_memory::(self.handle, address, value).await 597 | } 598 | 599 | #[napi] 600 | pub async fn write_memory_chain_usize(&self, base_address: BigInt, offsets: Vec, value: BigInt) -> Result<()> { 601 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 602 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 603 | let value = bigint_to_usize(value); 604 | write_memory_chain::(self.handle, base_address, offsets, value).await 605 | } 606 | 607 | #[napi] 608 | pub async fn write_memory_float32(&self, address: BigInt, value: f64) -> Result<()> { 609 | let address = bigint_to_u64(address); 610 | write_memory::(self.handle, address, value as f32).await 611 | } 612 | 613 | #[napi] 614 | pub async fn write_memory_chain_float32(&self, base_address: BigInt, offsets: Vec, value: f64) -> Result<()> { 615 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 616 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 617 | write_memory_chain::(self.handle, base_address, offsets, value as f32).await 618 | } 619 | 620 | #[napi] 621 | pub async fn write_memory_float64(&self, address: BigInt, value: f64) -> Result<()> { 622 | let address = bigint_to_u64(address); 623 | write_memory::(self.handle, address, value).await 624 | } 625 | 626 | #[napi] 627 | pub async fn write_memory_chain_float64(&self, base_address: BigInt, offsets: Vec, value: f64) -> Result<()> { 628 | let base_address = self.base_address as u64 + bigint_to_u64(base_address); 629 | let offsets: Vec = offsets.into_iter().map(|x| bigint_to_u64(x)).collect(); 630 | write_memory_chain::(self.handle, base_address, offsets, value).await 631 | } 632 | } 633 | 634 | impl Drop for OpenedProcess { 635 | fn drop(&mut self) { 636 | unsafe { 637 | CloseHandle(self.handle).unwrap(); 638 | } 639 | } 640 | } 641 | 642 | #[napi] 643 | pub async fn open_process(access: ProcessAccess, pid: u32) -> Result { 644 | let task = tokio::spawn(async move { 645 | match unsafe { OpenProcess(access.into(), false, pid) } { 646 | Ok(handle) => { 647 | let base_address = match get_base_address(handle) { 648 | Ok(address) => address, 649 | Err(err) => { 650 | return Err(format!("Failed to get base address: {:?}", err)); 651 | } 652 | }; 653 | 654 | Ok(OpenedProcess { 655 | handle, 656 | base_address 657 | }) 658 | }, 659 | Err(err) => { 660 | return Err(format!("Failed to open process: {:?}", err)); 661 | } 662 | } 663 | }); 664 | 665 | handle_result(task).await 666 | } 667 | 668 | #[napi] 669 | pub async fn get_processes() -> Result> { 670 | let task = tokio::spawn(async move { 671 | let mut processes = Vec::new(); 672 | 673 | let snapshot = match unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) } { 674 | Ok(snapshot) => snapshot, 675 | Err(err) => { 676 | return Err(format!("Failed to create snapshot: {:?}", err)); 677 | } 678 | }; 679 | 680 | let mut process_entry = PROCESSENTRY32W::default(); 681 | process_entry.dwSize = std::mem::size_of::() as u32; 682 | 683 | if let Err(err) = unsafe { Process32FirstW(snapshot, &mut process_entry) } { 684 | return Err(format!("Failed to get first process: {:?}", err)); 685 | } 686 | 687 | loop { 688 | let curr = decode_wide(&process_entry.szExeFile); 689 | 690 | processes.push(Process { 691 | pid: process_entry.th32ProcessID, 692 | name: curr, 693 | }); 694 | 695 | if let Err(_) = unsafe { Process32NextW(snapshot, &mut process_entry) } { 696 | break; 697 | } 698 | } 699 | 700 | if let Err(err) = unsafe { CloseHandle(snapshot) } { 701 | return Err(format!("Failed to close snapshot: {:?}", err)); 702 | } 703 | 704 | Ok(processes) 705 | }); 706 | 707 | handle_result(task).await 708 | } --------------------------------------------------------------------------------