1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! Window-procedure hook: turns Win32 messages into framework-neutral UiInput.
//!
//! While the menu is open we swallow input instead of forwarding it, so the
//! game never sees the clicks and keystrokes meant for the menu — no camera
//! spin while you drag a slider. Everything else is passed straight through.
use std::sync::atomic::{AtomicIsize, Ordering};
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows_sys::Win32::UI::WindowsAndMessaging::{
CallWindowProcW, GetWindowLongPtrW, SetWindowLongPtrW, GWLP_WNDPROC, WHEEL_DELTA, WM_CHAR, WM_KEYDOWN, WM_KEYUP,
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL,
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SYSKEYDOWN, WM_SYSKEYUP,
};
use crate::state;
/// The window procedure GLFW installed, which we chain to.
static ORIGINAL: AtomicIsize = AtomicIsize::new(0);
pub const VK_INSERT: usize = 0x2D;
/// Subclass the game's window.
///
/// # Safety
/// `hwnd` must be the game's top-level window.
pub unsafe fn install(hwnd: HWND) -> bool {
// Installing twice would store our own procedure as "the original", and
// restoring that later leaves a dangling pointer on the window.
if ORIGINAL.load(Ordering::SeqCst) != 0 {
return true;
}
let prev = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, wnd_proc as isize);
if prev == 0 {
return false;
}
ORIGINAL.store(prev, Ordering::SeqCst);
true
}
/// Put GLFW's window procedure back.
///
/// # Safety
/// `hwnd` must be the window `install` was called with.
pub unsafe fn remove(hwnd: HWND) -> bool {
let prev = ORIGINAL.swap(0, Ordering::SeqCst);
if prev == 0 {
return true;
}
// If something subclassed the window after us, putting the old procedure
// back would cut that other hook out of the chain. Leave ours in place and
// say so; the caller keeps the module resident rather than unmapping code
// the window still points at.
if GetWindowLongPtrW(hwnd, GWLP_WNDPROC) != wnd_proc as isize {
ORIGINAL.store(prev, Ordering::SeqCst);
return false;
}
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, prev);
true
}
fn chain(hwnd: HWND, msg: u32, w: WPARAM, l: LPARAM) -> LRESULT {
let prev = ORIGINAL.load(Ordering::SeqCst);
if prev == 0 {
return 0;
}
// SAFETY: `prev` is the procedure GLFW registered for this window.
unsafe {
CallWindowProcW(
Some(std::mem::transmute::<
isize,
unsafe extern "system" fn(HWND, u32, WPARAM, LPARAM) -> LRESULT,
>(prev)),
hwnd,
msg,
w,
l,
)
}
}
unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, w: WPARAM, l: LPARAM) -> LRESULT {
if msg == WM_KEYDOWN {
// A rebind in progress captures the next key first — including a new
// menu key — so it is never mistaken for a toggle or a module bind.
if state::with(|s| s.binding.is_some()).unwrap_or(false) {
state::with(|s| s.handle_key(w as u32));
return 0;
}
// The menu key (rebindable, default Insert) toggles the menu and is
// never forwarded to the game.
let menu_key = state::with(|s| s.cfg.menu_key).unwrap_or(VK_INSERT as u32);
if w as u32 == menu_key {
state::with(|s| s.toggle_menu());
return 0;
}
// Module binds fire only while the menu is shut, so typing in it cannot
// trigger a module.
let handled = state::with(|s| if !s.menu_open { s.handle_key(w as u32) } else { false })
.unwrap_or(false);
if handled {
return 0;
}
}
let open = state::with(|s| s.menu_open).unwrap_or(false);
if !open {
return chain(hwnd, msg, w, l);
}
let consumed = feed(msg, w, l);
if consumed {
return 0;
}
chain(hwnd, msg, w, l)
}
/// Translate one Win32 message into framework-neutral UiInput. Returns true if
/// the game should not also see it.
fn feed(msg: u32, w: WPARAM, l: LPARAM) -> bool {
use crate::state::UiInput;
match msg {
WM_MOUSEMOVE => {
let x = (l & 0xFFFF) as i16 as f32;
let y = ((l >> 16) & 0xFFFF) as i16 as f32;
state::with(|s| {
s.pointer = (x, y);
s.events.push(UiInput::MouseMove(x, y));
});
true
}
WM_LBUTTONDOWN => push(UiInput::MouseButton(0, true)),
WM_LBUTTONUP => push(UiInput::MouseButton(0, false)),
WM_RBUTTONDOWN => push(UiInput::MouseButton(1, true)),
WM_RBUTTONUP => push(UiInput::MouseButton(1, false)),
WM_MBUTTONDOWN => push(UiInput::MouseButton(2, true)),
WM_MBUTTONUP => push(UiInput::MouseButton(2, false)),
WM_MOUSEWHEEL => {
let delta = ((w >> 16) & 0xFFFF) as i16 as f32 / WHEEL_DELTA as f32;
push(UiInput::Wheel(delta))
}
WM_CHAR => {
if let Some(c) = char::from_u32(w as u32) {
if !c.is_control() {
let _ = push(UiInput::Char(c));
}
}
true
}
WM_KEYDOWN | WM_SYSKEYDOWN => push(UiInput::Key(w as u32, true)),
WM_KEYUP | WM_SYSKEYUP => push(UiInput::Key(w as u32, false)),
_ => false,
}
}
fn push(ev: crate::state::UiInput) -> bool {
state::with(|s| s.events.push(ev));
true
}