Refactor wxSDLJoy

This is a major refactor of the wxSDLJoy class.
* Move handling of SDL objects creation and destruction to its own
  class. This simplifies the lifespan of SDL-related objects.
* Re-add handling of HATs. This is necessary for DirectInput
  controllers.
* Rename the public API for wxSDLJoy to be clearer.
* Add documentation for every class related to wxSDLJoy.
This commit is contained in:
Fabrice de Gans-Riberi 2020-09-05 21:56:38 -07:00 committed by Rafael Kitover
parent a00f258588
commit edc34942a8
8 changed files with 585 additions and 582 deletions

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-05 19:36+0000\n"
"POT-Creation-Date: 2020-09-05 21:42-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -1156,17 +1156,7 @@ msgstr ""
msgid "CONTROL"
msgstr ""
#: widgets/sdljoy.cpp:122
#, c-format
msgid "Connected game controller %d: %s"
msgstr ""
#: widgets/sdljoy.cpp:137
#, c-format
msgid "Disconnected game controller %d"
msgstr ""
#: widgets/sdljoy.cpp:214
#: widgets/sdljoy.cpp:107
#, c-format
msgid "Connected joystick %d: %s"
msgstr ""

View File

@ -2736,7 +2736,7 @@ EVT_HANDLER(EmulatorDirectories, "Directories...")
EVT_HANDLER(JoypadConfigure, "Joypad options...")
{
wxDialog* dlg = GetXRCDialog("JoypadConfig");
joy.Add();
joy.PollAllJoysticks();
auto frame = wxGetApp().frame;
bool joy_timer = frame->IsJoyPollTimerRunning();
@ -2754,7 +2754,7 @@ EVT_HANDLER(JoypadConfigure, "Joypad options...")
EVT_HANDLER(Customize, "Customize UI...")
{
wxDialog* dlg = GetXRCDialog("AccelConfig");
joy.Add();
joy.PollAllJoysticks();
auto frame = wxGetApp().frame;
bool joy_timer = frame->IsJoyPollTimerRunning();

View File

@ -2582,12 +2582,14 @@ void MainFrame::set_global_accels()
// the menus will be added now
// first, zero out menu item on all accels
std::unordered_set<unsigned> needed_joysticks;
for (size_t i = 0; i < accels.size(); ++i) {
accels[i].Set(accels[i].GetUkey(), accels[i].GetJoystick(), accels[i].GetFlags(), accels[i].GetKeyCode(), accels[i].GetCommand());
if (accels[i].GetJoystick()) {
joy.Add(accels[i].GetJoystick() - 1);
needed_joysticks.insert(accels[i].GetJoystick());
}
}
joy.PollJoysticks(needed_joysticks);
// yet another O(n*m) loop. I really ought to sort the accel arrays
for (int i = 0; i < ncmds; i++) {

View File

@ -1391,25 +1391,25 @@ void GameArea::OnSize(wxSizeEvent& ev)
void GameArea::OnSDLJoy(wxSDLJoyEvent& ev)
{
int key = ev.GetControlIndex();
int key = ev.control_index();
int mod = wxJoyKeyTextCtrl::DigitalButton(ev);
int joy = ev.GetJoy() + 1;
int joy = ev.player_index();
// mutually exclusive key types unpress their opposite
if (mod == WXJB_AXIS_PLUS) {
process_key_press(false, key, WXJB_AXIS_MINUS, joy);
process_key_press(ev.GetControlValue() != 0, key, mod, joy);
process_key_press(ev.control_value() != 0, key, mod, joy);
} else if (mod == WXJB_AXIS_MINUS) {
process_key_press(false, key, WXJB_AXIS_PLUS, joy);
process_key_press(ev.GetControlValue() != 0, key, mod, joy);
process_key_press(ev.control_value() != 0, key, mod, joy);
} else if (mod >= WXJB_HAT_FIRST && mod <= WXJB_HAT_LAST) {
int value = ev.GetControlValue();
int value = ev.control_value();
process_key_press(value & SDL_HAT_UP, key, WXJB_HAT_N, joy);
process_key_press(value & SDL_HAT_DOWN, key, WXJB_HAT_S, joy);
process_key_press(value & SDL_HAT_RIGHT, key, WXJB_HAT_E, joy);
process_key_press(value & SDL_HAT_LEFT, key, WXJB_HAT_W, joy);
} else
process_key_press(ev.GetControlValue() != 0, key, mod, joy);
process_key_press(ev.control_value() != 0, key, mod, joy);
// tell Linux to turn off the screensaver/screen-blank if joystick button was pressed
// this shouldn't be necessary of course

View File

@ -19,8 +19,8 @@ wxJoyKeyBinding newWxJoyKeyBinding(int key, int mod, int joy)
int wxJoyKeyTextCtrl::DigitalButton(wxSDLJoyEvent& event)
{
int sdlval = event.GetControlValue();
int sdltype = event.GetControlType();
int16_t sdlval = event.control_value();
wxSDLControl sdltype = event.control();
switch (sdltype) {
case WXSDLJOY_AXIS:
@ -72,8 +72,8 @@ void wxJoyKeyTextCtrl::OnJoy(wxSDLJoyEvent& event)
{
static wxLongLong last_event = 0;
int val = event.GetControlValue();
int type = event.GetControlType();
int16_t val = event.control_value();
wxSDLControl type = event.control();
// Filter consecutive axis motions within 300ms, as this adds two bindings
// +1/-1 instead of the one intended.
@ -83,7 +83,8 @@ void wxJoyKeyTextCtrl::OnJoy(wxSDLJoyEvent& event)
last_event = wxGetUTCTimeMillis();
int mod = DigitalButton(event);
int key = event.GetControlIndex(), joy = event.GetJoy() + 1;
uint8_t key = event.control_index();
unsigned joy = event.player_index();
if (!val || mod < 0)
return;

View File

@ -1,35 +1,36 @@
#include <cstddef>
#include "wxvbam.h"
#include "wx/sdljoy.h"
#include "SDL.h"
#include <SDL_events.h>
#include "../common/range.hpp"
#include <algorithm>
#include <wx/timer.h>
#include <SDL.h>
#include "../common/contains.h"
#include "../wxvbam.h"
using namespace Range;
namespace {
// For testing a GameController as a Joystick:
//#define SDL_IsGameController(x) false
DEFINE_EVENT_TYPE(wxEVT_SDLJOY)
wxSDLJoy::wxSDLJoy()
: wxTimer()
{
// Start up joystick if not already started
// FIXME: check for errors
SDL_Init(SDL_INIT_JOYSTICK|SDL_INIT_GAMECONTROLLER);
SDL_GameControllerEventState(SDL_ENABLE);
SDL_JoystickEventState(SDL_ENABLE);
const std::string SDLEventTypeToDebugString(int32_t event_type) {
switch (event_type) {
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
return "SDL_CONTROLLERBUTTON";
case SDL_CONTROLLERAXISMOTION:
return "SDL_CONTROLLERAXISMOTION";
case SDL_JOYBUTTONDOWN:
case SDL_JOYBUTTONUP:
return "SDL_JOYBUTTON";
case SDL_JOYAXISMOTION:
return "SDL_JOYAXISMOTION";
case SDL_JOYHATMOTION:
return "SDL_JOYHATMOTION";
default:
assert(false);
return "UNKNOWN SDL EVENT";
}
}
wxSDLJoy::~wxSDLJoy()
{
// SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
}
static int16_t axisval(int16_t x)
{
// Converts an axis value to a direction since we do not need analog controls.
static int16_t AxisValueToDirection(int16_t x) {
if (x > 0x1fff)
return 1;
else if (x < -0x1fff)
@ -38,27 +39,288 @@ static int16_t axisval(int16_t x)
return 0;
}
void wxSDLJoy::CreateAndSendEvent(unsigned short joy, unsigned short ctrl_type, unsigned short ctrl_idx, short ctrl_val, short prev_val)
{
auto handler = wxGetApp().frame->GetJoyEventHandler();
// The interval between 2 polls in ms.
const wxLongLong kPollTimeInterval(25);
} // namespace
// For testing a GameController as a Joystick:
//#define SDL_IsGameController(x) false
DEFINE_EVENT_TYPE(wxEVT_SDLJOY)
wxSDLJoyEvent::wxSDLJoyEvent(
unsigned player_index,
wxSDLControl control,
uint8_t control_index,
int16_t control_value) :
wxCommandEvent(wxEVT_SDLJOY),
player_index_(player_index),
control_(control),
control_index_(control_index),
control_value_(control_value) {}
// Represents the current state of a joystick. This class takes care of
// initializing and destroying SDL resources on construction and destruction so
// every associated SDL state for a joystick dies with this object.
class wxSDLJoyState : public wxTimer {
public:
explicit wxSDLJoyState(int sdl_index);
~wxSDLJoyState() override;
// Disable copy constructor and assignment. This is to prevent double
// closure of the SDL objects.
wxSDLJoyState(const wxSDLJoyState&) = delete;
wxSDLJoyState& operator=(const wxSDLJoyState&) = delete;
// Returns true if this object was properly initialized.
bool IsValid() const;
// Processes an SDL event.
void ProcessEvent(int32_t event_type,
uint8_t control_index,
int16_t control_value);
// Polls the current state of the joystick and sends events as needed.
void Poll();
// Activates or deactivates rumble.
void SetRumble(bool activate_rumble);
SDL_JoystickID joystick_id() const { return joystick_id_; }
private:
// wxTimer implementation.
// Used to rumble on a timer.
void Notify() override;
// The Joystick player index.
unsigned player_index_;
// SDL Joystick ID used for events.
SDL_JoystickID joystick_id_;
// The SDL GameController instance.
SDL_GameController* game_controller_ = nullptr;
// The SDL Joystick instance.
SDL_Joystick* joystick_ = nullptr;
// Current state of Joystick axis.
std::unordered_map<uint8_t, int16_t> axis_{};
// Current state of Joystick buttons.
std::unordered_map<uint8_t, uint8_t> buttons_{};
// Current state of Joystick HAT. Unused for GameControllers.
std::unordered_map<uint8_t, uint8_t> hats_{};
// Set to true to activate joystick rumble.
bool rumbling_ = false;
};
wxSDLJoyState::wxSDLJoyState(int sdl_index)
: player_index_(sdl_index + 1) {
if (SDL_IsGameController(sdl_index)) {
game_controller_ = SDL_GameControllerOpen(sdl_index);
if (game_controller_)
joystick_ = SDL_GameControllerGetJoystick(game_controller_);
} else {
joystick_ = SDL_JoystickOpen(sdl_index);
}
if (!joystick_)
return;
joystick_id_ = SDL_JoystickInstanceID(joystick_);
systemScreenMessage(
wxString::Format(_("Connected joystick %d: %s"),
player_index_, SDL_JoystickNameForIndex(sdl_index)));
}
wxSDLJoyState::~wxSDLJoyState() {
// Nothing to do if this object is not initialized.
if (!joystick_)
return;
if (game_controller_)
SDL_GameControllerClose(game_controller_);
else
SDL_JoystickClose(joystick_);
systemScreenMessage(
wxString::Format(_("Disconnected joystick %d"), player_index_));
}
bool wxSDLJoyState::IsValid() const {
return joystick_;
}
void wxSDLJoyState::ProcessEvent(int32_t event_type,
uint8_t control_index,
int16_t control_value) {
int16_t previous_value = 0;
wxSDLControl control;
bool value_changed = false;
switch (event_type) {
case SDL_JOYBUTTONDOWN:
case SDL_JOYBUTTONUP:
// Do not process joystick events for game controllers.
if (game_controller_) {
return;
}
// Fallhrough.
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
control = wxSDLControl::WXSDLJOY_BUTTON;
previous_value = buttons_[control_index];
if (previous_value != control_value) {
buttons_[control_index] = control_value;
value_changed = true;
}
break;
case SDL_JOYHATMOTION:
// Do not process joystick events for game controllers.
if (game_controller_) {
return;
}
control = wxSDLControl::WXSDLJOY_HAT;
previous_value = hats_[control_index];
if (previous_value != control_value) {
hats_[control_index] = control_value;
value_changed = true;
}
break;
case SDL_JOYAXISMOTION:
// Do not process joystick events for game controllers.
if (game_controller_) {
return;
}
// Fallhrough.
case SDL_CONTROLLERAXISMOTION:
control = wxSDLControl::WXSDLJOY_AXIS;
previous_value = axis_[control_index];
if (previous_value != control_value) {
axis_[control_index] = control_value;
value_changed = true;
}
break;
default:
// This should never happen.
assert(false);
return;
}
if (value_changed) {
wxLogDebug("GOT %s: joy:%d ctrl_idx:%d val:%d prev_val:%d",
SDLEventTypeToDebugString(event_type), player_index_,
control_index, control_value, previous_value);
auto handler = wxGetApp().frame->GetJoyEventHandler();
if (!handler)
return;
wxSDLJoyEvent *ev = new wxSDLJoyEvent(wxEVT_SDLJOY);
ev->joy = joy;
ev->ctrl_type = ctrl_type;
ev->ctrl_idx = ctrl_idx;
ev->ctrl_val = ctrl_val;
ev->prev_val = prev_val;
wxQueueEvent(handler, ev);
wxQueueEvent(handler,
new wxSDLJoyEvent(
player_index_, control, control_index, control_value));
}
}
void wxSDLJoy::Poll()
{
SDL_Event e;
void wxSDLJoyState::Poll() {
if (game_controller_) {
for (uint8_t but = 0; but < SDL_CONTROLLER_BUTTON_MAX; but++) {
uint16_t previous_value = buttons_[but];
uint16_t current_value =
SDL_GameControllerGetButton(
game_controller_,
static_cast<SDL_GameControllerButton>(but));
if (previous_value != current_value)
ProcessEvent(SDL_CONTROLLERBUTTONUP, but, current_value);
}
for (uint8_t axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) {
uint16_t previous_value = axis_[axis];
uint16_t current_value =
AxisValueToDirection(
SDL_GameControllerGetAxis(
game_controller_,
static_cast<SDL_GameControllerAxis>(axis)));
if (previous_value != current_value)
ProcessEvent(SDL_CONTROLLERAXISMOTION, axis, current_value);
}
} else {
for (uint8_t but = 0; but < SDL_JoystickNumButtons(joystick_); but++) {
uint16_t previous_value = buttons_[but];
uint16_t current_value = SDL_JoystickGetButton(joystick_, but);
if (previous_value != current_value)
ProcessEvent(SDL_JOYBUTTONUP, but, current_value);
}
for (uint8_t axis = 0; axis < SDL_JoystickNumAxes(joystick_); axis++) {
uint16_t previous_value = axis_[axis];
uint16_t current_value =
AxisValueToDirection(SDL_JoystickGetButton(joystick_, axis));
if (previous_value != current_value)
ProcessEvent(SDL_JOYAXISMOTION, axis, current_value);
}
for (uint8_t hat = 0; hat < SDL_JoystickNumHats(joystick_); hat++) {
uint16_t previous_value = hats_[hat];
uint16_t current_value = SDL_JoystickGetHat(joystick_, hat);
if (previous_value != current_value)
ProcessEvent(SDL_JOYHATMOTION, hat, current_value);
}
}
}
void wxSDLJoyState::SetRumble(bool activate_rumble) {
rumbling_ = activate_rumble;
#if SDL_VERSION_ATLEAST(2, 0, 9)
if (!game_controller_)
return;
if (rumbling_) {
SDL_GameControllerRumble(game_controller_, 0xFFFF, 0xFFFF, 300);
if (!IsRunning())
Start(150);
} else {
SDL_GameControllerRumble(game_controller_, 0, 0, 0);
Stop();
}
#endif
}
void wxSDLJoyState::Notify() {
SetRumble(rumbling_);
}
wxSDLJoy::wxSDLJoy() {
// Start up joystick if not already started
// FIXME: check for errors
SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
SDL_GameControllerEventState(SDL_ENABLE);
SDL_JoystickEventState(SDL_ENABLE);
}
wxSDLJoy::~wxSDLJoy() {
// It is necessary to free all SDL resources before quitting SDL.
joystick_states_.clear();
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
}
void wxSDLJoy::Poll() {
SDL_Event e;
bool got_event = false;
while (SDL_PollEvent(&e)) {
@ -66,392 +328,171 @@ void wxSDLJoy::Poll()
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
{
auto joy = e.cbutton.which;
if (contains(instance_map, joy)) {
auto& state = *(instance_map[joy]);
auto but = e.cbutton.button;
auto val = e.cbutton.state;
auto prev_val = state.button[but];
if (val != prev_val) {
CreateAndSendEvent(state.index, WXSDLJOY_BUTTON, but, val, prev_val);
state.button[but] = val;
wxLogDebug("GOT SDL_CONTROLLERBUTTON: joy:%d but:%d val:%d prev_val:%d", state.index, but, val, prev_val);
wxSDLJoyState* joy_state = FindJoyState(e.cbutton.which);
if (joy_state) {
joy_state->ProcessEvent(
e.type, e.cbutton.button, e.cbutton.state);
}
}
got_event = true;
break;
}
case SDL_CONTROLLERAXISMOTION:
{
auto joy = e.caxis.which;
if (contains(instance_map, joy)) {
auto& state = *(instance_map[joy]);
auto axis = e.caxis.axis;
auto val = axisval(e.caxis.value);
auto prev_val = state.axis[axis];
if (val != prev_val) {
CreateAndSendEvent(state.index, WXSDLJOY_AXIS, axis, val, prev_val);
state.axis[axis] = val;
wxLogDebug("GOT SDL_CONTROLLERAXISMOTION: joy:%d axis:%d val:%d prev_val:%d", state.index, axis, val, prev_val);
wxSDLJoyState* joy_state = FindJoyState(e.caxis.which);
if (joy_state) {
joy_state->ProcessEvent(
e.type, e.caxis.axis, AxisValueToDirection(e.caxis.value));
}
}
got_event = true;
break;
}
case SDL_CONTROLLERDEVICEADDED:
{
auto joy = e.cdevice.which;
if (add_all || contains(joystate, joy)) {
RemapControllers();
auto& state = joystate[joy];
if (state.dev)
systemScreenMessage(wxString::Format(_("Connected game controller %d: %s"), joy, SDL_GameControllerName(state.dev)));
}
got_event = true;
break;
}
case SDL_CONTROLLERDEVICEREMOVED:
{
auto joy = e.cdevice.which;
if (contains(instance_map, joy)) {
auto index = instance_map[joy]->index;
RemapControllers();
systemScreenMessage(wxString::Format(_("Disconnected game controller %d"), index));
}
got_event = true;
// Do nothing. This will be handled with JOYDEVICEADDED and
// JOYDEVICEREMOVED events.
break;
}
// Joystck events for non-GameControllers.
// Joystick events for non-GameControllers.
case SDL_JOYBUTTONDOWN:
case SDL_JOYBUTTONUP:
{
auto joy = e.jbutton.which;
if (contains(instance_map, joy)) {
auto& state = *(instance_map[joy]);
if (SDL_IsGameController(state.index))
break;
auto but = e.jbutton.button;
auto val = e.jbutton.state;
auto prev_val = state.button[but];
if (val != prev_val) {
CreateAndSendEvent(state.index, WXSDLJOY_BUTTON, but, val, prev_val);
state.button[but] = val;
wxLogDebug("GOT SDL_JOYBUTTON: joy:%d but:%d val:%d prev_val:%d", state.index, but, val, prev_val);
wxSDLJoyState* joy_state = FindJoyState(e.jbutton.which);
if (joy_state) {
joy_state->ProcessEvent(
e.type, e.jbutton.button, e.jbutton.state);
}
}
got_event = true;
break;
}
case SDL_JOYAXISMOTION:
{
auto joy = e.jaxis.which;
if (contains(instance_map, joy)) {
auto& state = *(instance_map[joy]);
if (SDL_IsGameController(state.index))
break;
auto axis = e.jaxis.axis;
auto val = axisval(e.jaxis.value);
auto prev_val = state.axis[axis];
if (val != prev_val) {
CreateAndSendEvent(state.index, WXSDLJOY_AXIS, axis, val, prev_val);
state.axis[axis] = val;
wxLogDebug("GOT SDL_JOYAXISMOTION: joy:%d axis:%d val:%d prev_val:%d", state.index, axis, val, prev_val);
wxSDLJoyState* joy_state = FindJoyState(e.jaxis.which);
if (joy_state) {
joy_state->ProcessEvent(
e.type, e.jaxis.axis, AxisValueToDirection(e.jaxis.value));
}
}
got_event = true;
break;
}
case SDL_JOYHATMOTION:
{
wxSDLJoyState* joy_state = FindJoyState(e.jhat.which);
if (joy_state) {
joy_state->ProcessEvent(
e.type, e.jhat.hat, AxisValueToDirection(e.jhat.value));
}
got_event = true;
break;
}
case SDL_JOYDEVICEADDED:
{
auto joy = e.jdevice.which;
if (SDL_IsGameController(joy))
break;
if (add_all || contains(joystate, joy)) {
// Always remap all controllers.
RemapControllers();
auto& state = joystate[joy];
if (state.dev)
systemScreenMessage(wxString::Format(_("Connected joystick %d: %s"), joy, SDL_JoystickName(state.dev)));
}
got_event = true;
break;
}
case SDL_JOYDEVICEREMOVED:
{
auto joy = e.jdevice.which;
if (contains(instance_map, joy)) {
auto index = instance_map[joy]->index;
RemapControllers();
systemScreenMessage(wxString::Format(_("Disconnected joystick %d"), index));
joystick_states_.erase(e.jdevice.which);
got_event = true;
break;
}
got_event = true;
default:
// Ignore all other events.
break;
}
}
wxLongLong now = wxGetUTCTimeMillis();
if (got_event) {
last_poll_ = now;
} else if (now - last_poll_ > kPollTimeInterval) {
for (auto&& joy_state : joystick_states_) {
joy_state.second->Poll();
}
bool do_poll = false;
wxLongLong tm = wxGetUTCTimeMillis();
if (got_event)
last_poll = tm;
else if (tm - last_poll > POLL_TIME_MS) {
do_poll = true;
last_poll = tm;
}
if (do_poll) {
for (auto&& joy : joystate) {
if (!joy.second.dev) continue;
if (SDL_IsGameController(joy.first)) {
for (uint8_t but = 0; but < SDL_CONTROLLER_BUTTON_MAX; but++) {
auto last_state = joy.second.button[but];
auto state = SDL_GameControllerGetButton(joy.second.dev, static_cast<SDL_GameControllerButton>(but));
if (last_state != state) {
CreateAndSendEvent(joy.first, WXSDLJOY_BUTTON, but, state, last_state);
joy.second.button[but] = state;
wxLogDebug("POLLED SDL_CONTROLLERBUTTON: joy:%d but:%d val:%d prev_val:%d", joy.first, but, state, last_state);
last_poll_ = now;
}
}
for (uint8_t axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) {
auto val = axisval(SDL_GameControllerGetAxis(joy.second.dev, static_cast<SDL_GameControllerAxis>(axis)));
auto prev_val = joy.second.axis[axis];
if (val != prev_val) {
CreateAndSendEvent(joy.first, WXSDLJOY_AXIS, axis, val, prev_val);
joy.second.axis[axis] = val;
wxLogDebug("POLLED SDL_CONTROLLERAXISMOTION: joy:%d axis:%d val:%d prev_val:%d", joy.first, axis, val, prev_val);
}
}
}
else {
for (uint8_t but = 0; but < SDL_JoystickNumButtons(joy.second.dev); but++) {
auto last_state = joy.second.button[but];
auto state = SDL_JoystickGetButton(joy.second.dev, but);
if (last_state != state) {
CreateAndSendEvent(joy.first, WXSDLJOY_BUTTON, but, state, last_state);
joy.second.button[but] = state;
wxLogDebug("POLLED SDL_JOYBUTTON: joy:%d but:%d val:%d prev_val:%d", joy.first, but, state, last_state);
}
}
for (uint8_t axis = 0; axis < SDL_JoystickNumAxes(joy.second.dev); axis++) {
auto val = axisval(SDL_JoystickGetAxis(joy.second.dev, axis));
auto prev_val = joy.second.axis[axis];
if (val != prev_val) {
CreateAndSendEvent(joy.first, WXSDLJOY_AXIS, axis, val, prev_val);
joy.second.axis[axis] = val;
wxLogDebug("POLLED SDL_JOYAXISMOTION: joy:%d axis:%d val:%d prev_val:%d", joy.first, axis, val, prev_val);
}
}
}
}
}
}
void wxSDLJoy::ConnectController(uint8_t joy)
{
SDL_Joystick* js_dev = nullptr;
bool is_gc;
if ((is_gc = SDL_IsGameController(joy))) {
auto dev = SDL_GameControllerOpen(joy);
if (dev) {
joystate[joy].dev = dev;
js_dev = SDL_GameControllerGetJoystick(dev);
}
}
else {
if ((js_dev = SDL_JoystickOpen(joy)))
joystate[joy].dev = js_dev;
}
if (js_dev) {
auto instance = SDL_JoystickInstanceID(js_dev);
instance_map[instance] = &(joystate[joy]);
joystate[joy].instance = instance;
}
joystate[joy].index = joy;
joystate[joy].is_gc = is_gc;
}
void wxSDLJoy::RemapControllers()
{
for (auto&& joy : joystate) {
auto& state = joy.second;
DisconnectController(state);
ConnectController(joy.first);
}
}
void wxSDLJoy::DisconnectController(wxSDLJoyState& state)
{
if (auto& dev = state.dev) {
if (state.is_gc) {
SDL_GameControllerClose(dev);
}
else {
SDL_JoystickClose(dev);
}
dev = nullptr;
}
instance_map.erase(state.instance);
}
void wxSDLJoy::Add(int8_t joy_n)
{
if (joy_n < 0) {
for (uint8_t joy : range(0, SDL_NumJoysticks()))
ConnectController(joy);
add_all = true;
void wxSDLJoy::RemapControllers() {
if (!is_polling_active_) {
// Nothing to do when we're not actively polling.
return;
}
ConnectController(joy_n);
joystick_states_.clear();
if (requested_sdl_indexes_.empty()) {
// Connect all joysticks.
for (int i = 0; i < SDL_NumJoysticks(); i++) {
std::unique_ptr<wxSDLJoyState> joy_state(new wxSDLJoyState(i));
if (joy_state->IsValid()) {
joystick_states_.emplace(
joy_state->joystick_id(), std::move(joy_state));
}
}
} else {
// Only attempt to add the joysticks we care about.
for (const int& sdl_index : requested_sdl_indexes_) {
std::unique_ptr<wxSDLJoyState> joy_state(
new wxSDLJoyState(sdl_index));
if (joy_state->IsValid()) {
joystick_states_.emplace(
joy_state->joystick_id(), std::move(joy_state));
}
}
}
}
void wxSDLJoy::Remove(int8_t joy_n)
{
add_all = false;
wxSDLJoyState* wxSDLJoy::FindJoyState(const SDL_JoystickID& joy_id) {
const auto iter = joystick_states_.find(joy_id);
if (iter == joystick_states_.end())
return nullptr;
return iter->second.get();
}
if (joy_n < 0) {
for (auto&& joy : joystate)
DisconnectController(joy.second);
joystate.clear();
void wxSDLJoy::PollJoysticks(std::unordered_set<unsigned> indexes) {
// Reset the polling state.
StopPolling();
if (indexes.empty()) {
// Nothing to poll. Return early.
return;
}
DisconnectController(joystate[joy_n]);
joystate.erase(joy_n);
is_polling_active_ = true;
std::for_each(
indexes.begin(), indexes.end(),
[&](const unsigned& player_index) {
requested_sdl_indexes_.insert(player_index - 1);
});
RemapControllers();
}
void wxSDLJoy::SetRumble(bool do_rumble)
{
rumbling = do_rumble;
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Do rumble only on device 0, and only if it's a GameController.
auto dev = joystate[0].dev;
if (dev && SDL_IsGameController(0)) {
if (rumbling) {
SDL_GameControllerRumble(dev, 0xFFFF, 0xFFFF, 300);
if (!IsRunning())
Start(150);
}
else {
SDL_GameControllerRumble(dev, 0, 0, 0);
Stop();
}
}
#endif
void wxSDLJoy::PollAllJoysticks() {
// Reset the polling state.
StopPolling();
is_polling_active_ = true;
RemapControllers();
}
void wxSDLJoy::Notify()
{
SetRumble(rumbling);
void wxSDLJoy::StopPolling() {
joystick_states_.clear();
requested_sdl_indexes_.clear();
is_polling_active_ = false;
}
wxSDLJoyDev::operator SDL_GameController*&()
{
return dev_gc;
}
void wxSDLJoy::SetRumble(bool activate_rumble) {
if (joystick_states_.empty())
return;
SDL_GameController*& wxSDLJoyDev::operator=(SDL_GameController* ptr)
{
dev_gc = ptr;
return dev_gc;
}
wxSDLJoyDev::operator SDL_Joystick*&()
{
return dev_js;
}
SDL_Joystick*& wxSDLJoyDev::operator=(SDL_Joystick* ptr)
{
dev_js = ptr;
return dev_js;
}
wxSDLJoyDev::operator bool()
{
return dev_gc != nullptr;
}
std::nullptr_t& wxSDLJoyDev::operator=(std::nullptr_t&& null_ptr)
{
dev_gc = null_ptr;
return null_ptr;
// Do rumble only on the first device.
joystick_states_.begin()->second->SetRumble(activate_rumble);
}

View File

@ -1,138 +1,103 @@
#ifndef JOYEVT_H
#define JOYEVT_H
// This is my own SDL-based joystick handler, since wxJoystick is brain-dead.
// It's geared towards keyboard emulation
// To use, create a wxSDLJoy object Add() the joysticks you want to monitor.
//
// The target window will receive EVT_SDLJOY events of type wxSDLJoyEvent.
#include <cstddef>
#include <array>
#include <vector>
#include <memory>
#include <unordered_set>
#include <unordered_map>
#include <wx/time.h>
#include <wx/event.h>
#include <wx/timer.h>
#include <SDL_joystick.h>
#include <SDL_gamecontroller.h>
#include "../common/contains.h"
#include <SDL_events.h>
struct wxSDLJoyDev {
private:
union {
SDL_GameController* dev_gc = nullptr;
SDL_Joystick* dev_js;
// The different types of supported controls.
enum wxSDLControl {
WXSDLJOY_AXIS, // Control value is signed 16
WXSDLJOY_HAT, // Control value is bitmask NESW/URDL
WXSDLJOY_BUTTON // Control value is 0 or 1
};
// Represents a Joystick event.
class wxSDLJoyEvent : public wxCommandEvent {
public:
operator SDL_GameController*&();
SDL_GameController*& operator=(SDL_GameController* ptr);
wxSDLJoyEvent(
unsigned player_index,
wxSDLControl control,
uint8_t control_index,
int16_t control_value);
virtual ~wxSDLJoyEvent() = default;
operator SDL_Joystick*&();
SDL_Joystick*& operator=(SDL_Joystick* ptr);
unsigned player_index() const { return player_index_; }
wxSDLControl control() const { return control_; }
uint8_t control_index() const { return control_index_; }
int16_t control_value() const { return control_value_; }
operator bool();
std::nullptr_t& operator=(std::nullptr_t&& null_ptr);
private:
unsigned player_index_;
wxSDLControl control_;
uint8_t control_index_;
int16_t control_value_;
};
struct wxSDLJoyState {
wxSDLJoyDev dev;
uint8_t index = 0;
bool is_gc = true;
SDL_JoystickID instance = 0;
std::unordered_map<uint8_t, int16_t> axis{};
std::unordered_map<uint8_t, uint8_t> button{};
};
class wxSDLJoyState;
class wxSDLJoy : public wxTimer {
// This is my own SDL-based joystick handler, since wxJoystick is brain-dead.
// It's geared towards keyboard emulation.
//
// After initilization, use PollJoystick() or PollAllJoysticks() for the
// joysticks you wish to monitor. The target window will then receive
// EVT_SDLJOY events of type wxSDLJoyEvent.
// Handling of the player_index() value is different depending on the polling
// mode. After calls to PollJoysticks(), that value will remain constant for a
// given device, even if other joysticks disconnect. This ensures the joystick
// remains active during gameplay even if other joysticks disconnect.
// However, after calls to PollAllJoysticks(), all joysticks are re-connected
// on joystick connect/disconnect. This ensures the right player_index() value
// is sent to the UI during input event configuration.
class wxSDLJoy {
public:
wxSDLJoy();
// add another joystick to the list of polled sticks
// -1 == add all
// If joy > # of joysticks, it is ignored
// This will start polling if a valid joystick is selected
void Add(int8_t joy = -1);
// remove a joystick from the polled sticks
// -1 == remove all
// If joy > # of joysticks, it is ignored
// This will stop polling if all joysticks are disabled
void Remove(int8_t joy = -1);
// query if a stick is being polled
bool IsPolling(uint8_t joy) { return contains(joystate, joy); }
~wxSDLJoy();
// true = currently rumbling, false = turn off rumbling
void SetRumble(bool do_rumble);
// Adds a set of joysticks to the list of polled joysticks.
// This will disconnect every active joysticks, and reactivates the ones
// matching an index in |indexes|. Missing joysticks will be connected if
// they connect later on.
void PollJoysticks(std::unordered_set<unsigned> indexes);
// Adds all joysticks to the list of polled joysticks. This will
// disconnect every active joysticks, reconnect them and start polling.
void PollAllJoysticks();
// Removes all joysticks from the list of polled joysticks.
// This will stop polling.
void StopPolling();
// Activates or deactivates rumble on active joysticks.
void SetRumble(bool activate_rumble);
// Polls active joysticks and empties the SDL event buffer.
void Poll();
virtual ~wxSDLJoy();
protected:
// used to continue rumbling on a timer
void Notify();
void ConnectController(uint8_t joy);
void RemapControllers();
void DisconnectController(wxSDLJoyState& dev);
void CreateAndSendEvent(unsigned short joy, unsigned short ctrl_type, unsigned short ctrl_idx, short ctrl_val, short prev_val);
const uint8_t POLL_TIME_MS = 25;
private:
std::unordered_map<uint8_t, wxSDLJoyState> joystate;
std::unordered_map<SDL_JoystickID, wxSDLJoyState*> instance_map;
bool add_all = false, rumbling = false;
// Reconnects all controllers.
void RemapControllers();
wxLongLong last_poll = wxGetUTCTimeMillis();
};
// Helper method to find a joystick state from a joystick ID.
// Returns nullptr if not present.
wxSDLJoyState* FindJoyState(const SDL_JoystickID& joy_id);
enum {
// The types of supported controls
// values are signed-16 for axis, 0/1 for button
// hat is bitmask NESW/URDL
WXSDLJOY_AXIS,
WXSDLJOY_HAT,
WXSDLJOY_BUTTON
};
// Map of SDL joystick ID to joystick state. Only contains active joysticks.
std::unordered_map<SDL_JoystickID, std::unique_ptr<wxSDLJoyState>> joystick_states_;
class wxSDLJoyEvent : public wxCommandEvent {
friend class wxSDLJoy;
// Set of requested SDL joystick indexes.
std::unordered_set<int> requested_sdl_indexes_;
public:
// Default constructor
wxSDLJoyEvent(wxEventType commandType = wxEVT_NULL)
: wxCommandEvent(commandType)
{
}
// accessors
unsigned short GetJoy()
{
return joy;
}
unsigned short GetControlType()
{
return ctrl_type;
}
unsigned short GetControlIndex()
{
return ctrl_idx;
}
short GetControlValue()
{
return ctrl_val;
}
short GetControlPrevValue()
{
return prev_val;
}
// Set to true when we are actively polling controllers.
bool is_polling_active_ = false;
protected:
unsigned short joy;
unsigned short ctrl_type;
unsigned short ctrl_idx;
short ctrl_val;
short prev_val;
// Timestamp when the latest poll was done.
wxLongLong last_poll_ = wxGetUTCTimeMillis();
};
// Note: this means sdljoy can't be part of a library w/o extra work

View File

@ -881,13 +881,14 @@ int MainFrame::FilterEvent(wxEvent& event)
else if (event.GetEventType() == wxEVT_SDLJOY && !menus_opened && !dialog_opened)
{
wxSDLJoyEvent& je = (wxSDLJoyEvent&)event;
if (je.GetControlValue() == 0) return -1; // joystick button UP
int key = je.GetControlIndex();
if (je.control_value() == 0) return -1; // joystick button UP
uint8_t key = je.control_index();
int mod = wxJoyKeyTextCtrl::DigitalButton(je);
int joy = je.GetJoy() + 1;
int joy = je.player_index();
wxString label = wxJoyKeyTextCtrl::ToString(mod, key, joy);
wxAcceleratorEntry_v accels = wxGetApp().GetAccels();
for (size_t i = 0; i < accels.size(); ++i) {
for (size_t i = 0; i < accels.size(); ++i)
{
if (label == accels[i].GetUkey())
{
wxCommandEvent evh(wxEVT_COMMAND_MENU_SELECTED, accels[i].GetCommand());
@ -927,24 +928,27 @@ void MainFrame::SetJoystick()
{
/* Remove all attached joysticks to avoid errors while
* destroying and creating the GameArea `panel`. */
joy.Remove();
joy.StopPolling();
set_global_accels();
if (!emulating)
return;
for (int i = 0; i < 4; i++)
std::unordered_set<unsigned> needed_joysticks;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < NUM_KEYS; j++) {
wxJoyKeyBinding_v b = gopts.joykey_bindings[i][j];
for (size_t k = 0; k < b.size(); k++) {
int jn = b[k].joy;
if (jn) {
joy.Add(jn - 1);
needed_joysticks.insert(jn);
}
}
}
}
joy.PollJoysticks(needed_joysticks);
}
void MainFrame::StopJoyPollTimer()
{