From a90bde274eb9c0c1eb577c14580d921aa5c293fb Mon Sep 17 00:00:00 2001 From: Randy Heit Date: Sun, 26 Jul 2009 03:25:18 +0000 Subject: [PATCH] The menu code sucks! - 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. SVN r1739 (trunk) --- docs/rh-log.txt | 18 +- src/c_dispatch.cpp | 16 +- src/c_dispatch.h | 4 +- src/doomdef.h | 8 + src/m_joy.cpp | 52 +++- src/m_joy.h | 1 + src/m_menu.cpp | 542 +++++++++++++++++++++++++++-------------- src/m_menu.h | 22 +- src/m_options.cpp | 123 +++++----- src/win32/i_dijoy.cpp | 38 ++- src/win32/i_rawps2.cpp | 46 ++-- src/win32/i_xinput.cpp | 45 ++-- 12 files changed, 624 insertions(+), 291 deletions(-) diff --git a/docs/rh-log.txt b/docs/rh-log.txt index 69531e7bb..4fda7c1cc 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 91bf72509..6540fc4c9 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 52ca08801..cde53996a 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 bec8985d4..03a4fffd6 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 73689993a..51291dde7 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 6c8920ed0..92771762b 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 cc21a94c9..a3447e266 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 25f36a26d..ff899511d 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 06178be32..58c37e70e 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 ca0fe9dfc..5929d4a03 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 625a1cff9..68803fefb 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 d38c5372b..e3ad1eed0 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) {