2020-02-15 15:14:53 +00:00
|
|
|
#include "sdl_controller_interface.h"
|
|
|
|
#include "common/assert.h"
|
2020-08-21 19:00:22 +00:00
|
|
|
#include "common/file_system.h"
|
2020-02-15 15:14:53 +00:00
|
|
|
#include "common/log.h"
|
|
|
|
#include "core/controller.h"
|
|
|
|
#include "core/host_interface.h"
|
|
|
|
#include "core/system.h"
|
2020-02-15 15:15:05 +00:00
|
|
|
#include "sdl_initializer.h"
|
2020-02-15 15:14:53 +00:00
|
|
|
#include <SDL.h>
|
2020-02-17 15:06:11 +00:00
|
|
|
#include <cmath>
|
2020-02-15 15:14:53 +00:00
|
|
|
Log_SetChannel(SDLControllerInterface);
|
|
|
|
|
|
|
|
SDLControllerInterface::SDLControllerInterface() = default;
|
|
|
|
|
|
|
|
SDLControllerInterface::~SDLControllerInterface()
|
|
|
|
{
|
|
|
|
Assert(m_controllers.empty());
|
|
|
|
}
|
|
|
|
|
2020-08-22 06:44:06 +00:00
|
|
|
ControllerInterface::Backend SDLControllerInterface::GetBackend() const
|
|
|
|
{
|
|
|
|
return ControllerInterface::Backend::SDL;
|
|
|
|
}
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
bool SDLControllerInterface::Initialize(CommonHostInterface* host_interface)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-03-21 14:49:46 +00:00
|
|
|
if (!ControllerInterface::Initialize(host_interface))
|
|
|
|
return false;
|
|
|
|
|
2020-02-15 15:15:05 +00:00
|
|
|
FrontendCommon::EnsureSDLInitialized();
|
|
|
|
|
2020-08-21 19:00:22 +00:00
|
|
|
const std::string gcdb_file_name = GetGameControllerDBFileName();
|
2020-11-27 05:27:58 +00:00
|
|
|
if (!gcdb_file_name.empty())
|
2020-08-21 19:00:22 +00:00
|
|
|
{
|
|
|
|
Log_InfoPrintf("Loading game controller mappings from '%s'", gcdb_file_name.c_str());
|
|
|
|
if (SDL_GameControllerAddMappingsFromFile(gcdb_file_name.c_str()) < 0)
|
|
|
|
{
|
2020-08-29 12:19:28 +00:00
|
|
|
Log_ErrorPrintf("SDL_GameControllerAddMappingsFromFile(%s) failed: %s", gcdb_file_name.c_str(), SDL_GetError());
|
2020-08-21 19:00:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-05 16:47:05 +00:00
|
|
|
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-07-05 16:47:05 +00:00
|
|
|
Log_ErrorPrintf("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
|
2020-02-15 15:15:05 +00:00
|
|
|
return false;
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// we should open the controllers as the connected events come in, so no need to do any more here
|
2020-03-21 14:49:46 +00:00
|
|
|
m_sdl_subsystem_initialized = true;
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SDLControllerInterface::Shutdown()
|
|
|
|
{
|
|
|
|
while (!m_controllers.empty())
|
2020-03-22 02:37:11 +00:00
|
|
|
CloseGameController(m_controllers.begin()->joystick_id, false);
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
if (m_sdl_subsystem_initialized)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-07-05 16:47:05 +00:00
|
|
|
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
|
2020-03-21 14:49:46 +00:00
|
|
|
m_sdl_subsystem_initialized = false;
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
2020-03-22 02:37:11 +00:00
|
|
|
|
|
|
|
ControllerInterface::Shutdown();
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
|
|
|
|
2020-08-21 19:00:22 +00:00
|
|
|
std::string SDLControllerInterface::GetGameControllerDBFileName() const
|
|
|
|
{
|
2020-11-27 05:27:58 +00:00
|
|
|
// prefer the userdir copy
|
|
|
|
std::string filename(m_host_interface->GetUserDirectoryRelativePath("gamecontrollerdb.txt"));
|
|
|
|
if (FileSystem::FileExists(filename.c_str()))
|
|
|
|
return filename;
|
|
|
|
|
|
|
|
filename =
|
|
|
|
m_host_interface->GetProgramDirectoryRelativePath("database" FS_OSPATH_SEPARATOR_STR "gamecontrollerdb.txt");
|
|
|
|
if (FileSystem::FileExists(filename.c_str()))
|
|
|
|
return filename;
|
|
|
|
|
|
|
|
return {};
|
2020-08-21 19:00:22 +00:00
|
|
|
}
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
void SDLControllerInterface::PollEvents()
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
|
|
|
for (;;)
|
|
|
|
{
|
|
|
|
SDL_Event ev;
|
|
|
|
if (SDL_PollEvent(&ev))
|
|
|
|
ProcessSDLEvent(&ev);
|
|
|
|
else
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SDLControllerInterface::ProcessSDLEvent(const SDL_Event* event)
|
|
|
|
{
|
|
|
|
switch (event->type)
|
|
|
|
{
|
|
|
|
case SDL_CONTROLLERDEVICEADDED:
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("Controller %d inserted", event->cdevice.which);
|
|
|
|
OpenGameController(event->cdevice.which);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
case SDL_CONTROLLERDEVICEREMOVED:
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("Controller %d removed", event->cdevice.which);
|
2020-03-22 02:37:11 +00:00
|
|
|
CloseGameController(event->cdevice.which, true);
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
case SDL_CONTROLLERAXISMOTION:
|
2020-11-15 14:21:39 +00:00
|
|
|
return HandleControllerAxisEvent(&event->caxis);
|
2020-02-15 15:14:53 +00:00
|
|
|
|
|
|
|
case SDL_CONTROLLERBUTTONDOWN:
|
|
|
|
case SDL_CONTROLLERBUTTONUP:
|
2020-11-15 14:21:39 +00:00
|
|
|
return HandleControllerButtonEvent(&event->cbutton);
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-11-15 13:56:52 +00:00
|
|
|
case SDL_JOYDEVICEADDED:
|
|
|
|
if (SDL_IsGameController(event->jdevice.which))
|
|
|
|
return true;
|
|
|
|
|
|
|
|
Log_InfoPrintf("Joystick %d inserted", event->jdevice.which);
|
|
|
|
OpenJoystick(event->jdevice.which);
|
|
|
|
return true;
|
|
|
|
|
|
|
|
case SDL_JOYAXISMOTION:
|
|
|
|
return HandleJoystickAxisEvent(&event->jaxis);
|
|
|
|
|
|
|
|
case SDL_JOYHATMOTION:
|
|
|
|
return HandleJoystickHatEvent(&event->jhat);
|
|
|
|
|
|
|
|
case SDL_JOYBUTTONDOWN:
|
|
|
|
case SDL_JOYBUTTONUP:
|
|
|
|
return HandleJoystickButtonEvent(&event->jbutton);
|
|
|
|
|
2020-02-15 15:14:53 +00:00
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
SDLControllerInterface::ControllerDataVector::iterator SDLControllerInterface::GetControllerDataForJoystickId(int id)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-03-21 14:49:46 +00:00
|
|
|
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
|
|
|
[id](const ControllerData& cd) { return cd.joystick_id == id; });
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
SDLControllerInterface::ControllerDataVector::iterator SDLControllerInterface::GetControllerDataForPlayerId(int id)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-03-21 14:49:46 +00:00
|
|
|
return std::find_if(m_controllers.begin(), m_controllers.end(),
|
|
|
|
[id](const ControllerData& cd) { return cd.player_id == id; });
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
|
|
|
|
2020-04-11 09:36:53 +00:00
|
|
|
int SDLControllerInterface::GetFreePlayerId() const
|
|
|
|
{
|
|
|
|
for (int player_id = 0;; player_id++)
|
|
|
|
{
|
|
|
|
size_t i;
|
|
|
|
for (i = 0; i < m_controllers.size(); i++)
|
|
|
|
{
|
|
|
|
if (m_controllers[i].player_id == player_id)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (i == m_controllers.size())
|
|
|
|
return player_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-02-15 15:14:53 +00:00
|
|
|
bool SDLControllerInterface::OpenGameController(int index)
|
|
|
|
{
|
|
|
|
SDL_GameController* gcontroller = SDL_GameControllerOpen(index);
|
2020-03-21 14:49:46 +00:00
|
|
|
SDL_Joystick* joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
|
|
|
|
if (!gcontroller || !joystick)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
|
|
|
Log_WarningPrintf("Failed to open controller %d", index);
|
2020-03-21 14:49:46 +00:00
|
|
|
if (gcontroller)
|
|
|
|
SDL_GameControllerClose(gcontroller);
|
|
|
|
|
2020-02-15 15:14:53 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-03-22 11:29:58 +00:00
|
|
|
int joystick_id = SDL_JoystickInstanceID(joystick);
|
2020-03-22 02:27:58 +00:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
2020-03-22 11:29:58 +00:00
|
|
|
int player_id = SDL_GameControllerGetPlayerIndex(gcontroller);
|
2020-03-22 02:27:58 +00:00
|
|
|
#else
|
2020-03-22 11:29:58 +00:00
|
|
|
int player_id = -1;
|
2020-03-22 02:27:58 +00:00
|
|
|
#endif
|
2020-04-11 09:36:53 +00:00
|
|
|
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
|
2020-03-22 11:29:58 +00:00
|
|
|
{
|
2020-04-11 09:36:53 +00:00
|
|
|
const int free_player_id = GetFreePlayerId();
|
|
|
|
Log_WarningPrintf(
|
|
|
|
"Controller %d (joystick %d) returned player ID %d, which is invalid or in use. Using ID %d instead.", index,
|
|
|
|
joystick_id, player_id, free_player_id);
|
|
|
|
player_id = free_player_id;
|
2020-03-22 11:29:58 +00:00
|
|
|
}
|
2020-03-21 14:49:46 +00:00
|
|
|
|
2020-03-22 11:29:58 +00:00
|
|
|
Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, player_id,
|
2020-03-21 14:49:46 +00:00
|
|
|
SDL_GameControllerName(gcontroller));
|
2020-02-15 15:14:53 +00:00
|
|
|
|
|
|
|
ControllerData cd = {};
|
2020-03-22 11:29:58 +00:00
|
|
|
cd.player_id = player_id;
|
2020-03-21 14:49:46 +00:00
|
|
|
cd.joystick_id = joystick_id;
|
2020-04-14 06:34:39 +00:00
|
|
|
cd.haptic_left_right_effect = -1;
|
2020-12-31 09:38:28 +00:00
|
|
|
cd.game_controller = gcontroller;
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-12-31 09:38:28 +00:00
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
|
|
|
cd.use_game_controller_rumble = (SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
|
|
|
|
#else
|
|
|
|
cd.use_game_controller_rumble = false;
|
|
|
|
#endif
|
2020-04-14 06:34:39 +00:00
|
|
|
|
2020-12-31 09:38:28 +00:00
|
|
|
if (cd.use_game_controller_rumble)
|
|
|
|
{
|
|
|
|
Log_InfoPrintf("Rumble is supported on '%s' via gamecontroller", SDL_GameControllerName(gcontroller));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
|
|
|
if (haptic)
|
2020-04-14 06:34:39 +00:00
|
|
|
{
|
2020-12-31 09:38:28 +00:00
|
|
|
SDL_HapticEffect ef = {};
|
|
|
|
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
|
|
|
|
ef.leftright.length = 1000;
|
|
|
|
|
|
|
|
int ef_id = SDL_HapticNewEffect(haptic, &ef);
|
|
|
|
if (ef_id >= 0)
|
2020-04-14 06:34:39 +00:00
|
|
|
{
|
|
|
|
cd.haptic = haptic;
|
2020-12-31 09:38:28 +00:00
|
|
|
cd.haptic_left_right_effect = ef_id;
|
2020-04-14 06:34:39 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2020-12-31 09:38:28 +00:00
|
|
|
Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError());
|
|
|
|
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
|
|
|
|
{
|
|
|
|
cd.haptic = haptic;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
|
|
|
|
SDL_HapticClose(haptic);
|
|
|
|
}
|
2020-04-14 06:34:39 +00:00
|
|
|
}
|
|
|
|
}
|
2020-12-31 09:38:28 +00:00
|
|
|
|
|
|
|
if (cd.haptic)
|
|
|
|
Log_InfoPrintf("Rumble is supported on '%s' via haptic", SDL_GameControllerName(gcontroller));
|
2020-04-14 06:34:39 +00:00
|
|
|
}
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-12-31 09:38:28 +00:00
|
|
|
if (!cd.haptic && !cd.use_game_controller_rumble)
|
2020-02-15 15:14:53 +00:00
|
|
|
Log_WarningPrintf("Rumble is not supported on '%s'", SDL_GameControllerName(gcontroller));
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
m_controllers.push_back(std::move(cd));
|
2020-03-22 11:29:58 +00:00
|
|
|
OnControllerConnected(player_id);
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-03-22 02:37:11 +00:00
|
|
|
bool SDLControllerInterface::CloseGameController(int joystick_index, bool notify)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-03-21 14:49:46 +00:00
|
|
|
auto it = GetControllerDataForJoystickId(joystick_index);
|
2020-02-15 15:14:53 +00:00
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
2020-03-22 11:29:58 +00:00
|
|
|
const int player_id = it->player_id;
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
if (it->haptic)
|
|
|
|
SDL_HapticClose(static_cast<SDL_Haptic*>(it->haptic));
|
|
|
|
|
2020-12-31 09:38:28 +00:00
|
|
|
SDL_GameControllerClose(static_cast<SDL_GameController*>(it->game_controller));
|
2020-02-15 15:14:53 +00:00
|
|
|
m_controllers.erase(it);
|
2020-03-21 14:49:46 +00:00
|
|
|
|
2020-03-22 02:37:11 +00:00
|
|
|
if (notify)
|
2020-03-22 11:29:58 +00:00
|
|
|
OnControllerDisconnected(player_id);
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-11-15 13:56:52 +00:00
|
|
|
bool SDLControllerInterface::OpenJoystick(int index)
|
|
|
|
{
|
|
|
|
SDL_Joystick* joystick = SDL_JoystickOpen(index);
|
|
|
|
if (!joystick)
|
|
|
|
{
|
|
|
|
Log_WarningPrintf("Failed to open joystick %d", index);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
int joystick_id = SDL_JoystickInstanceID(joystick);
|
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
|
|
|
int player_id = SDL_JoystickGetDevicePlayerIndex(index);
|
|
|
|
#else
|
|
|
|
int player_id = -1;
|
|
|
|
#endif
|
|
|
|
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
|
|
|
|
{
|
|
|
|
const int free_player_id = GetFreePlayerId();
|
|
|
|
Log_WarningPrintf(
|
|
|
|
"Controller %d (joystick %d) returned player ID %d, which is invalid or in use. Using ID %d instead.", index,
|
|
|
|
joystick_id, player_id, free_player_id);
|
|
|
|
player_id = free_player_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
const char* name = SDL_JoystickName(joystick);
|
|
|
|
|
|
|
|
Log_InfoPrintf("Opened controller %d (instance id %d, player id %d): %s", index, joystick_id, player_id, name);
|
|
|
|
|
|
|
|
ControllerData cd = {};
|
|
|
|
cd.player_id = player_id;
|
|
|
|
cd.joystick_id = joystick_id;
|
|
|
|
cd.haptic_left_right_effect = -1;
|
2020-12-31 09:38:28 +00:00
|
|
|
cd.game_controller = nullptr;
|
|
|
|
cd.use_game_controller_rumble = false;
|
2020-11-15 13:56:52 +00:00
|
|
|
|
|
|
|
SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
|
|
|
|
if (haptic)
|
|
|
|
{
|
|
|
|
SDL_HapticEffect ef = {};
|
|
|
|
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
|
|
|
|
ef.leftright.length = 1000;
|
|
|
|
|
|
|
|
int ef_id = SDL_HapticNewEffect(haptic, &ef);
|
|
|
|
if (ef_id >= 0)
|
|
|
|
{
|
|
|
|
cd.haptic = haptic;
|
|
|
|
cd.haptic_left_right_effect = ef_id;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("Failed to create haptic left/right effect: %s", SDL_GetError());
|
|
|
|
if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
|
|
|
|
{
|
|
|
|
cd.haptic = haptic;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
Log_ErrorPrintf("No haptic rumble supported: %s", SDL_GetError());
|
|
|
|
SDL_HapticClose(haptic);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cd.haptic)
|
|
|
|
Log_InfoPrintf("Rumble is supported on '%s'", name);
|
|
|
|
else
|
|
|
|
Log_WarningPrintf("Rumble is not supported on '%s'", name);
|
|
|
|
|
|
|
|
m_controllers.push_back(std::move(cd));
|
|
|
|
OnControllerConnected(player_id);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SDLControllerInterface::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* event)
|
|
|
|
{
|
|
|
|
const float value = static_cast<float>(event->value) / (event->value < 0 ? 32768.0f : 32767.0f);
|
|
|
|
Log_DebugPrintf("controller %d axis %d %d %f", event->which, event->axis, event->value, value);
|
|
|
|
|
|
|
|
auto it = GetControllerDataForJoystickId(event->which);
|
2020-12-31 09:38:28 +00:00
|
|
|
if (it == m_controllers.end() || it->IsGameController())
|
2020-11-15 13:56:52 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
if (DoEventHook(Hook::Type::Axis, it->player_id, event->axis, value, true))
|
|
|
|
return true;
|
|
|
|
|
|
|
|
bool processed = false;
|
|
|
|
|
|
|
|
const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Full];
|
|
|
|
if (cb)
|
|
|
|
{
|
|
|
|
cb(value);
|
|
|
|
processed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value > 0.0f)
|
|
|
|
{
|
|
|
|
const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Positive];
|
|
|
|
if (cb)
|
|
|
|
{
|
|
|
|
// Expand 0..1 - -1..1
|
|
|
|
cb(value * 2.0f - 1.0f);
|
|
|
|
processed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (value < 0.0f)
|
|
|
|
{
|
|
|
|
const AxisCallback& cb = it->axis_mapping[event->axis][AxisSide::Negative];
|
|
|
|
if (cb)
|
|
|
|
{
|
|
|
|
// Expand 0..-1 - -1..1
|
|
|
|
cb(value * -2.0f - 1.0f);
|
|
|
|
processed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (processed)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
// set the other direction to false so large movements don't leave the opposite on
|
|
|
|
const bool outside_deadzone = (std::abs(value) >= it->deadzone);
|
|
|
|
const bool positive = (value >= 0.0f);
|
|
|
|
const ButtonCallback& other_button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(!positive)];
|
|
|
|
const ButtonCallback& button_cb = it->axis_button_mapping[event->axis][BoolToUInt8(positive)];
|
|
|
|
if (button_cb)
|
|
|
|
{
|
|
|
|
button_cb(outside_deadzone);
|
|
|
|
if (other_button_cb)
|
|
|
|
other_button_cb(false);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (other_button_cb)
|
|
|
|
{
|
|
|
|
other_button_cb(false);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SDLControllerInterface::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* event)
|
|
|
|
{
|
|
|
|
Log_DebugPrintf("controller %d button %d %s", event->which, event->button,
|
|
|
|
event->state == SDL_PRESSED ? "pressed" : "released");
|
|
|
|
|
|
|
|
auto it = GetControllerDataForJoystickId(event->which);
|
2020-12-31 09:38:28 +00:00
|
|
|
if (it == m_controllers.end() || it->IsGameController())
|
2020-11-15 13:56:52 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
const bool pressed = (event->state == SDL_PRESSED);
|
|
|
|
if (DoEventHook(Hook::Type::Button, it->player_id, event->button, pressed ? 1.0f : 0.0f))
|
|
|
|
return true;
|
|
|
|
|
|
|
|
const ButtonCallback& cb = it->button_mapping[event->button];
|
|
|
|
if (cb)
|
|
|
|
{
|
|
|
|
cb(pressed);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
const AxisCallback& axis_cb = it->button_axis_mapping[event->button];
|
|
|
|
if (axis_cb)
|
|
|
|
{
|
|
|
|
axis_cb(pressed ? 1.0f : -1.0f);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SDLControllerInterface::HandleJoystickHatEvent(const SDL_JoyHatEvent* event)
|
|
|
|
{
|
|
|
|
Log_DebugPrintf("controller %d hat %d %d", event->which, event->hat, event->value);
|
|
|
|
|
|
|
|
auto it = GetControllerDataForJoystickId(event->which);
|
2020-12-31 09:38:28 +00:00
|
|
|
if (it == m_controllers.end() || it->IsGameController())
|
2020-11-15 13:56:52 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
auto HatEventHook = [hat = event->hat, value = event->value, player_id = it->player_id, this](int hat_position) {
|
|
|
|
if ((value & hat_position) == 0)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
std::string_view position_str;
|
|
|
|
switch (value)
|
|
|
|
{
|
|
|
|
case SDL_HAT_UP:
|
|
|
|
position_str = "Up";
|
|
|
|
break;
|
|
|
|
case SDL_HAT_RIGHT:
|
|
|
|
position_str = "Right";
|
|
|
|
break;
|
|
|
|
case SDL_HAT_DOWN:
|
|
|
|
position_str = "Down";
|
|
|
|
break;
|
|
|
|
case SDL_HAT_LEFT:
|
|
|
|
position_str = "Left";
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return DoEventHook(Hook::Type::Hat, player_id, hat, position_str);
|
|
|
|
};
|
|
|
|
|
|
|
|
if (event->value == SDL_HAT_CENTERED)
|
|
|
|
{
|
|
|
|
if (HatEventHook(SDL_HAT_CENTERED))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// event->value can be a bitmask of multiple direction, so probe them all
|
|
|
|
if (HatEventHook(SDL_HAT_UP) || HatEventHook(SDL_HAT_RIGHT) || HatEventHook(SDL_HAT_DOWN) ||
|
|
|
|
HatEventHook(SDL_HAT_LEFT))
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool processed = false;
|
|
|
|
|
2021-03-07 16:48:18 +00:00
|
|
|
if (event->hat < it->hat_button_mapping.size())
|
2020-11-15 13:56:52 +00:00
|
|
|
{
|
2021-03-07 16:48:18 +00:00
|
|
|
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][0]; cb)
|
|
|
|
{
|
|
|
|
cb(event->value & SDL_HAT_UP);
|
|
|
|
processed = true;
|
|
|
|
}
|
|
|
|
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][1]; cb)
|
|
|
|
{
|
|
|
|
cb(event->value & SDL_HAT_RIGHT);
|
|
|
|
processed = true;
|
|
|
|
}
|
|
|
|
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][2]; cb)
|
|
|
|
{
|
|
|
|
cb(event->value & SDL_HAT_DOWN);
|
|
|
|
processed = true;
|
|
|
|
}
|
|
|
|
if (const ButtonCallback& cb = it->hat_button_mapping[event->hat][3]; cb)
|
|
|
|
{
|
|
|
|
cb(event->value & SDL_HAT_LEFT);
|
|
|
|
processed = true;
|
|
|
|
}
|
2020-11-15 13:56:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return processed;
|
|
|
|
}
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
void SDLControllerInterface::ClearBindings()
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
|
|
|
for (auto& it : m_controllers)
|
|
|
|
{
|
2020-11-15 13:56:52 +00:00
|
|
|
it.axis_mapping.fill({});
|
|
|
|
it.button_mapping.fill({});
|
|
|
|
it.axis_button_mapping.fill({});
|
|
|
|
it.button_axis_mapping.fill({});
|
|
|
|
it.hat_button_mapping.clear();
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-15 13:56:52 +00:00
|
|
|
bool SDLControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
|
|
|
AxisCallback callback)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-03-21 14:49:46 +00:00
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
2020-02-15 15:14:53 +00:00
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (axis_number < 0 || axis_number >= MAX_NUM_AXISES)
|
|
|
|
return false;
|
|
|
|
|
2020-11-15 13:56:52 +00:00
|
|
|
it->axis_mapping[axis_number][axis_side] = std::move(callback);
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SDLControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
|
|
|
{
|
2020-03-21 14:49:46 +00:00
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
2020-02-15 15:14:53 +00:00
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
|
|
|
|
return false;
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
it->button_mapping[button_number] = std::move(callback);
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SDLControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
|
|
|
ButtonCallback callback)
|
|
|
|
{
|
2020-03-21 14:49:46 +00:00
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
2020-02-15 15:14:53 +00:00
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (axis_number < 0 || axis_number >= MAX_NUM_AXISES)
|
|
|
|
return false;
|
|
|
|
|
2020-03-21 14:49:46 +00:00
|
|
|
it->axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-11-15 13:56:52 +00:00
|
|
|
bool SDLControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
|
|
|
std::string_view hat_position, ButtonCallback callback)
|
|
|
|
{
|
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
size_t index;
|
|
|
|
if (hat_position == "Up")
|
|
|
|
index = 0;
|
|
|
|
else if (hat_position == "Right")
|
|
|
|
index = 1;
|
|
|
|
else if (hat_position == "Down")
|
|
|
|
index = 2;
|
|
|
|
else if (hat_position == "Left")
|
|
|
|
index = 3;
|
|
|
|
else
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// We need 4 entries per hat_number
|
2021-02-06 09:19:55 +00:00
|
|
|
if (static_cast<int>(it->hat_button_mapping.size()) < hat_number + 1)
|
2020-11-15 13:56:52 +00:00
|
|
|
it->hat_button_mapping.resize(hat_number + 1);
|
|
|
|
|
|
|
|
it->hat_button_mapping[hat_number][index] = std::move(callback);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-08-29 12:19:28 +00:00
|
|
|
bool SDLControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback)
|
|
|
|
{
|
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (button_number < 0 || button_number >= MAX_NUM_BUTTONS)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
it->button_axis_mapping[button_number] = std::move(callback);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
bool SDLControllerInterface::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-11-15 14:21:39 +00:00
|
|
|
const float value = static_cast<float>(ev->value) / (ev->value < 0 ? 32768.0f : 32767.0f);
|
|
|
|
Log_DebugPrintf("controller %d axis %d %d %f", ev->which, ev->axis, ev->value, value);
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
auto it = GetControllerDataForJoystickId(ev->which);
|
2020-02-15 15:14:53 +00:00
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
if (DoEventHook(Hook::Type::Axis, it->player_id, ev->axis, value))
|
2020-04-04 15:28:06 +00:00
|
|
|
return true;
|
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
const AxisCallback& cb = it->axis_mapping[ev->axis][AxisSide::Full];
|
2020-02-15 15:14:53 +00:00
|
|
|
if (cb)
|
|
|
|
{
|
2020-11-15 13:56:52 +00:00
|
|
|
// Extend triggers from a 0 - 1 range to a -1 - 1 range for consistency with other inputs
|
2020-11-15 14:21:39 +00:00
|
|
|
if (ev->axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT || ev->axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT)
|
2020-11-15 13:56:52 +00:00
|
|
|
{
|
|
|
|
cb((value * 2.0f) - 1.0f);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
cb(value);
|
|
|
|
}
|
2020-02-15 15:14:53 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// set the other direction to false so large movements don't leave the opposite on
|
2020-06-23 02:30:19 +00:00
|
|
|
const bool outside_deadzone = (std::abs(value) >= it->deadzone);
|
2020-02-15 15:14:53 +00:00
|
|
|
const bool positive = (value >= 0.0f);
|
2020-11-15 14:21:39 +00:00
|
|
|
const ButtonCallback& other_button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(!positive)];
|
|
|
|
const ButtonCallback& button_cb = it->axis_button_mapping[ev->axis][BoolToUInt8(positive)];
|
2020-02-15 15:14:53 +00:00
|
|
|
if (button_cb)
|
|
|
|
{
|
|
|
|
button_cb(outside_deadzone);
|
|
|
|
if (other_button_cb)
|
|
|
|
other_button_cb(false);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (other_button_cb)
|
|
|
|
{
|
|
|
|
other_button_cb(false);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
bool SDLControllerInterface::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-11-15 14:21:39 +00:00
|
|
|
Log_DebugPrintf("controller %d button %d %s", ev->which, ev->button,
|
|
|
|
ev->state == SDL_PRESSED ? "pressed" : "released");
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
auto it = GetControllerDataForJoystickId(ev->which);
|
2020-02-15 15:14:53 +00:00
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
2021-01-30 11:25:49 +00:00
|
|
|
static constexpr std::array<FrontendCommon::ControllerNavigationButton, SDL_CONTROLLER_BUTTON_MAX>
|
|
|
|
nav_button_mapping = {{
|
|
|
|
FrontendCommon::ControllerNavigationButton::Activate, // SDL_CONTROLLER_BUTTON_A
|
|
|
|
FrontendCommon::ControllerNavigationButton::Cancel, // SDL_CONTROLLER_BUTTON_B
|
|
|
|
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_X
|
|
|
|
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_Y
|
|
|
|
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_BACK
|
|
|
|
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_GUIDE
|
|
|
|
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_START
|
|
|
|
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_LEFTSTICK
|
|
|
|
FrontendCommon::ControllerNavigationButton::Count, // SDL_CONTROLLER_BUTTON_RIGHTSTICK
|
|
|
|
FrontendCommon::ControllerNavigationButton::LeftShoulder, // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
|
|
|
|
FrontendCommon::ControllerNavigationButton::RightShoulder, // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
|
|
|
|
FrontendCommon::ControllerNavigationButton::DPadUp, // SDL_CONTROLLER_BUTTON_DPAD_UP
|
|
|
|
FrontendCommon::ControllerNavigationButton::DPadDown, // SDL_CONTROLLER_BUTTON_DPAD_DOWN
|
|
|
|
FrontendCommon::ControllerNavigationButton::DPadLeft, // SDL_CONTROLLER_BUTTON_DPAD_LEFT
|
|
|
|
FrontendCommon::ControllerNavigationButton::DPadRight, // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
|
|
|
|
}};
|
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
const bool pressed = (ev->state == SDL_PRESSED);
|
|
|
|
if (DoEventHook(Hook::Type::Button, it->player_id, ev->button, pressed ? 1.0f : 0.0f))
|
2020-04-04 15:28:06 +00:00
|
|
|
return true;
|
|
|
|
|
2021-01-30 11:25:49 +00:00
|
|
|
if (ev->button < nav_button_mapping.size() &&
|
|
|
|
nav_button_mapping[ev->button] != FrontendCommon::ControllerNavigationButton::Count &&
|
|
|
|
m_host_interface->SetControllerNavigationButtonState(nav_button_mapping[ev->button], pressed))
|
|
|
|
{
|
|
|
|
// UI consumed the event
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
const ButtonCallback& cb = it->button_mapping[ev->button];
|
2020-08-29 12:19:28 +00:00
|
|
|
if (cb)
|
|
|
|
{
|
|
|
|
cb(pressed);
|
|
|
|
return true;
|
|
|
|
}
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-11-15 14:21:39 +00:00
|
|
|
const AxisCallback& axis_cb = it->button_axis_mapping[ev->button];
|
2020-08-29 12:19:28 +00:00
|
|
|
if (axis_cb)
|
|
|
|
{
|
2020-11-15 13:56:52 +00:00
|
|
|
axis_cb(pressed ? 1.0f : -1.0f);
|
|
|
|
return true;
|
2020-08-29 12:19:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 06:34:39 +00:00
|
|
|
u32 SDLControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
2020-02-15 15:14:53 +00:00
|
|
|
{
|
2020-04-14 06:34:39 +00:00
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
|
|
|
if (it == m_controllers.end())
|
|
|
|
return 0;
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-12-31 09:38:28 +00:00
|
|
|
return (it->use_game_controller_rumble ? 2 : ((it->haptic_left_right_effect >= 0) ? 2 : (it->haptic ? 1 : 0)));
|
2020-04-14 06:34:39 +00:00
|
|
|
}
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-04-14 06:34:39 +00:00
|
|
|
void SDLControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors)
|
|
|
|
{
|
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
|
|
|
if (it == m_controllers.end())
|
|
|
|
return;
|
|
|
|
|
|
|
|
// we'll update before this duration is elapsed
|
2021-01-08 02:30:43 +00:00
|
|
|
static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS
|
2020-12-31 09:38:28 +00:00
|
|
|
|
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
|
|
|
if (it->use_game_controller_rumble)
|
|
|
|
{
|
|
|
|
const u16 large = static_cast<u16>(strengths[0] * 65535.0f);
|
|
|
|
const u16 small = static_cast<u32>(strengths[1] * 65535.0f);
|
|
|
|
SDL_GameControllerRumble(static_cast<SDL_GameController*>(it->game_controller), large, small, DURATION);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
#endif
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-04-14 06:34:39 +00:00
|
|
|
SDL_Haptic* haptic = static_cast<SDL_Haptic*>(it->haptic);
|
|
|
|
if (it->haptic_left_right_effect >= 0 && num_motors > 1)
|
|
|
|
{
|
2020-09-17 09:55:43 +00:00
|
|
|
if (strengths[0] > 0.0f || strengths[1] > 0.0f)
|
2020-04-14 06:34:39 +00:00
|
|
|
{
|
|
|
|
SDL_HapticEffect ef;
|
|
|
|
ef.type = SDL_HAPTIC_LEFTRIGHT;
|
|
|
|
ef.leftright.large_magnitude = static_cast<u32>(strengths[0] * 65535.0f);
|
|
|
|
ef.leftright.small_magnitude = static_cast<u32>(strengths[1] * 65535.0f);
|
|
|
|
ef.leftright.length = DURATION;
|
|
|
|
SDL_HapticUpdateEffect(haptic, it->haptic_left_right_effect, &ef);
|
|
|
|
SDL_HapticRunEffect(haptic, it->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
|
|
|
|
}
|
2020-02-15 15:14:53 +00:00
|
|
|
else
|
2020-04-14 06:34:39 +00:00
|
|
|
{
|
|
|
|
SDL_HapticStopEffect(haptic, it->haptic_left_right_effect);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
float max_strength = 0.0f;
|
|
|
|
for (u32 i = 0; i < num_motors; i++)
|
|
|
|
max_strength = std::max(max_strength, strengths[i]);
|
2020-02-15 15:14:53 +00:00
|
|
|
|
2020-09-17 09:55:43 +00:00
|
|
|
if (max_strength > 0.0f)
|
2020-04-14 06:34:39 +00:00
|
|
|
SDL_HapticRumblePlay(haptic, max_strength, DURATION);
|
|
|
|
else
|
|
|
|
SDL_HapticRumbleStop(haptic);
|
2020-02-15 15:14:53 +00:00
|
|
|
}
|
|
|
|
}
|
2020-06-07 01:49:25 +00:00
|
|
|
|
2020-06-23 02:30:19 +00:00
|
|
|
bool SDLControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
|
|
|
{
|
|
|
|
auto it = GetControllerDataForPlayerId(controller_index);
|
|
|
|
if (it == m_controllers.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
it->deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
|
|
|
Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index, it->deadzone);
|
|
|
|
return true;
|
|
|
|
}
|