diff --git a/Changelog.md b/Changelog.md index 09c6a259..a133bb0f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -23,6 +23,8 @@ Note: Numbers starting with a "#" like #330 refer to the bugreport with that num (0 = TGA, still the default, 1 = BMP, 2 = PNG, 3 = JPG). `r_screenshotJpgQuality` and `r_screenshotPngCompression` allow configuring how JPG/PNG are compressed. Thanks *eezstreet (Nick Whitlock)*! +* Support for gamepads (based on code from [Quadrilateral Cowboy](https://github.com/blendogames/quadrilateralcowboy), + but heavily expanded). See [Configuration.md](./Configuration.md#using-gamepads) for more information. 1.5.2 (2022-06-13) ------------------------------------------------------------------------ diff --git a/Configuration.md b/Configuration.md new file mode 100644 index 00000000..4747aa04 --- /dev/null +++ b/Configuration.md @@ -0,0 +1,143 @@ +# Configuration + +This document explains some dhewm3-specific configuration options. + +For general Doom3 configuration see for example [this list of CVars](https://modwiki.dhewm3.org/CVars_%28Doom_3%29) +and [this list of Console Commands](https://modwiki.dhewm3.org/Commands_%28Doom_3%29). + +**CVars** are set by entering `cvarName value` in the console, for example `com_showFPS 1`. +They can also be set as commandline arguments when starting dhewm3, for example `./dhewm3 +set r_fullscreen 0`. + +Just entering a CVar's name (without a value) will show its current value, its default value +and a short description of what it does. + +Starting dhewm3 with the commandline argument `-h` (for example `dhewm3.exe -h`) will show some +useful commandline arguments, for example how to tell dhewm3 where the game data can be found on your system. + +## The Console + +Like most id Software games from Quake 1 on, Doom3 has a console that allows entering commands +and setting Console Variables ("CVars"), often for advanced configuration or to aid development, +see also https://modwiki.dhewm3.org/Console. + +Unlike in original Doom3, in dhewm3 the console is always available (no need to set `com_allowconsole 1` +or similar), and **can be opened with the key combination `Shift + Esc`**. +The classic "console key" (the one between `Esc`, `Tab` and `1`) should also still work with +most keyboard layouts. However you can disable that, so you can bind that key like any other key +(for example to select the chainsaw), by setting `in_ignoreConsoleKey 1`. + +## Using Gamepads + +Starting with 1.5.3 (or the git commits preceding the one adding this document), dhewm3 supports +using gamepads, as long as they're supported by SDL2. +This includes XBox Controllers (and compatible ones), Playstation 3-5 controllers, +Nintendo Switch Pro Controllers, many thirdparty controllers for those consoles, and lots of other +gamepads for PC. + +Some notes: +* By default, no bindings for the gamepad exist, so you need to configure them once in the + Settings -> Controls menu. + - You need to bind *Turn Left*, *Turn Right*, *Look Up* and *Look Down* to the corresponding + directions of one stick to use it to look around or aim. + - Similarly, you need to bind *Forward*, *Backpedal*, *Move Left* and *Move Right* to the + corresponding directions of a stick to use it for player movement. +* The "Start" button ("+" on Nintendo gamepads, "Options" on Playstation 4/5 controllers) acts + like the Escape key, so it will **open/close the menu** and can not be bound. + The other buttons, axes and triggers can be bound to arbitrary actions in the Controls menu, + except for the Home button, which can't be used by dhewm3 at all (because it opens Steam when that is running). +* In **menus**, either stick will move the cursor, and the button you assign to *attack* (fire) acts + like the left mouse button, and so does the lower face button (A on XBox controllers, B on Nintendo + controllers, Cross on PS controllers) and the upper face button (Y on XBox, X on Nintendo, Triangle on PS). +* The layout of the controller (XBox-like, Nintendo-like, Playstation-like) should be automatically + detected and is used to display the button names according to the layout. If yours isn't detected + correctly, you can overwrite it with the `joy_gamepadLayout` CVar. +* Requires SDL2, layout detection requires SDL 2.0.12 or newer. +* Only one gamepad is supported or, more specifically, if multiple are connected, they all behave the same + and you can't bind their buttons/axes to different actions, and the auto-layout detection will use the + last gamepad it found to determine the layout. +* You can disable gamepads by setting the `in_useGamepad` CVar to `0`. +* There are several CVars to tweak the behavior: + - `joy_deadZone` Deadzone of the sticks, where `1.0` would be "stick moved fully in one direction". + This means that values below this register as 0. If you move or look around ingame even though + you're not moving a stick, try increasing the `joy_deadZone` value (default is `0.25`). + - `joy_triggerThreshold` Basically the deadzone for triggers. If your trigger triggers without + being touched, try increasing this value (default is `0.05`). + - `joy_gamepadLayout` overwrite automatically detected layout (XBox, Nintendo, PS), see above. + - `joy_pitchSpeed` How fast you look up/down (when the stick is at a maximum position) + - `joy_yawSpeed` Same for turning left/right + - `joy_invertLook` Inverts the controls for looking up/down (like in a flight simulator) + - `joy_gammaLook` If set to `1`, use a log curve instead of a power curve for looking around, + affects how fast you turn (or look up/down) when the stick is between center and maximum. + - `joy_powerScale` If `joy_gammaLook` is `0`, this is the exponent used for the power curve. + - `joy_dampenLook` if enabled (`1`), somehow reduced the speed of looking around, depending on + `joy_deltaPerMSLook`. + +I created gamepad configs for the base game and d3xp (Resurrection of Evil), based on the standard bindings +of Doom3 BFG, see gamepad.cfg and gamepad-d3xp.cfg in the [base/ directory](./base/). +Put them in your base/ folder, open the console and enter `exec gamepad.cfg` for the base game, +or `exec gamepad-d3xp.cfg` for Resurrection of Evil (probably also works for Doom3: Lost Mission). + +**_Note_** that in *configs* (or `bind` commands in the console), the following names are used for +gamepad buttons, sticks and triggers: + +
Click to see the list of gamepad button/stick/trigger names + +* "JOY_BTN_SOUTH" - `A` button on XBox-style gamepads, `B` on Nintendo-style gamepads or `Cross` on Playstation-style gamepads +* "JOY_BTN_EAST" - `B` (XBox), `A` (Nintendo), `Circle` (Playstation) +* "JOY_BTN_WEST" - `X` (XBox), `Y` (Nintendo), `Square` (Playstation) +* "JOY_BTN_NORTH" - `Y` (XBox), `X` (Nintendo), `Triangle` (Playstation) +* "JOY_BTN_BACK" - The `Back` button, aka `-` (Nintendo) or `Select`/`Share` (Playstation) +* "JOY_BTN_LSTICK" - Pressing the Left Stick down +* "JOY_BTN_RSTICK" - Pressing the Right Stick down +* "JOY_BTN_LSHOULDER" - Left Shoulder Button +* "JOY_BTN_RSHOULDER" - Right Shoulder button +* "JOY_DPAD_UP" - DPad Up +* "JOY_DPAD_DOWN" - DPad Down +* "JOY_DPAD_LEFT" - DPad Left +* "JOY_DPAD_RIGHT" - DPad Right +* "JOY_BTN_MISC1" - misc. additional button, like Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button +* "JOY_BTN_RPADDLE1" - Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1) +* "JOY_BTN_LPADDLE1" - Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3) +* "JOY_BTN_RPADDLE2" - Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2) +* "JOY_BTN_LPADDLE2" - Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4 +* "JOY_STICK1_UP" - Moving Left Stick up +* "JOY_STICK1_DOWN" - Moving Left Stick down +* "JOY_STICK1_LEFT" - Moving Left Stick to the left +* "JOY_STICK1_RIGHT" - Moving Left Stick to the right +* "JOY_STICK2_UP" - Moving Right Stick up +* "JOY_STICK2_DOWN" - Moving Right Stick down +* "JOY_STICK2_LEFT" - Moving Right Stick to the left +* "JOY_STICK2_RIGHT" - Moving Right Stick to the right +* "JOY_TRIGGER1" - Pressing the Left Trigger +* "JOY_TRIGGER2" - Pressing the Right Trigger + +
+ +## Screenshot configuration + +Doom3 always supported taking screenshots, but dhewm3 (from 1.5.3 on) supports using different +formats than TGA. +This can be configured with the following CVars: + +- `r_screenshotFormat` What format screenshots should be in: + `0` = TGA (default), `1` = BMP, `2` = PNG, `3` = JPG +- `r_screenshotJpgQuality` Quality when using JPG screenshots (`0` - `100`) +- `r_screenshotPngCompression` Compression level when using PNG screenshots (`0` - `9`) + +## CVars added in dhewm3 that I'm currently too lazy to document more thoroughly + +- g_hitEffect + +- in_nograb +- in_grabKeyboard + +- in_tty +- in_kbd + +- r_fullscreenDesktop +- r_fillWindowAlphaChan + +- r_useCarmacksReverse +- r_useStencilOpSeparate + +- s_alReverbGain diff --git a/README.md b/README.md index ce79d2a8..1df3653f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Compared to the original _DOOM 3_, the changes of _dhewm 3_ worth mentioning are - SDL for low-level OS support, OpenGL and input handling - OpenAL for audio output, all OS-specific audio backends are gone - OpenAL EFX for EAX reverb effects (read: EAX-like sound effects on all platforms/hardware) +- Gamepad support - Better support for widescreen (and arbitrary display resolutions) - A portable build system based on CMake - (Cross-)compilation with MinGW-w64 @@ -55,6 +56,11 @@ https://store.steampowered.com/app/208200/DOOM_3/ See https://dhewm3.org/#how-to-install for game data installation instructions. +## Configuration + +See [Configuration.md](./Configuration.md) for dhewm3-specific configuration, especially for +using gamepads. + ## Compiling The build system is based on CMake: http://cmake.org/ diff --git a/base/gamepad-d3xp.cfg b/base/gamepad-d3xp.cfg new file mode 100755 index 00000000..3ced364b --- /dev/null +++ b/base/gamepad-d3xp.cfg @@ -0,0 +1,21 @@ +bind "JOY_BTN_SOUTH" "_moveUp" +bind "JOY_BTN_WEST" "_impulse13" +bind "JOY_BTN_BACK" "_impulse19" +bind "JOY_BTN_LSTICK" "_speed" +bind "JOY_BTN_RSTICK" "_moveDown" +bind "JOY_BTN_LSHOULDER" "_impulse15" +bind "JOY_BTN_RSHOULDER" "_impulse14" +bind "JOY_DPAD_UP" "_impulse8" +bind "JOY_DPAD_DOWN" "_impulse11" +bind "JOY_DPAD_LEFT" "_impulse12" +bind "JOY_DPAD_RIGHT" "_impulse1" +bind "JOY_STICK1_UP" "_forward" +bind "JOY_STICK1_DOWN" "_back" +bind "JOY_STICK1_LEFT" "_moveLeft" +bind "JOY_STICK1_RIGHT" "_moveRight" +bind "JOY_STICK2_UP" "_lookUp" +bind "JOY_STICK2_DOWN" "_lookDown" +bind "JOY_STICK2_LEFT" "_left" +bind "JOY_STICK2_RIGHT" "_right" +bind "JOY_TRIGGER1" "_impulse0" +bind "JOY_TRIGGER2" "_attack" \ No newline at end of file diff --git a/base/gamepad.cfg b/base/gamepad.cfg new file mode 100755 index 00000000..99c470df --- /dev/null +++ b/base/gamepad.cfg @@ -0,0 +1,21 @@ +bind "JOY_BTN_SOUTH" "_moveUp" +bind "JOY_BTN_WEST" "_impulse13" +bind "JOY_BTN_BACK" "_impulse19" +bind "JOY_BTN_LSTICK" "_speed" +bind "JOY_BTN_RSTICK" "_moveDown" +bind "JOY_BTN_LSHOULDER" "_impulse15" +bind "JOY_BTN_RSHOULDER" "_impulse14" +bind "JOY_DPAD_UP" "_impulse5" +bind "JOY_DPAD_DOWN" "_impulse8" +bind "JOY_DPAD_LEFT" "_impulse9" +bind "JOY_DPAD_RIGHT" "_impulse0" +bind "JOY_STICK1_UP" "_forward" +bind "JOY_STICK1_DOWN" "_back" +bind "JOY_STICK1_LEFT" "_moveLeft" +bind "JOY_STICK1_RIGHT" "_moveRight" +bind "JOY_STICK2_UP" "_lookUp" +bind "JOY_STICK2_DOWN" "_lookDown" +bind "JOY_STICK2_LEFT" "_left" +bind "JOY_STICK2_RIGHT" "_right" +bind "JOY_TRIGGER1" "_impulse11" +bind "JOY_TRIGGER2" "_attack" \ No newline at end of file diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index bfc69f93..0e504481 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -2919,8 +2919,14 @@ void idCommonLocal::Init( int argc, char **argv ) { #endif #endif - if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK)) // init joystick to work around SDL 2.0.9 bug #4391 +#if SDL_VERSION_ATLEAST(2, 0, 0) + if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER)) // init joystick to work around SDL 2.0.9 bug #4391 +#else + if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO)) // no gamecontroller support in SDL1 +#endif + { Sys_Error("Error while initializing SDL: %s", SDL_GetError()); + } Sys_InitThreads(); diff --git a/neo/framework/KeyInput.cpp b/neo/framework/KeyInput.cpp index bc5c2e17..299519c9 100644 --- a/neo/framework/KeyInput.cpp +++ b/neo/framework/KeyInput.cpp @@ -109,38 +109,41 @@ static const keyname_t keynames[] = {"MWHEELUP", K_MWHEELUP, "#str_07131"}, {"MWHEELDOWN", K_MWHEELDOWN, "#str_07132"}, - {"JOY1", K_JOY1, "#str_07062"}, - {"JOY2", K_JOY2, "#str_07063"}, - {"JOY3", K_JOY3, "#str_07064"}, - {"JOY4", K_JOY4, "#str_07065"}, - {"JOY5", K_JOY5, "#str_07066"}, - {"JOY6", K_JOY6, "#str_07067"}, - {"JOY7", K_JOY7, "#str_07068"}, - {"JOY8", K_JOY8, "#str_07069"}, - {"JOY9", K_JOY9, "#str_07070"}, - {"JOY10", K_JOY10, "#str_07071"}, - {"JOY11", K_JOY11, "#str_07072"}, - {"JOY12", K_JOY12, "#str_07073"}, - {"JOY13", K_JOY13, "#str_07074"}, - {"JOY14", K_JOY14, "#str_07075"}, - {"JOY15", K_JOY15, "#str_07076"}, - {"JOY16", K_JOY16, "#str_07077"}, - {"JOY17", K_JOY17, "#str_07078"}, - {"JOY18", K_JOY18, "#str_07079"}, - {"JOY19", K_JOY19, "#str_07080"}, - {"JOY20", K_JOY20, "#str_07081"}, - {"JOY21", K_JOY21, "#str_07082"}, - {"JOY22", K_JOY22, "#str_07083"}, - {"JOY23", K_JOY23, "#str_07084"}, - {"JOY24", K_JOY24, "#str_07085"}, - {"JOY25", K_JOY25, "#str_07086"}, - {"JOY26", K_JOY26, "#str_07087"}, - {"JOY27", K_JOY27, "#str_07088"}, - {"JOY28", K_JOY28, "#str_07089"}, - {"JOY29", K_JOY29, "#str_07090"}, - {"JOY30", K_JOY30, "#str_07091"}, - {"JOY31", K_JOY31, "#str_07092"}, - {"JOY32", K_JOY32, "#str_07093"}, + // Note: for localized gamepad key names, we use Sys_GetLocalizedJoyKeyName() + // so the last column is just NULL + {"JOY_BTN_SOUTH", K_JOY_BTN_SOUTH, NULL}, + {"JOY_BTN_EAST", K_JOY_BTN_EAST, NULL}, + {"JOY_BTN_WEST", K_JOY_BTN_WEST, NULL}, + {"JOY_BTN_NORTH", K_JOY_BTN_NORTH, NULL}, + {"JOY_BTN_BACK", K_JOY_BTN_BACK, NULL}, + // leaving out K_JOY_BTN_GUIDE, as I think it shouldn't be used (might open Steam or similar) + {"JOY_BTN_START", K_JOY_BTN_START, NULL}, + {"JOY_BTN_LSTICK", K_JOY_BTN_LSTICK, NULL}, + {"JOY_BTN_RSTICK", K_JOY_BTN_RSTICK, NULL}, + {"JOY_BTN_LSHOULDER", K_JOY_BTN_LSHOULDER, NULL}, + {"JOY_BTN_RSHOULDER", K_JOY_BTN_RSHOULDER, NULL}, + {"JOY_DPAD_UP", K_JOY_DPAD_UP, NULL}, + {"JOY_DPAD_DOWN", K_JOY_DPAD_DOWN, NULL}, + {"JOY_DPAD_LEFT", K_JOY_DPAD_LEFT, NULL}, + {"JOY_DPAD_RIGHT", K_JOY_DPAD_RIGHT, NULL}, + {"JOY_BTN_MISC1", K_JOY_BTN_MISC1, NULL}, + {"JOY_BTN_RPADDLE1", K_JOY_BTN_RPADDLE1, NULL}, + {"JOY_BTN_LPADDLE1", K_JOY_BTN_LPADDLE1, NULL}, + {"JOY_BTN_RPADDLE2", K_JOY_BTN_RPADDLE2, NULL}, + {"JOY_BTN_LPADDLE2", K_JOY_BTN_LPADDLE2, NULL}, + + {"JOY_STICK1_UP", K_JOY_STICK1_UP, NULL}, + {"JOY_STICK1_DOWN", K_JOY_STICK1_DOWN, NULL}, + {"JOY_STICK1_LEFT", K_JOY_STICK1_LEFT, NULL}, + {"JOY_STICK1_RIGHT", K_JOY_STICK1_RIGHT, NULL}, + + {"JOY_STICK2_UP", K_JOY_STICK2_UP, NULL}, + {"JOY_STICK2_DOWN", K_JOY_STICK2_DOWN, NULL}, + {"JOY_STICK2_LEFT", K_JOY_STICK2_LEFT, NULL}, + {"JOY_STICK2_RIGHT", K_JOY_STICK2_RIGHT, NULL}, + + {"JOY_TRIGGER1", K_JOY_TRIGGER1, NULL}, + {"JOY_TRIGGER2", K_JOY_TRIGGER2, NULL}, {"AUX1", K_AUX1, "#str_07094"}, {"AUX2", K_AUX2, "#str_07095"}, @@ -409,6 +412,12 @@ const char *idKeyInput::KeyNumToString( int keynum, bool localized ) { } } + if ( localized && keynum >= K_FIRST_JOY && keynum <= K_LAST_JOY ) { + const char* jname = Sys_GetLocalizedJoyKeyName(keynum); + if(jname != NULL) + return jname; + } + // check for a key string for ( kn = keynames; kn->name; kn++ ) { if ( keynum == kn->keynum ) { diff --git a/neo/framework/KeyInput.h b/neo/framework/KeyInput.h index 77179d35..402649bd 100644 --- a/neo/framework/KeyInput.h +++ b/neo/framework/KeyInput.h @@ -138,39 +138,51 @@ typedef enum { K_MWHEELDOWN = 195, K_MWHEELUP, - K_JOY1 = 197, - K_JOY2, - K_JOY3, - K_JOY4, - K_JOY5, - K_JOY6, - K_JOY7, - K_JOY8, - K_JOY9, - K_JOY10, - K_JOY11, - K_JOY12, - K_JOY13, - K_JOY14, - K_JOY15, - K_JOY16, - K_JOY17, - K_JOY18, - K_JOY19, - K_JOY20, - K_JOY21, - K_JOY22, - K_JOY23, - K_JOY24, - K_JOY25, - K_JOY26, - K_JOY27, - K_GRAVE_A = 224, // lowercase a with grave accent - K_JOY28, - K_JOY29, - K_JOY30, - K_JOY31, - K_JOY32, + //------------------------ + // K_JOY codes must be contiguous, too, and K_JOY_BTN_* should be kept in sync with J_BTN_* of sys_jEvents + //------------------------ + + K_FIRST_JOY = 197, + K_JOY_BTN_SOUTH = K_FIRST_JOY, // bottom face button, like Xbox A + K_JOY_BTN_EAST, // right face button, like Xbox B + K_JOY_BTN_WEST, // left face button, like Xbox X + K_JOY_BTN_NORTH, // top face button, like Xbox Y + + K_JOY_BTN_BACK, + K_JOY_BTN_GUIDE, // Note: this one should probably not be used? + K_JOY_BTN_START, // hardcoded to generate Esc to open/close menu + K_JOY_BTN_LSTICK, // press left stick + K_JOY_BTN_RSTICK, // press right stick + K_JOY_BTN_LSHOULDER, + K_JOY_BTN_RSHOULDER, + + K_JOY_DPAD_UP, + K_JOY_DPAD_DOWN, + K_JOY_DPAD_LEFT, + K_JOY_DPAD_RIGHT, + + K_JOY_BTN_MISC1, // Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button) + K_JOY_BTN_RPADDLE1, // Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1) + K_JOY_BTN_LPADDLE1, // Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3) + K_JOY_BTN_RPADDLE2, // Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2) + K_JOY_BTN_LPADDLE2, // Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4) + + K_JOY_STICK1_UP, + K_JOY_STICK1_DOWN, + K_JOY_STICK1_LEFT, + K_JOY_STICK1_RIGHT, + + K_JOY_STICK2_UP, + K_JOY_STICK2_DOWN, + K_JOY_STICK2_LEFT, + K_JOY_STICK2_RIGHT, + + K_JOY_TRIGGER1, + K_JOY_TRIGGER2, + + K_LAST_JOY = K_JOY_TRIGGER2, + + K_GRAVE_A = 229, // lowercase a with grave accent FIXME: used to be 224; this probably isn't used anyway K_AUX1 = 230, K_CEDILLA_C = 231, // lowercase c with Cedilla diff --git a/neo/framework/Session.cpp b/neo/framework/Session.cpp index 609c178e..837f7f52 100644 --- a/neo/framework/Session.cpp +++ b/neo/framework/Session.cpp @@ -1447,6 +1447,9 @@ void idSessionLocal::UnloadMap() { } mapSpawned = false; + + // DG: that state needs to be reset now + Sys_SetInteractiveIngameGuiActive( false, NULL ); } /* diff --git a/neo/framework/Session_menu.cpp b/neo/framework/Session_menu.cpp index b979b825..c69859a7 100644 --- a/neo/framework/Session_menu.cpp +++ b/neo/framework/Session_menu.cpp @@ -39,6 +39,8 @@ If you have questions concerning this license or the applicable additional terms idCVar idSessionLocal::gui_configServerRate( "gui_configServerRate", "0", CVAR_GUI | CVAR_ARCHIVE | CVAR_ROM | CVAR_INTEGER, "" ); +extern idCVar joy_gamepadLayout; // DG: used here to update bindings window when cvar is changed + // implements the setup for, and commands from, the main menu /* @@ -1209,6 +1211,14 @@ void idSessionLocal::GuiFrameEvents() { sysEvent_t ev; idUserInterface *gui; + // DG: if joy_gamepadLayout changes, the binding names in the main/controls menu must be updated + if ( joy_gamepadLayout.IsModified() ) { + if ( guiMainMenu != NULL ) { + guiMainMenu->SetKeyBindingNames(); + } + joy_gamepadLayout.ClearModified(); + } + // stop generating move and button commands when a local console or menu is active // running here so SP, async networking and no game all go through it if ( console->Active() || guiActive ) { diff --git a/neo/framework/UsercmdGen.cpp b/neo/framework/UsercmdGen.cpp index f1b213f7..f52fde16 100644 --- a/neo/framework/UsercmdGen.cpp +++ b/neo/framework/UsercmdGen.cpp @@ -93,7 +93,7 @@ typedef enum { UB_BUTTON6, UB_BUTTON7, - UB_ATTACK, + UB_ATTACK, // NOTE: this value (20) is hardcoded in idUserInterfaceLocal::HandleEvent() ! UB_SPEED, UB_ZOOM, UB_SHOWSCORES, @@ -350,7 +350,10 @@ private: bool Inhibited( void ); void AdjustAngles( void ); void KeyMove( void ); + void CircleToSquare( float & axis_x, float & axis_y ) const; + void HandleJoystickAxis( int keyNum, float unclampedValue, float threshold, bool positive ); void JoystickMove( void ); + void JoystickFakeMouse(float axis_x, float axis_y, float deadzone); void MouseMove( void ); void CmdButtons( void ); @@ -384,7 +387,14 @@ private: bool mouseDown; int mouseDx, mouseDy; // added to by mouse events - int joystickAxis[MAX_JOYSTICK_AXIS]; // set by joystick events + float joystickAxis[MAX_JOYSTICK_AXIS]; // set by joystick events + + int pollTime; + int lastPollTime; + float lastLookValuePitch; + float lastLookValueYaw; + + bool heldJump; // TODO: ??? static idCVar in_yawSpeed; static idCVar in_pitchSpeed; @@ -419,6 +429,22 @@ idCVar idUsercmdGenLocal::m_smooth( "m_smooth", "1", CVAR_SYSTEM | CVAR_ARCHIVE idCVar idUsercmdGenLocal::m_strafeSmooth( "m_strafeSmooth", "4", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_INTEGER, "number of samples blended for mouse moving", 1, 8, idCmdSystem::ArgCompletion_Integer<1,8> ); idCVar idUsercmdGenLocal::m_showMouseRate( "m_showMouseRate", "0", CVAR_SYSTEM | CVAR_BOOL, "shows mouse movement" ); +idCVar joy_triggerThreshold( "joy_triggerThreshold", "0.05", CVAR_FLOAT | CVAR_ARCHIVE, "how far the joystick triggers have to be pressed before they register as down" ); +idCVar joy_deadZone( "joy_deadZone", "0.25", CVAR_FLOAT | CVAR_ARCHIVE, "specifies how large the dead-zone is on the joystick" ); +idCVar joy_gammaLook( "joy_gammaLook", "1", CVAR_INTEGER | CVAR_ARCHIVE, "use a log curve instead of a power curve for movement" ); +idCVar joy_powerScale( "joy_powerScale", "2", CVAR_FLOAT | CVAR_ARCHIVE, "Raise joystick values to this power" ); +idCVar joy_pitchSpeed( "joy_pitchSpeed", "130", CVAR_ARCHIVE | CVAR_FLOAT, "pitch speed when pressing up or down on the joystick", 60, 600 ); +idCVar joy_yawSpeed( "joy_yawSpeed", "240", CVAR_ARCHIVE | CVAR_FLOAT, "pitch speed when pressing left or right on the joystick", 60, 600 ); +idCVar joy_invertLook( "joy_invertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" ); + +// these were a bad idea! +idCVar joy_dampenLook( "joy_dampenLook", "1", CVAR_BOOL | CVAR_ARCHIVE, "Do not allow full acceleration on look" ); +idCVar joy_deltaPerMSLook( "joy_deltaPerMSLook", "0.003", CVAR_FLOAT | CVAR_ARCHIVE, "Max amount to be added on look per MS" ); + +idCVar in_useGamepad( "in_useGamepad", "1", CVAR_ARCHIVE | CVAR_BOOL, "enables/disables the gamepad for PC use" ); + +// TODO idCVar in_mouseInvertLook( "in_mouseInvertLook", "0", CVAR_ARCHIVE | CVAR_BOOL, "inverts the look controls so the forward looks up (flight controls) - the proper way to play games!" ); + static idUsercmdGenLocal localUsercmdGen; idUsercmdGen *usercmdGen = &localUsercmdGen; @@ -439,6 +465,8 @@ idUsercmdGenLocal::idUsercmdGenLocal( void ) { toggled_zoom.Clear(); toggled_run.on = in_alwaysRun.GetBool(); + lastLookValuePitch = lastLookValueYaw = 0.0f; + ClearAngles(); Clear(); } @@ -573,9 +601,17 @@ void idUsercmdGenLocal::KeyMove( void ) { forward += KEY_MOVESPEED * ButtonState( UB_FORWARD ); forward -= KEY_MOVESPEED * ButtonState( UB_BACK ); - cmd.forwardmove = idMath::ClampChar( forward ); - cmd.rightmove = idMath::ClampChar( side ); - cmd.upmove = idMath::ClampChar( up ); + // only set each movement variable if its unset at this point. + // NOTE: joystick input happens before this. + if (cmd.forwardmove == 0) { + cmd.forwardmove = idMath::ClampChar( forward ); + } + if (cmd.rightmove == 0) { + cmd.rightmove = idMath::ClampChar( side ); + } + if (cmd.upmove == 0) { + cmd.upmove = idMath::ClampChar( up ); + } } /* @@ -672,29 +708,233 @@ void idUsercmdGenLocal::MouseMove( void ) { } } +/* +======================== +idUsercmdGenLocal::CircleToSquare +======================== +*/ +void idUsercmdGenLocal::CircleToSquare( float & axis_x, float & axis_y ) const { + // bring everything in the first quadrant + bool flip_x = false; + if ( axis_x < 0.0f ) { + flip_x = true; + axis_x *= -1.0f; + } + bool flip_y = false; + if ( axis_y < 0.0f ) { + flip_y = true; + axis_y *= -1.0f; + } + + // swap the two axes so we project against the vertical line X = 1 + bool swap = false; + if ( axis_y > axis_x ) { + float tmp = axis_x; + axis_x = axis_y; + axis_y = tmp; + swap = true; + } + + if ( axis_x < 0.001f ) { + // on one of the axes where no correction is needed + return; + } + + // length (max 1.0f at the unit circle) + float len = idMath::Sqrt( axis_x * axis_x + axis_y * axis_y ); + if ( len > 1.0f ) { + len = 1.0f; + } + // thales + float axis_y_us = axis_y / axis_x; + + // use a power curve to shift the correction to happen closer to the unit circle + float correctionRatio = Square( len ); + axis_x += correctionRatio * ( len - axis_x ); + axis_y += correctionRatio * ( axis_y_us - axis_y ); + + // go back through the symmetries + if ( swap ) { + float tmp = axis_x; + axis_x = axis_y; + axis_y = tmp; + } + if ( flip_x ) { + axis_x *= -1.0f; + } + if ( flip_y ) { + axis_y *= -1.0f; + } +} + +/* +======================== +idUsercmdGenLocal::HandleJoystickAxis +======================== +*/ +void idUsercmdGenLocal::HandleJoystickAxis( int keyNum, float unclampedValue, float threshold, bool positive ) { + if ( ( unclampedValue > 0.0f ) && !positive ) { + return; + } + if ( ( unclampedValue < 0.0f ) && positive ) { + return; + } + float value = 0.0f; + bool pressed = false; + if ( unclampedValue > threshold ) { + value = idMath::Fabs( ( unclampedValue - threshold ) / ( 1.0f - threshold ) ); + pressed = true; + } else if ( unclampedValue < -threshold ) { + value = idMath::Fabs( ( unclampedValue + threshold ) / ( 1.0f - threshold ) ); + pressed = true; + } + + int action = idKeyInput::GetUsercmdAction( keyNum ); + if ( action >= UB_ATTACK ) { + Key( keyNum, pressed ); + return; + } + if ( !pressed ) { + return; + } + + float lookValue = 0.0f; + if ( joy_gammaLook.GetBool() ) { + lookValue = idMath::Pow( 1.04712854805f, value * 100.0f ) * 0.01f; + } else { + lookValue = idMath::Pow( value, joy_powerScale.GetFloat() ); + } + +#if 0 // TODO: aim assist maybe. + idGame * game = common->Game(); + if ( game != NULL ) { + lookValue *= game->GetAimAssistSensitivity(); + } +#endif + + switch ( action ) { + case UB_FORWARD: { + float move = (float)cmd.forwardmove + ( KEY_MOVESPEED * value ); + cmd.forwardmove = idMath::ClampChar( idMath::Ftoi( move ) ); + break; + } + case UB_BACK: { + float move = (float)cmd.forwardmove - ( KEY_MOVESPEED * value ); + cmd.forwardmove = idMath::ClampChar( idMath::Ftoi( move ) ); + break; + } + case UB_MOVELEFT: { + float move = (float)cmd.rightmove - ( KEY_MOVESPEED * value ); + cmd.rightmove = idMath::ClampChar( idMath::Ftoi( move ) ); + break; + } + case UB_MOVERIGHT: { + float move = (float)cmd.rightmove + ( KEY_MOVESPEED * value ); + cmd.rightmove = idMath::ClampChar( idMath::Ftoi( move ) ); + break; + } + case UB_LOOKUP: { + if ( joy_dampenLook.GetBool() ) { + lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValuePitch ); + lastLookValuePitch = lookValue; + } + + float invertPitch = joy_invertLook.GetBool() ? -1.0f : 1.0f; + viewangles[PITCH] -= MS2SEC( pollTime - lastPollTime ) * lookValue * joy_pitchSpeed.GetFloat() * invertPitch; + break; + } + case UB_LOOKDOWN: { + if ( joy_dampenLook.GetBool() ) { + lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValuePitch ); + lastLookValuePitch = lookValue; + } + + float invertPitch = joy_invertLook.GetBool() ? -1.0f : 1.0f; + viewangles[PITCH] += MS2SEC( pollTime - lastPollTime ) * lookValue * joy_pitchSpeed.GetFloat() * invertPitch; + break; + } + case UB_LEFT: { + if ( joy_dampenLook.GetBool() ) { + lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValueYaw ); + lastLookValueYaw = lookValue; + } + viewangles[YAW] += MS2SEC( pollTime - lastPollTime ) * lookValue * joy_yawSpeed.GetFloat(); + break; + } + case UB_RIGHT: { + if ( joy_dampenLook.GetBool() ) { + lookValue = Min( lookValue, ( pollTime - lastPollTime ) * joy_deltaPerMSLook.GetFloat() + lastLookValueYaw ); + lastLookValueYaw = lookValue; + } + viewangles[YAW] -= MS2SEC( pollTime - lastPollTime ) * lookValue * joy_yawSpeed.GetFloat(); + break; + } + } +} + +static float joyAxisToMouseDelta(float axis, float deadzone) +{ + float ret = 0.0f; + float val = fabsf(axis); // calculations below require a positive value + if(val > deadzone) { + // from deadzone .. 1 to 0 .. 1-deadzone + val -= deadzone; + // and then to 0..1 + val = val * (1.0f / (1.0f - deadzone)); + + // make it exponential curve - exp(val*3) should return sth between 1 and 20; + // then turning that into 0.5 .. 10 + ret = expf( val * 3.0f ) * 0.5f; + if(axis < 0.0f) // restore sign + ret = -ret; + } + return ret; +} + +extern bool D3_IN_interactiveIngameGuiActive; // from sys/events.cpp +void idUsercmdGenLocal::JoystickFakeMouse(float axis_x, float axis_y, float deadzone) +{ + if ( D3_IN_interactiveIngameGuiActive ) { + float x = joyAxisToMouseDelta(axis_x, deadzone); + float y = joyAxisToMouseDelta(axis_y, deadzone); + continuousMouseX += x; + continuousMouseY += y; + } +} + /* ================= idUsercmdGenLocal::JoystickMove ================= */ -void idUsercmdGenLocal::JoystickMove( void ) { - float anglespeed; +void idUsercmdGenLocal::JoystickMove() { + float threshold = joy_deadZone.GetFloat(); + float triggerThreshold = joy_triggerThreshold.GetFloat(); - if ( toggled_run.on ^ ( in_alwaysRun.GetBool() && idAsyncNetwork::IsActive() ) ) { - anglespeed = idMath::M_MS2SEC * USERCMD_MSEC * in_angleSpeedKey.GetFloat(); - } else { - anglespeed = idMath::M_MS2SEC * USERCMD_MSEC; - } + float axis_y = joystickAxis[ AXIS_LEFT_Y ]; + float axis_x = joystickAxis[ AXIS_LEFT_X ]; + CircleToSquare( axis_x, axis_y ); - if ( !ButtonState( UB_STRAFE ) ) { - viewangles[YAW] += anglespeed * in_yawSpeed.GetFloat() * joystickAxis[AXIS_SIDE]; - viewangles[PITCH] += anglespeed * in_pitchSpeed.GetFloat() * joystickAxis[AXIS_FORWARD]; - } else { - cmd.rightmove = idMath::ClampChar( cmd.rightmove + joystickAxis[AXIS_SIDE] ); - cmd.forwardmove = idMath::ClampChar( cmd.forwardmove + joystickAxis[AXIS_FORWARD] ); - } + HandleJoystickAxis( K_JOY_STICK1_UP, axis_y, threshold, false ); + HandleJoystickAxis( K_JOY_STICK1_DOWN, axis_y, threshold, true ); + HandleJoystickAxis( K_JOY_STICK1_LEFT, axis_x, threshold, false ); + HandleJoystickAxis( K_JOY_STICK1_RIGHT, axis_x, threshold, true ); - cmd.upmove = idMath::ClampChar( cmd.upmove + joystickAxis[AXIS_UP] ); + JoystickFakeMouse( axis_x, axis_y, threshold ); + + axis_y = joystickAxis[ AXIS_RIGHT_Y ]; + axis_x = joystickAxis[ AXIS_RIGHT_X ]; + CircleToSquare( axis_x, axis_y ); + + HandleJoystickAxis( K_JOY_STICK2_UP, axis_y, threshold, false ); + HandleJoystickAxis( K_JOY_STICK2_DOWN, axis_y, threshold, true ); + HandleJoystickAxis( K_JOY_STICK2_LEFT, axis_x, threshold, false ); + HandleJoystickAxis( K_JOY_STICK2_RIGHT, axis_x, threshold, true ); + + JoystickFakeMouse( axis_x, axis_y, threshold ); + + HandleJoystickAxis( K_JOY_TRIGGER1, joystickAxis[ AXIS_LEFT_TRIG ], triggerThreshold, true ); + HandleJoystickAxis( K_JOY_TRIGGER2, joystickAxis[ AXIS_RIGHT_TRIG ], triggerThreshold, true ); } /* @@ -778,6 +1018,9 @@ void idUsercmdGenLocal::MakeCurrent( void ) { // keyboard angle adjustment AdjustAngles(); + // get basic movement from joystick + JoystickMove(); + // set button bits CmdButtons(); @@ -787,9 +1030,6 @@ void idUsercmdGenLocal::MakeCurrent( void ) { // get basic movement from mouse MouseMove(); - // get basic movement from joystick - JoystickMove(); - // check to make sure the angles haven't wrapped if ( viewangles[PITCH] - oldAngles[PITCH] > 90 ) { viewangles[PITCH] = oldAngles[PITCH] + 90; @@ -877,6 +1117,7 @@ void idUsercmdGenLocal::Clear( void ) { // clears all key states memset( buttonState, 0, sizeof( buttonState ) ); memset( keyState, false, sizeof( keyState ) ); + memset( joystickAxis, 0, sizeof( joystickAxis ) ); inhibitCommands = false; @@ -939,6 +1180,8 @@ void idUsercmdGenLocal::Key( int keyNum, bool down ) { int action = idKeyInput::GetUsercmdAction( keyNum ); + // TODO: if action == 0 return ? + if ( down ) { buttonState[ action ]++; @@ -1039,7 +1282,25 @@ idUsercmdGenLocal::Joystick =============== */ void idUsercmdGenLocal::Joystick( void ) { - memset( joystickAxis, 0, sizeof( joystickAxis ) ); + int numEvents = Sys_PollJoystickInputEvents( 0 ); + + // Study each of the buffer elements and process them. + for ( int i = 0; i < numEvents; i++ ) { + int action; + int value; + if ( Sys_ReturnJoystickInputEvent( i, action, value ) ) { + if ( action >= J_ACTION_FIRST && action <= J_ACTION_MAX ) { + int joyButton = K_FIRST_JOY + ( action - J_ACTION_FIRST ); + Key( joyButton, ( value != 0 ) ); + } else if ( ( action >= J_AXIS_MIN ) && ( action <= J_AXIS_MAX ) ) { + joystickAxis[ action - J_AXIS_MIN ] = static_cast( value ) / 32767.0f; + } else { + //assert( !"Unknown joystick event" ); + } + } + } + + Sys_EndJoystickInputEvents(); } /* @@ -1065,7 +1326,9 @@ void idUsercmdGenLocal::UsercmdInterrupt( void ) { Keyboard(); // process the system joystick events - Joystick(); + if ( in_useGamepad.GetBool() ) { + Joystick(); + } // create the usercmd for com_ticNumber+1 MakeCurrent(); @@ -1095,6 +1358,11 @@ idUsercmdGenLocal::GetDirectUsercmd */ usercmd_t idUsercmdGenLocal::GetDirectUsercmd( void ) { + pollTime = Sys_Milliseconds(); + if ( pollTime - lastPollTime > 100 ) { + lastPollTime = pollTime - 100; + } + // initialize current usercmd InitCurrent(); @@ -1105,12 +1373,15 @@ usercmd_t idUsercmdGenLocal::GetDirectUsercmd( void ) { Keyboard(); // process the system joystick events - Joystick(); - + if ( in_useGamepad.GetBool() ) { + Joystick(); + } // create the usercmd MakeCurrent(); cmd.duplicateCount = 0; + lastPollTime = pollTime; + return cmd; } diff --git a/neo/framework/UsercmdGen.h b/neo/framework/UsercmdGen.h index 17e88fe8..c9e9ecae 100644 --- a/neo/framework/UsercmdGen.h +++ b/neo/framework/UsercmdGen.h @@ -96,8 +96,8 @@ public: signed char rightmove; // left/right movement signed char upmove; // up/down movement short angles[3]; // view angles - short mx; // mouse delta x - short my; // mouse delta y + short mx; // mouse delta x - DG: not really delta, but from continuousMouseX which accumulates + short my; // mouse delta y - DG: same but from continuousMouseY signed char impulse; // impulse command byte flags; // additional flags int sequence; // just for debugging diff --git a/neo/sys/events.cpp b/neo/sys/events.cpp index 6cc04516..7da003bf 100644 --- a/neo/sys/events.cpp +++ b/neo/sys/events.cpp @@ -60,6 +60,9 @@ If you have questions concerning this license or the applicable additional terms #define SDLK_PRINTSCREEN SDLK_PRINT #endif +extern idCVar in_useGamepad; // from UsercmdGen.cpp +extern idCVar joy_deadZone; // ditto + // NOTE: g++-4.7 doesn't like when this is static (for idCmdSystem::ArgCompletion_String) const char *_in_kbdNames[] = { #if SDL_VERSION_ATLEAST(2, 0, 0) // auto-detection is only available for SDL2 @@ -77,11 +80,21 @@ static idCVar in_nograb("in_nograb", "0", CVAR_SYSTEM | CVAR_NOCHEAT, "prevents static idCVar in_grabKeyboard("in_grabKeyboard", "0", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_BOOL, "if enabled, grabs all keyboard input if mouse is grabbed (so keyboard shortcuts from the OS like Alt-Tab or Windows Key won't work)"); +idCVar joy_gamepadLayout("joy_gamepadLayout", "-1", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_NOCHEAT | CVAR_INTEGER, + "Button layout of gamepad. -1: auto (needs SDL 2.0.12 or newer), 0: XBox-style, 1: Nintendo-style, 2: PS4/5-style, 3: PS2/3-style", idCmdSystem::ArgCompletion_Integer<-1, 3> ); + // set in handleMouseGrab(), used in Sys_GetEvent() to decide what kind of internal mouse event to generate static bool in_relativeMouseMode = true; // set in Sys_GetEvent() on window focus gained/lost events static bool in_hasFocus = true; +static enum D3_Gamepad_Type { + D3_GAMEPAD_XINPUT, // XBox/XInput standard, the default + D3_GAMEPAD_NINTENDO, // nintendo-like (A/B and X/Y are switched) + D3_GAMEPAD_PLAYSTATION, // PS-like (geometric symbols instead of A/B/X/Y) + D3_GAMEPAD_PLAYSTATION_OLD // PS2/PS3-like: the back button is called "select" instead of "share" +} gamepadType = D3_GAMEPAD_XINPUT; + struct kbd_poll_t { int key; bool state; @@ -108,8 +121,26 @@ struct mouse_poll_t { } }; +struct joystick_poll_t { + int action; + int value; + + joystick_poll_t() : action(0), value(0) {} // TODO: or -1? + + joystick_poll_t(int a, int v) { + action = a; + value = v; + } +}; + static idList kbd_polls; static idList mouse_polls; +static idList joystick_polls; + +static bool buttonStates[K_LAST_KEY]; +static float joyAxis[MAX_JOYSTICK_AXIS]; + +static idList event_overflow; #if SDL_VERSION_ATLEAST(2, 0, 0) // for utf8ToISO8859_1() - used for non-ascii text input and Sys_GetLocalizedScancodeName() @@ -171,7 +202,7 @@ static scancodename_t scancodemappings[] = { D3_SC_MAPPING(COMMA), D3_SC_MAPPING(PERIOD), D3_SC_MAPPING(SLASH), - // leaving out lots of key incl. from keypad, we already handle them as normal keys + // leaving out lots of keys incl. from keypad, we already handle them as normal keys D3_SC_MAPPING(NONUSBACKSLASH), D3_SC_MAPPING(INTERNATIONAL1), /**< used on Asian keyboards, see footnotes in USB doc */ D3_SC_MAPPING(INTERNATIONAL2), @@ -246,6 +277,137 @@ static bool utf8ToISO8859_1(const char* inbuf, char* outbuf, size_t outsize) { } #endif // SDL2 +const char* Sys_GetLocalizedJoyKeyName( int key ) { + // Note: trying to keep the returned names short, because the Doom3 binding window doesn't have much space for names.. + +#if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1 + if (key >= K_FIRST_JOY && key <= K_LAST_JOY) { + + if (key <= K_JOY_BTN_BACK) { +#if SDL_VERSION_ATLEAST(3, 0, 0) + // TODO: or use the SDL2 code and just set joy_gamepadLayout automatically based on SDL_GetGamepadType() ? + SDL_GamepadButton gpbtn = SDL_GAMEPAD_BUTTON_SOUTH + (key - K_JOY_BTN_SOUTH); + SDL_GamepadButtonLabel label = SDL_GetGamepadButtonLabelForType(TODO, gpbtn); + switch(label) { + case SDL_GAMEPAD_BUTTON_LABEL_A: + return "Pad A"; + case SDL_GAMEPAD_BUTTON_LABEL_B: + return "Pad B"; + case SDL_GAMEPAD_BUTTON_LABEL_X: + return "Pad X"; + case SDL_GAMEPAD_BUTTON_LABEL_Y: + return "Pad Y"; + case SDL_GAMEPAD_BUTTON_LABEL_CROSS: + return "Pad Cross"; + case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: + return "Pad Circle"; + case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: + return "Pad Square"; + case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: + return "Pad Triangle"; + } + +#else // SDL2 + // South, East, West, North Back + static const char* xboxBtnNames[5] = { "Pad A", "Pad B", "Pad X", "Pad Y", "Pad Back" }; + static const char* nintendoBtnNames[5] = { "Pad B", "Pad A", "Pad Y", "Pad X", "Pad -" }; + static const char* psBtnNames[5] = { "Pad Cross", "Pad Circle", "Pad Square", "Pad Triangle", "Pad Share" }; + + int layout = joy_gamepadLayout.GetInteger(); + if ( layout == -1 ) { + layout = gamepadType; + } + + unsigned btnIdx = key - K_JOY_BTN_SOUTH; + assert(btnIdx < 5); + + switch( layout ) { + default: + common->Warning( "joy_gamepadLayout has invalid value %d !\n", joy_gamepadLayout.GetInteger() ); + // fall-through + case D3_GAMEPAD_XINPUT: + return xboxBtnNames[btnIdx]; + case D3_GAMEPAD_NINTENDO: + return nintendoBtnNames[btnIdx]; + case D3_GAMEPAD_PLAYSTATION_OLD: + if ( key == K_JOY_BTN_BACK ) + return "Pad Select"; + // the other button names are identical for PS2/3 and PS4/5 + // fall-through + case D3_GAMEPAD_PLAYSTATION: + return psBtnNames[btnIdx]; + } +#endif // face button names for SDL2 + } + + // the labels for the remaining keys are the same for SDL2 and SDL3 (and all controllers) + switch(key) { + case K_JOY_BTN_GUIDE: // can't be used in dhewm3, because it opens steam on some systems + case K_JOY_BTN_START: // can't be used for bindings, because it's hardcoded to generate Esc + return NULL; + + case K_JOY_BTN_LSTICK: + return "Pad LStick"; + case K_JOY_BTN_RSTICK: + return "Pad RStick"; + case K_JOY_BTN_LSHOULDER: + return "Pad LShoulder"; + case K_JOY_BTN_RSHOULDER: + return "Pad RShoulder"; + + case K_JOY_DPAD_UP: + return "DPad Up"; + case K_JOY_DPAD_DOWN: + return "DPad Down"; + case K_JOY_DPAD_LEFT: + return "DPad Left"; + case K_JOY_DPAD_RIGHT: + return "DPad Right"; + + case K_JOY_BTN_MISC1: + return "Pad Misc"; + case K_JOY_BTN_RPADDLE1: + return "Pad P1"; + case K_JOY_BTN_LPADDLE1: + return "Pad P3"; + case K_JOY_BTN_RPADDLE2: + return "Pad P2"; + case K_JOY_BTN_LPADDLE2: + return "Pad P4"; + + // Note: Would be nicer with "Pad " (or even "Gamepad ") at the beginning, + // but then it's too long for the keybinding window :-/ + case K_JOY_STICK1_UP: + return "Stick1 Up"; + case K_JOY_STICK1_DOWN: + return "Stick1 Down"; + case K_JOY_STICK1_LEFT: + return "Stick1 Left"; + case K_JOY_STICK1_RIGHT: + return "Stick1 Right"; + + case K_JOY_STICK2_UP: + return "Stick2 Up"; + case K_JOY_STICK2_DOWN: + return "Stick2 Down"; + case K_JOY_STICK2_LEFT: + return "Stick2 Left"; + case K_JOY_STICK2_RIGHT: + return "Stick2 Right"; + + case K_JOY_TRIGGER1: + return "Trigger 1"; + case K_JOY_TRIGGER2: + return "Trigger 2"; + + default: + assert(0 && "missing a case in Sys_GetLocalizedJoyKeyName()!"); + } + } +#endif // SDL2+ + return NULL; +} + // returns localized name of the key (between K_FIRST_SCANCODE and K_LAST_SCANCODE), // regarding the current keyboard layout - if that name is in ASCII or corresponds // to a "High-ASCII" char supported by Doom3. @@ -477,6 +639,162 @@ static byte mapkey(SDL_Keycode key) { return 0; } +#if SDL_VERSION_ATLEAST(2, 0, 0) + +#if ! SDL_VERSION_ATLEAST(2, 0, 14) +// Hack: to support newer SDL2 runtime versions than the one built against, +// define these controller buttons if needed +enum { + SDL_CONTROLLER_BUTTON_MISC1 = 15, /* Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button */ + SDL_CONTROLLER_BUTTON_PADDLE1, /* Xbox Elite paddle P1 */ + SDL_CONTROLLER_BUTTON_PADDLE2, /* Xbox Elite paddle P3 */ + SDL_CONTROLLER_BUTTON_PADDLE3, /* Xbox Elite paddle P2 */ + SDL_CONTROLLER_BUTTON_PADDLE4, /* Xbox Elite paddle P4 */ + SDL_CONTROLLER_BUTTON_TOUCHPAD, /* PS4/PS5 touchpad button */ +}; +#endif // ! SDL_VERSION_ATLEAST(2, 0, 14) + +static sys_jEvents mapjoybutton(SDL_GameControllerButton button) { + switch (button) + { + case SDL_CONTROLLER_BUTTON_A: + return J_BTN_SOUTH; + case SDL_CONTROLLER_BUTTON_B: + return J_BTN_EAST; + case SDL_CONTROLLER_BUTTON_X: + return J_BTN_WEST; + case SDL_CONTROLLER_BUTTON_Y: + return J_BTN_NORTH; + case SDL_CONTROLLER_BUTTON_BACK: + return J_BTN_BACK; + case SDL_CONTROLLER_BUTTON_GUIDE: + // TODO: this one should probably not be bindable? + //return J_BTN_GUIDE; + break; + case SDL_CONTROLLER_BUTTON_START: + return J_BTN_START; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + return J_BTN_LSTICK; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return J_BTN_RSTICK; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + return J_BTN_LSHOULDER; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + return J_BTN_RSHOULDER; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + return J_DPAD_UP; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + return J_DPAD_DOWN; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + return J_DPAD_LEFT; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + return J_DPAD_RIGHT; + + case SDL_CONTROLLER_BUTTON_MISC1: + return J_BTN_MISC1; + case SDL_CONTROLLER_BUTTON_PADDLE1: + return J_BTN_RPADDLE1; + case SDL_CONTROLLER_BUTTON_PADDLE2: + return J_BTN_RPADDLE2; + case SDL_CONTROLLER_BUTTON_PADDLE3: + return J_BTN_LPADDLE1; + case SDL_CONTROLLER_BUTTON_PADDLE4: + return J_BTN_LPADDLE2; + default: + common->Warning("unknown game controller button %u", button); + break; + } + return MAX_JOY_EVENT; +} + +static sys_jEvents mapjoyaxis(SDL_GameControllerAxis axis) { + switch (axis) + { + case SDL_CONTROLLER_AXIS_LEFTX: + return J_AXIS_LEFT_X; + case SDL_CONTROLLER_AXIS_LEFTY: + return J_AXIS_LEFT_Y; + case SDL_CONTROLLER_AXIS_RIGHTX: + return J_AXIS_RIGHT_X; + case SDL_CONTROLLER_AXIS_RIGHTY: + return J_AXIS_RIGHT_Y; + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return J_AXIS_LEFT_TRIG; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return J_AXIS_RIGHT_TRIG; + default: + common->Warning("unknown game controller axis %u", axis); + break; + } + return J_AXIS_MAX; +} + +#if ! SDL_VERSION_ATLEAST(2, 24, 0) +// Hack: to support newer SDL2 runtime versions than the one compiled against, +// define some controller types that were added after 2.0.12 +enum { +#if ! SDL_VERSION_ATLEAST(2, 0, 14) + SDL_CONTROLLER_TYPE_PS5 = 7, +#endif + + // leaving out luna and stadia (from 2.0.16) + // and nvidia shield (from 2.24), they're similar enough to XBox/XInput + + // the following were added in 2.24 + SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT = 11, + SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT, + SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR +}; +#endif // ! SDL_VERSION_ATLEAST(2, 24, 0) + +static void setGamepadType( SDL_GameController* gc ) +{ +#if SDL_VERSION_ATLEAST(2, 0, 12) + const char* typestr = NULL; + switch( SDL_GameControllerGetType( gc ) ) { + default: // the other controller like luna, stadia, whatever, have a very similar layout + case SDL_CONTROLLER_TYPE_UNKNOWN: + case SDL_CONTROLLER_TYPE_XBOX360: + case SDL_CONTROLLER_TYPE_XBOXONE: + gamepadType = D3_GAMEPAD_XINPUT; + typestr = "XBox-like"; + break; + + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO: + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT: + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT: + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: + gamepadType = D3_GAMEPAD_NINTENDO; + typestr = "Nintendo-like"; + break; + + case SDL_CONTROLLER_TYPE_PS3: + gamepadType = D3_GAMEPAD_PLAYSTATION_OLD; + typestr = "Playstation2/3-like"; + break; + case SDL_CONTROLLER_TYPE_PS4: + case SDL_CONTROLLER_TYPE_PS5: + gamepadType = D3_GAMEPAD_PLAYSTATION; + typestr = "Playstation-like"; + break; + } + + common->Printf( "Detected Gamepad %s as type %s\n", SDL_GameControllerName( gc ), typestr ); + SDL_Joystick* joy = SDL_GameControllerGetJoystick( gc ); + SDL_JoystickGUID guid = SDL_JoystickGetGUID( joy ); + char guidstr[34] = {}; + SDL_JoystickGetGUIDString( guid, guidstr, sizeof(guidstr) ); + Uint16 vendor = SDL_GameControllerGetVendor( gc ); + Uint16 product = SDL_GameControllerGetProduct( gc ); + const char* joyname = SDL_JoystickName( joy ); + + common->Printf( " USB IDs: %.4hx:%.4hx Joystick Name: \"%s\" GUID: %s\n", vendor, product, joyname, guidstr ); + +#endif // SDL_VERSION_ATLEAST(2, 0, 12) +} + +#endif // SDL2+ gamecontroller code + static void PushConsoleEvent(const char *s) { char *b; size_t len; @@ -531,6 +849,28 @@ void Sys_InitInput() { #else // SDL1.2 doesn't support this in_grabKeyboard.ClearModified(); #endif + + joystick_polls.SetGranularity(64); + event_overflow.SetGranularity(64); + + memset( buttonStates, 0, sizeof( buttonStates ) ); + memset( joyAxis, 0, sizeof( joyAxis ) ); + +#if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1 + // use button positions instead of button labels, + // Sys_GetLocalizedJoyKeyName() will do the translation + // (I think this also was the default before 2.0.12?) + SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0"); + + const int NumJoysticks = SDL_NumJoysticks(); + for( int i = 0; i < NumJoysticks; ++i ) + { + SDL_GameController* gc = SDL_GameControllerOpen( i ); + if ( gc != NULL ) { + setGamepadType( gc ); + } + } +#endif } /* @@ -541,6 +881,8 @@ Sys_ShutdownInput void Sys_ShutdownInput() { kbd_polls.Clear(); mouse_polls.Clear(); + joystick_polls.Clear(); + event_overflow.Clear(); #if SDL_VERSION_ATLEAST(2, 0, 0) SDL_iconv_close( iconvDesc ); // used by utf8ToISO8859_1() iconvDesc = ( SDL_iconv_t ) -1; @@ -666,6 +1008,67 @@ void Sys_GrabMouseCursor(bool grabIt) { GLimp_GrabInput(flags); } + +/* +=============== +Sys_SetInteractiveIngameGuiActive +Tell the input system that currently an interactive *ingame* UI has focus, +so there is an active cursor. +Used for an ungodly hack to make gamepad button south (A) behave like +left mouse button in that case, so "clicking" with gamepad in the PDA +(and ingame GUIs) works as expected. +Not set for proper menus like main menu etc - the gamepad hacks for that +are in idUserInterfaceLocal::HandleEvent(). +Call with ui = NULL to clear the state. +I hope this won't explode in my face :-p +=============== +*/ +bool D3_IN_interactiveIngameGuiActive = false; +void Sys_SetInteractiveIngameGuiActive( bool active, idUserInterface* ui ) +{ + static idList lastuis; + if ( ui == NULL ) { + // special case for clearing + D3_IN_interactiveIngameGuiActive = false; + lastuis.Clear(); + return; + } + int idx = lastuis.FindIndex( ui ); + + if ( sessLocal.GetActiveMenu() == NULL && active ) { + // add ui to lastuis, if it has been activated and no proper menu + // (like main menu) is currently open + lastuis.Append( ui ); + } else if ( idx != -1 ) { + // if the UI is in lastuis and has been deactivated, or there + // is a proper menu opened, remove it from the list. + // this both handles the regular deactivate case and also works around + // main-menu-in-multiplayer weirdness: that menu calls idUserInterface::Activate() + // with activate = true twice, but on first call sessLocal.GetActiveMenu() is NULL + // so we want to remove it once we realize that it really is a "proper" menu after all. + // And because it's possible that we have an ingame UI focussed while opening + // the multiplayer-main-menu, we keep a list of lastuis, instead of just one, + // so D3_IN_interactiveIngameGuiActive remains true in that case + // (the ingame UI is still in the list) + + lastuis.RemoveIndex( idx ); + } + + D3_IN_interactiveIngameGuiActive = lastuis.Num() != 0; +} + + +static void PushButton( int key, bool value ) { + // So we don't keep sending the same SE_KEY message over and over again + if ( buttonStates[key] != value ) { + buttonStates[key] = value; + sysEvent_t res = { SE_KEY, key, value ? 1 : 0, 0, NULL }; + // this is done to generate two events per controller axis event + // one SE_JOYSTICK and one SE_KEY + event_overflow.Append(res); + } +} + /* ================ Sys_GetEvent @@ -678,6 +1081,14 @@ sysEvent_t Sys_GetEvent() { static const sysEvent_t res_none = { SE_NONE, 0, 0, 0, NULL }; + // process any overflow. + if (event_overflow.Num() > 0) + { + res = event_overflow[0]; + event_overflow.RemoveIndex(0); + return res; + } + #if SDL_VERSION_ATLEAST(2, 0, 0) static char s[SDL_TEXTINPUTEVENT_TEXT_SIZE] = {0}; static size_t s_pos = 0; @@ -958,6 +1369,120 @@ sysEvent_t Sys_GetEvent() { return res; +#if SDL_VERSION_ATLEAST(2, 0, 0) // gamecontroller/gamepad not supported in SDL1 + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + { + if ( !in_useGamepad.GetBool() ) { + common->Warning( "Gamepad support is disabled! Set the in_useGamepad CVar to 1 to enable it!\n" ); + continue; + } + + res.evType = SE_KEY; + res.evValue2 = ev.cbutton.state == SDL_PRESSED ? 1 : 0; + + // special case: always treat the start button as escape so it opens/closes the menu + // (also makes that button non-bindable) + if ( ev.cbutton.button == SDL_CONTROLLER_BUTTON_START ) { + res.evValue = K_ESCAPE; + return res; + } else if( (ev.cbutton.button == SDL_CONTROLLER_BUTTON_A || ev.cbutton.button == SDL_CONTROLLER_BUTTON_Y) + && D3_IN_interactiveIngameGuiActive && sessLocal.GetActiveMenu() == NULL ) + { + // ugly hack: currently an interactive ingame GUI (with a cursor) is active/focused + // so pretend that the gamepads A (south) or Y (north, used by D3BFG to click ingame GUIs) button + // is the left mouse button so it can be used for "clicking".. + mouse_polls.Append( mouse_poll_t(M_ACTION1, res.evValue2) ); + res.evValue = K_MOUSE1; + return res; + } + + sys_jEvents jEvent = mapjoybutton( (SDL_GameControllerButton)ev.cbutton.button ); + joystick_polls.Append( joystick_poll_t(jEvent, ev.cbutton.state == SDL_PRESSED ? 1 : 0) ); + + if ( ( jEvent >= J_ACTION_FIRST ) && ( jEvent <= J_ACTION_MAX ) ) { + res.evValue = K_FIRST_JOY + ( jEvent - J_ACTION_FIRST ); + return res; + } + + continue; // try to get a decent event. + } + + case SDL_CONTROLLERAXISMOTION: + { + const int range = 16384; + + if ( !in_useGamepad.GetBool() ) { + // not printing a message here, I guess we get lots of spurious axis events.. + // TODO: or print a message if value is big enough? + continue; + } + + sys_jEvents jEvent = mapjoyaxis( (SDL_GameControllerAxis)ev.caxis.axis); + joystick_polls.Append(joystick_poll_t( jEvent, ev.caxis.value) ); + + if ( jEvent == J_AXIS_LEFT_X ) { + PushButton( K_JOY_STICK1_LEFT, ( ev.caxis.value < -range ) ); + PushButton( K_JOY_STICK1_RIGHT, ( ev.caxis.value > range ) ); + } else if ( jEvent == J_AXIS_LEFT_Y ) { + PushButton( K_JOY_STICK1_UP, ( ev.caxis.value < -range ) ); + PushButton( K_JOY_STICK1_DOWN, ( ev.caxis.value > range ) ); + } else if ( jEvent == J_AXIS_RIGHT_X ) { + PushButton( K_JOY_STICK2_LEFT, ( ev.caxis.value < -range ) ); + PushButton( K_JOY_STICK2_RIGHT, ( ev.caxis.value > range ) ); + } else if ( jEvent == J_AXIS_RIGHT_Y ) { + PushButton( K_JOY_STICK2_UP, ( ev.caxis.value < -range ) ); + PushButton( K_JOY_STICK2_DOWN, ( ev.caxis.value > range ) ); + } else if ( jEvent == J_AXIS_LEFT_TRIG ) { + PushButton( K_JOY_TRIGGER1, ( ev.caxis.value > range ) ); + } else if ( jEvent == J_AXIS_RIGHT_TRIG ) { + PushButton( K_JOY_TRIGGER2, ( ev.caxis.value > range ) ); + } + if ( jEvent >= J_AXIS_MIN && jEvent <= J_AXIS_MAX ) { + // NOTE: the stuff set here is only used to move the cursor in menus + // ingame movement is done via joystick_polls + int axis = jEvent - J_AXIS_MIN; + float dz = joy_deadZone.GetFloat(); + + float val = fabsf(ev.caxis.value * (1.0f / 32767.0f)); + if(val < dz) { + val = 0.0f; + } else { + // from deadzone .. 1 to 0 .. 1-deadzone + val -= dz; + // and then to 0..1 + val = val * (1.0f / (1.0f - dz)); + + if( ev.caxis.value < 0 ) { + val = -val; + } + } + + joyAxis[axis] = val; + } + + // handle next event; joy axis events are generated below, + // when there are no further SDL events + continue; + } + break; + + case SDL_JOYDEVICEADDED: + { + SDL_GameController* gc = SDL_GameControllerOpen( ev.jdevice.which ); + if ( gc != NULL ) { + setGamepadType( gc ); + } + // TODO: hot swapping maybe. + //lbOnControllerPlugIn(event.jdevice.which); + break; + } + case SDL_JOYDEVICEREMOVED: + // TODO: hot swapping maybe. + //lbOnControllerUnPlug(event.jdevice.which); + break; +#endif // SDL2+ + case SDL_QUIT: PushConsoleEvent("quit"); return res_none; @@ -980,6 +1505,29 @@ sysEvent_t Sys_GetEvent() { } } + // before returning res_none for "these were all events for now", + // first return joyaxis events, if gamepad is enabled and 16ms are over + // (or we haven't returned the values for all axis yet) + if ( in_useGamepad.GetBool() ) { + static unsigned int lastMS = 0; + static int joyAxisToSend = 0; + unsigned int nowMS = Sys_Milliseconds(); + if ( nowMS - lastMS >= 16 ) { + int val = joyAxis[joyAxisToSend] * 100; // float to percent + res.evType = SE_JOYSTICK; + res.evValue = joyAxisToSend; + res.evValue2 = val; + ++joyAxisToSend; + if(joyAxisToSend == MAX_JOYSTICK_AXIS) { + // we're done for this frame, so update lastMS and reset joyAxisToSend + joyAxisToSend = 0; + lastMS = nowMS; + } + return res; + } + + } + return res_none; } @@ -996,6 +1544,12 @@ void Sys_ClearEvents() { kbd_polls.SetNum(0, false); mouse_polls.SetNum(0, false); + joystick_polls.SetNum(0, false); + + memset( buttonStates, 0, sizeof( buttonStates ) ); + memset( joyAxis, 0, sizeof( joyAxis ) ); + + event_overflow.SetNum(0, false); } static void handleMouseGrab() { @@ -1146,3 +1700,35 @@ Sys_EndMouseInputEvents void Sys_EndMouseInputEvents() { mouse_polls.SetNum(0, false); } + +/* +================ +Joystick Input Methods +================ +*/ +void Sys_SetRumble( int device, int low, int hi ) { + // TODO: support multiple controllers. + assert(device == 0); + // TODO: support rumble maybe. + assert(0); +} + +int Sys_PollJoystickInputEvents( int deviceNum ) { + // TODO: support multiple controllers. + assert(deviceNum == 0); + return joystick_polls.Num(); +} + +int Sys_ReturnJoystickInputEvent( const int n, int &action, int &value ) { + if (n >= joystick_polls.Num()) + return 0; + + action = joystick_polls[n].action; + value = joystick_polls[n].value; + return 1; +} + +void Sys_EndJoystickInputEvents() { + joystick_polls.SetNum(0, false); +} + diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h index d84ae8a9..b006a394 100644 --- a/neo/sys/sys_public.h +++ b/neo/sys/sys_public.h @@ -44,12 +44,12 @@ typedef enum { } cpuidSimd_t; typedef enum { - AXIS_SIDE, - AXIS_FORWARD, - AXIS_UP, - AXIS_ROLL, - AXIS_YAW, - AXIS_PITCH, + AXIS_LEFT_X, + AXIS_LEFT_Y, + AXIS_RIGHT_X, + AXIS_RIGHT_Y, + AXIS_LEFT_TRIG, + AXIS_RIGHT_TRIG, MAX_JOYSTICK_AXIS } joystickAxis_t; @@ -59,7 +59,7 @@ typedef enum { SE_CHAR, // evValue is an ascii char SE_MOUSE, // evValue and evValue2 are relative signed x / y moves SE_MOUSE_ABS, // evValue and evValue2 are absolute x / y coordinates in the window - SE_JOYSTICK_AXIS, // evValue is an axis number and evValue2 is the current state (-127 to 127) + SE_JOYSTICK, // evValue is an axis number and evValue2 is the current state (-127 to 127) SE_CONSOLE // evPtr is a char*, from typing something at a non-game console } sysEventType_t; @@ -77,10 +77,52 @@ typedef enum { M_DELTAZ } sys_mEvents; +typedef enum { + J_ACTION_FIRST, + // these names are similar to the SDL3 SDL_GamepadButton names + J_BTN_SOUTH = J_ACTION_FIRST, // bottom face button, like Xbox A + J_BTN_EAST, // right face button, like Xbox B + J_BTN_WEST, // left face button, like Xbox X + J_BTN_NORTH, // top face button, like Xbox Y + J_BTN_BACK, + J_BTN_GUIDE, // Note: this one should probably not be used? + J_BTN_START, + J_BTN_LSTICK, // press left stick + J_BTN_RSTICK, // press right stick + J_BTN_LSHOULDER, + J_BTN_RSHOULDER, + + J_DPAD_UP, + J_DPAD_DOWN, + J_DPAD_LEFT, + J_DPAD_RIGHT, + + J_BTN_MISC1, // Additional button (e.g. Xbox Series X share button, PS5 microphone button, Nintendo Switch Pro capture button, Amazon Luna microphone button) + J_BTN_RPADDLE1, // Upper or primary paddle, under your right hand (e.g. Xbox Elite paddle P1) + J_BTN_LPADDLE1, // Upper or primary paddle, under your left hand (e.g. Xbox Elite paddle P3) + J_BTN_RPADDLE2, // Lower or secondary paddle, under your right hand (e.g. Xbox Elite paddle P2) + J_BTN_LPADDLE2, // Lower or secondary paddle, under your left hand (e.g. Xbox Elite paddle P4) + + J_ACTION_MAX = J_BTN_LPADDLE2, + // leaving some space here for about 12 additional J_ACTIONs, if needed + + J_AXIS_MIN = 32, + J_AXIS_LEFT_X = J_AXIS_MIN + AXIS_LEFT_X, + J_AXIS_LEFT_Y = J_AXIS_MIN + AXIS_LEFT_Y, + J_AXIS_RIGHT_X = J_AXIS_MIN + AXIS_RIGHT_X, + J_AXIS_RIGHT_Y = J_AXIS_MIN + AXIS_RIGHT_Y, + J_AXIS_LEFT_TRIG = J_AXIS_MIN + AXIS_LEFT_TRIG, + J_AXIS_RIGHT_TRIG = J_AXIS_MIN + AXIS_RIGHT_TRIG, + + J_AXIS_MAX = J_AXIS_MIN + MAX_JOYSTICK_AXIS - 1, + + MAX_JOY_EVENT +} sys_jEvents; + struct sysEvent_t { sysEventType_t evType; - int evValue; - int evValue2; + int evValue; // for keys: K_* or ASCII code; for joystick: axis; for mouse: mouseX + int evValue2; // for keys: 0/1 for up/down; for axis: value; for mouse: mouseY int evPtrLength; // bytes of data pointed to by evPtr, for journaling void * evPtr; // this must be manually freed if not NULL }; @@ -179,6 +221,12 @@ const char* Sys_GetLocalizedScancodeName( int key ); // returns keyNum_t (K_SC_* constant) for given scancode name (like "SC_A") int Sys_GetKeynumForScancodeName( const char* name ); +// returns display name of the key (between K_FIRST_JOY and K_LAST_JOY) +// With SDL2 it'll return the name in the SDL_GameController standard layout +// (which is based on XBox/XInput => on Nintendo gamepads, A/B and X/Y will be flipped), +// with SDL3 it will return the "real" button name +const char* Sys_GetLocalizedJoyKeyName( int key ); + // keyboard input polling int Sys_PollKeyboardInputEvents( void ); int Sys_ReturnKeyboardInputEvent( const int n, int &ch, bool &state ); @@ -189,11 +237,23 @@ int Sys_PollMouseInputEvents( void ); int Sys_ReturnMouseInputEvent( const int n, int &action, int &value ); void Sys_EndMouseInputEvents( void ); +// joystick input polling +void Sys_SetRumble( int device, int low, int hi ); +int Sys_PollJoystickInputEvents( int deviceNum ); +int Sys_ReturnJoystickInputEvent( const int n, int &action, int &value ); +void Sys_EndJoystickInputEvents(); + // when the console is down, or the game is about to perform a lengthy // operation like map loading, the system can release the mouse cursor // when in windowed mode void Sys_GrabMouseCursor( bool grabIt ); +// DG: added this for an ungodly hack for gamepad support +// active = true means "currently a GUI with a cursor is active/focused" +// active = false means "that GUI is not active anymore" +class idUserInterface; +void Sys_SetInteractiveIngameGuiActive( bool active, idUserInterface* ui ); + void Sys_ShowWindow( bool show ); bool Sys_IsWindowVisible( void ); void Sys_ShowConsole( int visLevel, bool quitOnClose ); diff --git a/neo/ui/UserInterface.cpp b/neo/ui/UserInterface.cpp index 032b3c4b..bab78000 100644 --- a/neo/ui/UserInterface.cpp +++ b/neo/ui/UserInterface.cpp @@ -118,6 +118,9 @@ void idUserInterfaceManagerLocal::EndLevelLoad() { } } } + + // DG: this should probably be reset at this point + Sys_SetInteractiveIngameGuiActive( false, NULL ); } void idUserInterfaceManagerLocal::Reload( bool all ) { @@ -344,6 +347,9 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim return ret; } + // DG: used to translate gamepad input into events the UI system is familiar with + sysEvent_t fakedEvent = {}; + if ( event->evType == SE_MOUSE || event->evType == SE_MOUSE_ABS ) { if ( !desktop || (desktop->GetFlags() & WIN_MENUGUI) ) { // DG: this is a fullscreen GUI, scale the mousedelta added to cursorX/Y @@ -401,6 +407,80 @@ const char *idUserInterfaceLocal::HandleEvent( const sysEvent_t *event, int _tim cursorY = 0; } } + else if ( event->evType == SE_JOYSTICK && event->evValue2 != 0 && event->evValue < 4 ) + { + // evValue: axis = jEvent - J_AXIS_MIN; + // evValue2: percent (-100 to 100) + + // currently uses both sticks for cursor movement + // TODO could use one stick for scrolling (maybe by generating K_UPARROW/DOWNARROW events?) + float addVal = expf( fabsf(event->evValue2 * 0.03f) ) * 0.5f; + if(event->evValue2 < 0) + addVal = -addVal; + + if( event->evValue == 0 || event->evValue == 2 ) { + cursorX += addVal; + } else if( event->evValue == 1 || event->evValue == 3 ) { + cursorY += addVal; + } + + if (cursorX < 0) { + cursorX = 0; + } + if (cursorY < 0) { + cursorY = 0; + } + + // some things like highlighting hovered UI elements need a mouse event, + // so create a fake mouse event + fakedEvent.evType = SE_MOUSE; + // the coordinates (evValue/evValue2) aren't used, but keeping them at 0 + // (as default-initialized above) shouldn't hurt either way + event = &fakedEvent; + } + else if ( event->evType == SE_KEY && event->evValue >= K_FIRST_JOY && event->evValue <= K_LAST_JOY ) + { + // map some gamepad buttons to SE_KEY events that the UI already knows how to use + int key = 0; + if( idKeyInput::GetUsercmdAction( event->evValue ) == 20 /* UB_ATTACK*/ ) { + // if this button is bound to _attack (fire), treat it as left mouse button + key = K_MOUSE1; + } else { + switch(event->evValue) { + // emulate mouse buttons + case K_JOY_BTN_SOUTH: // A on xbox controller + key = K_MOUSE1; + break; + case K_JOY_BTN_EAST: // B on xbox controller + key = K_MOUSE2; + break; + // emulate cursor keys (sometimes used for scrolling or selecting in a list) + case K_JOY_DPAD_UP: + key = K_UPARROW; + break; + case K_JOY_DPAD_DOWN: + key = K_DOWNARROW; + break; + case K_JOY_DPAD_LEFT: + key = K_LEFTARROW; + break; + case K_JOY_DPAD_RIGHT: + key = K_RIGHTARROW; + break; + // enter is useful after selecting something with cursor keys (or dpad) + // in a list, like selecting a savegame - I guess left trigger is suitable for that? + // (right trigger is often used for shooting, which we use as K_MOUSE1 here) + case K_JOY_TRIGGER1: + key = K_ENTER; + break; + } + } + if (key != 0) { + fakedEvent = *event; + fakedEvent.evValue = key; + event = &fakedEvent; + } + } if ( desktop ) { return desktop->HandleEvent( event, updateVisuals ); @@ -507,6 +587,10 @@ const char *idUserInterfaceLocal::Activate(bool activate, int _time) { time = _time; active = activate; if ( desktop ) { + // DG: added this hack for gamepad input + if ( interactive ) { + Sys_SetInteractiveIngameGuiActive( activate, this ); + } // DG end activateStr = ""; desktop->Activate( activate, activateStr ); return activateStr;