/* * Descent 3 * Copyright (C) 2024 Parallax Software * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /* * $Logfile: /DescentIII/Main/ddio_win/winkey.cpp $ * $Revision: 36 $ * $Date: 4/24/99 5:41p $ * $Author: Samir $ * * Keyboard IO with DirectInput 3.0 * * $Log: /DescentIII/Main/ddio_win/winkey.cpp $ * * 36 4/24/99 5:41p Samir * moved key to ascii, ascii to key to the ddio_common library. * * 35 4/22/99 2:02a Jeff * pass ddio_init_info through to keyboard handlers * * 34 3/05/99 3:27p Samir * screenshot key works properly now. * * 33 3/02/99 2:01p Samir * some small directinput changes. (note, for D3, we're now using emulated * key input because of the silly pause and numlock issue which for some * reason fails to work properly under DirectInput. * * 32 3/01/99 12:39a Samir * key ups only occur when key was initially down. because of this, * ddio_KeyFlush cleared the down events, but if the user released after * flush, the up event would leak through and cause the illusion of key * presses leaking through. HOPEFULLY this doesn't break anything else, * but I played through the game a bit and it seems to work fine. * * 31 2/25/99 10:43p Matt * Took out mprintf() * * 30 2/21/99 6:38p Samir * mouse and key input better. buffered mouse. * * 29 2/05/99 1:16p Samir * reset low level keys when flushing keyboard in the high level. added a * function to the low level to reset the status of a key, called from * high level key flush. * * 28 2/04/99 12:31p Samir * use our timer for key capture if we're using the hook method. * * 27 1/28/99 6:22p Samir * may fix thread issues. * * 26 1/25/99 7:27p Samir * fixed timing issues with emulated keyboard key down times. (Win32 sends * multiple keydown messages, so ignore them.) * * 25 1/25/99 6:47p Samir * allow slow keyboard * * 24 1/25/99 11:02a Samir * revamped mouse and key controls. * * 23 10/22/98 11:06a Samir * added numeric keypad ascii translations. * * 22 10/21/98 12:02p Samir * properly update odd keys when they are released throug * ddio_KeyGetDownTime. * * 21 10/16/98 12:15p Samir * took out ddio_KeyFrame * * 20 10/15/98 6:48p Samir * added timer hooks. * * 19 9/17/98 12:50p Samir * language. * * 18 6/29/98 6:43p Samir * Took out GetMsgProc and legacy keyboard variables. * * 17 4/09/98 6:56p Samir * PRINTSCREEN MUST WORK. * * 16 4/08/98 8:40p Samir * Screen shots work with print screen by checking VK code. * * 15 3/31/98 12:46p Samir * keyboard IO system better. uses getmsgproc windows hook. * * 14 3/24/98 11:21a Samir * redid key handler. * * 13 2/25/98 6:11p Samir * Added functions to better deal with key flushing. * * 12 1/21/98 6:46p Samir * NT and 95 should have same keyboard behavior for framed handlers. * * 11 1/02/98 12:52p Samir * Added ascii->keycode translation tables * * 10 12/10/97 1:12p Samir * Use timestamp from DirectInput Key calls. * * 9 11/07/97 6:17p Samir * Rollbacked some more efficient code since it slowed down some machines * in the editor. Sleep thread until closed a little faster. * * 8 10/23/97 2:59p Samir * Keyboard thread uses C runtime lib functions, and definitely ends. * * 7 10/22/97 4:37p Samir * Thread doesn't end if thread is still blocking, I think. * * 6 10/17/97 5:03p Samir * Default to preemptive keyboard handler (not under NT). * * 5 10/16/97 5:35p Samir * Use different cooperative level for Win95 vs. NT. * * 4 10/16/97 2:29p Samir * Changed DirectInput Keyboard to FOREGROUND * * 3 8/01/97 8:14p Samir * Fixed keyboard handler for NT to work with extended keys. * * 2 8/01/97 7:30p Samir * Better NT keyboard support. * * 14 5/23/97 4:09p Samir * Keyboard thread uses new task system. * * 13 5/09/97 6:45p Samir * Took out shared keyboard code from handler to ddio_common * * 12 5/08/97 1:56p Samir * Moved a bunch of keyboard code to ddio_common library. * * 11 3/20/97 11:08a Samir * Added function to peek for keys in queue without removing them. * * 10 3/13/97 3:02p Samir * Hopefully fixed keyboard thread problem. * * 9 2/20/97 9:58a Matt * Took out Int3() for directinput key buffer overflow, which would get * hit if you switched to another task, typed a bunch, and switched back * into the editor * * 8 1/23/97 2:22p Samir * Keyboard thread now blocks properly, and added functionality for * nonpreemptive keyboard polling. * * 7 1/20/97 3:46p Samir * RCS header check in * * $NoKeywords: $ */ // ---------------------------------------------------------------------------- // Keyboard Interface // ---------------------------------------------------------------------------- #include "DDAccess.h" #include "pserror.h" #include "mono.h" #include "ddio.h" #include "ddio_win.h" #include "Application.h" #include "TaskSystem.h" #include #include // ---------------------------------------------------------------------------- // Local global data // ---------------------------------------------------------------------------- #define DIKEY_BUFFER_SIZE 32 static struct tWinKeyData { LPDIRECTINPUTDEVICE lpdikey; // key device HANDLE evtnotify; // notify event HHOOK winhook; // windows hook unsigned long thread; // thread id // osMutex keyframe_mutex; // mutex between // internal key frame and key thread. bool nextframe; // used for mutexing between keyframe and thread. bool acquired; // device acquired? bool thread_active; // used in thread. bool suspended; } WKD = {NULL, NULL, NULL, 0xffffffff, false, false, false, true}; volatile struct tWinKeys { union { DWORD up_ticks; // windows ticks when key went up last float up_time; }; union { DWORD down_ticks; // windows ticks when key went down last float down_time; }; bool status; // is it down? bool mutex_flag; // done for mutexing between ddio_Internal and KeyThread ushort mutex_data; } WKeys[DDIO_MAX_KEYS]; static int DDIO_key_language = KBLANG_AMERICAN; /////////////////////////////////////////////////////////////////////////////// // Initializes DirectInputDevice keyboard if we are under Win9x or at least NT5 // we set the cooperative level, etc. LPDIRECTINPUTDEVICE dikey_Init(LPDIRECTINPUT lpdi, HWND hwnd); // Shutsdown DirectInputDevice keyboard if passed device is valid. void dikey_Shutdown(LPDIRECTINPUTDEVICE lpdikey); // sets up preemptive keyboard handling. HANDLE dikey_EnableNotify(LPDIRECTINPUTDEVICE lpdikey); // disables event based notification. bool dikey_DisableNotify(LPDIRECTINPUTDEVICE lpdikey, HANDLE evthandle); // acquires or unacquires device // returns device acquisition state. bool dikey_Acquire(LPDIRECTINPUTDEVICE lpdikey, bool acquire); // emulated keyboard functionality bool ddio_Win_KeyInit(); void ddio_Win_KeyClose(); int ddio_KeyHandler(HWnd wnd, unsigned msg, unsigned wParam, long lParam); void CALLBACK key_TimerProc(UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2); LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam); // DirectInput Keyboard Thread void __cdecl dikey_Thread(void *dp); // translates scan code to foreign equivs. ubyte xlate_scancode(ubyte scan_code); /////////////////////////////////////////////////////////////////////////////// // ---------------------------------------------------------------------------- // Initialization of keyboard device. // ---------------------------------------------------------------------------- // this version will try to initialize a direct input keyboard device. if it fails // it falls back to the old keyboard hook (less reliable but works.) bool ddio_InternalKeyInit(ddio_init_info *init_info) { bool emulated = init_info->key_emulation; LPDIRECTINPUTDEVICE lpdikey = NULL; int i; // reset key list for (i = 0; i < DDIO_MAX_KEYS; i++) { WKeys[i].down_ticks = 0; WKeys[i].up_ticks = 0; WKeys[i].status = false; } // start init. if (!emulated) { lpdikey = dikey_Init(DInputData.lpdi, (HWND)DInputData.app->m_hWnd); } retry_key_init: if (lpdikey) { // direct input keyboard can work, so we initialize preemptive handling of the keyboard HANDLE hevt = dikey_EnableNotify(lpdikey); if (hevt) { // event handling will work, so let's create the keyboard thread. bool acquired = dikey_Acquire(lpdikey, true); if (acquired) { // create keyboard thread unsigned long thrid; WKD.thread_active = true; WKD.lpdikey = lpdikey; WKD.thread = 0; WKD.evtnotify = hevt; WKD.acquired = acquired; WKD.winhook = NULL; WKD.suspended = false; WKD.nextframe = false; thrid = _beginthread(dikey_Thread, 0, NULL); if (thrid == (unsigned long)(-1)) { mprintf((0, "DDIO: DI_Keyboard thread failed to initialize.\n")); WKD.thread_active = false; WKD.lpdikey = NULL; WKD.thread = 0xffffffff; WKD.evtnotify = NULL; WKD.acquired = false; WKD.winhook = NULL; WKD.suspended = false; acquired = dikey_Acquire(lpdikey, false); dikey_DisableNotify(lpdikey, hevt); dikey_Shutdown(lpdikey); lpdikey = NULL; } else { // success! init data. // set priority of thread too. WKD.thread = thrid; if (SetThreadPriority((HANDLE)thrid, THREAD_PRIORITY_TIME_CRITICAL) == FALSE) { SetThreadPriority((HANDLE)thrid, THREAD_PRIORITY_HIGHEST); } mprintf((0, "DDIO: DI_Keyboard initialized.\n")); } } else { // failed to acquire device? can't do. dikey_DisableNotify(lpdikey, hevt); dikey_Shutdown(lpdikey); lpdikey = NULL; } } else { // somehow event notification failed, can't do. dikey_Shutdown(lpdikey); lpdikey = NULL; } if (lpdikey == NULL) { goto retry_key_init; } } else { // here, initialize hook procedure. return ddio_Win_KeyInit(); } return true; } // this will shutdown direct input or the windows hook, whichever was chosen. void ddio_InternalKeyClose() { if (WKD.lpdikey) { // deactivate thread and free it, then unacquire device, disable event notification, and shutdown! WKD.thread_active = false; SetEvent(WKD.evtnotify); Sleep(500); WKD.acquired = dikey_Acquire(WKD.lpdikey, false); dikey_DisableNotify(WKD.lpdikey, WKD.evtnotify); dikey_Shutdown(WKD.lpdikey); WKD.thread = 0xffffffff; WKD.evtnotify = NULL; WKD.lpdikey = NULL; mprintf((0, "DDIO: DI_Keyboard shutdown.\n")); } if (WKD.winhook) { ddio_Win_KeyClose(); } } // handled internally if keyboard system needs additional processing per frame void ddio_InternalKeyFrame() {} ////////////////////////////////////////////////////////////////////////////// // Miscellaneous API ////////////////////////////////////////////////////////////////////////////// // returns if key is up or down bool ddio_InternalKeyState(ubyte key) { return WKeys[key].status; } float ddio_InternalKeyDownTime(ubyte key) { float down_time = 0.0f; if (WKeys[key].status) { if (WKD.winhook) { float timer = timer_GetTime(); down_time = timer - WKeys[key].down_time; WKeys[key].down_time = timer; } else { DWORD curtickcount = GetTickCount(); DWORD ticks = curtickcount - WKeys[key].down_ticks; if (ticks == 0) { // mprintf((0, "ticks=%d\n", ticks)); } WKeys[key].down_ticks = curtickcount; down_time = (ticks / 1000.0f); } } else { if (WKD.winhook) { down_time = WKeys[key].up_time - WKeys[key].down_time; WKeys[key].down_time = WKeys[key].up_time = 0.0f; } else { DWORD ticks = WKeys[key].up_ticks - WKeys[key].down_ticks; WKeys[key].down_ticks = 0; WKeys[key].up_ticks = 0; down_time = (ticks / 1000.0f); } } return down_time; } // flush a key internally void ddio_InternalResetKey(ubyte key) { WKeys[key].down_ticks = 0; WKeys[key].up_ticks = 0; WKeys[key].status = false; WKeys[key].mutex_flag = false; WKeys[key].mutex_data = 0; } // sets type of keyboard to emulate // #define KBLANG_AMERICAN 0 // #define KBLANG_BRITISH 1 // #define KBLANG_FRENCH 2 // #define KBLANG_GERMAN 3 void ddio_SetKeyboardLanguage(int language) { DDIO_key_language = language; } // translates scan code to foreign equivs. ubyte xlate_scancode(ubyte scan_code) { ubyte code = scan_code; if (DDIO_key_language == KBLANG_FRENCH) { switch (scan_code) { case KEY_A: code = KEY_Q; break; case KEY_M: code = KEY_COMMA; break; case KEY_Q: code = KEY_A; break; case KEY_W: code = KEY_Z; break; case KEY_Z: code = KEY_W; break; case KEY_SEMICOL: code = KEY_M; break; case KEY_COMMA: code = KEY_SEMICOL; break; } } else if (DDIO_key_language == KBLANG_GERMAN) { switch (scan_code) { case KEY_Y: code = KEY_Z; break; case KEY_Z: code = KEY_Y; break; } } else if (DDIO_key_language == KBLANG_BRITISH) { if (scan_code == KEY_BSLASH_UK) { // KEY_SLASH_UK == 0x56 code = KEY_SLASH; // KEY_SLASH is really the backslash, 0x2B } } return code; } ////////////////////////////////////////////////////////////////////////////// // DirectInput Functions ////////////////////////////////////////////////////////////////////////////// inline bool IS_NT4_OR_LOWER() { int maj, min; tWin32OS os = oeWin32Application::version(&maj, &min); return (os == WinNT && maj < 5) ? true : false; } // Initializes DirectInputDevice keyboard if we are under Win9x or at least NT5 // we set the cooperative level, etc. LPDIRECTINPUTDEVICE dikey_Init(LPDIRECTINPUT lpdi, HWND hwnd) { LPDIRECTINPUTDEVICE lpdikey; HRESULT hr; if (IS_NT4_OR_LOWER()) { return NULL; } // see if we can get the keyboard device. hr = lpdi->CreateDevice(GUID_SysKeyboard, &lpdikey, NULL); if (hr != DI_OK) { DDIO_MESSAGE((hr, "DI_Keyboard initialization failed.")); return NULL; } hr = lpdikey->SetDataFormat(&c_dfDIKeyboard); if (hr != DI_OK) { DDIO_MESSAGE((hr, "DI_Keyboard data format specification failed.")); lpdikey->Release(); return NULL; } hr = lpdikey->SetCooperativeLevel(hwnd, DISCL_NONEXCLUSIVE | DISCL_BACKGROUND); if (hr != DI_OK) { DDIO_MESSAGE((hr, "DI_Keyboard set cooperative level failed.")); lpdikey->Release(); return NULL; } return lpdikey; } // Shutsdown DirectInputDevice keyboard if passed device is valid. void dikey_Shutdown(LPDIRECTINPUTDEVICE lpdikey) { if (lpdikey) { lpdikey->Release(); } } // sets up preemptive keyboard handling. HANDLE dikey_EnableNotify(LPDIRECTINPUTDEVICE lpdikey) { if (lpdikey) { HANDLE hEvt = CreateEvent(NULL, TRUE, FALSE, "DDIOKeyEvent"); HRESULT hr; DIPROPDWORD dipropdw = { { sizeof(DIPROPDWORD), sizeof(DIPROPHEADER), 0, DIPH_DEVICE, }, DIKEY_BUFFER_SIZE, }; if (!hEvt) { DDIO_MESSAGE((0, "DI_Keyboard failed to init system event.")); return NULL; } // set event hr = lpdikey->SetEventNotification(hEvt); if (hr != DI_OK) { DDIO_MESSAGE((hr, "DI_Keyboard failed to set preemptive key event notification.")); CloseHandle(hEvt); return NULL; } // set key buffer size hr = lpdikey->SetProperty(DIPROP_BUFFERSIZE, &dipropdw.diph); if (FAILED(hr)) { DDIO_MESSAGE((hr, "DI_keyboard buffering failed.")); lpdikey->SetEventNotification(NULL); CloseHandle(hEvt); return NULL; } return hEvt; } return NULL; } // disables event based notification. bool dikey_DisableNotify(LPDIRECTINPUTDEVICE lpdikey, HANDLE evthandle) { if (lpdikey) { lpdikey->SetEventNotification(NULL); CloseHandle(evthandle); return true; } return false; } // acquires or unacquires device // returns device acquisition state. bool dikey_Acquire(LPDIRECTINPUTDEVICE lpdikey, bool acquire) { HRESULT hr = acquire ? lpdikey->Acquire() : lpdikey->Unacquire(); if (FAILED(hr)) { DDIO_MESSAGE((hr, "DI_keyboard acquire/unacquire fail.")); return !acquire; } return acquire; } // DirectInput Keyboard Thread void __cdecl dikey_Thread(void *dp) { unsigned event_count = 0; GetAsyncKeyState(VK_PAUSE); // this will tell if the 'pause' key is toggled while (WKD.thread_active) { // this thread will hold until the direct input key event has been signaled by the OS. // after this, we will get all the keys in the io buffer and register them with the // key system. switch (WaitForSingleObject(WKD.evtnotify, INFINITE)) { case WAIT_TIMEOUT: // this shouldn't happen, but if it does, no big deal. break; case WAIT_ABANDONED: // usually means calling thread quit. WKD.thread_active = false; break; case WAIT_OBJECT_0: // event was signalled normally { DIDEVICEOBJECTDATA diobjdata[DIKEY_BUFFER_SIZE]; DWORD diobjitems = DIKEY_BUFFER_SIZE; HRESULT hr; // SHORT async_key_state; ubyte key; int i; // don't read if suspended! if (WKD.suspended) { break; } event_count++; // attempt acquisition if keyboard not already acquired if (!WKD.acquired) { WKD.acquired = dikey_Acquire(WKD.lpdikey, true); } if (WKD.acquired) { hr = WKD.lpdikey->GetDeviceData(sizeof(DIDEVICEOBJECTDATA), diobjdata, &diobjitems, 0); if (SUCCEEDED(hr)) { // the pause is key under Windows seems to act kinda strange. it's a toggle button // and USUALLY is preceeded by a KEY_LCTRL press. If a pause toggle was indicated, flush the // keyboard and quit. // note that dwOfs is the acutal key that is either down or up, and dwData tells us if // it is up or down. So place on key array, and in queue. for (i = 0; i < (int)diobjitems; i++) { key = xlate_scancode((ubyte)(diobjdata[i].dwOfs)); if (diobjdata[i].dwData & 0x80) { if (WKeys[key].status) { WKeys[key].up_ticks = 0; } WKeys[key].down_ticks = diobjdata[i].dwTimeStamp; WKeys[key].status = true; if (key == KEY_LCTRL) { WKeys[key].mutex_flag = true; } mprintf((0, "dkey=%x\n", key)); } else { if (WKeys[key].status) { WKeys[key].up_ticks = diobjdata[i].dwTimeStamp; WKeys[key].status = false; mprintf((0, "ukey=%x\n", key)); } } ddio_UpdateKeyState(key, WKeys[key].status); } } else { if (hr == DIERR_INPUTLOST) { WKD.acquired = dikey_Acquire(WKD.lpdikey, true); } else { DDIO_MESSAGE((hr, "DI_keyboard unable to read.")); } } } } break; } ResetEvent(WKD.evtnotify); } DDIO_MESSAGE((0, "DI_Keyboard thread ended.")); } void ddio_InternalKeySuspend() { if (WKD.lpdikey && WKD.acquired) { WKD.acquired = dikey_Acquire(WKD.lpdikey, false); } WKD.suspended = true; } void ddio_InternalKeyResume() { if (WKD.lpdikey && !WKD.acquired) { WKD.acquired = dikey_Acquire(WKD.lpdikey, true); } WKD.suspended = false; } // Win32 non-threaded version bool ddio_Win_KeyInit() { /* Initialize hook handlers */ WKD.winhook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, (HINSTANCE)DInputData.app->m_hInstance, GetCurrentThreadId()); if (!WKD.winhook) { return false; } mprintf((0, "Keyboard initialized.\n")); return true; } void ddio_Win_KeyClose() { /* Free up message handlers */ if (WKD.winhook) { UnhookWindowsHookEx(WKD.winhook); WKD.winhook = NULL; } mprintf((0, "Keyboard released.\n")); } // ---------------------------------------------------------------------------- // non DirectInput keyboard handler // ---------------------------------------------------------------------------- LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam) { int res; if (code < 0) { return CallNextHookEx(WKD.winhook, code, wParam, lParam); } if (lParam & 0x80000000) res = ddio_KeyHandler(0, WM_KEYUP, wParam, lParam); else res = ddio_KeyHandler(0, WM_KEYDOWN, wParam, lParam); return (!res); } int ddio_KeyHandler(HWnd wnd, unsigned msg, unsigned wParam, long lParam) { ubyte scan_code; float timer = timer_GetTime(); if (!WKD.winhook) return 1; switch ((UINT)msg) { case WM_KEYDOWN: case WM_SYSKEYDOWN: scan_code = (unsigned char)((lParam >> 16) & 0xff); if (lParam & 0x01000000) scan_code |= 0x80; scan_code = xlate_scancode(scan_code); // print screen is a weird case. only accept key ups. if (wParam != VK_SNAPSHOT) { if (!WKeys[scan_code].status) { WKeys[scan_code].up_time = 0; WKeys[scan_code].down_time = timer; } else { WKeys[scan_code].up_time = 0; } WKeys[scan_code].status = true; ddio_UpdateKeyState(scan_code, true); } break; case WM_KEYUP: case WM_SYSKEYUP: scan_code = (unsigned char)((lParam >> 16) & 0xff); if (lParam & 0x01000000) scan_code |= 0x80; scan_code = xlate_scancode(scan_code); // handle special keys. print screen, we will simulate the keypress. if (wParam == VK_SNAPSHOT) { scan_code = KEY_PRINT_SCREEN; WKeys[scan_code].down_time = timer; WKeys[scan_code].status = true; ddio_UpdateKeyState(scan_code, true); } if (WKeys[scan_code].status) { WKeys[scan_code].status = false; WKeys[scan_code].up_time = timer; ddio_UpdateKeyState(scan_code, false); } break; } return 1; }