From edc34942a8e3fa885fdf4e03b99ea07009e7644d Mon Sep 17 00:00:00 2001 From: Fabrice de Gans-Riberi Date: Sat, 5 Sep 2020 21:56:38 -0700 Subject: [PATCH] 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. --- po/wxvbam/wxvbam.pot | 14 +- src/wx/cmdevents.cpp | 4 +- src/wx/guiinit.cpp | 4 +- src/wx/panel.cpp | 12 +- src/wx/widgets/joyedit.cpp | 11 +- src/wx/widgets/sdljoy.cpp | 841 +++++++++++++++++++------------------ src/wx/widgets/wx/sdljoy.h | 181 ++++---- src/wx/wxvbam.cpp | 100 ++--- 8 files changed, 585 insertions(+), 582 deletions(-) diff --git a/po/wxvbam/wxvbam.pot b/po/wxvbam/wxvbam.pot index b7e50179..5ae0681d 100644 --- a/po/wxvbam/wxvbam.pot +++ b/po/wxvbam/wxvbam.pot @@ -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 \n" "Language-Team: LANGUAGE \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 "" diff --git a/src/wx/cmdevents.cpp b/src/wx/cmdevents.cpp index e083bc24..6c4ebf4b 100644 --- a/src/wx/cmdevents.cpp +++ b/src/wx/cmdevents.cpp @@ -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(); diff --git a/src/wx/guiinit.cpp b/src/wx/guiinit.cpp index 7a83f420..b5e426c8 100644 --- a/src/wx/guiinit.cpp +++ b/src/wx/guiinit.cpp @@ -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 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++) { diff --git a/src/wx/panel.cpp b/src/wx/panel.cpp index 8d263ba4..6f5b828f 100644 --- a/src/wx/panel.cpp +++ b/src/wx/panel.cpp @@ -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 diff --git a/src/wx/widgets/joyedit.cpp b/src/wx/widgets/joyedit.cpp index 9bbd7c69..af1fd0e3 100644 --- a/src/wx/widgets/joyedit.cpp +++ b/src/wx/widgets/joyedit.cpp @@ -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; diff --git a/src/wx/widgets/sdljoy.cpp b/src/wx/widgets/sdljoy.cpp index cdadce66..07e8960d 100644 --- a/src/wx/widgets/sdljoy.cpp +++ b/src/wx/widgets/sdljoy.cpp @@ -1,35 +1,36 @@ -#include -#include "wxvbam.h" #include "wx/sdljoy.h" -#include "SDL.h" -#include -#include "../common/range.hpp" + +#include +#include +#include + #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,420 +39,460 @@ 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); - if (!handler) +} // 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 axis_{}; + + // Current state of Joystick buttons. + std::unordered_map buttons_{}; + + // Current state of Joystick HAT. Unused for GameControllers. + std::unordered_map 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; - 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); + joystick_id_ = SDL_JoystickInstanceID(joystick_); + systemScreenMessage( + wxString::Format(_("Connected joystick %d: %s"), + player_index_, SDL_JoystickNameForIndex(sdl_index))); } -void wxSDLJoy::Poll() -{ - SDL_Event e; - - bool got_event = false; - - while (SDL_PollEvent(&e)) { - switch (e.type) { - 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); - } - } - - 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); - } - } - - 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; - - break; - } - - // Joystck 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); - } - } - - 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); - } - } - - got_event = true; - - break; - } - case SDL_JOYDEVICEADDED: - { - auto joy = e.jdevice.which; - - if (SDL_IsGameController(joy)) - break; - - if (add_all || contains(joystate, joy)) { - 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)); - } - - got_event = true; - - break; - } - } - } - - 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(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); - } - } - - for (uint8_t axis = 0; axis < SDL_CONTROLLER_AXIS_MAX; axis++) { - auto val = axisval(SDL_GameControllerGetAxis(joy.second.dev, static_cast(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); - } - } - } - } - } +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_)); } -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; +bool wxSDLJoyState::IsValid() const { + return joystick_; } -void wxSDLJoy::RemapControllers() -{ - for (auto&& joy : joystate) { - auto& state = joy.second; +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; - DisconnectController(state); - ConnectController(joy.first); - } -} - -void wxSDLJoy::DisconnectController(wxSDLJoyState& state) -{ - if (auto& dev = state.dev) { - if (state.is_gc) { - SDL_GameControllerClose(dev); + switch (event_type) { + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + // Do not process joystick events for game controllers. + if (game_controller_) { + return; } - else { - SDL_JoystickClose(dev); + // 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; - dev = nullptr; - } + 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; - 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; + 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; } - ConnectController(joy_n); -} + 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); -void wxSDLJoy::Remove(int8_t joy_n) -{ - add_all = false; + auto handler = wxGetApp().frame->GetJoyEventHandler(); + if (!handler) + return; - if (joy_n < 0) { - for (auto&& joy : joystate) - DisconnectController(joy.second); - - joystate.clear(); - - return; + wxQueueEvent(handler, + new wxSDLJoyEvent( + player_index_, control, control_index, control_value)); } - - DisconnectController(joystate[joy_n]); - joystate.erase(joy_n); } -void wxSDLJoy::SetRumble(bool do_rumble) -{ - rumbling = do_rumble; +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(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(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) - // 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(); - } + 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 wxSDLJoy::Notify() -{ - SetRumble(rumbling); +void wxSDLJoyState::Notify() { + SetRumble(rumbling_); } -wxSDLJoyDev::operator SDL_GameController*&() -{ - return dev_gc; +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); } -SDL_GameController*& wxSDLJoyDev::operator=(SDL_GameController* ptr) -{ - dev_gc = ptr; - return dev_gc; +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; -wxSDLJoyDev::operator SDL_Joystick*&() -{ - return dev_js; + while (SDL_PollEvent(&e)) { + switch (e.type) { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + { + 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: + { + 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: + case SDL_CONTROLLERDEVICEREMOVED: + // Do nothing. This will be handled with JOYDEVICEADDED and + // JOYDEVICEREMOVED events. + break; + + // Joystick events for non-GameControllers. + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + { + 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: + { + 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: + { + // Always remap all controllers. + RemapControllers(); + got_event = true; + break; + } + + case SDL_JOYDEVICEREMOVED: + { + joystick_states_.erase(e.jdevice.which); + got_event = true; + break; + } + + 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(); + } + last_poll_ = now; + } } -SDL_Joystick*& wxSDLJoyDev::operator=(SDL_Joystick* ptr) -{ - dev_js = ptr; - return dev_js; +void wxSDLJoy::RemapControllers() { + if (!is_polling_active_) { + // Nothing to do when we're not actively polling. + return; + } + + joystick_states_.clear(); + + if (requested_sdl_indexes_.empty()) { + // Connect all joysticks. + for (int i = 0; i < SDL_NumJoysticks(); i++) { + std::unique_ptr 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 joy_state( + new wxSDLJoyState(sdl_index)); + if (joy_state->IsValid()) { + joystick_states_.emplace( + joy_state->joystick_id(), std::move(joy_state)); + } + } + } } -wxSDLJoyDev::operator bool() -{ - return dev_gc != nullptr; +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(); } -std::nullptr_t& wxSDLJoyDev::operator=(std::nullptr_t&& null_ptr) -{ - dev_gc = null_ptr; - return null_ptr; +void wxSDLJoy::PollJoysticks(std::unordered_set indexes) { + // Reset the polling state. + StopPolling(); + + if (indexes.empty()) { + // Nothing to poll. Return early. + return; + } + + 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::PollAllJoysticks() { + // Reset the polling state. + StopPolling(); + is_polling_active_ = true; + RemapControllers(); +} + +void wxSDLJoy::StopPolling() { + joystick_states_.clear(); + requested_sdl_indexes_.clear(); + is_polling_active_ = false; +} + +void wxSDLJoy::SetRumble(bool activate_rumble) { + if (joystick_states_.empty()) + return; + + // Do rumble only on the first device. + joystick_states_.begin()->second->SetRumble(activate_rumble); } diff --git a/src/wx/widgets/wx/sdljoy.h b/src/wx/widgets/wx/sdljoy.h index 22116a89..8255f58e 100644 --- a/src/wx/widgets/wx/sdljoy.h +++ b/src/wx/widgets/wx/sdljoy.h @@ -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 -#include -#include +#include +#include #include #include #include -#include #include #include -#include "../common/contains.h" +#include -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 axis{}; - std::unordered_map 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 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 joystate; - std::unordered_map 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> joystick_states_; -class wxSDLJoyEvent : public wxCommandEvent { - friend class wxSDLJoy; + // Set of requested SDL joystick indexes. + std::unordered_set 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 diff --git a/src/wx/wxvbam.cpp b/src/wx/wxvbam.cpp index 34755b20..f086d4ff 100644 --- a/src/wx/wxvbam.cpp +++ b/src/wx/wxvbam.cpp @@ -109,8 +109,8 @@ static void get_config_path(wxPathList& path, bool exists = true) } else { - // config is in $HOME/.vbam/ - add_nonstandard_path(old_config); + // config is in $HOME/.vbam/ + add_nonstandard_path(old_config); } #endif @@ -240,7 +240,7 @@ bool wxvbamApp::OnInit() return false; if (console_mode) - return true; + return true; // prepare for loading xrc files wxXmlResource* xr = wxXmlResource::Get(); @@ -449,13 +449,13 @@ bool wxvbamApp::OnInit() return false; if (x >= 0 && y >= 0 && width > 0 && height > 0) - frame->SetSize(x, y, width, height); + frame->SetSize(x, y, width, height); if (isMaximized) frame->Maximize(); if (isFullscreen && wxGetApp().pending_load != wxEmptyString) - frame->ShowFullScreen(isFullscreen); + frame->ShowFullScreen(isFullscreen); frame->Show(true); #ifndef NO_ONLINEUPDATES @@ -468,12 +468,12 @@ int wxvbamApp::OnRun() { if (console_mode) { - // we could check for our own error codes here... - return console_status; + // we could check for our own error codes here... + return console_status; } else { - return wxApp::OnRun(); + return wxApp::OnRun(); } } @@ -512,30 +512,30 @@ void wxvbamApp::OnInitCmdLine(wxCmdLineParser& cl) static wxCmdLineEntryDesc opttab[] = { { wxCMD_LINE_OPTION, NULL, t("save-xrc"), N_("Save built-in XRC file and exit"), - wxCMD_LINE_VAL_STRING, 0 }, + wxCMD_LINE_VAL_STRING, 0 }, { wxCMD_LINE_OPTION, NULL, t("save-over"), N_("Save built-in vba-over.ini and exit"), - wxCMD_LINE_VAL_STRING, 0 }, + wxCMD_LINE_VAL_STRING, 0 }, { wxCMD_LINE_SWITCH, NULL, t("print-cfg-path"), N_("Print configuration path and exit"), - wxCMD_LINE_VAL_NONE, 0 }, + wxCMD_LINE_VAL_NONE, 0 }, { wxCMD_LINE_SWITCH, t("f"), t("fullscreen"), N_("Start in full-screen mode"), - wxCMD_LINE_VAL_NONE, 0 }, + wxCMD_LINE_VAL_NONE, 0 }, { wxCMD_LINE_OPTION, t("c"), t("config"), N_("Set a configuration file"), - wxCMD_LINE_VAL_STRING, 0 }, + wxCMD_LINE_VAL_STRING, 0 }, #if !defined(NO_LINK) && !defined(__WXMSW__) { wxCMD_LINE_SWITCH, t("s"), t("delete-shared-state"), N_("Delete shared link state first, if it exists"), - wxCMD_LINE_VAL_NONE, 0 }, + wxCMD_LINE_VAL_NONE, 0 }, #endif // stupid wx cmd line parser doesn't support duplicate options - // { wxCMD_LINE_OPTION, t("o"), t("option"), - // _("Set configuration option; = or help for list"), + // { wxCMD_LINE_OPTION, t("o"), t("option"), + // _("Set configuration option; = or help for list"), { wxCMD_LINE_SWITCH, t("o"), t("list-options"), N_("List all settable options and exit"), - wxCMD_LINE_VAL_NONE, 0 }, + wxCMD_LINE_VAL_NONE, 0 }, { wxCMD_LINE_PARAM, NULL, NULL, N_("ROM file"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, { wxCMD_LINE_PARAM, NULL, NULL, @@ -720,8 +720,8 @@ wxString wxvbamApp::GetDataDir() wxvbamApp::~wxvbamApp() { if (home != NULL) { - free(home); - home = NULL; + free(home); + home = NULL; } delete overrides; @@ -797,8 +797,8 @@ void MainFrame::OnMenu(wxContextMenuEvent& event) wxPoint p(event.GetPosition()); #if 0 // wx actually recommends ignoring the position - if (p != wxDefaultPosition) - p = ScreenToClient(p); + if (p != wxDefaultPosition) + p = ScreenToClient(p); #endif PopupMenu(ctx_menu, p); @@ -839,16 +839,16 @@ void MainFrame::OnSize(wxSizeEvent& event) bool isMaximized = IsMaximized(); if (!isFullscreen && !isMaximized) { - if (height > 0 && width > 0) - { - windowHeight = height; - windowWidth = width; - } - if (x >= 0 && y >= 0) - { - windowPositionX = x; - windowPositionY = y; - } + if (height > 0 && width > 0) + { + windowHeight = height; + windowWidth = width; + } + if (x >= 0 && y >= 0) + { + windowPositionX = x; + windowPositionY = y; + } } else { @@ -876,25 +876,26 @@ int MainFrame::FilterEvent(wxEvent& event) evh.SetEventObject(this); GetEventHandler()->ProcessEvent(evh); return true; - } + } } 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) { - if (label == accels[i].GetUkey()) - { - wxCommandEvent evh(wxEVT_COMMAND_MENU_SELECTED, accels[i].GetCommand()); - evh.SetEventObject(this); - GetEventHandler()->ProcessEvent(evh); - return true; - } + for (size_t i = 0; i < accels.size(); ++i) + { + if (label == accels[i].GetUkey()) + { + wxCommandEvent evh(wxEVT_COMMAND_MENU_SELECTED, accels[i].GetCommand()); + evh.SetEventObject(this); + GetEventHandler()->ProcessEvent(evh); + return true; + } } } return -1; @@ -916,8 +917,8 @@ wxString MainFrame::GetGamePath(wxString path) if (!wxIsWritable(game_path)) { - game_path = wxGetApp().GetAbsolutePath(wxString((get_xdg_user_data_home() + DOT_DIR).c_str(), wxConvLibc)); - wxFileName::Mkdir(game_path, 0777, wxPATH_MKDIR_FULL); + game_path = wxGetApp().GetAbsolutePath(wxString((get_xdg_user_data_home() + DOT_DIR).c_str(), wxConvLibc)); + wxFileName::Mkdir(game_path, 0777, wxPATH_MKDIR_FULL); } return game_path; @@ -927,23 +928,26 @@ 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 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()