diff --git a/docs/rh-log.txt b/docs/rh-log.txt index 69531e7bbf..4fda7c1cc0 100644 --- a/docs/rh-log.txt +++ b/docs/rh-log.txt @@ -1,4 +1,20 @@ -July 25, 2009 (Changes by Graf Zahl) +July 25, 2009 +- Joystick devices now send key up events for any buttons that are held + down when they are destroyed. +- Changed the joystick enable-y menu items to be nonrepeatable so that you + can't create several device scans per second. Changing them with a + controller is also disabled so that you can't, for example, toggle XInput + support using an XInput controller and have things go haywire when the + game receives an infinite number of key down events when the controller + is reinitialized with the other input system. +- Changed menu input to use a consolidated set of buttons, so most menus + can be navigated with a controller as well as a keyboard. +- Changed the way that X/Y analog axes are converted to digital axes. + Previously, this was done by checking if each axis was outside its deadzone. + Now they are checked together based on their angle, so straight up/down/ + left/right are much easier to achieve. + +July 25, 2009 (Changes by Graf Zahl) - Fixed: The composer for complex multipatch textures did not clear the palette buffer before filling it. diff --git a/src/c_dispatch.cpp b/src/c_dispatch.cpp index 91bf725097..6540fc4c9e 100644 --- a/src/c_dispatch.cpp +++ b/src/c_dispatch.cpp @@ -421,7 +421,7 @@ FButtonStatus *FindButton (unsigned int key) return bit ? bit->Button : NULL; } -void FButtonStatus::PressKey (int keynum) +bool FButtonStatus::PressKey (int keynum) { int i, open; @@ -445,22 +445,26 @@ void FButtonStatus::PressKey (int keynum) } else if (Keys[i] == keynum) { // Key is already down; do nothing - return; + return false; } } if (open < 0) { // No free key slots, so do nothing Printf ("More than %u keys pressed for a single action!\n", MAX_KEYS); - return; + return false; } Keys[open] = keynum; } + BYTE wasdown = bDown; bDown = bWentDown = true; + // Returns true if this key caused the button to go down. + return !wasdown; } -void FButtonStatus::ReleaseKey (int keynum) +bool FButtonStatus::ReleaseKey (int keynum) { int i, numdown, match; + BYTE wasdown = bDown; keynum &= KEY_DBLCLICKED-1; @@ -488,7 +492,7 @@ void FButtonStatus::ReleaseKey (int keynum) } if (match < 0) { // Key was not down; do nothing - return; + return false; } Keys[match] = 0; bWentUp = true; @@ -497,6 +501,8 @@ void FButtonStatus::ReleaseKey (int keynum) bDown = false; } } + // Returns true if releasing this key caused the button to go up. + return wasdown && !bDown; } void ResetButtonTriggers () diff --git a/src/c_dispatch.h b/src/c_dispatch.h index 52ca088014..cde53996a9 100644 --- a/src/c_dispatch.h +++ b/src/c_dispatch.h @@ -140,8 +140,8 @@ struct FButtonStatus BYTE bWentUp; // Button went up this tic BYTE padTo16Bytes; - void PressKey (int keynum); - void ReleaseKey (int keynum); + bool PressKey (int keynum); // Returns true if this key caused the button to be pressed. + bool ReleaseKey (int keynum); // Returns true if this key is no longer pressed. void ResetTriggers () { bWentDown = bWentUp = false; } }; diff --git a/src/doomdef.h b/src/doomdef.h index bec8985d49..03a4fffd62 100644 --- a/src/doomdef.h +++ b/src/doomdef.h @@ -156,6 +156,14 @@ enum ESkillLevels #define KEY_MOUSE8 0x107 #define KEY_FIRSTJOYBUTTON 0x108 +#define KEY_JOY1 (KEY_FIRSTJOYBUTTON+0) +#define KEY_JOY2 (KEY_FIRSTJOYBUTTON+1) +#define KEY_JOY3 (KEY_FIRSTJOYBUTTON+2) +#define KEY_JOY4 (KEY_FIRSTJOYBUTTON+3) +#define KEY_JOY5 (KEY_FIRSTJOYBUTTON+4) +#define KEY_JOY6 (KEY_FIRSTJOYBUTTON+5) +#define KEY_JOY7 (KEY_FIRSTJOYBUTTON+6) +#define KEY_JOY8 (KEY_FIRSTJOYBUTTON+7) #define KEY_LASTJOYBUTTON 0x187 #define KEY_JOYPOV1_UP 0x188 #define KEY_JOYPOV1_RIGHT 0x189 diff --git a/src/m_joy.cpp b/src/m_joy.cpp index 73689993a8..51291dde72 100644 --- a/src/m_joy.cpp +++ b/src/m_joy.cpp @@ -33,6 +33,9 @@ CUSTOM_CVAR(Bool, use_joystick, false, CVAR_ARCHIVE|CVAR_GLOBALCONFIG|CVAR_NOINI // PRIVATE DATA DEFINITIONS ------------------------------------------------ +// Bits 0 is X+, 1 is X-, 2 is Y+, and 3 is Y-. +static BYTE JoyAngleButtons[8] = { 1, 1+4, 4, 2+4, 2, 2+8, 8, 1+8 }; + // CODE -------------------------------------------------------------------- //========================================================================== @@ -170,26 +173,69 @@ void M_SaveJoystickConfig(IJoystickConfig *joy) double Joy_RemoveDeadZone(double axisval, double deadzone, BYTE *buttons) { + BYTE butt; + // Cancel out deadzone. if (fabs(axisval) < deadzone) { axisval = 0; - *buttons = 0; + butt = 0; } // Make the dead zone the new 0. else if (axisval < 0) { axisval = (axisval + deadzone) / (1.0 - deadzone); - *buttons = 2; // button minus + butt = 2; // button minus } else { axisval = (axisval - deadzone) / (1.0 - deadzone); - *buttons = 1; // button plus + butt = 1; // button plus + } + if (buttons != NULL) + { + *buttons = butt; } return axisval; } +//=========================================================================== +// +// Joy_XYAxesToButtons +// +// Given two axes, returns a button set for them. They should have already +// been sent through Joy_RemoveDeadZone. For axes that share the same +// physical stick, the angle the stick forms should determine whether or +// not the four component buttons are present. Going by deadzone alone gives +// you huge areas where you have to buttons pressed and thin strips where +// you only have one button pressed. For DirectInput gamepads, there is +// not much standard for how the right stick is presented, so we can only +// do this for the left stick for those, since X and Y axes are pretty +// standard. For XInput and Raw PS2 controllers, both sticks are processed +// through here. +// +//=========================================================================== + +int Joy_XYAxesToButtons(double x, double y) +{ + if (x == 0 && y == 0) + { + return 0; + } + double rad = atan2(y, x); + if (rad < 0) + { + rad += 2*M_PI; + } + // The circle is divided into eight segments for corresponding + // button combinations. Each segment is pi/4 radians wide. We offset + // by half this so that the segments are centered around the ideal lines + // their buttons represent instead of being right on the lines. + rad += M_PI/8; // Offset + rad *= 4/M_PI; // Convert range from [0,2pi) to [0,8) + return JoyAngleButtons[int(rad) & 7]; +} + //=========================================================================== // // Joy_GenerateButtonEvents diff --git a/src/m_joy.h b/src/m_joy.h index 6c8920ed05..92771762b4 100644 --- a/src/m_joy.h +++ b/src/m_joy.h @@ -51,6 +51,7 @@ void M_SaveJoystickConfig(IJoystickConfig *joy); void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, int base); void Joy_GenerateButtonEvents(int oldbuttons, int newbuttons, int numbuttons, const int *keys); +int Joy_XYAxesToButtons(double x, double y); double Joy_RemoveDeadZone(double axisval, double deadzone, BYTE *buttons); // These ought to be provided by a system-specific i_input.cpp. diff --git a/src/m_menu.cpp b/src/m_menu.cpp index cc21a94c95..a3447e2668 100644 --- a/src/m_menu.cpp +++ b/src/m_menu.cpp @@ -79,6 +79,9 @@ #define SELECTOR_XOFFSET (-28) #define SELECTOR_YOFFSET (-1) +#define KEY_REPEAT_DELAY (TICRATE*5/12) +#define KEY_REPEAT_RATE (3) + // TYPES ------------------------------------------------------------------- struct FSaveGameNode : public Node @@ -149,6 +152,7 @@ static void M_ExtractSaveData (const FSaveGameNode *file); static void M_UnloadSaveData (); static void M_InsertSaveNode (FSaveGameNode *node); static bool M_SaveLoadResponder (event_t *ev); +static void M_SaveLoadButtonHandler(EMenuKey key); static void M_DeleteSaveResponse (int choice); static void M_DrawMainMenu (); @@ -205,6 +209,8 @@ int skullAnimCounter; // skull animation counter bool drawSkull; // [RH] don't always draw skull bool M_DemoNoPlay; bool OptionsActive; +FButtonStatus MenuButtons[NUM_MKEYS]; +int MenuButtonTickers[NUM_MKEYS]; // PRIVATE DATA DEFINITIONS ------------------------------------------------ @@ -2783,184 +2789,346 @@ bool M_Responder (event_t *ev) { int ch; int i; + EMenuKey mkey = NUM_MKEYS; + bool keyup = true; ch = -1; + if (chatmodeon) + { + return false; + } if (menuactive == MENU_Off && ev->type == EV_KeyDown) { // Pop-up menu? if (ev->data1 == KEY_ESCAPE) { - M_StartControlPanel (true); - M_SetupNextMenu (TopLevelMenu); + M_StartControlPanel(true); + M_SetupNextMenu(TopLevelMenu); return true; } // If devparm is set, pressing F1 always takes a screenshot no matter // what it's bound to. (for those who don't bother to read the docs) if (devparm && ev->data1 == KEY_F1) { - G_ScreenShot (NULL); + G_ScreenShot(NULL); return true; } + return false; } - else if (ev->type == EV_GUI_Event && menuactive == MENU_On && !chatmodeon) + if (menuactive == MENU_WaitKey && OptionsActive) { - if (ev->subtype == EV_GUI_KeyDown || ev->subtype == EV_GUI_KeyRepeat) - { - ch = ev->data1; - } - else if (ev->subtype == EV_GUI_Char && genStringEnter) - { - ch = ev->data1; - if (saveCharIndex < genStringLen && - (genStringEnter==2 || (size_t)SmallFont->StringWidth (savegamestring) < (genStringLen-1)*8)) - { - savegamestring[saveCharIndex] = ch; - savegamestring[++saveCharIndex] = 0; - } - return true; - } - else if (ev->subtype == EV_GUI_Char && messageToPrint && messageNeedsInput) - { - ch = ev->data1; - } - } - - if (OptionsActive && !chatmodeon) - { - M_OptResponder (ev); + M_OptResponder(ev); return true; } - - if (ch == -1) - return false; - - // Save Game string input - // [RH] and Player Name string input - if (genStringEnter) + if (menuactive != MENU_On && menuactive != MENU_OnNoPause && + !genStringEnter && !messageToPrint) { + return false; + } + + // There are a few input sources we are interested in: + // + // EV_KeyDown / EV_KeyUp : joysticks/gamepads/controllers + // EV_GUI_KeyDown / EV_GUI_KeyUp : the keyboard + // EV_GUI_Char : printable characters, which we want in string input mode + // + // This code previously listened for EV_GUI_KeyRepeat to handle repeating + // in the menus, but that doesn't work with gamepads, so now we combine + // the multiple inputs into buttons and handle the repetition manually. + // + // FIXME: genStringEnter and messageToPrint do not play well with game + // controllers. In fact, you can still interact with the rest of + // the menu using a controller while the menu waits for input. Especially + // bad if you, say, select quit from the main menu. (Ideally, + // genStringEnter will pop up a keypad if it detects a controller is + // being used to navigate the menu. At the very least, it should + // disable the controller.) + if (ev->type == EV_GUI_Event) + { + // Save game and player name string input + if (genStringEnter) + { + if (ev->subtype == EV_GUI_Char) + { + if (saveCharIndex < genStringLen && + (genStringEnter == 2 || (size_t)SmallFont->StringWidth(savegamestring) < (genStringLen-1)*8)) + { + savegamestring[saveCharIndex] = (char)ev->data1; + savegamestring[++saveCharIndex] = 0; + } + return true; + } + if (ev->subtype == EV_GUI_KeyDown || ev->subtype == EV_GUI_KeyUp) + { + ch = ev->data1; + + if (genStringEnter) + { + switch (ch) + { + case '\b': + if (saveCharIndex > 0) + { + saveCharIndex--; + savegamestring[saveCharIndex] = 0; + } + break; + + case GK_ESCAPE: + genStringEnter = 0; + genStringCancel (); // [RH] Function to call when escape is pressed + break; + + case '\r': + if (savegamestring[0]) + { + genStringEnter = 0; + if (messageToPrint) + M_ClearMenus (); + genStringEnd (SelSaveGame); // [RH] Function to call when enter is pressed + } + break; + } + return true; + } + } + } + if (ev->subtype != EV_GUI_KeyDown && ev->subtype != EV_GUI_KeyUp) + { + return false; + } + ch = ev->data1; + keyup = ev->subtype == EV_GUI_KeyUp; switch (ch) { - case '\b': - if (saveCharIndex > 0) + case GK_ESCAPE: mkey = MKEY_Back; break; + case GK_RETURN: mkey = MKEY_Enter; break; + case GK_UP: mkey = MKEY_Up; break; + case GK_DOWN: mkey = MKEY_Down; break; + case GK_LEFT: mkey = MKEY_Left; break; + case GK_RIGHT: mkey = MKEY_Right; break; + case GK_BACKSPACE: mkey = MKEY_Clear; break; + case GK_PGUP: mkey = MKEY_PageUp; break; + case GK_PGDN: mkey = MKEY_PageDown; break; + default: + if (ch == ' ' && currentMenu == &PSetupDef) { - saveCharIndex--; - savegamestring[saveCharIndex] = 0; + mkey = MKEY_Clear; } - break; - - case GK_ESCAPE: - genStringEnter = 0; - genStringCancel (); // [RH] Function to call when escape is pressed - break; - - case '\r': - if (savegamestring[0]) + else if (!keyup && !OptionsActive) { - genStringEnter = 0; + ch = tolower (ch); if (messageToPrint) - M_ClearMenus (); - genStringEnd (SelSaveGame); // [RH] Function to call when enter is pressed + { + // Take care of any messages that need input + ch = tolower (ch); + if (messageNeedsInput) + { + // For each printable keystroke, both EV_GUI_KeyDown and + // EV_GUI_Char will be generated, in that order. If we close + // the menu after the first event arrives and the fullscreen + // console is up, the console will get the EV_GUI_Char event + // next. Therefore, the message input should only respond to + // EV_GUI_Char events (sans Escape, which only generates + // EV_GUI_KeyDown.) + if (ev->subtype != EV_GUI_Char && ch != GK_ESCAPE) + { +// return false; + } + if (ch != ' ' && ch != 'n' && ch != 'y' && ch != GK_ESCAPE) + { + return false; + } + } + + menuactive = messageLastMenuActive; + messageToPrint = 0; + if (messageRoutine) + messageRoutine (ch); + + if (menuactive != MENU_Off) + { + M_DeactivateMenuInput(); + } + SB_state = screen->GetPageCount(); // refresh the status bar + BorderNeedRefresh = screen->GetPageCount(); + S_Sound(CHAN_VOICE | CHAN_UI, "menu/dismiss", 1, ATTN_NONE); + return true; + } + else + { + // Search for a menu item associated with the pressed key. + for (i = (itemOn + 1) % currentMenu->numitems; + i != itemOn; + i = (i + 1) % currentMenu->numitems) + { + if (currentMenu->menuitems[i].alphaKey == ch) + { + break; + } + } + if (currentMenu->menuitems[i].alphaKey == ch) + { + itemOn = i; + S_Sound(CHAN_VOICE | CHAN_UI, "menu/cursor", 1, ATTN_NONE); + return true; + } + } } break; } - return true; + } + else if (ev->type == EV_KeyDown || ev->type == EV_KeyUp) + { + keyup = ev->type == EV_KeyUp; + ch = ev->data1; + switch (ch) + { + case KEY_JOY1: + case KEY_PAD_A: + mkey = MKEY_Enter; + break; + + case KEY_JOY2: + case KEY_PAD_B: + mkey = MKEY_Back; + break; + + case KEY_JOY3: + case KEY_PAD_X: + mkey = MKEY_Clear; + break; + + case KEY_JOY5: + case KEY_PAD_LSHOULDER: + mkey = MKEY_PageUp; + break; + + case KEY_JOY6: + case KEY_PAD_RSHOULDER: + mkey = MKEY_PageDown; + break; + + case KEY_PAD_DPAD_UP: + case KEY_PAD_LTHUMB_UP: + case KEY_JOYAXIS1MINUS: + case KEY_JOYPOV1_UP: + mkey = MKEY_Up; + break; + + case KEY_PAD_DPAD_DOWN: + case KEY_PAD_LTHUMB_DOWN: + case KEY_JOYAXIS1PLUS: + case KEY_JOYPOV1_DOWN: + mkey = MKEY_Down; + break; + + case KEY_PAD_DPAD_LEFT: + case KEY_PAD_LTHUMB_LEFT: + case KEY_JOYAXIS2MINUS: + case KEY_JOYPOV1_LEFT: + mkey = MKEY_Left; + break; + + case KEY_PAD_DPAD_RIGHT: + case KEY_PAD_LTHUMB_RIGHT: + case KEY_JOYAXIS2PLUS: + case KEY_JOYPOV1_RIGHT: + mkey = MKEY_Right; + break; + } } - // Take care of any messages that need input - if (messageToPrint) + if (mkey != NUM_MKEYS) { - ch = tolower (ch); - if (messageNeedsInput) + if (keyup) { - // For each printable keystroke, both EV_GUI_KeyDown and - // EV_GUI_Char will be generated, in that order. If we close - // the menu after the first event arrives and the fullscreen - // console is up, the console will get the EV_GUI_Char event - // next. Therefore, the message input should only respond to - // EV_GUI_Char events (sans Escape, which only generates - // EV_GUI_KeyDown.) - if (ev->subtype != EV_GUI_Char && ch != GK_ESCAPE) + MenuButtons[mkey].ReleaseKey(ch); + } + else + { + if (MenuButtons[mkey].PressKey(ch)) { - return false; - } - if (ch != ' ' && ch != 'n' && ch != 'y' && ch != GK_ESCAPE) - { - return false; + if (mkey <= MKEY_PageDown) + { + MenuButtonTickers[mkey] = KEY_REPEAT_DELAY; + } + M_ButtonHandler(mkey, false); } } - - menuactive = messageLastMenuActive; - messageToPrint = 0; - if (messageRoutine) - messageRoutine (ch); - - if (menuactive != MENU_Off) - { - M_DeactivateMenuInput (); - } - SB_state = screen->GetPageCount (); // refresh the statbar - BorderNeedRefresh = screen->GetPageCount (); - S_Sound (CHAN_VOICE | CHAN_UI, "menu/dismiss", 1, ATTN_NONE); - return true; } - if (ch == GK_ESCAPE) - { - // [RH] Escape now moves back one menu instead of quitting - // the menu system. Thus, backspace is ignored. - currentMenu->lastOn = itemOn; - M_PopMenuStack (); - return true; - } - - if (currentMenu == &SaveDef || currentMenu == &LoadDef) + if (ev->type == EV_GUI_Event && (currentMenu == &SaveDef || currentMenu == &LoadDef)) { return M_SaveLoadResponder (ev); } - // Keys usable within menu - switch (ch) + // Eat key downs, but let the rest through. + return !keyup; +} + +void M_ButtonHandler(EMenuKey key, bool repeat) +{ + if (OptionsActive) { - case GK_DOWN: + M_OptButtonHandler(key, repeat); + return; + } + if (key == MKEY_Back) + { + // Save the cursor position on the current menu, and pop it off the stack + // to go back to the previous menu. + currentMenu->lastOn = itemOn; + M_PopMenuStack(); + return; + } + if (currentMenu == &SaveDef || currentMenu == &LoadDef) + { + M_SaveLoadButtonHandler(key); + return; + } + switch (key) + { + case MKEY_Down: do { - if (itemOn+1 > currentMenu->numitems-1) + if (itemOn + 1 >= currentMenu->numitems) itemOn = 0; else itemOn++; S_Sound (CHAN_VOICE | CHAN_UI, "menu/cursor", 1, ATTN_NONE); - } while(currentMenu->menuitems[itemOn].status==-1); - return true; + } while (currentMenu->menuitems[itemOn].status == -1); + break; - case GK_UP: + case MKEY_Up: do { - if (!itemOn) - itemOn = currentMenu->numitems-1; + if (itemOn == 0) + itemOn = currentMenu->numitems - 1; else itemOn--; S_Sound (CHAN_VOICE | CHAN_UI, "menu/cursor", 1, ATTN_NONE); - } while(currentMenu->menuitems[itemOn].status==-1); - return true; + } while (currentMenu->menuitems[itemOn].status == -1); + break; - case GK_LEFT: + case MKEY_Left: if (currentMenu->menuitems[itemOn].routine && currentMenu->menuitems[itemOn].status == 2) { S_Sound (CHAN_VOICE | CHAN_UI, "menu/change", 1, ATTN_NONE); currentMenu->menuitems[itemOn].routine(0); } - return true; + break; - case GK_RIGHT: + case MKEY_Right: if (currentMenu->menuitems[itemOn].routine && currentMenu->menuitems[itemOn].status == 2) { S_Sound (CHAN_VOICE | CHAN_UI, "menu/change", 1, ATTN_NONE); currentMenu->menuitems[itemOn].routine(1); } - return true; + break; - case '\r': + case MKEY_Enter: if (currentMenu->menuitems[itemOn].routine && currentMenu->menuitems[itemOn].status) { @@ -2976,47 +3144,75 @@ bool M_Responder (event_t *ev) S_Sound (CHAN_VOICE | CHAN_UI, "menu/choose", 1, ATTN_NONE); } } - return true; + break; - case ' ': + case MKEY_Clear: if (currentMenu == &PSetupDef) { PlayerRotation ^= 8; - break; - } - // intentional fall-through - - default: - if (ch) - { - ch = tolower (ch); - for (i = (itemOn + 1) % currentMenu->numitems; - i != itemOn; - i = (i + 1) % currentMenu->numitems) - { - if (currentMenu->menuitems[i].alphaKey == ch) - { - break; - } - } - if (currentMenu->menuitems[i].alphaKey == ch) - { - itemOn = i; - S_Sound (CHAN_VOICE | CHAN_UI, "menu/cursor", 1, ATTN_NONE); - return true; - } } break; } - - // [RH] Menu eats all keydown events while active - return (ev->subtype == EV_GUI_KeyDown); } -bool M_SaveLoadResponder (event_t *ev) +static void M_SaveLoadButtonHandler(EMenuKey key) { - char workbuf[512]; + if (SelSaveGame == NULL || SelSaveGame->Succ == NULL) + { + return; + } + switch (key) + { + case MKEY_Up: + if (SelSaveGame != SaveGames.Head) + { + if (SelSaveGame == TopSaveGame) + { + TopSaveGame = static_cast(TopSaveGame->Pred); + } + SelSaveGame = static_cast(SelSaveGame->Pred); + } + else + { + SelSaveGame = static_cast(SaveGames.TailPred); + } + M_UnloadSaveData (); + M_ExtractSaveData (SelSaveGame); + break; + case MKEY_Down: + if (SelSaveGame != SaveGames.TailPred) + { + SelSaveGame = static_cast(SelSaveGame->Succ); + } + else + { + SelSaveGame = TopSaveGame = + static_cast(SaveGames.Head); + } + M_UnloadSaveData (); + M_ExtractSaveData (SelSaveGame); + break; + + case MKEY_Enter: + if (currentMenu == &LoadDef) + { + M_LoadSelect (SelSaveGame); + } + else + { + M_SaveSelect (SelSaveGame); + } + break; + } +} + +static bool M_SaveLoadResponder (event_t *ev) +{ + if (ev->subtype != EV_GUI_KeyDown) + { + return false; + } if (SelSaveGame != NULL && SelSaveGame->Succ != NULL) { switch (ev->data1) @@ -3024,6 +3220,8 @@ bool M_SaveLoadResponder (event_t *ev) case GK_F1: if (!SelSaveGame->Filename.IsEmpty()) { + char workbuf[512]; + mysnprintf (workbuf, countof(workbuf), "File on disk:\n%s", SelSaveGame->Filename.GetChars()); if (SaveComment != NULL) { @@ -3033,37 +3231,6 @@ bool M_SaveLoadResponder (event_t *ev) } break; - case GK_UP: - if (SelSaveGame != SaveGames.Head) - { - if (SelSaveGame == TopSaveGame) - { - TopSaveGame = static_cast(TopSaveGame->Pred); - } - SelSaveGame = static_cast(SelSaveGame->Pred); - } - else - { - SelSaveGame = static_cast(SaveGames.TailPred); - } - M_UnloadSaveData (); - M_ExtractSaveData (SelSaveGame); - break; - - case GK_DOWN: - if (SelSaveGame != SaveGames.TailPred) - { - SelSaveGame = static_cast(SelSaveGame->Succ); - } - else - { - SelSaveGame = TopSaveGame = - static_cast(SaveGames.Head); - } - M_UnloadSaveData (); - M_ExtractSaveData (SelSaveGame); - break; - case GK_DEL: case '\b': if (SelSaveGame != &NewSaveNode) @@ -3082,20 +3249,9 @@ bool M_SaveLoadResponder (event_t *ev) M_UnloadSaveData (); } break; - - case '\r': - if (currentMenu == &LoadDef) - { - M_LoadSelect (SelSaveGame); - } - else - { - M_SaveSelect (SelSaveGame); - } } } - - return (ev->subtype == EV_GUI_KeyDown); + return true; } static void M_LoadSelect (const FSaveGameNode *file) @@ -3166,7 +3322,11 @@ void M_StartControlPanel (bool makeSound) // intro might call this repeatedly if (menuactive == MENU_On) return; - + + for (int i = 0; i < NUM_MKEYS; ++i) + { + MenuButtons[i].ResetTriggers(); + } drawSkull = true; MenuStackDepth = 0; currentMenu = TopLevelMenu; @@ -3437,7 +3597,21 @@ void M_Ticker (void) skullAnimCounter = 8; } if (currentMenu == &PSetupDef || currentMenu == &ClassMenuDef) - M_PlayerSetupTicker (); + { + M_PlayerSetupTicker(); + } + + for (int i = 0; i < NUM_MKEYS; ++i) + { + if (MenuButtons[i].bDown) + { + if (MenuButtonTickers[i] > 0 && --MenuButtonTickers[i] <= 0) + { + MenuButtonTickers[i] = KEY_REPEAT_RATE; + M_ButtonHandler(EMenuKey(i), true); + } + } + } } diff --git a/src/m_menu.h b/src/m_menu.h index 25f36a26d3..ff899511d3 100644 --- a/src/m_menu.h +++ b/src/m_menu.h @@ -124,6 +124,7 @@ typedef enum { struct IJoystickConfig; void UpdateJoystickMenu(IJoystickConfig *selected); +// Yeargh! It's a monster! struct menuitem_t { itemtype type; @@ -150,7 +151,7 @@ struct menuitem_t int key2; char *res2; void *extra; - float discretecenter; + float discretecenter; // 1 to center or 2 to disable repeat (do I even use centered discretes?) } c; union { float step; @@ -238,6 +239,25 @@ struct menustack_t bool drawSkull; }; +enum EMenuKey +{ + MKEY_Up, + MKEY_Down, + MKEY_Left, + MKEY_Right, + MKEY_PageUp, + MKEY_PageDown, + //----------------- Keys past here do not repeat. + MKEY_Enter, + MKEY_Back, // Back to previous menu + MKEY_Clear, // Clear keybinding/flip player sprite preview + + NUM_MKEYS +}; + +void M_ButtonHandler(EMenuKey key, bool repeat); +void M_OptButtonHandler(EMenuKey key, bool repeat); + extern value_t YesNo[2]; extern value_t NoYes[2]; extern value_t OnOff[2]; diff --git a/src/m_options.cpp b/src/m_options.cpp index 06178be321..58c37e70e6 100644 --- a/src/m_options.cpp +++ b/src/m_options.cpp @@ -86,6 +86,8 @@ // Data. #include "m_menu.h" +extern FButtonStatus MenuButtons[NUM_MKEYS]; + EXTERN_CVAR(Bool, nomonsterinterpolation) EXTERN_CVAR(Int, showendoom) EXTERN_CVAR(Bool, hud_althud) @@ -1509,7 +1511,7 @@ static void CalcIndent (menu_t *menu) { item = menu->items + i; if (item->type != whitetext && item->type != redtext && item->type != screenres && - item->type != joymore && (item->type != discrete || !item->c.discretecenter)) + item->type != joymore && (item->type != discrete || item->c.discretecenter != 1)) { thiswidth = SmallFont->StringWidth (item->label); if (thiswidth > widest) @@ -1681,7 +1683,7 @@ void M_OptDrawer () item = CurrentMenu->items + i; overlay = 0; - if (item->type == discrete && item->c.discretecenter) + if (item->type == discrete && item->c.discretecenter == 1) { indent = 160; } @@ -2095,50 +2097,79 @@ void M_OptDrawer () } } -void M_OptResponder (event_t *ev) +void M_OptResponder(event_t *ev) { - menuitem_t *item; - int ch = tolower (ev->data1); - UCVarValue value; - - item = CurrentMenu->items + CurrentItem; + menuitem_t *item = CurrentMenu->items + CurrentItem; if (menuactive == MENU_WaitKey && ev->type == EV_KeyDown) { if (ev->data1 != KEY_ESCAPE) { - C_ChangeBinding (item->e.command, ev->data1); - M_BuildKeyList (CurrentMenu->items, CurrentMenu->numitems); + C_ChangeBinding(item->e.command, ev->data1); + M_BuildKeyList(CurrentMenu->items, CurrentMenu->numitems); } menuactive = MENU_On; CurrentMenu->items[0].label = OldMessage; CurrentMenu->items[0].type = OldType; - return; } - - if (ev->subtype == EV_GUI_KeyRepeat) + else if (ev->type == EV_GUI_Event && ev->subtype == EV_GUI_KeyDown && tolower(ev->data1) == 't') { - if (ch != GK_LEFT && ch != GK_RIGHT && ch != GK_UP && ch != GK_DOWN) + // Test selected resolution + if (CurrentMenu == &ModesMenu) { - return; + if (!(item->type == screenres && + GetSelectedSize (CurrentItem, &NewWidth, &NewHeight))) + { + NewWidth = SCREENWIDTH; + NewHeight = SCREENHEIGHT; + } + OldWidth = SCREENWIDTH; + OldHeight = SCREENHEIGHT; + OldBits = DisplayBits; + NewBits = BitTranslate[DummyDepthCvar]; + setmodeneeded = true; + testingmode = I_GetTime(false) + 5 * TICRATE; + S_Sound (CHAN_VOICE | CHAN_UI, "menu/choose", 1, ATTN_NONE); + SetModesMenu (NewWidth, NewHeight, NewBits); } } - else if (ev->subtype != EV_GUI_KeyDown) - { - return; - } - +} + +void M_OptButtonHandler(EMenuKey key, bool repeat) +{ + menuitem_t *item; + UCVarValue value; + + item = CurrentMenu->items + CurrentItem; + if (item->type == bitflag && - (ch == GK_LEFT || ch == GK_RIGHT || ch == '\r') + (key == MKEY_Left || key == MKEY_Right || key == MKEY_Enter) && !demoplayback) { *(item->a.intcvar) = (*(item->a.intcvar)) ^ item->e.flagmask; return; } - switch (ch) + // The controls that manipulate joystick interfaces can only be changed from the + // keyboard, because I can't think of a good way to avoid problems otherwise. + if (item->type == discrete && item->c.discretecenter == 2 && (key == MKEY_Left || key == MKEY_Right)) { - case GK_DOWN: + if (repeat) + { + return; + } + for (int i = 0; i < FButtonStatus::MAX_KEYS; ++i) + { + if (MenuButtons[key].Keys[i] >= KEY_FIRSTJOYBUTTON) + { + return; + } + } + } + + switch (key) + { + case MKEY_Down: if (CurrentMenu->numitems > 1) { int modecol; @@ -2187,7 +2218,7 @@ void M_OptResponder (event_t *ev) } break; - case GK_UP: + case MKEY_Up: if (CurrentMenu->numitems > 1) { int modecol; @@ -2255,7 +2286,7 @@ void M_OptResponder (event_t *ev) } break; - case GK_PGUP: + case MKEY_PageUp: if (CurrentMenu->scrollpos > 0) { CurrentMenu->scrollpos -= VisBottom - CurrentMenu->scrollpos - CurrentMenu->scrolltop; @@ -2277,7 +2308,7 @@ void M_OptResponder (event_t *ev) } break; - case GK_PGDN: + case MKEY_PageDown: if (CanScrollDown) { int pagesize = VisBottom - CurrentMenu->scrollpos - CurrentMenu->scrolltop; @@ -2300,7 +2331,7 @@ void M_OptResponder (event_t *ev) } break; - case GK_LEFT: + case MKEY_Left: switch (item->type) { case slider: @@ -2497,7 +2528,7 @@ void M_OptResponder (event_t *ev) } break; - case GK_RIGHT: + case MKEY_Right: switch (item->type) { case slider: @@ -2697,14 +2728,14 @@ void M_OptResponder (event_t *ev) } break; - case '\b': + case MKEY_Clear: if (item->type == control) { C_UnbindACommand (item->e.command); item->b.key1 = item->c.key2 = 0; } break; - +/* case '0': case '1': case '2': @@ -2732,8 +2763,8 @@ void M_OptResponder (event_t *ev) } // Otherwise, fall through to '\r' below } - - case '\r': +*/ + case MKEY_Enter: if (CurrentMenu == &ModesMenu && item->type == screenres) { if (!GetSelectedSize (CurrentItem, &NewWidth, &NewHeight)) @@ -2839,7 +2870,7 @@ void M_OptResponder (event_t *ev) } break; - case GK_ESCAPE: + case MKEY_Back: CurrentMenu->lastOn = CurrentItem; if (CurrentMenu->EscapeHandler != NULL) { @@ -2847,30 +2878,6 @@ void M_OptResponder (event_t *ev) } M_PopMenuStack (); break; - - default: - if (ch == 't') - { - // Test selected resolution - if (CurrentMenu == &ModesMenu) - { - if (!(item->type == screenres && - GetSelectedSize (CurrentItem, &NewWidth, &NewHeight))) - { - NewWidth = SCREENWIDTH; - NewHeight = SCREENHEIGHT; - } - OldWidth = SCREENWIDTH; - OldHeight = SCREENHEIGHT; - OldBits = DisplayBits; - NewBits = BitTranslate[DummyDepthCvar]; - setmodeneeded = true; - testingmode = I_GetTime(false) + 5 * TICRATE; - S_Sound (CHAN_VOICE | CHAN_UI, "menu/choose", 1, ATTN_NONE); - SetModesMenu (NewWidth, NewHeight, NewBits); - } - } - break; } } @@ -3203,6 +3210,7 @@ void UpdateJoystickMenu(IJoystickConfig *selected) item.label = "Enable controller support"; item.a.cvar = &use_joystick; item.b.numvalues = 2; + item.c.discretecenter = 2; item.e.values = YesNo; JoystickItems.Push(item); @@ -3222,6 +3230,7 @@ void UpdateJoystickMenu(IJoystickConfig *selected) item.type = redtext; item.label = " "; + item.c.discretecenter = 0; JoystickItems.Push(item); if (Joysticks.Size() == 0) diff --git a/src/win32/i_dijoy.cpp b/src/win32/i_dijoy.cpp index ca0fe9dfc1..5929d4a03b 100644 --- a/src/win32/i_dijoy.cpp +++ b/src/win32/i_dijoy.cpp @@ -296,6 +296,8 @@ FDInputJoystick::FDInputJoystick(const GUID *instance, FString &name) FDInputJoystick::~FDInputJoystick() { + unsigned int i; + if (Device != NULL) { M_SaveJoystickConfig(this); @@ -306,6 +308,31 @@ FDInputJoystick::~FDInputJoystick() { delete[] DataFormat.rgodf; } + // Send key ups before destroying this. + if (Axes.Size() == 1) + { + Joy_GenerateButtonEvents(Axes[0].ButtonValue, 0, 2, KEY_JOYAXIS1PLUS); + } + else + { + Joy_GenerateButtonEvents(Axes[1].ButtonValue, 0, 4, KEY_JOYAXIS1PLUS); + for (i = 2; i < Axes.Size(); ++i) + { + Joy_GenerateButtonEvents(Axes[i].ButtonValue, 0, 2, KEY_JOYAXIS1PLUS + i*2); + } + } + for (i = 0; i < Buttons.Size(); ++i) + { + if (Buttons[i].Value) + { + event_t ev = { EV_KeyUp }; + ev.data1 = KEY_FIRSTJOYBUTTON + i; + } + } + for (i = 0; i < POVs.Size(); ++i) + { + Joy_GenerateButtonEvents(POVs[i].Value, 0, 4, KEY_JOYPOV1_UP + i*4); + } } //=========================================================================== @@ -399,17 +426,24 @@ void FDInputJoystick::ProcessInput() AxisInfo *info = &Axes[i]; LONG value = *(LONG *)(state + info->Ofs); double axisval; - BYTE buttonstate; + BYTE buttonstate = 0; // Scale to [-1.0, 1.0] axisval = (value - info->Min) * 2.0 / (info->Max - info->Min) - 1.0; // Cancel out dead zone axisval = Joy_RemoveDeadZone(axisval, info->DeadZone, &buttonstate); info->Value = float(axisval); - if (i < NUM_JOYAXISBUTTONS) + if (i < NUM_JOYAXISBUTTONS && (i > 2 || Axes.Size() == 1)) { Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 2, KEY_JOYAXIS1PLUS + i*2); } + else if (i == 1) + { + // Since we sorted the axes, we know that the first two are definitely X and Y. + // They are probably a single stick, so use angular position to determine buttons. + buttonstate = Joy_XYAxesToButtons(axisval, Axes[0].Value); + Joy_GenerateButtonEvents(info->ButtonValue, buttonstate, 4, KEY_JOYAXIS1PLUS); + } info->ButtonValue = buttonstate; } diff --git a/src/win32/i_rawps2.cpp b/src/win32/i_rawps2.cpp index 625a1cff98..68803fefb0 100644 --- a/src/win32/i_rawps2.cpp +++ b/src/win32/i_rawps2.cpp @@ -136,7 +136,7 @@ protected: void Detached(); void NeutralInput(); - static void ProcessThumbstick(int value, AxisInfo *axis, int base); + static void ProcessThumbstick(int value1, AxisInfo *axis1, int value2, AxisInfo *axis2, int base); friend class FRawPS2Manager; }; @@ -375,6 +375,8 @@ FRawPS2Controller::FRawPS2Controller(HANDLE handle, EAdapterType type, int seque FRawPS2Controller::~FRawPS2Controller() { + // Make sure to send any key ups before destroying this. + NeutralInput(); M_SaveJoystickConfig(this); } @@ -463,19 +465,19 @@ bool FRawPS2Controller::ProcessInput(RAWHID *raw, int code) } // Convert axes to floating point and cancel out deadzones. - ProcessThumbstick(rawdata[desc->LeftX], &Axes[AXIS_ThumbLX], KEY_PAD_LTHUMB_RIGHT); - ProcessThumbstick(rawdata[desc->LeftY], &Axes[AXIS_ThumbLY], KEY_PAD_LTHUMB_DOWN); + ProcessThumbstick(rawdata[desc->LeftX], &Axes[AXIS_ThumbLX], + rawdata[desc->LeftY], &Axes[AXIS_ThumbLY], KEY_PAD_LTHUMB_RIGHT); // If we know we are digital, ignore the right stick. if (digital) { - ProcessThumbstick(0x80, &Axes[AXIS_ThumbRX], KEY_PAD_RTHUMB_RIGHT); - ProcessThumbstick(0x80, &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_DOWN); + ProcessThumbstick(0x80, &Axes[AXIS_ThumbRX], + 0x80, &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_RIGHT); } else { - ProcessThumbstick(rawdata[desc->RightX], &Axes[AXIS_ThumbRX], KEY_PAD_RTHUMB_RIGHT); - ProcessThumbstick(rawdata[desc->RightY], &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_DOWN); + ProcessThumbstick(rawdata[desc->RightX], &Axes[AXIS_ThumbRX], + rawdata[desc->RightY], &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_RIGHT); } // Generate events for buttons that have changed. @@ -506,21 +508,27 @@ bool FRawPS2Controller::ProcessInput(RAWHID *raw, int code) // // FRawPS2Controller :: ProcessThumbstick STATIC // -// Converts one axis of a thumb stick to floating point, cancels out the -// deadzone, and generates button up/down events for that axis. +// Converts both axie of a thumb stick to floating point, cancels out the +// deadzone, and generates button up/down events for them. // //========================================================================== -void FRawPS2Controller::ProcessThumbstick(int value, AxisInfo *axis, int base) +void FRawPS2Controller::ProcessThumbstick(int value1, AxisInfo *axis1, int value2, AxisInfo *axis2, int base) { BYTE buttonstate; - double axisval; + double axisval1, axisval2; - axisval = value * (2.0 / 255) - 1.0; - axisval = Joy_RemoveDeadZone(axisval, axis->DeadZone, &buttonstate); - Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 2, base); - axis->ButtonValue = buttonstate; - axis->Value = float(axisval); + axisval1 = value1 * (2.0 / 255) - 1.0; + axisval2 = value2 * (2.0 / 255) - 1.0; + axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL); + axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL); + axis1->Value = float(axisval1); + axis2->Value = float(axisval2); + + // We store all four buttons in the first axis and ignore the second. + buttonstate = Joy_XYAxesToButtons(axisval1, axisval2); + Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base); + axis1->ButtonValue = buttonstate; } //========================================================================== @@ -573,11 +581,9 @@ void FRawPS2Controller::Detached() void FRawPS2Controller::NeutralInput() { - int i; - - for (i = 0; i < NUM_AXES; ++i) + for (int i = 0; i < NUM_AXES; i += 2) { - ProcessThumbstick(0x80, &Axes[i], KEY_PAD_LTHUMB_RIGHT + i*2); + ProcessThumbstick(0x80, &Axes[i], 0x80, &Axes[i+1], KEY_PAD_LTHUMB_RIGHT + i*2); } Joy_GenerateButtonEvents(LastButtons, 0, 16, ButtonKeys); LastButtons = 0; diff --git a/src/win32/i_xinput.cpp b/src/win32/i_xinput.cpp index d38c5372b7..e3ad1eed0d 100644 --- a/src/win32/i_xinput.cpp +++ b/src/win32/i_xinput.cpp @@ -105,7 +105,7 @@ protected: void Attached(); void Detached(); - static void ProcessThumbstick(int value, AxisInfo *axis, int base); + static void ProcessThumbstick(int value1, AxisInfo *axis1, int value2, AxisInfo *axis2, int base); static void ProcessTrigger(int value, AxisInfo *axis, int base); }; @@ -195,6 +195,12 @@ FXInputController::FXInputController(int index) FXInputController::~FXInputController() { + // Send button up events before destroying this. + ProcessThumbstick(0, &Axes[AXIS_ThumbLX], 0, &Axes[AXIS_ThumbLY], KEY_PAD_LTHUMB_RIGHT); + ProcessThumbstick(0, &Axes[AXIS_ThumbRX], 0, &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_RIGHT); + ProcessTrigger(0, &Axes[AXIS_LeftTrigger], KEY_PAD_LTRIGGER); + ProcessTrigger(0, &Axes[AXIS_RightTrigger], KEY_PAD_RTRIGGER); + Joy_GenerateButtonEvents(LastButtons, 0, 16, KEY_PAD_DPAD_UP); M_SaveJoystickConfig(this); } @@ -239,10 +245,10 @@ void FXInputController::ProcessInput() // Convert axes to floating point and cancel out deadzones. // XInput's Y axes are reversed compared to DirectInput. - ProcessThumbstick(state.Gamepad.sThumbLX, &Axes[AXIS_ThumbLX], KEY_PAD_LTHUMB_RIGHT); - ProcessThumbstick(-state.Gamepad.sThumbLY, &Axes[AXIS_ThumbLY], KEY_PAD_LTHUMB_DOWN); - ProcessThumbstick(state.Gamepad.sThumbRX, &Axes[AXIS_ThumbRX], KEY_PAD_RTHUMB_RIGHT); - ProcessThumbstick(-state.Gamepad.sThumbRY, &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_DOWN); + ProcessThumbstick(state.Gamepad.sThumbLX, &Axes[AXIS_ThumbLX], + -state.Gamepad.sThumbLY, &Axes[AXIS_ThumbLY], KEY_PAD_LTHUMB_RIGHT); + ProcessThumbstick(state.Gamepad.sThumbRX, &Axes[AXIS_ThumbRX], + -state.Gamepad.sThumbRY, &Axes[AXIS_ThumbRY], KEY_PAD_RTHUMB_RIGHT); ProcessTrigger(state.Gamepad.bLeftTrigger, &Axes[AXIS_LeftTrigger], KEY_PAD_LTRIGGER); ProcessTrigger(state.Gamepad.bRightTrigger, &Axes[AXIS_RightTrigger], KEY_PAD_RTRIGGER); @@ -257,21 +263,28 @@ void FXInputController::ProcessInput() // // FXInputController :: ProcessThumbstick STATIC // -// Converts one axis of a thumb stick to floating point, cancels out the -// deadzone, and generates button up/down events for that axis. +// Converts both axes of a thumb stick to floating point, cancels out the +// deadzone, and generates button up/down events for them. // //========================================================================== -void FXInputController::ProcessThumbstick(int value, AxisInfo *axis, int base) +void FXInputController::ProcessThumbstick(int value1, AxisInfo *axis1, + int value2, AxisInfo *axis2, int base) { BYTE buttonstate; - double axisval; + double axisval1, axisval2; - axisval = (value - SHRT_MIN) * 2.0 / 65536 - 1.0; - axisval = Joy_RemoveDeadZone(axisval, axis->DeadZone, &buttonstate); - Joy_GenerateButtonEvents(axis->ButtonValue, buttonstate, 2, base); - axis->ButtonValue = buttonstate; - axis->Value = float(axisval); + axisval1 = (value1 - SHRT_MIN) * 2.0 / 65536 - 1.0; + axisval2 = (value2 - SHRT_MIN) * 2.0 / 65536 - 1.0; + axisval1 = Joy_RemoveDeadZone(axisval1, axis1->DeadZone, NULL); + axisval2 = Joy_RemoveDeadZone(axisval2, axis2->DeadZone, NULL); + axis1->Value = float(axisval1); + axis2->Value = float(axisval2); + + // We store all four buttons in the first axis and ignore the second. + buttonstate = Joy_XYAxesToButtons(axisval1, axisval2); + Joy_GenerateButtonEvents(axis1->ButtonValue, buttonstate, 4, base); + axis1->ButtonValue = buttonstate; } //========================================================================== @@ -331,9 +344,9 @@ void FXInputController::Detached() int i; Connected = false; - for (i = 0; i < 4; ++i) + for (i = 0; i < 4; i += 2) { - ProcessThumbstick(0, &Axes[i], KEY_PAD_LTHUMB_RIGHT + i*2); + ProcessThumbstick(0, &Axes[i], 0, &Axes[i+1], KEY_PAD_LTHUMB_RIGHT + i*2); } for (i = 0; i < 2; ++i) {