source: Daodan/src/Patches/Input.c@ 1169

Last change on this file since 1169 was 1163, checked in by rossy, 3 years ago

Daodan: Add new local input system based on Raw Input

File size: 29.2 KB
Line 
1#include <stdlib.h>
2#include <stdarg.h>
3
4#include "../Daodan.h"
5#include "Input.h"
6#include "../Oni/Oni.h"
7#include "../Daodan_Config.h"
8#include "../Daodan_Patch.h"
9#include "Utility.h"
10
11typedef struct {
12 LItActionDescription descr;
13 DDtActionEventType eventType;
14
15 DDtCustomActionCallback callback;
16 intptr_t ctx;
17} DDtCustomAction;
18
19static DDtCustomAction DDgCustomActions[100] = { 0 };
20
21// Extra keys (make sure these don't collide with Oni's LIc_* keys)
22enum {
23 DDcKey_MouseButton5 = LIcKey_Max,
24 DDcKey_ScrollUp,
25 DDcKey_ScrollDown,
26 DDcKey_ScrollLeft,
27 DDcKey_ScrollRight,
28};
29
30// Enhanced version of LIgInputNames from Oni with some extra keys
31static const LItInputName DDgInputNames[] = {
32 // The following key names are mapped in Oni
33 {"fkey1", LIcKey_FKey1}, {"fkey2", LIcKey_FKey2}, {"fkey3", LIcKey_FKey3},
34 {"fkey4", LIcKey_FKey4}, {"fkey5", LIcKey_FKey5}, {"fkey6", LIcKey_FKey6},
35 {"fkey7", LIcKey_FKey7}, {"fkey8", LIcKey_FKey8}, {"fkey9", LIcKey_FKey9},
36 {"fkey10", LIcKey_FKey10}, {"fkey11", LIcKey_FKey11},
37 {"fkey12", LIcKey_FKey12}, {"fkey13", LIcKey_FKey13},
38 {"fkey14", LIcKey_FKey14}, {"fkey15", LIcKey_FKey15},
39 {"backspace", LIcKey_Backspace}, {"tab", LIcKey_Tab},
40 {"capslock", LIcKey_CapsLock}, {"enter", LIcKey_Enter},
41 {"leftshift", LIcKey_LeftShift}, {"rightshift", LIcKey_RightShift},
42 {"leftcontrol", LIcKey_LeftControl},
43 {"leftwindows", 0x94}, {"leftoption", 0x94}, // Does nothing in Oni
44 {"leftalt", LIcKey_LeftAlt}, {"space", ' '}, {"rightalt", LIcKey_RightAlt},
45 {"rightoption", 0x97}, {"rightwindows", 0x97}, // Does nothing in Oni
46 {"rightcontrol", LIcKey_RightControl}, {"printscreen", LIcKey_PrintScreen},
47 {"scrolllock", LIcKey_ScrollLock}, {"pause", LIcKey_Pause},
48 {"insert", LIcKey_Insert}, {"home", LIcKey_Home}, {"pageup", LIcKey_PageUp},
49 {"delete", LIcKey_Delete}, {"end", LIcKey_End},
50 {"pagedown", LIcKey_PageDown}, {"uparrow", LIcKey_UpArrow},
51 {"leftarrow", LIcKey_LeftArrow}, {"downarrow", LIcKey_DownArrow},
52 {"rightarrow", LIcKey_RightArrow}, {"numlock", LIcKey_NumLock},
53 {"divide", LIcKey_Divide}, {"multiply", LIcKey_Multiply},
54 {"subtract", LIcKey_Subtract}, {"add", LIcKey_Add},
55 {"numpadequals", LIcKey_NumpadEquals}, {"numpadenter", LIcKey_NumpadEnter},
56 {"decimal", LIcKey_Decimal}, {"numpad0", LIcKey_Numpad0},
57 {"numpad1", LIcKey_Numpad1}, {"numpad2", LIcKey_Numpad2},
58 {"numpad3", LIcKey_Numpad3}, {"numpad4", LIcKey_Numpad4},
59 {"numpad5", LIcKey_Numpad5}, {"numpad6", LIcKey_Numpad6},
60 {"numpad7", LIcKey_Numpad7}, {"numpad8", LIcKey_Numpad8},
61 {"numpad9", LIcKey_Numpad9}, {"backslash", '\\'}, {"semicolon", ';'},
62 {"period", '.'}, {"apostrophe", '\''}, {"slash", '/'}, {"leftbracket", '['},
63 {"rightbracket", ']'}, {"comma", ','},
64 {"mousebutton1", LIcKey_MouseButton1},
65 {"mousebutton2", LIcKey_MouseButton2},
66 {"mousebutton3", LIcKey_MouseButton3},
67 {"mousebutton4", LIcKey_MouseButton4},
68 {"mousexaxis", LIcKey_MouseXAxis}, {"mouseyaxis", LIcKey_MouseYAxis},
69 {"mousezaxis", LIcKey_MouseZAxis},
70
71 // Extra keys for Daodan Input
72 {"mousebutton5", DDcKey_MouseButton5},
73 {"scrollup", DDcKey_ScrollUp},
74 {"scrolldown", DDcKey_ScrollDown},
75 {"scrollleft", DDcKey_ScrollLeft},
76 {"scrollright", DDcKey_ScrollRight},
77
78 {"", 0}
79};
80
81// Enhanced version of LIgPlatform_ScanCodeToChar from Oni
82static const uint8_t DDgPlatform_ScanCodeToChar[256] = {
83 // The following scan codes are mapped in Oni
84 [0x01] = LIcKey_Escape, [0x02] = '1', [0x03] = '2', [0x04] = '3',
85 [0x05] = '4', [0x06] = '5', [0x07] = '6', [0x08] = '7', [0x09] = '8',
86 [0x0a] = '9', [0x0b] = '0', [0x0c] = '-', [0x0d] = '=',
87 [0x0e] = LIcKey_Backspace, [0x0f] = LIcKey_Tab, [0x10] = 'q', [0x11] = 'w',
88 [0x12] = 'e', [0x13] = 'r', [0x14] = 't', [0x15] = 'y', [0x16] = 'u',
89 [0x17] = 'i', [0x18] = 'o', [0x19] = 'p', [0x1a] = '[', [0x1b] = ']',
90 [0x1c] = LIcKey_Enter, [0x1d] = LIcKey_LeftControl, [0x1e] = 'a',
91 [0x1f] = 's', [0x20] = 'd', [0x21] = 'f', [0x22] = 'g', [0x23] = 'h',
92 [0x24] = 'j', [0x25] = 'k', [0x26] = 'l', [0x27] = ';', [0x28] = '\'',
93 [0x29] = '`', [0x2a] = LIcKey_LeftShift, [0x2b] = '\\', [0x2c] = 'z',
94 [0x2d] = 'x', [0x2e] = 'c', [0x2f] = 'v', [0x30] = 'b', [0x31] = 'n',
95 [0x32] = 'm', [0x33] = ',', [0x34] = '.', [0x35] = '/',
96 [0x36] = LIcKey_RightShift, [0x37] = LIcKey_Multiply,
97 [0x38] = LIcKey_LeftAlt, [0x39] = ' ', [0x3a] = LIcKey_CapsLock,
98 [0x3b] = LIcKey_FKey1, [0x3c] = LIcKey_FKey2, [0x3d] = LIcKey_FKey3,
99 [0x3e] = LIcKey_FKey4, [0x3f] = LIcKey_FKey5, [0x40] = LIcKey_FKey6,
100 [0x41] = LIcKey_FKey7, [0x42] = LIcKey_FKey8, [0x43] = LIcKey_FKey9,
101 [0x44] = LIcKey_FKey10, [0x45] = LIcKey_NumLock, [0x46] = LIcKey_ScrollLock,
102 [0x47] = LIcKey_Numpad7, [0x48] = LIcKey_Numpad8, [0x49] = LIcKey_Numpad9,
103 [0x4a] = LIcKey_Subtract, [0x4b] = LIcKey_Numpad4, [0x4c] = LIcKey_Numpad5,
104 [0x4d] = LIcKey_Numpad6, [0x4e] = LIcKey_Add, [0x4f] = LIcKey_Numpad1,
105 [0x50] = LIcKey_Numpad2, [0x51] = LIcKey_Numpad3, [0x52] = LIcKey_Numpad0,
106 [0x53] = LIcKey_Decimal, [0x57] = LIcKey_FKey11, [0x58] = LIcKey_FKey12,
107 [0x64] = LIcKey_FKey13, [0x65] = LIcKey_FKey14, [0x66] = LIcKey_FKey15,
108 [0x8d] = LIcKey_NumpadEquals, [0x9c] = LIcKey_NumpadEnter,
109 [0x9d] = LIcKey_RightControl, [0xb3] = LIcKey_NumpadComma,
110 [0xb5] = LIcKey_Divide, [0xb8] = LIcKey_RightAlt, [0xc7] = LIcKey_Home,
111 [0xc8] = LIcKey_UpArrow, [0xc9] = LIcKey_PageUp, [0xcb] = LIcKey_LeftArrow,
112 [0xcd] = LIcKey_RightArrow, [0xcf] = LIcKey_End, [0xd0] = LIcKey_DownArrow,
113 [0xd2] = LIcKey_Insert, [0xd3] = LIcKey_Delete, [0xdb] = LIcKey_LeftWindows,
114 [0xdc] = LIcKey_RightWindows, [0xdd] = LIcKey_Apps,
115
116 // Missing in Oni
117 [0xd1] = LIcKey_PageDown,
118};
119
120// Set in Patches.c if the Daodan input patches are applied. This just enables
121// the windows message handling for now
122bool DDgUseDaodanInput = false;
123
124// The Oni key codes that correspond to the togglable keys
125static uint8_t DDgCapsOniKey = 0;
126static uint8_t DDgScrollOniKey = 0;
127static uint8_t DDgNumLockOniKey = 0;
128
129// Multiplier for mouse values
130static float DDgMouseSensitivity = 1.0;
131
132// Accumulators for mouse scrolling. These are needed because some mice have
133// continuous scroll wheels (not to mention touchpads.) We should only add an
134// action to Oni's input if one of these accumulators exceeds +/-WHEEL_DELTA.
135static int DDgWheelDelta_V = 0;
136static int DDgWheelDelta_H = 0;
137
138// UUrMachineTime_High for the last update of the accumulators. Used so they can
139// be reset after a period of no scroll events.
140static int64_t DDgWheelDelta_Updated = 0;
141
142// Temporary action buffer that we build over the duration of a frame with the
143// input we're going to send to the engine. This includes the accumulated
144// movement of the mouse cursor and all actions (keyboard and mouse buttons)
145// that were pressed this frame (but not held down from previous frames - that
146// gets added later from DDgInputState.)
147static LItActionBuffer DDgActionBuffer = { 0 };
148
149// Temporary buffer containing the current state of the keyboard and mouse
150// buttons, that is, if they're being held now
151static char DDgInputState[256] = { 0 };
152
153static short ONICALL DDrBinding_Add(int key, const char *name)
154{
155 // First try to replace an existing binding for the same key
156 LItBinding *binding = NULL;
157 for (int i = 0; i < 100; i++) {
158 if (LIgBindingArray[i].key == key) {
159 binding = &LIgBindingArray[i];
160 break;
161 }
162 }
163
164 // If there are no existing bindings for this key, find a free entry
165 if (!binding) {
166 for (int i = 0; i < 100; i++) {
167 if (!LIgBindingArray[i].key) {
168 binding = &LIgBindingArray[i];
169 break;
170 }
171 }
172 }
173 // No free entries, so give up
174 if (!binding)
175 return 2;
176
177 // Now try to find the action to bind to. First check Oni's built-in list
178 // of actions.
179 LItActionDescription *descr = NULL;
180 for (int i = 0; LIgActionDescriptions[i].name[0]; i++) {
181 if (!UUrString_Compare_NoCase(name, LIgActionDescriptions[i].name)) {
182 descr = &LIgActionDescriptions[i];
183 break;
184 }
185 }
186
187 // Next, try Daodan's list of custom actions
188 if (!descr) {
189 for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
190 if (!DDgCustomActions[i].descr.name[0])
191 break;
192
193 if (!UUrString_Compare_NoCase(name, DDgCustomActions[i].descr.name)) {
194 descr = &DDgCustomActions[i].descr;
195 break;
196 }
197 }
198 }
199 if (!descr)
200 return 0;
201
202 binding->key = key;
203 binding->descr = descr;
204 return 0;
205}
206
207static void ONICALL DDrGameState_HandleUtilityInput(GameInput *input)
208{
209 // Mac Oni 1.2.1 checks the cheat binds here, so we should too. Note that
210 // unlike Mac Oni, which hardcodes each cheat here, we use our flexible
211 // custom action system.
212 for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
213 if (!DDgCustomActions[i].descr.name[0])
214 break;
215
216 uint64_t action = 1ull << DDgCustomActions[i].descr.index;
217 bool active = false;
218
219 switch (DDgCustomActions[i].eventType) {
220 case DDcEventType_KeyPress:
221 if (input->ActionsPressed & action)
222 active = true;
223 break;
224 case DDcEventType_KeyDown:
225 if (input->ActionsDown & action)
226 active = true;
227 break;
228 }
229
230 if (active)
231 DDgCustomActions[i].callback(DDgCustomActions[i].ctx);
232 }
233
234 // Now do everything Oni does in this function
235 ONrGameState_HandleUtilityInput(input);
236
237 // This is for show_triggervolumes. Mac Oni does this at the end of
238 // HandleUtilityInput too.
239 if (ONrDebugKey_WentDown(7))
240 OBJgTriggerVolume_Visible = !OBJgTriggerVolume_Visible;
241}
242
243static int GetLowestFreeDigitalAction(void)
244{
245 // Get the digital action indexes that Oni is using right now, plus any in
246 // use by our custom actions
247 uint64_t used = 0;
248 for (int i = 0; LIgActionDescriptions[i].name[0]; i++) {
249 if (LIgActionDescriptions[i].type != LIcActionType_Digital)
250 continue;
251 used |= 1ull << LIgActionDescriptions[i].index;
252 }
253 for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
254 if (!DDgCustomActions[i].descr.name[0])
255 break;
256
257 if (DDgCustomActions[i].descr.type != LIcActionType_Digital)
258 continue;
259 used |= 1ull << DDgCustomActions[i].descr.index;
260 }
261
262 // Get the lowest unused action index and use it. This isn't totally safe
263 // since Oni _might_ have "orphaned" actions that are checked in the code
264 // but not bindable, but Mac Oni 1.2.1 seems to have allocated its new
265 // bindings this way, including filling the gaps between eg. f12 and
266 // lookmode, so we're probably fine to do the same thing.
267 unsigned long lowest;
268 if (BitScanForward(&lowest, ~(unsigned long)used))
269 return lowest;
270 if (BitScanForward(&lowest, ~(unsigned long)(used >> 32)))
271 return lowest + 32;
272 return -1;
273}
274
275void DDrInput_RegisterCustomAction(const char *name, DDtActionEventType type,
276 DDtCustomActionCallback callback,
277 intptr_t ctx)
278{
279 int index = GetLowestFreeDigitalAction();
280 if (index < 0) {
281 STARTUPMESSAGE("Registering action %s failed, maximum actions reached",
282 name);
283 return;
284 }
285
286 DDtCustomAction *action;
287 for (int i = 0; i < ARRAY_SIZE(DDgCustomActions); i++) {
288 if (!DDgCustomActions[i].descr.name[0]) {
289 action = &DDgCustomActions[i];
290 break;
291 }
292 }
293 if (!action) {
294 STARTUPMESSAGE("Registering action %s failed, maximum actions reached",
295 name);
296 return;
297 }
298
299 *action = (DDtCustomAction) {
300 .descr = {
301 .type = 1,
302 .index = index,
303 },
304 .callback = callback,
305 .ctx = ctx,
306 };
307 UUrString_Copy(action->descr.name, name, sizeof(action->descr.name));
308}
309
310static uint8_t VKeyToChar(UINT vkey)
311{
312 int sc = MapVirtualKeyA(vkey, MAPVK_VK_TO_VSC_EX);
313 if ((sc & 0xff00) == 0xe000)
314 sc |= 0x80;
315 sc &= 0xff;
316 return DDgPlatform_ScanCodeToChar[sc];
317}
318
319static int ONICALL DDrTranslate_InputName(char *name)
320{
321 // Mutate the source argument to convert to lowercase. It's ugly but Oni
322 // does this too. Unlike Oni, we don't use tolower, since passing
323 // potentially out-of-range values to tolower is undefined.
324 for (char *c = name; *c; c++) {
325 if (*c >= 'A' && *c <= 'Z')
326 *c = *c - 0x20;
327 }
328
329 // Single character names just use that character as the key code. Unlike
330 // Oni, we restrict this to printable ASCII.
331 if (strlen(name) == 1 && name[0] >= ' ' && name[0] <= '~')
332 return name[0];
333
334 // Otherwise, look up the name in DDgInputNames
335 for (int i = 0; DDgInputNames[i].name[0]; i++) {
336 if (!strcmp(name, DDgInputNames[i].name))
337 return DDgInputNames[i].key;
338 }
339 return 0;
340}
341
342static void CenterCursor(void)
343{
344 // This can be set to false by script. Not sure why you'd turn it off, but
345 // let's respect it.
346 if (!LIgCenterCursor)
347 return;
348
349 RECT rc;
350 if (!GetClientRect(LIgPlatform_HWND, &rc))
351 return;
352 POINT mid = { rc.right / 2, rc.bottom / 2 };
353 if (!ClientToScreen(LIgPlatform_HWND, &mid))
354 return;
355 SetCursorPos(mid.x, mid.y);
356}
357
358static void ONICALL DDrPlatform_Mode_Set(int active)
359{
360 // Oni's input system uses LIgPlatform_HWND instead of
361 // ONgPlatformData.Window, but they should both have the same value
362 DDmAssert(LIgPlatform_HWND);
363
364 // Clear the input state when switching input modes
365 for (int i = 0; i < ARRAY_SIZE(DDgInputState); i++)
366 DDgInputState[i] = 0;
367 DDgActionBuffer = (LItActionBuffer) { 0 };
368
369 // Center the cursor before switching modes. Raw Input doesn't need the
370 // cursor to be centered, but when switching modes, centering the cursor
371 // means it will be in a predictable position for using the pause or F1
372 // menu, which are centered on the screen. Also, the cursor must be inside
373 // the clip region when we call ClipCursor, otherwise it doesn't work.
374 CenterCursor();
375
376 // If leaving input mode (switching from gameplay to menus,) unregister the
377 // input device. Otherwise, register it.
378 RegisterRawInputDevices(&(RAWINPUTDEVICE) {
379 .usUsagePage = 0x01, // HID_USAGE_PAGE_GENERIC
380 .usUsage = 0x02, // HID_USAGE_GENERIC_MOUSE
381 .hwndTarget = LIgPlatform_HWND,
382 .dwFlags = active ? 0 : RIDEV_REMOVE,
383 }, 1, sizeof(RAWINPUTDEVICE));
384
385 if (active) {
386 DDgMouseSensitivity =
387 DDrConfig_GetOptOfType("windows.mousesensitivity", C_FLOAT)->value.floatVal;
388
389 // Get the Oni key codes corresponding to the togglable keys
390 DDgCapsOniKey = VKeyToChar(VK_CAPITAL);
391 DDgScrollOniKey = VKeyToChar(VK_SCROLL);
392 DDgNumLockOniKey = VKeyToChar(VK_NUMLOCK);
393
394 // Clip the cursor to the window bounds when entering input mode to
395 // prevent other programs being clicked in windowed mode
396 RECT rc;
397 if (GetClientRect(LIgPlatform_HWND, &rc)) {
398 if (MapWindowRect(LIgPlatform_HWND, NULL, &rc))
399 ClipCursor(&rc);
400 }
401 } else {
402 ClipCursor(NULL);
403 }
404}
405
406static void ONICALL DDrPlatform_InputEvent_GetMouse(int active,
407 LItInputEvent *event)
408{
409 POINT pt;
410 if (!GetCursorPos(&pt))
411 goto error;
412
413 // Unlike Oni's version of this function, we support windowed mode by
414 // mapping the cursor coordinates to the window's client area
415 if (!ScreenToClient(LIgPlatform_HWND, &pt))
416 goto error;
417
418 *event = (LItInputEvent) { .mouse_pos = { pt.x, pt.y } };
419 return;
420
421error:
422 *event = (LItInputEvent) { 0 };
423 return;
424}
425
426static UUtBool ONICALL DDrPlatform_TestKey(int ch, int active)
427{
428 // DDrPlatform_TestKey is always called with active = LIgMode_Internal
429
430 if (active) {
431 // The input system is running, which means DDgInputState will be up to
432 // date, so just use that
433 return DDgInputState[ch];
434 } else {
435 // Use Oni's map from key codes to DirectInput scan codes to get the
436 // scan code we want to test for
437 int sc = 0;
438 for (int i = 0; i < 256; i++) {
439 if (DDgPlatform_ScanCodeToChar[i] == ch) {
440 sc = i;
441 break;
442 }
443 }
444 if (!sc)
445 return UUcFalse;
446
447 // DirectInput scan codes have 0x80 set for extended keys. Replace this
448 // with an 0xe0 prefix for MapVirtualKey.
449 if (sc & 0x80) {
450 sc &= 0x7f;
451 sc |= 0xe000;
452 }
453 int vkey = MapVirtualKeyA(sc, MAPVK_VSC_TO_VK_EX);
454
455 // Now check if the key is down. We must use GetAsyncKeyState here
456 // because DDrPlatform_TestKey can be called from anywhere, even before
457 // we have a message loop or game loop. For example, it's called from
458 // ONiMain to test the state of the shift key on startup.
459 return (GetAsyncKeyState(vkey) & 0x8000) ? UUcTrue : UUcFalse;
460 }
461}
462
463// Update DDgInputState and DDgActionBuffer with a new key state
464static void SetKeyState(int key, bool down)
465{
466 // Keep track of held keys. Held keys are added to every buffer and they're
467 // also checked in DDrPlatform_TestKey.
468 DDgInputState[key] = down;
469
470 if (down) {
471 // Add the key to the next buffer. This is so key presses are never
472 // dropped, even if the key is released before Oni checks the buffer.
473 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
474 .input = key,
475 .analog = 1.0,
476 });
477 }
478}
479
480static void ProcessRawInputPacket(RAWINPUT *ri)
481{
482 if (ri->header.dwType != RIM_TYPEMOUSE)
483 return;
484
485 // We don't handle MOUSE_MOVE_ABSOLUTE at all yet
486 if (!(ri->data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)) {
487 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
488 .input = LIcKey_MouseXAxis,
489 .analog = (float)ri->data.mouse.lLastX * 0.25 * DDgMouseSensitivity,
490 });
491 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
492 .input = LIcKey_MouseYAxis,
493 .analog = (float)ri->data.mouse.lLastY *
494 (LIgMode_InvertMouse ? -0.25 : 0.25) * DDgMouseSensitivity,
495 });
496 }
497
498 // Oni supports using the mouse wheel to look up and down or left and right
499 // by binding mousezaxis to aim_lr or aim_ud. We don't support this
500 // incredibly useful feature, but if you need it, let me know. Instead, we
501 // allow scrolling to be bound to digital actions.
502 if (ri->data.mouse.usButtonFlags & (RI_MOUSE_WHEEL | RI_MOUSE_HWHEEL)) {
503 int64_t now = UUrMachineTime_High();
504 int64_t last_updated = now - DDgWheelDelta_Updated;
505 DDgWheelDelta_Updated = now;
506
507 // Reset the accumulators if too much time has passed since the last
508 // scroll event. The player is assumed to have finished scrolling.
509 if (last_updated / UUrMachineTime_High_Frequency() > 0.3) {
510 DDgWheelDelta_V = 0;
511 DDgWheelDelta_H = 0;
512 }
513
514 int neg_key, pos_key;
515 int *delta;
516 if (ri->data.mouse.usButtonFlags & RI_MOUSE_WHEEL) {
517 neg_key = DDcKey_ScrollUp;
518 pos_key = DDcKey_ScrollDown;
519 delta = &DDgWheelDelta_V;
520 } else {
521 neg_key = DDcKey_ScrollLeft;
522 pos_key = DDcKey_ScrollRight;
523 delta = &DDgWheelDelta_H;
524 }
525
526 // To support touchpad scrolling and mice with continuous scroll wheels,
527 // accumulate the wheel delta and only generate an input event once it
528 // crosses the WHEEL_DELTA threshold
529 *delta += (short)ri->data.mouse.usButtonData;
530 if (*delta >= WHEEL_DELTA) {
531 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
532 .input = neg_key,
533 .analog = 1.0,
534 });
535
536 *delta -= (*delta / WHEEL_DELTA) * WHEEL_DELTA;
537 } else if (*delta <= -WHEEL_DELTA) {
538 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
539 .input = pos_key,
540 .analog = 1.0,
541 });
542
543 *delta -= (*delta / -WHEEL_DELTA) * -WHEEL_DELTA;
544 }
545 }
546
547 // This probably doesn't obey SM_SWAPBUTTON... should it?
548 if (ri->data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN)
549 SetKeyState(LIcKey_MouseButton1, true);
550 if (ri->data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP)
551 SetKeyState(LIcKey_MouseButton1, false);
552 if (ri->data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_DOWN)
553 SetKeyState(LIcKey_MouseButton2, true);
554 if (ri->data.mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_UP)
555 SetKeyState(LIcKey_MouseButton2, false);
556 if (ri->data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_DOWN)
557 SetKeyState(LIcKey_MouseButton3, true);
558 if (ri->data.mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_UP)
559 SetKeyState(LIcKey_MouseButton3, false);
560
561 // Oni supports binding this button too. It's the back button on most mice.
562 if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_DOWN)
563 SetKeyState(LIcKey_MouseButton4, true);
564 if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_4_UP)
565 SetKeyState(LIcKey_MouseButton4, false);
566
567 // Daodan supports binding the forward button too
568 if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_DOWN)
569 SetKeyState(DDcKey_MouseButton5, true);
570 if (ri->data.mouse.usButtonFlags & RI_MOUSE_BUTTON_5_UP)
571 SetKeyState(DDcKey_MouseButton5, false);
572}
573
574static void DrainRawInput(void)
575{
576 if (!LIgMode_Internal)
577 return;
578
579 UINT ri_size = 10240;
580 static RAWINPUT *ri_buf = NULL;
581 if (!ri_buf)
582 ri_buf = calloc(1, ri_size);
583
584 BOOL wow_hack;
585 IsWow64Process(GetCurrentProcess(), &wow_hack);
586
587 for (;;) {
588 UINT count = GetRawInputBuffer(ri_buf, &ri_size, sizeof ri_buf->header);
589 if (count == 0 || count == (UINT)-1)
590 return;
591
592 RAWINPUT *ri = ri_buf;
593 for (UINT i = 0; i < count; i++) {
594 // In WOW64, these structures are aligned like in Win64 and they
595 // have to be fixed to use from 32-bit code. Yes, really.
596 if (wow_hack) {
597 memmove(&ri->data, ((char *)&ri->data) + 8,
598 ri->header.dwSize - offsetof(RAWINPUT, data) - 8);
599 }
600
601 ProcessRawInputPacket(ri);
602 ri = NEXTRAWINPUTBLOCK(ri);
603 }
604 }
605}
606
607static UUtBool ONICALL DDiPlatform_InputEvent_GetEvent(void)
608{
609 // Center the cursor just in case. Raw Input doesn't need it, but sometimes
610 // ClipCursor doesn't work for some reason and in that case we should still
611 // prevent the user from accidentally clicking on other windows.
612 if (LIgMode_Internal)
613 CenterCursor();
614
615 // Do a buffered read of raw input. Apparently this is faster for high-res
616 // mice. Note that we still have to handle WM_INPUT in our wndproc in case
617 // a WM_INPUT message arrives during the message loop.
618 DrainRawInput();
619
620 // Oni only processes a maximum of three messages here (for performance
621 // reasons?) We're actually using Windows messages for input so we need to
622 // process all of them.
623 MSG msg;
624 while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
625 TranslateMessage(&msg);
626 DispatchMessageA(&msg);
627 }
628
629 // Oni returns true here if there are still messages to process, so that
630 // LIrMode_Set and LIrMode_Set_Internal can call this repeatedly to drain
631 // all messages. We always drain all messages so return false.
632 return UUcFalse;
633}
634
635static bool HandleWmInput(HRAWINPUT hri, WPARAM wparam)
636{
637 if (!LIgMode_Internal)
638 return false;
639
640 static RAWINPUT* ri = NULL;
641 static UINT ri_size = 0;
642 UINT minsize = 0;
643
644 GetRawInputData(hri, RID_INPUT, NULL, &minsize, sizeof ri->header);
645 if (ri_size < minsize) {
646 if (ri)
647 free(ri);
648 ri_size = minsize;
649 ri = calloc(1, ri_size);
650 }
651 if (GetRawInputData(hri, RID_INPUT, ri, &ri_size, sizeof ri->header) == (UINT)-1)
652 return false;
653
654 ProcessRawInputPacket(ri);
655 return true;
656}
657
658static void HandleWmWindowPosChanged(WINDOWPOS *pos)
659{
660 if (!LIgMode_Internal)
661 return;
662
663 CenterCursor();
664
665 RECT rc = { pos->x, pos->y, pos->x + pos->cx, pos->y + pos->cy };
666 ClipCursor(&rc);
667}
668
669static void HandleWmKeyboard(int vkey, WORD repeat_count, WORD flags)
670{
671 if (!LIgMode_Internal)
672 return;
673
674 bool is_extended = flags & KF_EXTENDED;
675 bool is_repeat = flags & KF_REPEAT;
676 bool is_up = flags & KF_UP;
677 BYTE sc = LOBYTE(flags);
678
679 // Ignore togglable keys since we handle them specially
680 if (vkey == VK_CAPITAL || vkey == VK_SCROLL || vkey == VK_NUMLOCK)
681 return;
682
683 // Ignore key down messages sent because of key-repeat
684 if (!is_up && is_repeat)
685 return;
686
687 // Apparently some synthetic keyboard messages can be missing the scancode,
688 // so get it from the vkey
689 if (!sc)
690 sc = MapVirtualKeyA(vkey, MAPVK_VK_TO_VSC);
691
692 // DirectInput scan codes have 0x80 set for extended keys, and we're using
693 // a map based on Oni's DirectInput map to convert to key codes
694 if (is_extended)
695 sc |= 0x80;
696 uint8_t ch = DDgPlatform_ScanCodeToChar[sc];
697 if (!ch)
698 return;
699
700 SetKeyState(ch, !is_up);
701}
702
703bool DDrInput_WindowProc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam,
704 LRESULT* res)
705{
706 // This is called from our own window proc for now, so we only want to use
707 // it when Daodan input is enabled
708 if (!DDgUseDaodanInput)
709 return false;
710
711 switch (msg) {
712 case WM_INPUT:
713 if (HandleWmInput((HRAWINPUT)lparam, wparam)) {
714 *res = 0;
715 return true;
716 }
717 break;
718 case WM_WINDOWPOSCHANGED:
719 HandleWmWindowPosChanged((WINDOWPOS *)lparam);
720 break;
721 case WM_KEYDOWN:
722 case WM_SYSKEYDOWN:
723 case WM_KEYUP:
724 case WM_SYSKEYUP:
725 HandleWmKeyboard(LOWORD(wparam), LOWORD(lparam), HIWORD(lparam));
726 break;
727 }
728
729 return false;
730}
731
732static void ONICALL DDrActionBuffer_Get(short* count, LItActionBuffer** buffers)
733{
734 // So, Oni's version of this function was totally different. In unpatched
735 // Oni, action buffers were produced at 60Hz by a separate high-priority
736 // input thread, LIiInterruptHandleProc, and consumed by
737 // LIrActionBuffer_Get, which was called from the game loop and could
738 // provide multiple outstanding action buffers to the engine at once.
739 //
740 // That was a problem for a couple of reasons. Firstly, the resolution of
741 // Windows timers is limited by the timer frequency, which can be as low as
742 // 15.6ms and in the worst case would cause a delay of 31.2ms between action
743 // buffers. That meant that even if Oni was running at a steady 60 fps, the
744 // input thread would provide no action buffers on a lot of frames.
745 //
746 // Secondly, even though Oni drained the list of pending action buffers by
747 // calling DDrActionBuffer_Get on every frame, the engine only uses them
748 // when the internal game time advances, and that happens on a separate 60Hz
749 // timer which was totally unsynchronized with the 60Hz timer on the input
750 // thread. That wasn't too much of a problem when the game loop was running
751 // at less than 60 fps, but when it ran faster, the only action buffers that
752 // got processed were the ones produced when the game timer and the input
753 // thread timer happened to tick at the same time, meaning potentially a lot
754 // of dropped input.
755 //
756 // Oni's input system was probably designed that way so that input would
757 // still run at 60Hz even on PCs that weren't powerful enough to render at
758 // 60 fps. It was a well-meaning design, but due to the aforementioned
759 // flaws, we do something much different and simpler here. On the frames
760 // that Oni will consume input, we generate a single action buffer inside
761 // DDrActionBuffer_Get based on most up-to-date input.
762
763 // Update ONgGameState->TargetGameTime. We use TargetGameTime to determine
764 // if Oni is going to consume input on this frame. Unfortunately, in
765 // unpatched Oni, the call to ONrGameState_UpdateServerTime happened after
766 // LIrActionBuffer_Get. In Daodan, we NOOP out the original call and call it
767 // here instead, so it runs before our code.
768 ONrGameState_UpdateServerTime(ONgGameState);
769 bool time_updated = ONgGameState->GameTime != ONgGameState->TargetGameTime;
770
771 // Only produce input buffers when input is enabled. LIrActionBuffer_Get
772 // does the same thing. Also only produce them when Oni will consume them.
773 if (!LIgMode_Internal || !time_updated) {
774 *count = 0;
775 *buffers = NULL;
776 return;
777 }
778
779 // Add held keys to the action buffer
780 for (int i = 0; i < ARRAY_SIZE(DDgInputState); i++) {
781 if (DDgInputState[i]) {
782 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
783 .input = i,
784 .analog = 1.0,
785 });
786 }
787 }
788
789 // Add togglable keys to the action buffer
790 if (DDgCapsOniKey && (GetKeyState(VK_CAPITAL) & 0x01)) {
791 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
792 .input = DDgCapsOniKey,
793 .analog = 1.0,
794 });
795 }
796 if (DDgScrollOniKey && (GetKeyState(VK_SCROLL) & 0x01)) {
797 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
798 .input = DDgScrollOniKey,
799 .analog = 1.0,
800 });
801 }
802 if (DDgNumLockOniKey && (GetKeyState(VK_NUMLOCK) & 0x01)) {
803 LIrActionBuffer_Add(&DDgActionBuffer, &(LItDeviceInput) {
804 .input = DDgNumLockOniKey,
805 .analog = 1.0,
806 });
807 }
808
809 // Make a copy of our temporary action buffer with all the input we've
810 // gathered this frame. This is the copy that Oni's engine will see.
811 static LItActionBuffer buf = { 0 };
812 buf = DDgActionBuffer;
813 DDgActionBuffer = (LItActionBuffer) { 0 };
814
815 *count = 1;
816 *buffers = &buf;
817}
818
819void DDrInput_PatchUtilityInput(void)
820{
821 // Patch the call to ONrGameState_HandleUtilityInput in
822 // ONrGameState_ProcessHeartbeat. This is where Oni checks a bunch of
823 // miscellaneous inputs, and where Mac Oni 1.2.1 checks the cheat bindings.
824 // It's also where Mac Oni toggles the show_triggervolumes flag.
825 DDrPatch_MakeCall((void *)0x004fa91c, (void *)DDrGameState_HandleUtilityInput);
826}
827
828void DDrInput_PatchCustomActions(void)
829{
830 DDrInput_PatchUtilityInput();
831
832 // Replace the function which adds bindings with ours, which checks the list
833 // of custom bindings as well
834 DDrPatch_MakeJump((void *)LIrBinding_Add, (void *)DDrBinding_Add);
835}
836
837void DDrInput_PatchDaodanInput(void)
838{
839 // In LIrInitialize, NOOP the call to UUrInterruptProc_Install and
840 // associated error checking
841 DDrPatch_NOOP((char *)(OniExe + 0x421f), 106);
842
843 // In LIrPlatform_Initialize, NOOP the Windows version checks so we never
844 // use DirectInput
845 DDrPatch_NOOP((char *)(OniExe + 0x2e64), 11);
846
847 // Replace Oni's Windows message loop with one that does buffered raw input
848 // reads and processes all messages
849 DDrPatch_MakeJump((void *)LIiPlatform_InputEvent_GetEvent, (void *)DDiPlatform_InputEvent_GetEvent);
850
851 // Replace the function that gets the latest input frames
852 DDrPatch_MakeJump((void *)LIrActionBuffer_Get, (void *)DDrActionBuffer_Get);
853
854 // Replace the function that gets the mouse cursor position
855 DDrPatch_MakeJump((void *)LIrPlatform_InputEvent_GetMouse, (void *)DDrPlatform_InputEvent_GetMouse);
856
857 // Replace the function that performs platform-specific actions when the
858 // input mode changes
859 DDrPatch_MakeJump((void *)LIrPlatform_Mode_Set, (void *)DDrPlatform_Mode_Set);
860
861 // Replaces the function that tests the state of keyboard keys
862 DDrPatch_MakeJump((void *)LIrPlatform_TestKey, (void *)DDrPlatform_TestKey);
863
864 // Enable extra key names in key_config.txt
865 DDrPatch_MakeJump((void *)LIrTranslate_InputName, (void *)DDrTranslate_InputName);
866
867 // Patch out the call to ONrGameState_UpdateServerTime in ONiRunGame because
868 // we want to do it earlier, in DDrActionBuffer_Get
869 DDrPatch_NOOP((char *)(OniExe + 0xd4708), 11);
870
871 DDgUseDaodanInput = true;
872}
Note: See TracBrowser for help on using the repository browser.