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
// 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.
#pragma once
#include <windows.h>
#include "state.hpp"
namespace lodestone::input {
/// The window procedure GLFW installed, which we chain to.
inline LONG_PTR g_original = 0;
inline LRESULT chain(HWND hwnd, UINT msg, WPARAM w, LPARAM l) {
if (!g_original) return 0;
return CallWindowProcW(reinterpret_cast<WNDPROC>(g_original), hwnd, msg, w, l);
}
inline void push(const state::UiInput& ev) {
state::with([&](state::Shared& s) { s.events.push_back(ev); });
}
/// Translate one message into UiInput. Returns true when the game should not
/// also see it.
inline bool feed(UINT msg, WPARAM w, LPARAM l) {
using K = state::UiInput::Kind;
state::UiInput ev{};
switch (msg) {
case WM_MOUSEMOVE:
ev.kind = K::MouseMove;
ev.x = static_cast<float>(static_cast<short>(LOWORD(l)));
ev.y = static_cast<float>(static_cast<short>(HIWORD(l)));
push(ev);
return true;
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP:
ev.kind = K::MouseButton;
ev.button = (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP) ? 0
: (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONUP) ? 1
: 2;
ev.down = (msg == WM_LBUTTONDOWN || msg == WM_RBUTTONDOWN || msg == WM_MBUTTONDOWN);
push(ev);
return true;
case WM_MOUSEWHEEL:
ev.kind = K::Wheel;
ev.y = static_cast<float>(GET_WHEEL_DELTA_WPARAM(w)) / static_cast<float>(WHEEL_DELTA);
push(ev);
return true;
case WM_CHAR:
if (w >= 32 && w != 127) {
ev.kind = K::Char;
ev.codepoint = static_cast<unsigned int>(w);
push(ev);
}
return true;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
ev.kind = K::Key;
ev.vk = static_cast<uint32_t>(w);
ev.down = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
push(ev);
return true;
default:
return false;
}
}
inline LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM w, LPARAM l) {
if (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN) {
// 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.
bool binding = state::with([](state::Shared& s) { return s.binding_active; });
if (binding) {
state::with([&](state::Shared& s) { s.handle_key(static_cast<uint32_t>(w)); });
return 0;
}
// The menu key (rebindable, Insert by default) is never forwarded.
uint32_t menu_key = state::with([](state::Shared& s) { return s.cfg.menu_key; });
if (static_cast<uint32_t>(w) == menu_key) {
state::with([](state::Shared& s) {
s.menu_open = !s.menu_open;
if (!s.menu_open) s.events.clear();
});
return 0;
}
// Module binds fire only while the menu is shut, so typing in it cannot
// trigger a module.
bool handled = state::with([&](state::Shared& s) {
return s.menu_open ? false : s.handle_key(static_cast<uint32_t>(w));
});
if (handled) return 0;
}
bool open = state::with([](state::Shared& s) { return s.menu_open; });
if (open && feed(msg, w, l)) return 0;
return chain(hwnd, msg, w, l);
}
/// Subclass the game's window.
inline bool install(HWND hwnd) {
// Installing twice would store our own procedure as "the original", and
// restoring that later leaves the window pointing at a dangling address.
if (g_original) return true;
LONG_PTR prev = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&wnd_proc));
if (!prev) return false;
g_original = prev;
return true;
}
/// Put GLFW's window procedure back.
inline bool remove(HWND hwnd) {
if (!g_original) return true;
// If something subclassed the window after us, restoring the old procedure
// 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) != reinterpret_cast<LONG_PTR>(&wnd_proc)) {
return false;
}
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, g_original);
g_original = 0;
return true;
}
} // namespace lodestone::input