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;