#include #include #include #include "devilution.h" #include "miniwin/ddraw.h" #include "stubs.h" #include "controls/controller_motion.h" #include "controls/game_controls.h" #include "controls/plrctrls.h" #include "controls/touch.h" #include "miniwin/ddraw.h" #include "controls/controller.h" #ifdef __SWITCH__ #include "platform/switch/docking.h" #include #endif /** @file * * * Windows message handling and keyboard event conversion for SDL. */ namespace dvl { static std::deque message_queue; bool mouseWarping = false; int mouseWarpingX; int mouseWarpingY; void SetCursorPos(int X, int Y) { mouseWarpingX = X; mouseWarpingY = Y; mouseWarping = true; LogicalToOutput(&X, &Y); SDL_WarpMouseInWindow(window, X, Y); } // Moves the mouse to the first attribute "+" button. void FocusOnCharInfo() { if (invflag || plr[myplr]._pStatPts <= 0) return; // Find the first incrementable stat. int pc = plr[myplr]._pClass; int stat = -1; for (int i = 4; i >= 0; --i) { switch (i) { case ATTRIB_STR: if (plr[myplr]._pBaseStr >= MaxStats[pc][ATTRIB_STR]) continue; break; case ATTRIB_MAG: if (plr[myplr]._pBaseMag >= MaxStats[pc][ATTRIB_MAG]) continue; break; case ATTRIB_DEX: if (plr[myplr]._pBaseDex >= MaxStats[pc][ATTRIB_DEX]) continue; break; case ATTRIB_VIT: if (plr[myplr]._pBaseVit >= MaxStats[pc][ATTRIB_VIT]) continue; break; default: continue; } stat = i; } if (stat == -1) return; const auto &rect = ChrBtnsRect[stat]; SetCursorPos(rect.x + (rect.w / 2), rect.y + (rect.h / 2)); } static int translate_sdl_key(SDL_Keysym key) { // ref: https://wiki.libsdl.org/SDL_Keycode // ref: https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes SDL_Keycode sym = key.sym; switch (sym) { case SDLK_BACKSPACE: return DVL_VK_BACK; case SDLK_TAB: return DVL_VK_TAB; case SDLK_RETURN: return DVL_VK_RETURN; case SDLK_ESCAPE: return DVL_VK_ESCAPE; case SDLK_SPACE: return DVL_VK_SPACE; case SDLK_QUOTE: return DVL_VK_OEM_7; case SDLK_COMMA: return DVL_VK_OEM_COMMA; case SDLK_MINUS: return DVL_VK_OEM_MINUS; case SDLK_PERIOD: return DVL_VK_OEM_PERIOD; case SDLK_SLASH: return DVL_VK_OEM_2; case SDLK_SEMICOLON: return DVL_VK_OEM_1; case SDLK_EQUALS: return DVL_VK_OEM_PLUS; case SDLK_LEFTBRACKET: return DVL_VK_OEM_4; case SDLK_BACKSLASH: return DVL_VK_OEM_5; case SDLK_RIGHTBRACKET: return DVL_VK_OEM_6; case SDLK_BACKQUOTE: return DVL_VK_OEM_3; case SDLK_DELETE: return DVL_VK_DELETE; case SDLK_CAPSLOCK: return DVL_VK_CAPITAL; case SDLK_F1: return DVL_VK_F1; case SDLK_F2: return DVL_VK_F2; case SDLK_F3: return DVL_VK_F3; case SDLK_F4: return DVL_VK_F4; case SDLK_F5: return DVL_VK_F5; case SDLK_F6: return DVL_VK_F6; case SDLK_F7: return DVL_VK_F7; case SDLK_F8: return DVL_VK_F8; case SDLK_F9: return DVL_VK_F9; case SDLK_F10: return DVL_VK_F10; case SDLK_F11: return DVL_VK_F11; case SDLK_F12: return DVL_VK_F12; case SDLK_PRINTSCREEN: return DVL_VK_SNAPSHOT; case SDLK_SCROLLLOCK: return DVL_VK_SCROLL; case SDLK_PAUSE: return DVL_VK_PAUSE; case SDLK_INSERT: return DVL_VK_INSERT; case SDLK_HOME: return DVL_VK_HOME; case SDLK_PAGEUP: return DVL_VK_PRIOR; case SDLK_END: return DVL_VK_END; case SDLK_PAGEDOWN: return DVL_VK_NEXT; case SDLK_RIGHT: return DVL_VK_RIGHT; case SDLK_LEFT: return DVL_VK_LEFT; case SDLK_DOWN: return DVL_VK_DOWN; case SDLK_UP: return DVL_VK_UP; case SDLK_NUMLOCKCLEAR: return DVL_VK_NUMLOCK; case SDLK_KP_DIVIDE: return DVL_VK_DIVIDE; case SDLK_KP_MULTIPLY: return DVL_VK_MULTIPLY; case SDLK_KP_MINUS: // Returning DVL_VK_OEM_MINUS to play nice with Devilution automap zoom. // // For a distinct keypad key-code, DVL_VK_SUBTRACT should be returned. return DVL_VK_OEM_MINUS; case SDLK_KP_PLUS: // Returning DVL_VK_OEM_PLUS to play nice with Devilution automap zoom. // // For a distinct keypad key-code, DVL_VK_ADD should be returned. return DVL_VK_OEM_PLUS; case SDLK_KP_ENTER: return DVL_VK_RETURN; case SDLK_KP_1: return DVL_VK_NUMPAD1; case SDLK_KP_2: return DVL_VK_NUMPAD2; case SDLK_KP_3: return DVL_VK_NUMPAD3; case SDLK_KP_4: return DVL_VK_NUMPAD4; case SDLK_KP_5: return DVL_VK_NUMPAD5; case SDLK_KP_6: return DVL_VK_NUMPAD6; case SDLK_KP_7: return DVL_VK_NUMPAD7; case SDLK_KP_8: return DVL_VK_NUMPAD8; case SDLK_KP_9: return DVL_VK_NUMPAD9; case SDLK_KP_0: return DVL_VK_NUMPAD0; case SDLK_KP_PERIOD: return DVL_VK_DECIMAL; case SDLK_MENU: return DVL_VK_MENU; #ifndef USE_SDL1 case SDLK_KP_COMMA: return DVL_VK_OEM_COMMA; #endif case SDLK_LCTRL: return DVL_VK_LCONTROL; case SDLK_LSHIFT: return DVL_VK_LSHIFT; case SDLK_LALT: return DVL_VK_LMENU; case SDLK_LGUI: return DVL_VK_LWIN; case SDLK_RCTRL: return DVL_VK_RCONTROL; case SDLK_RSHIFT: return DVL_VK_RSHIFT; case SDLK_RALT: return DVL_VK_RMENU; case SDLK_RGUI: return DVL_VK_RWIN; default: if (sym >= SDLK_a && sym <= SDLK_z) { return 'A' + (sym - SDLK_a); } else if (sym >= SDLK_0 && sym <= SDLK_9) { return '0' + (sym - SDLK_0); } else if (sym >= SDLK_F1 && sym <= SDLK_F12) { return DVL_VK_F1 + (sym - SDLK_F1); } SDL_Log("unknown key: name=%s sym=0x%X scan=%d mod=0x%X", SDL_GetKeyName(sym), sym, key.scancode, key.mod); return -1; } } namespace { LPARAM position_for_mouse(short x, short y) { return (((uint16_t)(y & 0xFFFF)) << 16) | (uint16_t)(x & 0xFFFF); } WPARAM keystate_for_mouse(WPARAM ret) { ret |= (SDL_GetModState() & KMOD_SHIFT) ? DVL_MK_SHIFT : 0; // XXX: other DVL_MK_* codes not implemented return ret; } WINBOOL false_avail(const char *name, int value) { SDL_Log("Unhandled SDL event: %s %d", name, value); return true; } void StoreSpellCoords() { constexpr int START_X = 20; constexpr int END_X = 636; constexpr int END_Y = 495; constexpr int BOX_SIZE = 56; speedspellcount = 0; int xo = END_X, yo = END_Y; for (int i = 0; i < 4; i++) { std::uint64_t spells; switch (i) { case RSPLTYPE_SKILL: spells = plr[myplr]._pAblSpells; break; case RSPLTYPE_SPELL: spells = plr[myplr]._pMemSpells; break; case RSPLTYPE_SCROLL: spells = plr[myplr]._pScrlSpells; break; case RSPLTYPE_CHARGES: spells = plr[myplr]._pISpells; break; default: continue; } std::uint64_t spell = 1; for (int j = 1; j < MAX_SPELLS; j++) { if ((spell & spells)) { speedspellscoords[speedspellcount] = { xo - 36, yo - 188 }; ++speedspellcount; xo -= BOX_SIZE; if (xo == START_X) { xo = END_X; yo -= BOX_SIZE; } } spell <<= 1; } if (spells && xo != END_X) xo -= BOX_SIZE; if (xo == START_X) { xo = END_X; yo -= BOX_SIZE; } } } } // namespace /** * @brief Try to clean the inventory related cursor states. * @return True if it is safe to close the inventory */ bool BlurInventory() { if (pcurs >= CURSOR_FIRSTITEM) { if (!TryDropItem()) { if (plr[myplr]._pClass == PC_WARRIOR) { PlaySFX(PS_WARR16); // "Where would I put this?" #ifndef SPAWN } else if (plr[myplr]._pClass == PC_ROGUE) { PlaySFX(PS_ROGUE16); } else if (plr[myplr]._pClass == PC_SORCERER) { PlaySFX(PS_MAGE16); #endif } return false; } } invflag = false; if (pcurs > CURSOR_HAND) SetCursor_(CURSOR_HAND); if (chrflag) FocusOnCharInfo(); return true; } WINBOOL PeekMessageA(LPMSG lpMsg) { #ifdef __SWITCH__ HandleDocking(); #endif if (!message_queue.empty()) { *lpMsg = message_queue.front(); message_queue.pop_front(); return true; } SDL_Event e; if (!SDL_PollEvent(&e)) { return false; } lpMsg->message = 0; lpMsg->lParam = 0; lpMsg->wParam = 0; if (e.type == SDL_QUIT) { lpMsg->message = DVL_WM_QUIT; return true; } #ifndef USE_SDL1 handle_touch(&e, MouseX, MouseY); #endif #ifdef USE_SDL1 if (e.type == SDL_MOUSEMOTION) { OutputToLogical(&e.motion.x, &e.motion.y); } else if (e.type == SDL_MOUSEBUTTONDOWN || e.type == SDL_MOUSEBUTTONUP) { OutputToLogical(&e.button.x, &e.button.y); } #endif if ((e.type == SDL_KEYUP || e.type == SDL_KEYDOWN) && e.key.keysym.sym == SDLK_UNKNOWN) { // Erroneous events generated by RG350 kernel. return true; } if (ProcessControllerMotion(e)) { ScaleJoysticks(); return true; } GameAction action; if (GetGameAction(e, &action)) { if (action.type != GameActionType::NONE) { sgbControllerActive = true; if (movie_playing) { lpMsg->message = DVL_WM_KEYDOWN; if (action.type == GameActionType::SEND_KEY) lpMsg->wParam = action.send_key.vk_code; return true; } } switch (action.type) { case GameActionType::NONE: break; case GameActionType::USE_HEALTH_POTION: UseBeltItem(BLT_HEALING); break; case GameActionType::USE_MANA_POTION: UseBeltItem(BLT_MANA); break; case GameActionType::PRIMARY_ACTION: PerformPrimaryAction(); break; case GameActionType::SECONDARY_ACTION: PerformSecondaryAction(); break; case GameActionType::CAST_SPELL: PerformSpellAction(); break; case GameActionType::TOGGLE_QUICK_SPELL_MENU: if (!invflag || BlurInventory()) { if (!spselflag) DoSpeedBook(); else spselflag = false; chrflag = false; questlog = false; sbookflag = false; StoreSpellCoords(); } break; case GameActionType::TOGGLE_CHARACTER_INFO: chrflag = !chrflag; if (chrflag) { questlog = false; spselflag = false; if (pcurs == CURSOR_DISARM) SetCursor_(CURSOR_HAND); FocusOnCharInfo(); } break; case GameActionType::TOGGLE_QUEST_LOG: if (!questlog) { StartQuestlog(); chrflag = false; spselflag = false; } else { questlog = false; } break; case GameActionType::TOGGLE_INVENTORY: if (invflag) { BlurInventory(); } else { sbookflag = false; spselflag = false; invflag = true; if (pcurs == CURSOR_DISARM) SetCursor_(CURSOR_HAND); FocusOnInventory(); } break; case GameActionType::TOGGLE_SPELL_BOOK: if (BlurInventory()) { invflag = false; spselflag = false; sbookflag = !sbookflag; } break; case GameActionType::SEND_KEY: lpMsg->message = action.send_key.up ? DVL_WM_KEYUP : DVL_WM_KEYDOWN; lpMsg->wParam = action.send_key.vk_code; return true; case GameActionType::SEND_MOUSE_CLICK: sgbControllerActive = false; switch (action.send_mouse_click.button) { case GameActionSendMouseClick::LEFT: lpMsg->message = action.send_mouse_click.up ? DVL_WM_LBUTTONUP : DVL_WM_LBUTTONDOWN; break; case GameActionSendMouseClick::RIGHT: lpMsg->message = action.send_mouse_click.up ? DVL_WM_RBUTTONUP : DVL_WM_RBUTTONDOWN; break; } lpMsg->lParam = position_for_mouse(MouseX, MouseY); break; } return true; } else if (e.type < SDL_JOYAXISMOTION || e.type >= 0x700) { if (!mouseWarping || e.type != SDL_MOUSEMOTION) sgbControllerActive = false; if (mouseWarping && e.type == SDL_MOUSEMOTION) mouseWarping = false; } switch (e.type) { #ifndef USE_SDL1 case SDL_CONTROLLERDEVICEADDED: case SDL_CONTROLLERDEVICEREMOVED: break; case SDL_JOYDEVICEADDED: case SDL_JOYDEVICEREMOVED: InitController(); break; #endif case SDL_QUIT: lpMsg->message = DVL_WM_QUIT; break; case SDL_KEYDOWN: case SDL_KEYUP: { int key = translate_sdl_key(e.key.keysym); if (key == -1) return false_avail(e.type == SDL_KEYDOWN ? "SDL_KEYDOWN" : "SDL_KEYUP", e.key.keysym.sym); lpMsg->message = e.type == SDL_KEYDOWN ? DVL_WM_KEYDOWN : DVL_WM_KEYUP; lpMsg->wParam = (DWORD)key; // HACK: Encode modifier in lParam for TranslateMessage later lpMsg->lParam = e.key.keysym.mod << 16; } break; case SDL_MOUSEMOTION: lpMsg->message = DVL_WM_MOUSEMOVE; lpMsg->lParam = position_for_mouse(e.motion.x, e.motion.y); lpMsg->wParam = keystate_for_mouse(0); break; case SDL_MOUSEBUTTONDOWN: { int button = e.button.button; if (button == SDL_BUTTON_LEFT) { lpMsg->message = DVL_WM_LBUTTONDOWN; lpMsg->lParam = position_for_mouse(e.button.x, e.button.y); lpMsg->wParam = keystate_for_mouse(DVL_MK_LBUTTON); } else if (button == SDL_BUTTON_RIGHT) { lpMsg->message = DVL_WM_RBUTTONDOWN; lpMsg->lParam = position_for_mouse(e.button.x, e.button.y); lpMsg->wParam = keystate_for_mouse(DVL_MK_RBUTTON); } } break; case SDL_MOUSEBUTTONUP: { int button = e.button.button; if (button == SDL_BUTTON_LEFT) { lpMsg->message = DVL_WM_LBUTTONUP; lpMsg->lParam = position_for_mouse(e.button.x, e.button.y); lpMsg->wParam = keystate_for_mouse(0); } else if (button == SDL_BUTTON_RIGHT) { lpMsg->message = DVL_WM_RBUTTONUP; lpMsg->lParam = position_for_mouse(e.button.x, e.button.y); lpMsg->wParam = keystate_for_mouse(0); } } break; #ifndef USE_SDL1 case SDL_MOUSEWHEEL: lpMsg->message = DVL_WM_KEYDOWN; if (e.wheel.y > 0) { lpMsg->wParam = GetAsyncKeyState(DVL_VK_CONTROL) ? DVL_VK_OEM_PLUS : DVL_VK_UP; } else if (e.wheel.y < 0) { lpMsg->wParam = GetAsyncKeyState(DVL_VK_CONTROL) ? DVL_VK_OEM_MINUS : DVL_VK_DOWN; } else if (e.wheel.x > 0) { lpMsg->wParam = DVL_VK_LEFT; } else if (e.wheel.x < 0) { lpMsg->wParam = DVL_VK_RIGHT; } break; #if SDL_VERSION_ATLEAST(2, 0, 4) case SDL_AUDIODEVICEADDED: return false_avail("SDL_AUDIODEVICEADDED", e.adevice.which); case SDL_AUDIODEVICEREMOVED: return false_avail("SDL_AUDIODEVICEREMOVED", e.adevice.which); case SDL_KEYMAPCHANGED: return false_avail("SDL_KEYMAPCHANGED", 0); #endif case SDL_TEXTEDITING: return false_avail("SDL_TEXTEDITING", e.edit.length); case SDL_TEXTINPUT: return false_avail("SDL_TEXTINPUT", e.text.windowID); case SDL_WINDOWEVENT: switch (e.window.event) { case SDL_WINDOWEVENT_SHOWN: gbActive = true; lpMsg->message = DVL_WM_PAINT; break; case SDL_WINDOWEVENT_HIDDEN: gbActive = false; break; case SDL_WINDOWEVENT_EXPOSED: lpMsg->message = DVL_WM_PAINT; break; case SDL_WINDOWEVENT_MOVED: case SDL_WINDOWEVENT_RESIZED: case SDL_WINDOWEVENT_SIZE_CHANGED: case SDL_WINDOWEVENT_MINIMIZED: case SDL_WINDOWEVENT_MAXIMIZED: case SDL_WINDOWEVENT_RESTORED: case SDL_WINDOWEVENT_LEAVE: case SDL_WINDOWEVENT_FOCUS_GAINED: case SDL_WINDOWEVENT_FOCUS_LOST: #if SDL_VERSION_ATLEAST(2, 0, 5) case SDL_WINDOWEVENT_TAKE_FOCUS: #endif break; case SDL_WINDOWEVENT_ENTER: lpMsg->message = DVL_WM_MOUSEHOVER; // Bug in SDL, SDL_WarpMouseInWindow doesn't emit SDL_MOUSEMOTION // and SDL_GetMouseState gives previous location if mouse was // outside window (observed on Ubuntu 19.04) if (mouseWarping) { MouseX = mouseWarpingX; MouseY = mouseWarpingY; mouseWarping = false; } break; case SDL_WINDOWEVENT_CLOSE: lpMsg->message = DVL_WM_QUERYENDSESSION; break; default: return false_avail("SDL_WINDOWEVENT", e.window.event); } break; #endif default: return false_avail("unknown", e.type); } return true; } WINBOOL TranslateMessage(const MSG *lpMsg) { if (lpMsg->message == DVL_WM_KEYDOWN) { int key = lpMsg->wParam; unsigned mod = (DWORD)lpMsg->lParam >> 16; bool shift = (mod & KMOD_SHIFT) != 0; bool upper = shift != (mod & KMOD_CAPS); bool is_alpha = (key >= 'A' && key <= 'Z'); bool is_numeric = (key >= '0' && key <= '9'); bool is_control = key == DVL_VK_SPACE || key == DVL_VK_BACK || key == DVL_VK_ESCAPE || key == DVL_VK_TAB || key == DVL_VK_RETURN; bool is_oem = (key >= DVL_VK_OEM_1 && key <= DVL_VK_OEM_7); if (is_control || is_alpha || is_numeric || is_oem) { if (!upper && is_alpha) { key = tolower(key); } else if (shift && is_numeric) { key = key == '0' ? ')' : key - 0x10; } else if (is_oem) { // XXX: This probably only supports US keyboard layout switch (key) { case DVL_VK_OEM_1: key = shift ? ':' : ';'; break; case DVL_VK_OEM_2: key = shift ? '?' : '/'; break; case DVL_VK_OEM_3: key = shift ? '~' : '`'; break; case DVL_VK_OEM_4: key = shift ? '{' : '['; break; case DVL_VK_OEM_5: key = shift ? '|' : '\\'; break; case DVL_VK_OEM_6: key = shift ? '}' : ']'; break; case DVL_VK_OEM_7: key = shift ? '"' : '\''; break; case DVL_VK_OEM_MINUS: key = shift ? '_' : '-'; break; case DVL_VK_OEM_PLUS: key = shift ? '+' : '='; break; case DVL_VK_OEM_PERIOD: key = shift ? '>' : '.'; break; case DVL_VK_OEM_COMMA: key = shift ? '<' : ','; break; default: UNIMPLEMENTED(); } } #ifdef _DEBUG if (key >= 32) { DUMMY_PRINT("char: %c", key); } #endif // XXX: This does not add extended info to lParam PostMessageA(DVL_WM_CHAR, key, 0); } } return true; } SHORT GetAsyncKeyState(int vKey) { if (vKey == DVL_MK_LBUTTON) return SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(SDL_BUTTON_LEFT); if (vKey == DVL_MK_RBUTTON) return SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(SDL_BUTTON_RIGHT); const Uint8 *state = SDLC_GetKeyState(); switch (vKey) { case DVL_VK_CONTROL: return state[SDLC_KEYSTATE_LEFTCTRL] || state[SDLC_KEYSTATE_RIGHTCTRL] ? 0x8000 : 0; case DVL_VK_SHIFT: return state[SDLC_KEYSTATE_LEFTSHIFT] || state[SDLC_KEYSTATE_RIGHTSHIFT] ? 0x8000 : 0; case DVL_VK_MENU: return state[SDLC_KEYSTATE_MENU] ? 0x8000 : 0; case DVL_VK_LEFT: return state[SDLC_KEYSTATE_LEFT] ? 0x8000 : 0; case DVL_VK_UP: return state[SDLC_KEYSTATE_UP] ? 0x8000 : 0; case DVL_VK_RIGHT: return state[SDLC_KEYSTATE_RIGHT] ? 0x8000 : 0; case DVL_VK_DOWN: return state[SDLC_KEYSTATE_DOWN] ? 0x8000 : 0; default: return 0; } } LRESULT DispatchMessageA(const MSG *lpMsg) { assert(CurrentProc); return CurrentProc(NULL, lpMsg->message, lpMsg->wParam, lpMsg->lParam); } WINBOOL PostMessageA(UINT Msg, WPARAM wParam, LPARAM lParam) { MSG msg; msg.message = Msg; msg.wParam = wParam; msg.lParam = lParam; message_queue.push_back(msg); return true; } } // namespace dvl