/* NetHack 3.6 nhraykey.c $NHDT-Date: 1457207047 2016/03/05 19:44:07 $ $NHDT-Branch: chasonr $:$NHDT-Revision: 1.16 $ */ /* Copyright (c) NetHack PC Development Team 2003 */ /* NetHack may be freely redistributed. See license for details. */ /* * Keystroke handling contributed by Ray Chason. * The following text was written by Ray Chason. * * The problem * =========== * * The console-mode Nethack wants both keyboard and mouse input. The * problem is that the Windows API provides no easy way to get mouse input * and also keyboard input properly translated according to the user's * chosen keyboard layout. * * The ReadConsoleInput function returns a stream of keyboard and mouse * events. Nethack is interested in those events that represent a key * pressed, or a click on a mouse button. The keyboard events from * ReadConsoleInput are not translated according to the keyboard layout, * and do not take into account the shift, control, or alt keys. * * The PeekConsoleInput function works similarly to ReadConsoleInput, * except that it does not remove an event from the queue and it returns * instead of blocking when the queue is empty. * * A program can also use ReadConsole to get a properly translated stream * of characters. Unfortunately, ReadConsole does not return mouse events, * does not distinguish the keypad from the main keyboard, does not return * keys shifted with Alt, and does not even return the ESC key when * pressed. * * We want both the functionality of ReadConsole and the functionality of * ReadConsoleInput. But Microsoft didn't seem to think of that. * * * The solution, in the original code * ================================== * * The original 3.4.1 distribution tries to get proper keyboard translation * by passing keyboard events to the ToAscii function. This works, to some * extent -- it takes the shift key into account, and it processes dead * keys properly. But it doesn't take non-US keyboards into account. It * appears that ToAscii is meant for windowed applications, and does not * have enough information to do its job properly in a console application. * * * The Finnish keyboard patch * ========================== * * This patch adds the "subkeyvalue" option to the defaults.nh file. The * user can then add OPTIONS=sukeyvalue:171/92, for instance, to replace * the 171 character with 92, which is \. This works, once properly * configured, but places too much burden on the user. It also bars the * use of the substituted characters in naming objects or monsters. * * * The solution presented here * =========================== * * The best way I could find to combine the functionality of ReadConsole * with that of ReadConsoleInput is simple in concept. First, call * PeekConsoleInput to get the first event. If it represents a key press, * call ReadConsole to retrieve the key. Otherwise, pop it off the queue * with ReadConsoleInput and, if it's a mouse click, return it as such. * * But the Devil, as they say, is in the details. The problem is in * recognizing an event that ReadConsole will return as a key. We don't * want to call ReadConsole unless we know that it will immediately return: * if it blocks, the mouse and the Alt sequences will cease to function * until it returns. * * Separating process_keystroke into two functions, one for commands and a * new one, process_keystroke2, for answering prompts, makes the job a lot * easier. process_keystroke2 doesn't have to worry about mouse events or * Alt sequences, and so the consequences are minor if ReadConsole blocks. * process_keystroke, OTOH, never needs to return a non-ASCII character * that was read from ReadConsole; it returns bytes with the high bit set * only in response to an Alt sequence. * * So in process_keystroke, before calling ReadConsole, a bogus key event * is pushed on the queue. This event causes ReadConsole to return, even * if there was no other character available. Because the bogus key has * the eighth bit set, it is filtered out. This is not done in * process_keystroke2, because that would render dead keys unusable. * * A separate process_keystroke2 can also process the numeric keypad in a * way that makes sense for prompts: just return the corresponding symbol, * and pay no mind to number_pad or the num lock key. * * The recognition of Alt sequences is modified, to support the use of * characters generated with the AltGr key. A keystroke is an Alt sequence * if an Alt key is seen that can't be an AltGr (since an AltGr sequence * could be a character, and in some layouts it could even be an ASCII * character). This recognition is different on NT-based and 95-based * Windows: * * * On NT-based Windows, AltGr signals as right Alt and left Ctrl * together. So an Alt sequence is recognized if either Alt key is * pressed and if right Alt and left Ctrl are not both present. This * is true even if the keyboard in use does not have an AltGr key, and * uses right Alt for AltGr. * * * On 95-based Windows, with a keyboard that lacks the AltGr key, the * right Alt key is used instead. But it still signals as right Alt, * without left Ctrl. There is no way for the application to know * whether right Alt is Alt or AltGr, and so it is always assumed * to be AltGr. This means that Alt sequences must be formed with * left Alt. * * So the patch processes keystrokes as follows: * * * If the scan and virtual key codes are both 0, it's the bogus key, * and we ignore it. * * * Keys on the numeric keypad are processed for commands as in the * unpatched Nethack, and for prompts by returning the ASCII * character, even if the num lock is off. * * * Alt sequences are processed for commands as in the unpatched * Nethack, and ignored for prompts. * * * Control codes are returned as received, because ReadConsole will * not return the ESC key. * * * Other key-down events are passed to ReadConsole. The use of * ReadConsole is different for commands than for prompts: * * o For commands, the bogus key is pushed onto the queue before * ReadConsole is called. On return, non-ASCII characters are * filtered, so they are not mistaken for Alt sequences; this also * filters the bogus key. * * o For prompts, the bogus key is not used, because that would * interfere with dead keys. Eight bit characters may be returned, * and are coded in the configured code page. * * * Possible improvements * ===================== * * Some possible improvements remain: * * * Integrate the existing Finnish keyboard patch, for use with non- * QWERTY layouts such as the German QWERTZ keyboard or Dvorak. * * * Fix the keyboard glitches in the graphical version. Namely, dead * keys don't work, and input comes in as ISO-8859-1 but is displayed * as code page 437 if IBMgraphics is set on startup. * * * Transform incoming text to ISO-8859-1, for full compatibility with * the graphical version. * * * After pushing the bogus key and calling ReadConsole, check to see * if we got the bogus key; if so, and an Alt is pressed, process the * event as an Alt sequence. * */ static char where_to_get_source[] = "http://www.nethack.org/"; static char author[] = "Ray Chason"; #include "win32api.h" #include "hack.h" #include "wintty.h" extern HANDLE hConIn; extern INPUT_RECORD ir; char dllname[512]; char *shortdllname; int FDECL(__declspec(dllexport) __stdcall ProcessKeystroke, (HANDLE hConIn, INPUT_RECORD *ir, boolean *valid, BOOLEAN_P numberpad, int portdebug)); static INPUT_RECORD bogus_key; int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved) { char dlltmpname[512]; char *tmp = dlltmpname, *tmp2; *(tmp + GetModuleFileName(hInstance, tmp, 511)) = '\0'; (void) strcpy(dllname, tmp); tmp2 = strrchr(dllname, '\\'); if (tmp2) { tmp2++; shortdllname = tmp2; } /* A bogus key that will be filtered when received, to keep ReadConsole * from blocking */ bogus_key.EventType = KEY_EVENT; bogus_key.Event.KeyEvent.bKeyDown = 1; bogus_key.Event.KeyEvent.wRepeatCount = 1; bogus_key.Event.KeyEvent.wVirtualKeyCode = 0; bogus_key.Event.KeyEvent.wVirtualScanCode = 0; bogus_key.Event.KeyEvent.uChar.AsciiChar = (uchar) 0x80; bogus_key.Event.KeyEvent.dwControlKeyState = 0; return TRUE; } /* * Keyboard translation tables. * (Adopted from the MSDOS port) */ #define KEYPADLO 0x47 #define KEYPADHI 0x53 #define PADKEYS (KEYPADHI - KEYPADLO + 1) #define iskeypad(x) (KEYPADLO <= (x) && (x) <= KEYPADHI) #define isnumkeypad(x) \ (KEYPADLO <= (x) && (x) <= 0x51 && (x) != 0x4A && (x) != 0x4E) #ifdef QWERTZ_SUPPORT /* when 'numberpad' is 0 and Cmd.swap_yz is True (signaled by setting 0x10 on boolean numpad argument) treat keypress of numpad 7 as 'z' rather than 'y' */ static boolean qwertz = FALSE; #endif /* * Keypad keys are translated to the normal values below. * Shifted keypad keys are translated to the * shift values below. */ static const struct pad { uchar normal, shift, cntrl; } keypad[PADKEYS] = { { 'y', 'Y', C('y') }, /* 7 */ { 'k', 'K', C('k') }, /* 8 */ { 'u', 'U', C('u') }, /* 9 */ { 'm', C('p'), C('p') }, /* - */ { 'h', 'H', C('h') }, /* 4 */ { 'g', 'G', 'g' }, /* 5 */ { 'l', 'L', C('l') }, /* 6 */ { '+', 'P', C('p') }, /* + */ { 'b', 'B', C('b') }, /* 1 */ { 'j', 'J', C('j') }, /* 2 */ { 'n', 'N', C('n') }, /* 3 */ { 'i', 'I', C('i') }, /* Ins */ { '.', ':', ':' } /* Del */ }, numpad[PADKEYS] = { { '7', M('7'), '7' }, /* 7 */ { '8', M('8'), '8' }, /* 8 */ { '9', M('9'), '9' }, /* 9 */ { 'm', C('p'), C('p') }, /* - */ { '4', M('4'), '4' }, /* 4 */ { 'g', 'G', 'g' }, /* 5 */ { '6', M('6'), '6' }, /* 6 */ { '+', 'P', C('p') }, /* + */ { '1', M('1'), '1' }, /* 1 */ { '2', M('2'), '2' }, /* 2 */ { '3', M('3'), '3' }, /* 3 */ { 'i', 'I', C('i') }, /* Ins */ { '.', ':', ':' } /* Del */ }; #define inmap(x, vk) (((x) > 'A' && (x) < 'Z') || (vk) == 0xBF || (x) == '2') /* Use process_keystroke for key commands, process_keystroke2 for prompts */ /* int FDECL(process_keystroke, (INPUT_RECORD *ir, boolean *valid, int * portdebug)); */ int FDECL(process_keystroke2, (HANDLE, INPUT_RECORD *ir, boolean *valid)); static int FDECL(is_altseq, (unsigned long shiftstate)); static int is_altseq(shiftstate) unsigned long shiftstate; { /* We need to distinguish the Alt keys from the AltGr key. * On NT-based Windows, AltGr signals as right Alt and left Ctrl together; * on 95-based Windows, AltGr signals as right Alt only. * So on NT, we signal Alt if either Alt is pressed and left Ctrl is not, * and on 95, we signal Alt for left Alt only. */ switch (shiftstate & (RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED)) { case LEFT_ALT_PRESSED: case LEFT_ALT_PRESSED | LEFT_CTRL_PRESSED: return 1; case RIGHT_ALT_PRESSED: case RIGHT_ALT_PRESSED | LEFT_ALT_PRESSED: return (GetVersion() & 0x80000000) == 0; default: return 0; } } int __declspec(dllexport) __stdcall ProcessKeystroke(hConIn, ir, valid, numberpad, portdebug) HANDLE hConIn; INPUT_RECORD *ir; boolean *valid; boolean numberpad; int portdebug; { int metaflags = 0, k = 0; int keycode, vk; unsigned char ch, pre_ch, mk = 0; unsigned short int scan; unsigned long shiftstate; int altseq = 0; const struct pad *kpad; DWORD count; #ifdef QWERTZ_SUPPORT if (numberpad & 0x10) { numberpad &= ~0x10; qwertz = TRUE; } else { qwertz = FALSE; } #endif shiftstate = 0L; ch = pre_ch = ir->Event.KeyEvent.uChar.AsciiChar; scan = ir->Event.KeyEvent.wVirtualScanCode; vk = ir->Event.KeyEvent.wVirtualKeyCode; keycode = MapVirtualKey(vk, 2); shiftstate = ir->Event.KeyEvent.dwControlKeyState; if (scan == 0 && vk == 0) { /* It's the bogus_key */ ReadConsoleInput(hConIn, ir, 1, &count); *valid = FALSE; return 0; } if (is_altseq(shiftstate)) { if (ch || inmap(keycode, vk)) altseq = 1; else altseq = -1; /* invalid altseq */ } if (ch || (iskeypad(scan)) || (altseq > 0)) *valid = TRUE; /* if (!valid) return 0; */ /* * shiftstate can be checked to see if various special * keys were pressed at the same time as the key. * Currently we are using the ALT & SHIFT & CONTROLS. * * RIGHT_ALT_PRESSED, LEFT_ALT_PRESSED, * RIGHT_CTRL_PRESSED, LEFT_CTRL_PRESSED, * SHIFT_PRESSED,NUMLOCK_ON, SCROLLLOCK_ON, * CAPSLOCK_ON, ENHANCED_KEY * * are all valid bit masks to use on shiftstate. * eg. (shiftstate & LEFT_CTRL_PRESSED) is true if the * left control key was pressed with the keystroke. */ if (iskeypad(scan)) { ReadConsoleInput(hConIn, ir, 1, &count); kpad = numberpad ? numpad : keypad; if (shiftstate & SHIFT_PRESSED) { ch = kpad[scan - KEYPADLO].shift; } else if (shiftstate & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED)) { ch = kpad[scan - KEYPADLO].cntrl; } else { ch = kpad[scan - KEYPADLO].normal; } #ifdef QWERTZ_SUPPORT /* OPTIONS=number_pad:-1 is for qwertz keyboard; for that setting, 'numberpad' will be 0; core swaps y to zap, z to move northwest; we want numpad 7 to move northwest, so when qwertz is set, tell core that user who types numpad 7 typed z rather than y */ if (qwertz && kpad[scan - KEYPADLO].normal == 'y') ch += 1; /* changes y to z, Y to Z, ^Y to ^Z */ #endif /*QWERTZ_SUPPORT*/ } else if (altseq > 0) { /* ALT sequence */ ReadConsoleInput(hConIn, ir, 1, &count); if (vk == 0xBF) ch = M('?'); else ch = M(tolower((uchar) keycode)); } else if (ch < 32 && !isnumkeypad(scan)) { /* Control code; ReadConsole seems to filter some of these, * including ESC */ ReadConsoleInput(hConIn, ir, 1, &count); } /* Attempt to work better with international keyboards. */ else { CHAR ch2; DWORD written; /* The bogus_key guarantees that ReadConsole will return, * and does not itself do anything */ WriteConsoleInput(hConIn, &bogus_key, 1, &written); ReadConsole(hConIn, &ch2, 1, &count, NULL); /* Prevent high characters from being interpreted as alt * sequences; also filter the bogus_key */ if (ch2 & 0x80) *valid = FALSE; else ch = ch2; if (ch == 0) *valid = FALSE; } if (ch == '\r') ch = '\n'; #ifdef PORT_DEBUG if (portdebug) { char buf[BUFSZ]; Sprintf(buf, "PORTDEBUG: ch=%u, scan=%u, vk=%d, pre=%d, " "shiftstate=0x%X (ESC to end)\n", ch, scan, vk, pre_ch, shiftstate); fprintf(stdout, "\n%s", buf); } #endif return ch; } int process_keystroke2(hConIn, ir, valid) HANDLE hConIn; INPUT_RECORD *ir; boolean *valid; { /* Use these values for the numeric keypad */ static const char keypad_nums[] = "789-456+1230."; unsigned char ch; int vk; unsigned short int scan; unsigned long shiftstate; int altseq; DWORD count; ch = ir->Event.KeyEvent.uChar.AsciiChar; vk = ir->Event.KeyEvent.wVirtualKeyCode; scan = ir->Event.KeyEvent.wVirtualScanCode; shiftstate = ir->Event.KeyEvent.dwControlKeyState; if (scan == 0 && vk == 0) { /* It's the bogus_key */ ReadConsoleInput(hConIn, ir, 1, &count); *valid = FALSE; return 0; } altseq = is_altseq(shiftstate); if (ch || (iskeypad(scan)) || altseq) *valid = TRUE; /* if (!valid) return 0; */ /* * shiftstate can be checked to see if various special * keys were pressed at the same time as the key. * Currently we are using the ALT & SHIFT & CONTROLS. * * RIGHT_ALT_PRESSED, LEFT_ALT_PRESSED, * RIGHT_CTRL_PRESSED, LEFT_CTRL_PRESSED, * SHIFT_PRESSED,NUMLOCK_ON, SCROLLLOCK_ON, * CAPSLOCK_ON, ENHANCED_KEY * * are all valid bit masks to use on shiftstate. * eg. (shiftstate & LEFT_CTRL_PRESSED) is true if the * left control key was pressed with the keystroke. */ if (iskeypad(scan) && !altseq) { ReadConsoleInput(hConIn, ir, 1, &count); ch = keypad_nums[scan - KEYPADLO]; } else if (ch < 32 && !isnumkeypad(scan)) { /* Control code; ReadConsole seems to filter some of these, * including ESC */ ReadConsoleInput(hConIn, ir, 1, &count); } /* Attempt to work better with international keyboards. */ else { CHAR ch2; ReadConsole(hConIn, &ch2, 1, &count, NULL); ch = ch2 & 0xFF; if (ch == 0) *valid = FALSE; } if (ch == '\r') ch = '\n'; return ch; } int __declspec(dllexport) __stdcall CheckInput(hConIn, ir, count, numpad, mode, mod, cc) HANDLE hConIn; INPUT_RECORD *ir; DWORD *count; int mode; int *mod; boolean numpad; coord *cc; { #if defined(SAFERHANGUP) DWORD dwWait; #endif int ch; boolean valid = 0, done = 0; #ifdef QWERTZ_SUPPORT if (numpad & 0x10) { numpad &= ~0x10; qwertz = TRUE; } else { qwertz = FALSE; } #endif while (!done) { *count = 0; dwWait = WaitForSingleObject(hConIn, INFINITE); #if defined(SAFERHANGUP) if (dwWait == WAIT_FAILED) return '\033'; #endif PeekConsoleInput(hConIn, ir, 1, count); if (mode == 0) { if ((ir->EventType == KEY_EVENT) && ir->Event.KeyEvent.bKeyDown) { ch = process_keystroke2(hConIn, ir, &valid); done = valid; } else ReadConsoleInput(hConIn, ir, 1, count); } else { ch = 0; if (count > 0) { if (ir->EventType == KEY_EVENT && ir->Event.KeyEvent.bKeyDown) { #ifdef QWERTZ_SUPPORT if (qwertz) numpad |= 0x10; #endif ch = ProcessKeystroke(hConIn, ir, &valid, numpad, #ifdef PORTDEBUG 1); #else 0); #endif #ifdef QWERTZ_SUPPORT numpad &= ~0x10; #endif if (valid) return ch; } else { ReadConsoleInput(hConIn, ir, 1, count); if (ir->EventType == MOUSE_EVENT) { if ((ir->Event.MouseEvent.dwEventFlags == 0) && (ir->Event.MouseEvent.dwButtonState & MOUSEMASK)) { cc->x = ir->Event.MouseEvent.dwMousePosition.X + 1; cc->y = ir->Event.MouseEvent.dwMousePosition.Y - 1; if (ir->Event.MouseEvent.dwButtonState & LEFTBUTTON) *mod = CLICK_1; else if (ir->Event.MouseEvent.dwButtonState & RIGHTBUTTON) *mod = CLICK_2; #if 0 /* middle button */ else if (ir->Event.MouseEvent.dwButtonState & MIDBUTTON) *mod = CLICK_3; #endif return 0; } } #if 0 /* We ignore these types of console events */ else if (ir->EventType == FOCUS_EVENT) { } else if (ir->EventType == MENU_EVENT) { } #endif } } else done = 1; } } *mod = 0; return ch; } int __declspec(dllexport) __stdcall NHkbhit(hConIn, ir) HANDLE hConIn; INPUT_RECORD *ir; { int done = 0; /* true = "stop searching" */ int retval; /* true = "we had a match" */ DWORD count; unsigned short int scan; unsigned char ch; unsigned long shiftstate; int altseq = 0, keycode, vk; done = 0; retval = 0; while (!done) { count = 0; PeekConsoleInput(hConIn, ir, 1, &count); if (count > 0) { if (ir->EventType == KEY_EVENT && ir->Event.KeyEvent.bKeyDown) { ch = ir->Event.KeyEvent.uChar.AsciiChar; scan = ir->Event.KeyEvent.wVirtualScanCode; shiftstate = ir->Event.KeyEvent.dwControlKeyState; vk = ir->Event.KeyEvent.wVirtualKeyCode; if (scan == 0 && vk == 0) { /* It's the bogus_key. Discard it */ ReadConsoleInput(hConIn,ir,1,&count); } else { keycode = MapVirtualKey(vk, 2); if (is_altseq(shiftstate)) { if (ch || inmap(keycode, vk)) altseq = 1; else altseq = -1; /* invalid altseq */ } if (ch || iskeypad(scan) || altseq) { done = 1; /* Stop looking */ retval = 1; /* Found what we sought */ } else { /* Strange Key event; let's purge it to avoid trouble */ ReadConsoleInput(hConIn, ir, 1, &count); } } } else if ((ir->EventType == MOUSE_EVENT && (ir->Event.MouseEvent.dwButtonState & MOUSEMASK))) { done = 1; retval = 1; } else /* Discard it, it's an insignificant event */ ReadConsoleInput(hConIn, ir, 1, &count); } else /* There are no events in console event queue */ { done = 1; /* Stop looking */ retval = 0; } } return retval; } int __declspec(dllexport) __stdcall SourceWhere(buf) char **buf; { if (!buf) return 0; *buf = where_to_get_source; return 1; } int __declspec(dllexport) __stdcall SourceAuthor(buf) char **buf; { if (!buf) return 0; *buf = author; return 1; } int __declspec(dllexport) __stdcall KeyHandlerName(buf, full) char **buf; int full; { if (!buf) return 0; if (full) *buf = dllname; else *buf = shortdllname; return 1; }