From 13221f67c465017f44ca46aeca23b0d820935825 Mon Sep 17 00:00:00 2001 From: Leviathan Linden Date: Tue, 19 Sep 2023 09:40:08 -0700 Subject: add GameControl feature and SDL2 dependency --- indra/llwindow/llgamecontrol.cpp | 605 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 indra/llwindow/llgamecontrol.cpp (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp new file mode 100644 index 0000000000..5dc01c5e54 --- /dev/null +++ b/indra/llwindow/llgamecontrol.cpp @@ -0,0 +1,605 @@ +/** + * @file llgamecontrol.h + * @brief GameController detection and management + * + * $LicenseInfo:firstyear=2023&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2023, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llgamecontrol.h" + +#include +#include +#include + +#include "SDL2/SDL.h" +#include "SDL2/SDL_gamecontroller.h" +#include "SDL2/SDL_joystick.h" + +constexpr size_t NUM_AXES = 6; + +// internal class for managing list of controllers and per-controller state +class LLGameControllerManager +{ +public: + void addController(SDL_JoystickID id, SDL_GameController* controller); + void removeController(SDL_JoystickID id); + + void onAxis(SDL_JoystickID id, U8 axis, S16 value); + void onButton(SDL_JoystickID id, U8 button, bool pressed); + + void onKeyButton(U8 button, bool pressed); + void onKeyAxis(U8 axis, U16 value); + + void clearAllInput(); + void clearAllKeys(); + size_t getControllerIndex(SDL_JoystickID id) const; + + void computeFinalState(LLGameControl::State& state); + + void clear(); + +private: + std::vector mControllerIDs; + std::vector mControllers; + std::vector mStates; + LLGameControl::State mKeyboardState; +}; + +// local globals +namespace +{ + LLGameControl* g_gameControl = nullptr; + LLGameControllerManager g_manager; + + // The GameControlInput message is sent via UDP which is lossy. + // Since we send the only the list of pressed buttons the receiving + // side can compute the difference between subsequent states to + // find button-down/button-up events. + // + // To reduce the likelihood of buttons being stuck "pressed" forever + // on the receiving side (for lost final packet) we resend the last + // data state. However, to keep th ambient resend bandwidth low we + // expand the resend period at a geometric rate. + // + constexpr U64 MSEC_PER_NSEC = 1e6; + constexpr U64 FIRST_RESEND_PERIOD = 100 * MSEC_PER_NSEC; + constexpr U64 RESEND_EXPANSION_RATE = 10; + LLGameControl::State g_gameControlState; + U64 g_lastSend = 0; + U64 g_nextResendPeriod = FIRST_RESEND_PERIOD; + + std::map g_keyButtonMap; + std::map g_keyAxisMapPositive; + std::map g_keyAxisMapNegative; + + bool g_includeKeyboardButtons = false; + + constexpr U8 MAX_AXIS = 5; + constexpr U8 MAX_BUTTON = 31; +} + +LLGameControl::~LLGameControl() +{ + terminate(); +} + +LLGameControl::State::State() : mButtons(0) +{ + mAxes.resize(NUM_AXES, 0); + mPrevAxes.resize(NUM_AXES, 0); +} + +bool LLGameControl::State::onButton(U8 button, bool pressed) +{ + U32 old_buttons = mButtons; + if (button <= MAX_BUTTON) + { + if (pressed) + { + mButtons |= (0x01 << button); + } + else + { + mButtons &= ~(0x01 << button); + } + } + bool changed = (old_buttons != mButtons); + return changed; +} + +void LLGameControllerManager::addController(SDL_JoystickID id, SDL_GameController* controller) +{ + if (controller) + { + size_t i = 0; + for (; i < mControllerIDs.size(); ++i) + { + if (id == mControllerIDs[i]) + { + break; + } + } + if (i == mControllerIDs.size()) + { + mControllerIDs.push_back(id); + mControllers.push_back(controller); + mStates.push_back(LLGameControl::State()); + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " controller=" << controller + << LL_ENDL; + } + } +} + +void LLGameControllerManager::removeController(SDL_JoystickID id) +{ + size_t i = 0; + size_t num_controllers = mControllerIDs.size(); + for (; i < num_controllers; ++i) + { + if (id == mControllerIDs[i]) + { + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " controller=" << mControllers[i] + << LL_ENDL; + + mControllerIDs[i] = mControllerIDs[num_controllers - 1]; + mControllers[i] = mControllers[num_controllers - 1]; + mStates[i] = mStates[num_controllers - 1]; + + mControllerIDs.pop_back(); + mControllers.pop_back(); + mStates.pop_back(); + break; + } + } +} + +size_t LLGameControllerManager::getControllerIndex(SDL_JoystickID id) const +{ + constexpr size_t UNREASONABLY_HIGH_INDEX = 1e6; + size_t index = UNREASONABLY_HIGH_INDEX; + for (size_t i = 0; i < mControllers.size(); ++i) + { + if (id == mControllerIDs[i]) + { + index = i; + break; + } + } + return index; +} + +void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) +{ + if (axis > MAX_AXIS) + { + return; + } + size_t index = getControllerIndex(id); + if (index < mControllers.size()) + { + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " axis=" << (S32)(axis) + << " value=" << (S32)(value) << LL_ENDL; + mStates[index].mAxes[axis] = value; + } +} + +void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool pressed) +{ + size_t index = getControllerIndex(id); + if (index < mControllers.size()) + { + if (mStates[index].onButton(button, pressed)) + { + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " button i=" << (S32)(button) + << " pressed=" << pressed << LL_ENDL; + } + } +} + +void LLGameControllerManager::onKeyButton(U8 button, bool pressed) +{ + if (mKeyboardState.onButton(button, pressed)) + { + LL_DEBUGS("SDL2") << " keyboard button i=" << (S32)(button) << " pressed=" << pressed << LL_ENDL; + } +} + +void LLGameControllerManager::onKeyAxis(U8 axis, U16 value) +{ + if (mKeyboardState.mAxes[axis] != value) + { + mKeyboardState.mAxes[axis] = value; + LL_DEBUGS("SDL2") << " keyboard axis i=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; + } +} + +void LLGameControllerManager::clearAllInput() +{ + for (auto& state : mStates) + { + state.mButtons = 0; + std::fill(state.mAxes.begin(), state.mAxes.end(), 0); + } + mKeyboardState.mButtons = 0; + std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); +} + +void LLGameControllerManager::clearAllKeys() +{ + mKeyboardState.mButtons = 0; + std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); +} + +void LLGameControllerManager::computeFinalState(LLGameControl::State& state) +{ + // clear the slate + std::vector axes_accumulator; + axes_accumulator.resize(NUM_AXES, 0); + U32 old_buttons = state.mButtons; + state.mButtons = 0; + + // accumulate the controllers + for (const auto& s : mStates) + { + state.mButtons |= s.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + axes_accumulator[i] += (S32)(s.mAxes[i]); + } + } + + // accumulate the keyboard + state.mButtons |= mKeyboardState.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + axes_accumulator[i] += (S32)(mKeyboardState.mAxes[i]); + } + if (old_buttons != state.mButtons) + { + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + + // clamp the axes + for (size_t i = 0; i < NUM_AXES; ++i) + { + S32 new_axis = (S16)(std::min(std::max(axes_accumulator[i], -32768), 32767)); + // check for change + if (state.mAxes[i] != new_axis) + { + // When axis changes we explicitly update the corresponding prevAxis + // otherwise, we let prevAxis get updated in updateResendPeriod() + // which is explicitly called after a packet is sent. This allows + // unchanged axes to be included in first resend but not later ones. + state.mPrevAxes[i] = state.mAxes[i]; + state.mAxes[i] = new_axis; + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + } +} + +void LLGameControllerManager::clear() +{ + mControllerIDs.clear(); + mControllers.clear(); + mStates.clear(); +} + + +U64 get_now_nsec() +{ + std::chrono::time_point t0; + return (std::chrono::steady_clock::now() - t0).count(); +} + +// util for dumping SDL_GameController info +std::ostream& operator<<(std::ostream& out, SDL_GameController* c) +{ + if (! c) + { + return out << "nullptr"; + } + out << "{"; + out << " name='" << SDL_GameControllerName(c) << "'"; + out << " type='" << SDL_GameControllerGetType(c) << "'"; + out << " vendor='" << SDL_GameControllerGetVendor(c) << "'"; + out << " product='" << SDL_GameControllerGetProduct(c) << "'"; + out << " version='" << SDL_GameControllerGetProductVersion(c) << "'"; + //CRASH! out << " serial='" << SDL_GameControllerGetSerial(c) << "'"; + out << " }"; + return out; +} + +// util for dumping SDL_Joystick info +std::ostream& operator<<(std::ostream& out, SDL_Joystick* j) +{ + if (! j) + { + return out << "nullptr"; + } + out << "{"; + out << " p=0x" << (void*)(j); + out << " name='" << SDL_JoystickName(j) << "'"; + out << " type='" << SDL_JoystickGetType(j) << "'"; + out << " instance='" << SDL_JoystickInstanceID(j) << "'"; + out << " product='" << SDL_JoystickGetProduct(j) << "'"; + out << " version='" << SDL_JoystickGetProductVersion(j) << "'"; + out << " num_axes=" << SDL_JoystickNumAxes(j); + out << " num_balls=" << SDL_JoystickNumBalls(j); + out << " num_hats=" << SDL_JoystickNumHats(j); + out << " num_buttons=" << SDL_JoystickNumHats(j); + out << " }"; + return out; +} + +void onControllerDeviceAdded(const SDL_Event& event) +{ + int device_index = event.cdevice.which; + SDL_JoystickID id = SDL_JoystickGetDeviceInstanceID(device_index); + SDL_GameController* controller = SDL_GameControllerOpen(device_index); + + g_manager.addController(id, controller); +} + +void onControllerDeviceRemoved(const SDL_Event& event) +{ + SDL_JoystickID id = event.cdevice.which; + g_manager.removeController(id); +} + +void onControllerButton(const SDL_Event& event) +{ + g_manager.onButton(event.cbutton.which, event.cbutton.button, event.cbutton.state == SDL_PRESSED); +} + +void onControllerAxis(const SDL_Event& event) +{ + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << event.caxis.which << std::dec + << " axis=" << (S32)(event.caxis.axis) + << " value=" << (S32)(event.caxis.value) << LL_ENDL; + g_manager.onAxis(event.caxis.which, event.caxis.axis, event.caxis.value); +} + +// static +bool LLGameControl::isInitialized() +{ + return g_gameControl != nullptr; +} + +void sdl_logger(void *userdata, int category, SDL_LogPriority priority, const char *message) +{ + LL_DEBUGS("SDL2") << "log='" << message << "'" << LL_ENDL; +} + +// static +void LLGameControl::init() +{ + if (!g_gameControl) + { + g_gameControl = LLGameControl::getInstance(); + SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + SDL_LogSetOutputFunction(&sdl_logger, nullptr); + } +} + +// static +void LLGameControl::terminate() +{ + g_manager.clear(); + SDL_Quit(); +} + +// static +void LLGameControl::addKeyButtonMap(U16 key, U8 button) +{ + g_keyButtonMap[key] = button; +} + +// static +void LLGameControl::removeKeyButtonMap(U16 key) +{ + g_keyButtonMap.erase(key); +} + +// static +void LLGameControl::addKeyAxisMap(U16 key, U8 axis, bool positive) +{ + if (axis > MAX_AXIS) + { + return; + } + if (positive) + { + g_keyAxisMapPositive[key] = axis; + g_keyAxisMapNegative.erase(key); + } + else + { + g_keyAxisMapNegative[key] = axis; + g_keyAxisMapPositive.erase(key); + } +} + +// static +void LLGameControl::removeKeyAxisMap(U16 key) +{ + g_keyAxisMapPositive.erase(key); + g_keyAxisMapNegative.erase(key); +} + +// static +void LLGameControl::onKeyDown(U16 key, U32 mask) +{ + auto itr = g_keyButtonMap.find(key); + if (itr != g_keyButtonMap.end()) + { + g_manager.onKeyButton(itr->second, true); + } + else + { + itr = g_keyAxisMapPositive.find(key); + if (itr != g_keyAxisMapPositive.end()) + { + g_manager.onKeyAxis(itr->second, 32767); + } + else + { + itr = g_keyAxisMapNegative.find(key); + if (itr != g_keyAxisMapNegative.end()) + { + g_manager.onKeyAxis(itr->second, -32768); + } + } + } +} + +// static +void LLGameControl::onKeyUp(U16 key, U32 mask) +{ + auto itr = g_keyButtonMap.find(key); + if (itr != g_keyButtonMap.end()) + { + g_manager.onKeyButton(itr->second, true); + } + else + { + itr = g_keyAxisMapPositive.find(key); + if (itr != g_keyAxisMapPositive.end()) + { + g_manager.onKeyAxis(itr->second, 0); + } + else + { + itr = g_keyAxisMapNegative.find(key); + if (itr != g_keyAxisMapNegative.end()) + { + g_manager.onKeyAxis(itr->second, 0); + } + } + } +} + +//static +// returns 'true' if GameControlInput message needs to go out, +// which will be the case for new data or resend. Call this right +// before deciding to put a GameControlInput packet on the wire +// or not. +bool LLGameControl::computeFinalInputAndCheckForChanges() +{ + g_manager.computeFinalState(g_gameControlState); + return g_lastSend + g_nextResendPeriod < get_now_nsec(); +} + +// static +void LLGameControl::clearAllInput() +{ + g_manager.clearAllInput(); +} + +// static +void LLGameControl::clearAllKeys() +{ + g_manager.clearAllKeys(); +} + +// static +void LLGameControl::processEvents(bool app_has_focus) +{ + SDL_Event event; + if (!app_has_focus) + { + // when SL window lacks focus: pump SDL events but ignore them + while (g_gameControl && SDL_PollEvent(&event)) + { + // do nothing: SDL_PollEvent() is the operator + } + clearAllInput(); + return; + } + + while (g_gameControl && SDL_PollEvent(&event)) + { + switch (event.type) + { + case SDL_CONTROLLERDEVICEADDED: + onControllerDeviceAdded(event); + break; + case SDL_CONTROLLERDEVICEREMOVED: + onControllerDeviceRemoved(event); + break; + case SDL_CONTROLLERBUTTONDOWN: + /* FALLTHROUGH */ + case SDL_CONTROLLERBUTTONUP: + onControllerButton(event); + break; + case SDL_CONTROLLERAXISMOTION: + onControllerAxis(event); + break; + default: + break; + } + } +} + +// static +const LLGameControl::State& LLGameControl::getState() +{ + return g_gameControlState; +} + +// static +void LLGameControl::setIncludeKeyboardButtons(bool include) +{ + g_includeKeyboardButtons = include; +} + +// static +bool LLGameControl::getIncludeKeyboardButtons() +{ + return g_includeKeyboardButtons; +} + +//static +void LLGameControl::updateResendPeriod() +{ + // we expect this method to be called right after data is sent + g_lastSend = get_now_nsec(); + if (g_nextResendPeriod == 0) + { + g_nextResendPeriod = FIRST_RESEND_PERIOD; + } + else + { + // Reset mPrevAxes only on second resend or higher + // because when the joysticks are being used we expect a steady stream + // of recorrection data rather than sparse changes. + // + // In other words: we want to include changed axes in the first resend + // so we only overrite g_gameControlState.mPrevAxes on higher resends. + g_gameControlState.mPrevAxes = g_gameControlState.mAxes; + g_nextResendPeriod *= RESEND_EXPANSION_RATE; + } +} + -- cgit v1.2.3 From ed6ecca2a45e52d9be1d91107b9643b5ecdfb8bf Mon Sep 17 00:00:00 2001 From: Leviathan Linden Date: Thu, 16 Nov 2023 13:53:37 -0800 Subject: avatar_motion-->GameControl translation and flycam --- indra/llwindow/llgamecontrol.cpp | 687 +++++++++++++++++++++++++++++---------- 1 file changed, 518 insertions(+), 169 deletions(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 5dc01c5e54..9853eb763a 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -28,40 +28,198 @@ #include #include -#include +#include #include "SDL2/SDL.h" #include "SDL2/SDL_gamecontroller.h" #include "SDL2/SDL_joystick.h" +#include "indra_constants.h" +#include "llgamecontroltranslator.h" + constexpr size_t NUM_AXES = 6; +std::string LLGameControl::InputChannel::getLocalName() const +{ + // HACK: we hard-code English channel names, but + // they should be loaded from localized XML config files. + std::string name = " "; + if (mType == LLGameControl::InputChannel::TYPE_AXIS) + { + if (mIndex < (U8)(NUM_AXES)) + { + name = "AXIS_"; + name.append(std::to_string((S32)(mIndex))); + if (mSign < 0) + { + name.append("-"); + } + else if (mSign > 0) + { + name.append("+"); + } + } + } + else if (mType == LLGameControl::InputChannel::TYPE_BUTTON) + { + constexpr U8 NUM_BUTTONS = 32; + if (mIndex < NUM_BUTTONS) + { + name = "BUTTON_"; + name.append(std::to_string((S32)(mIndex))); + } + } + return name; +} + +std::string LLGameControl::InputChannel::getRemoteName() const +{ + // HACK: we hard-code English channel names, but + // they should be loaded from localized XML config files. + std::string name = " "; + // GAME_CONTROL_AXIS_LEFTX, GAME_CONTROL_BUTTON_A, etc + if (mType == LLGameControl::InputChannel::TYPE_AXIS) + { + switch(mIndex) + { + case 0: + name = "GAME_CONTROL_AXIS_LEFTX"; + break; + case 1: + name = "GAME_CONTROL_AXIS_LEFTY"; + break; + case 2: + name = "GAME_CONTROL_AXIS_RIGHTX"; + break; + case 3: + name = "GAME_CONTROL_AXIS_RIGHTY"; + break; + case 4: + name = "GAME_CONTROL_AXIS_PADDLELEFT"; + break; + case 5: + name = "GAME_CONTROL_AXIS_PADDLERIGHT"; + break; + default: + break; + } + } + else if (mType == LLGameControl::InputChannel::TYPE_BUTTON) + { + switch(mIndex) + { + case 0: + name = "GAME_CONTROL_BUTTON_A"; + break; + case 1: + name = "GAME_CONTROL_BUTTON_B"; + break; + case 2: + name = "GAME_CONTROL_BUTTON_X"; + break; + case 3: + name = "GAME_CONTROL_BUTTON_Y"; + break; + case 4: + name = "GAME_CONTROL_BUTTON_BACK"; + break; + case 5: + name = "GAME_CONTROL_BUTTON_GUIDE"; + break; + case 6: + name = "GAME_CONTROL_BUTTON_START"; + break; + case 7: + name = "GAME_CONTROL_BUTTON_LEFTSTICK"; + break; + case 8: + name = "GAME_CONTROL_BUTTON_RIGHTSTICK"; + break; + case 9: + name = "GAME_CONTROL_BUTTON_LEFTSHOULDER"; + break; + case 10: + name = "GAME_CONTROL_BUTTON_RIGHTSHOULDER"; + break; + case 11: + name = "GAME_CONTROL_BUTTON_DPAD_UP"; + break; + case 12: + name = "GAME_CONTROL_BUTTON_DPAD_DOWN"; + break; + case 13: + name = "GAME_CONTROL_BUTTON_DPAD_LEFT"; + break; + case 14: + name = "GAME_CONTROL_BUTTON_DPAD_RIGHT"; + break; + case 15: + name = "GAME_CONTROL_BUTTON_MISC1"; + break; + case 16: + name = "GAME_CONTROL_BUTTON_PADDLE1"; + break; + case 17: + name = "GAME_CONTROL_BUTTON_PADDLE2"; + break; + case 18: + name = "GAME_CONTROL_BUTTON_PADDLE3"; + break; + case 19: + name = "GAME_CONTROL_BUTTON_PADDLE4"; + break; + case 20: + name = "GAME_CONTROL_BUTTON_TOUCHPAD"; + break; + default: + break; + } + } + return name; +} + + // internal class for managing list of controllers and per-controller state class LLGameControllerManager { public: + using ActionToChannelMap = std::map< std::string, LLGameControl::InputChannel >; + LLGameControllerManager(); + void addController(SDL_JoystickID id, SDL_GameController* controller); void removeController(SDL_JoystickID id); void onAxis(SDL_JoystickID id, U8 axis, S16 value); void onButton(SDL_JoystickID id, U8 button, bool pressed); - void onKeyButton(U8 button, bool pressed); - void onKeyAxis(U8 axis, U16 value); - - void clearAllInput(); - void clearAllKeys(); + void clearAllState(); size_t getControllerIndex(SDL_JoystickID id) const; + void accumulateInternalState(); void computeFinalState(LLGameControl::State& state); + LLGameControl::InputChannel getChannelByName(const std::string& name) const; + LLGameControl::InputChannel getChannelByActionName(const std::string& action_name) const; + + bool updateActionMap(const std::string& name, LLGameControl::InputChannel channel); + U32 computeInternalActionFlags(); + void getCameraInputs(std::vector& inputs_out); + void setExternalActionFlags(U32 action_flags); + void clear(); private: std::vector mControllerIDs; std::vector mControllers; - std::vector mStates; - LLGameControl::State mKeyboardState; + std::vector mStates; // one state per device + + LLGameControl::State mExternalState; + LLGameControlTranslator mActionTranslator; + ActionToChannelMap mCameraChannelMap; + std::vector mAxesAccumulator; + U32 mButtonAccumulator { 0 }; + U32 mLastActionFlags { 0 }; + U32 mLastCameraActionFlags { 0 }; }; // local globals @@ -83,15 +241,16 @@ namespace constexpr U64 MSEC_PER_NSEC = 1e6; constexpr U64 FIRST_RESEND_PERIOD = 100 * MSEC_PER_NSEC; constexpr U64 RESEND_EXPANSION_RATE = 10; - LLGameControl::State g_gameControlState; + LLGameControl::State g_outerState; // from controller devices + LLGameControl::State g_innerState; // state from gAgent + LLGameControl::State g_finalState; // sum of inner and outer U64 g_lastSend = 0; U64 g_nextResendPeriod = FIRST_RESEND_PERIOD; - std::map g_keyButtonMap; - std::map g_keyAxisMapPositive; - std::map g_keyAxisMapNegative; - - bool g_includeKeyboardButtons = false; + bool g_sendToServer = false; + bool g_controlAgent = false; + bool g_translateAgentActions = false; + LLGameControl::AgentControlMode g_agentControlMode = LLGameControl::CONTROL_MODE_AVATAR; constexpr U8 MAX_AXIS = 5; constexpr U8 MAX_BUTTON = 31; @@ -108,6 +267,16 @@ LLGameControl::State::State() : mButtons(0) mPrevAxes.resize(NUM_AXES, 0); } +void LLGameControl::State::clear() +{ + std::fill(mAxes.begin(), mAxes.end(), 0); + + // DO NOT clear mPrevAxes because those are managed by external logic. + //std::fill(mPrevAxes.begin(), mPrevAxes.end(), 0); + + mButtons = 0; +} + bool LLGameControl::State::onButton(U8 button, bool pressed) { U32 old_buttons = mButtons; @@ -126,6 +295,71 @@ bool LLGameControl::State::onButton(U8 button, bool pressed) return changed; } +LLGameControllerManager::LLGameControllerManager() +{ + mAxesAccumulator.resize(NUM_AXES, 0); + + // Here we build an invarient map between the named agent actions + // and control bit sent to the server. This map will be used, + // in combination with the action->InputChannel map below, + // to maintain an inverse map from control bit masks to GameControl data. + LLGameControlTranslator::ActionToMaskMap actions; + actions["push+"] = AGENT_CONTROL_AT_POS | AGENT_CONTROL_FAST_AT; + actions["push-"] = AGENT_CONTROL_AT_NEG | AGENT_CONTROL_FAST_AT; + actions["slide+"] = AGENT_CONTROL_LEFT_POS | AGENT_CONTROL_FAST_LEFT; + actions["slide-"] = AGENT_CONTROL_LEFT_NEG | AGENT_CONTROL_FAST_LEFT; + actions["jump+"] = AGENT_CONTROL_UP_POS | AGENT_CONTROL_FAST_UP; + actions["jump-"] = AGENT_CONTROL_UP_NEG | AGENT_CONTROL_FAST_UP; + actions["turn+"] = AGENT_CONTROL_YAW_POS; + actions["turn-"] = AGENT_CONTROL_YAW_NEG; + actions["look+"] = AGENT_CONTROL_PITCH_POS; + actions["look-"] = AGENT_CONTROL_PITCH_NEG; + actions["stop"] = AGENT_CONTROL_STOP; + // These are HACKs. We borrow some AGENT_CONTROL bits for "unrelated" features. + // Not a problem because these bits are only used internally. + actions["toggle_run"] = AGENT_CONTROL_NUDGE_AT_POS; // HACK + actions["toggle_fly"] = AGENT_CONTROL_FLY; // HACK + actions["toggle_sit"] = AGENT_CONTROL_SIT_ON_GROUND; // HACK + actions["toggle_flycam"] = AGENT_CONTROL_NUDGE_AT_NEG; // HACK + mActionTranslator.setAvailableActions(actions); + + // Here we build a list of pairs between named agent actions and + // GameControl channels. Note: we only supply the non-signed names + // (e.g. "push" instead of "push+" and "push-") because mActionTranator + // automatially expands action names as necessary. + using type = LLGameControl::InputChannel::Type; + std::vector< std::pair< std::string, LLGameControl::InputChannel> > agent_defaults = + { + { "push", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), 1 } }, + { "slide", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), 1 } }, + { "jump", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERLEFT), 1 } }, + { "turn", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), 1 } }, + { "look", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), 1 } }, + { "toggle_run", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSHOULDER) } }, + { "toggle_fly", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_DPAD_UP) } }, + { "toggle_sit", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_DPAD_DOWN) } }, + { "toggle_flycam", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_RIGHTSHOULDER) } }, + { "stop", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSTICK) } } + }; + mActionTranslator.setMappings(agent_defaults); + + // Camera actions don't need bitwise translation, so we maintain the map in + // here directly rather than using an LLGameControlTranslator. + // Note: there must NOT be duplicate names between avatar and camera actions + mCameraChannelMap = + { + { "move", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), -1 } }, + { "pan", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), -1 } }, + { "rise", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT), -1 } }, + { "pitch", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), -1 } }, + { "yaw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), -1 } }, + { "zoom", { type::TYPE_NONE, 0 } }, + // TODO?: allow flycam to roll + //{ "roll_ccw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT) } }, + //{ "roll_cw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERLEFT) } } + }; +} + void LLGameControllerManager::addController(SDL_JoystickID id, SDL_GameController* controller) { if (controller) @@ -198,6 +432,25 @@ void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) size_t index = getControllerIndex(id); if (index < mControllers.size()) { + // Note: the RAW analog joystics provide NEGATIVE X,Y values for LEFT,FORWARD + // whereas those directions are actually POSITIVE in SL's local right-handed + // reference frame. Therefore we implicitly negate those axes here where + // they are extracted from SDL, before being used anywhere. + if (axis < SDL_CONTROLLER_AXIS_TRIGGERLEFT) + { + // Note: S16 value is in range [-32768, 32767] which means + // the negative range has an extra possible value. We need + // to add (or subtract) one during negation. + if (value < 0) + { + value = - (value + 1); + } + else if (value > 0) + { + value = (-value) - 1; + } + } + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec << " axis=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; @@ -219,83 +472,145 @@ void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool presse } } -void LLGameControllerManager::onKeyButton(U8 button, bool pressed) +void LLGameControllerManager::clearAllState() { - if (mKeyboardState.onButton(button, pressed)) + for (auto& state : mStates) { - LL_DEBUGS("SDL2") << " keyboard button i=" << (S32)(button) << " pressed=" << pressed << LL_ENDL; + state.clear(); } + mExternalState.clear(); + mLastActionFlags = 0; + mLastCameraActionFlags = 0; } -void LLGameControllerManager::onKeyAxis(U8 axis, U16 value) +void LLGameControllerManager::accumulateInternalState() { - if (mKeyboardState.mAxes[axis] != value) + // clear the old state + std::fill(mAxesAccumulator.begin(), mAxesAccumulator.end(), 0); + mButtonAccumulator = 0; + + // accumulate the controllers + for (const auto& state : mStates) { - mKeyboardState.mAxes[axis] = value; - LL_DEBUGS("SDL2") << " keyboard axis i=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; + mButtonAccumulator |= state.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + // Note: we don't bother to clamp the axes yet + // because at this stage we haven't yet accumulated the "inner" state. + mAxesAccumulator[i] += (S32)(state.mAxes[i]); + } } } -void LLGameControllerManager::clearAllInput() +void LLGameControllerManager::computeFinalState(LLGameControl::State& final_state) { - for (auto& state : mStates) + // We assume accumulateInternalState() has already been called and we will + // finish by accumulating "external" state (if enabled) + U32 old_buttons = final_state.mButtons; + final_state.mButtons = mButtonAccumulator; + if (g_translateAgentActions) + { + // accumulate from mExternalState + final_state.mButtons |= mExternalState.mButtons; + for (size_t i = 0; i < NUM_AXES; ++i) + { + mAxesAccumulator[i] += (S32)(mExternalState.mAxes[i]); + } + } + if (old_buttons != final_state.mButtons) { - state.mButtons = 0; - std::fill(state.mAxes.begin(), state.mAxes.end(), 0); + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + + // clamp the accumulated axes + for (size_t i = 0; i < NUM_AXES; ++i) + { + S32 new_axis = (S16)(std::min(std::max(mAxesAccumulator[i], -32768), 32767)); + // check for change + if (final_state.mAxes[i] != new_axis) + { + // When axis changes we explicitly update the corresponding prevAxis + // prior to storing new_axis. The only other place where prevAxis + // is updated in updateResendPeriod() which is explicitly called after + // a packet is sent. The result is: unchanged axes are included in + // first resend but not later ones. + final_state.mPrevAxes[i] = final_state.mAxes[i]; + final_state.mAxes[i] = new_axis; + g_nextResendPeriod = 0; // packet needs to go out ASAP + } } - mKeyboardState.mButtons = 0; - std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); } -void LLGameControllerManager::clearAllKeys() +LLGameControl::InputChannel LLGameControllerManager::getChannelByActionName(const std::string& name) const { - mKeyboardState.mButtons = 0; - std::fill(mKeyboardState.mAxes.begin(), mKeyboardState.mAxes.end(), 0); + LLGameControl::InputChannel channel = mActionTranslator.getChannelByAction(name); + if (channel.isNone()) + { + //maybe we're looking for a camera action + ActionToChannelMap::const_iterator itr = mCameraChannelMap.find(name); + if (itr != mCameraChannelMap.end()) + { + channel = itr->second; + } + } + return channel; } -void LLGameControllerManager::computeFinalState(LLGameControl::State& state) +bool LLGameControllerManager::updateActionMap(const std::string& action, LLGameControl::InputChannel channel) { - // clear the slate - std::vector axes_accumulator; - axes_accumulator.resize(NUM_AXES, 0); - U32 old_buttons = state.mButtons; - state.mButtons = 0; - - // accumulate the controllers - for (const auto& s : mStates) + bool success = mActionTranslator.updateMap(action, channel); + if (success) { - state.mButtons |= s.mButtons; - for (size_t i = 0; i < NUM_AXES; ++i) + mLastActionFlags = 0; + } + else + { + // maybe we're looking for a camera action + ActionToChannelMap::iterator itr = mCameraChannelMap.find(action); + if (itr != mCameraChannelMap.end()) { - axes_accumulator[i] += (S32)(s.mAxes[i]); + itr->second = channel; + success = true; } } + if (!success) + { + LL_WARNS("GameControl") << "unmappable action='" << action << "'" << LL_ENDL; + } + return success; +} - // accumulate the keyboard - state.mButtons |= mKeyboardState.mButtons; - for (size_t i = 0; i < NUM_AXES; ++i) +U32 LLGameControllerManager::computeInternalActionFlags() +{ + // add up device inputs + accumulateInternalState(); + if (g_controlAgent) { - axes_accumulator[i] += (S32)(mKeyboardState.mAxes[i]); + return mActionTranslator.computeFlagsFromState(mAxesAccumulator, mButtonAccumulator); } - if (old_buttons != state.mButtons) + return 0; +} + +void LLGameControllerManager::getCameraInputs(std::vector& inputs_out) +{ + // TODO: fill inputs_out with real data + inputs_out.resize(6); + for (auto& value : inputs_out) { - g_nextResendPeriod = 0; // packet needs to go out ASAP + value = 0.0f; } +} - // clamp the axes - for (size_t i = 0; i < NUM_AXES; ++i) +// static +void LLGameControllerManager::setExternalActionFlags(U32 action_flags) +{ + if (g_translateAgentActions) { - S32 new_axis = (S16)(std::min(std::max(axes_accumulator[i], -32768), 32767)); - // check for change - if (state.mAxes[i] != new_axis) + U32 active_flags = action_flags & mActionTranslator.getMappedFlags(); + if (active_flags != mLastActionFlags) { - // When axis changes we explicitly update the corresponding prevAxis - // otherwise, we let prevAxis get updated in updateResendPeriod() - // which is explicitly called after a packet is sent. This allows - // unchanged axes to be included in first resend but not later ones. - state.mPrevAxes[i] = state.mAxes[i]; - state.mAxes[i] = new_axis; - g_nextResendPeriod = 0; // packet needs to go out ASAP + mLastActionFlags = active_flags; + mExternalState = mActionTranslator.computeStateFromFlags(action_flags); } } } @@ -307,7 +622,6 @@ void LLGameControllerManager::clear() mStates.clear(); } - U64 get_now_nsec() { std::chrono::time_point t0; @@ -411,117 +725,27 @@ void LLGameControl::terminate() SDL_Quit(); } -// static -void LLGameControl::addKeyButtonMap(U16 key, U8 button) -{ - g_keyButtonMap[key] = button; -} - -// static -void LLGameControl::removeKeyButtonMap(U16 key) -{ - g_keyButtonMap.erase(key); -} - -// static -void LLGameControl::addKeyAxisMap(U16 key, U8 axis, bool positive) -{ - if (axis > MAX_AXIS) - { - return; - } - if (positive) - { - g_keyAxisMapPositive[key] = axis; - g_keyAxisMapNegative.erase(key); - } - else - { - g_keyAxisMapNegative[key] = axis; - g_keyAxisMapPositive.erase(key); - } -} - -// static -void LLGameControl::removeKeyAxisMap(U16 key) -{ - g_keyAxisMapPositive.erase(key); - g_keyAxisMapNegative.erase(key); -} - -// static -void LLGameControl::onKeyDown(U16 key, U32 mask) -{ - auto itr = g_keyButtonMap.find(key); - if (itr != g_keyButtonMap.end()) - { - g_manager.onKeyButton(itr->second, true); - } - else - { - itr = g_keyAxisMapPositive.find(key); - if (itr != g_keyAxisMapPositive.end()) - { - g_manager.onKeyAxis(itr->second, 32767); - } - else - { - itr = g_keyAxisMapNegative.find(key); - if (itr != g_keyAxisMapNegative.end()) - { - g_manager.onKeyAxis(itr->second, -32768); - } - } - } -} - -// static -void LLGameControl::onKeyUp(U16 key, U32 mask) -{ - auto itr = g_keyButtonMap.find(key); - if (itr != g_keyButtonMap.end()) - { - g_manager.onKeyButton(itr->second, true); - } - else - { - itr = g_keyAxisMapPositive.find(key); - if (itr != g_keyAxisMapPositive.end()) - { - g_manager.onKeyAxis(itr->second, 0); - } - else - { - itr = g_keyAxisMapNegative.find(key); - if (itr != g_keyAxisMapNegative.end()) - { - g_manager.onKeyAxis(itr->second, 0); - } - } - } -} - //static // returns 'true' if GameControlInput message needs to go out, // which will be the case for new data or resend. Call this right // before deciding to put a GameControlInput packet on the wire // or not. -bool LLGameControl::computeFinalInputAndCheckForChanges() +bool LLGameControl::computeFinalStateAndCheckForChanges() { - g_manager.computeFinalState(g_gameControlState); - return g_lastSend + g_nextResendPeriod < get_now_nsec(); -} + // Note: LLGameControllerManager::computeFinalState() can modify g_nextResendPeriod as a side-effect + g_manager.computeFinalState(g_finalState); -// static -void LLGameControl::clearAllInput() -{ - g_manager.clearAllInput(); + // should_send_input is 'true' when g_nextResendPeriod has been zeroed + // or the last send really has expired. + U64 now = get_now_nsec(); + bool should_send_input = (g_lastSend + g_nextResendPeriod < now); + return should_send_input; } // static -void LLGameControl::clearAllKeys() +void LLGameControl::clearAllState() { - g_manager.clearAllKeys(); + g_manager.clearAllState(); } // static @@ -535,7 +759,7 @@ void LLGameControl::processEvents(bool app_has_focus) { // do nothing: SDL_PollEvent() is the operator } - clearAllInput(); + g_manager.clearAllState(); return; } @@ -566,19 +790,141 @@ void LLGameControl::processEvents(bool app_has_focus) // static const LLGameControl::State& LLGameControl::getState() { - return g_gameControlState; + return g_finalState; } // static -void LLGameControl::setIncludeKeyboardButtons(bool include) +void LLGameControl::getCameraInputs(std::vector& inputs_out) { - g_includeKeyboardButtons = include; + return g_manager.getCameraInputs(inputs_out); } // static -bool LLGameControl::getIncludeKeyboardButtons() +void LLGameControl::enableSendToServer(bool enable) { - return g_includeKeyboardButtons; + g_sendToServer = enable; +} + +// static +void LLGameControl::enableControlAgent(bool enable) +{ + g_controlAgent = enable; +} + +// static +void LLGameControl::enableTranslateAgentActions(bool enable) +{ + g_translateAgentActions = enable; +} + +void LLGameControl::setAgentControlMode(LLGameControl::AgentControlMode mode) +{ + g_agentControlMode = mode; +} + +// static +bool LLGameControl::willSendToServer() +{ + return g_sendToServer; +} + +// static +bool LLGameControl::willControlAvatar() +{ + return g_controlAgent && g_agentControlMode == CONTROL_MODE_AVATAR; +} + +// static +bool LLGameControl::willControlFlycam() +{ + return g_controlAgent && g_agentControlMode == CONTROL_MODE_FLYCAM; +} + +// static +bool LLGameControl::willTranslateAgentActions() +{ + return g_translateAgentActions; +} + +/* +// static +LLGameControl::LocalControlMode LLGameControl::getLocalControlMode() +{ + return g_agentControlMode; +} +*/ + +// static +// +// Given a name like "AXIS_1-" or "BUTTON_5" returns the corresponding InputChannel +// If the axis name lacks the +/- postfix it assumes '+' postfix. +LLGameControl::InputChannel LLGameControl::getChannelByName(const std::string& name) +{ + LLGameControl::InputChannel channel; + // 'name' has two acceptable formats: AXIS_[sign] or BUTTON_ + if (name.length() < 6) + { + // name must be at least as long as 'AXIS_n' + return channel; + } + if (name.rfind("AXIS_", 0) == 0) + { + char c = name[5]; + if (c >= '0') + { + channel.mType = LLGameControl::InputChannel::Type::TYPE_AXIS; + channel.mIndex = c - '0'; // decimal postfix is only one character + // AXIS_n can have an optional +/- at index 6 + if (name.length() >= 6) + { + channel.mSign = (name[6] == '-') ? -1 : 1; + } + else + { + // assume positive axis when sign not provided + channel.mSign = 1; + } + } + } + else if (name.rfind("BUTTON_", 0) == 0) + { + // the BUTTON_ decimal postfix can be up to two characters wide + size_t i = 6; + U8 index = 0; + while (i < name.length() && i < 8 && name[i] <= '0') + { + index = index * 10 + name[i] - '0'; + } + channel.mType = LLGameControl::InputChannel::Type::TYPE_BUTTON; + channel.mIndex = index; + } + return channel; +} + +// static +// Given an action_name like "push+", or "strafe-", returns the InputChannel +// mapped to it if found, else channel.isNone() will be true. +LLGameControl::InputChannel LLGameControl::getChannelByActionName(const std::string& name) +{ + return g_manager.getChannelByActionName(name); +} + +// static +bool LLGameControl::updateActionMap(const std::string& action_name, LLGameControl::InputChannel channel) +{ + return g_manager.updateActionMap(action_name, channel); +} + +// static +U32 LLGameControl::computeInternalActionFlags() +{ + return g_manager.computeInternalActionFlags(); +} + +// static +void LLGameControl::setExternalActionFlags(U32 action_flags) +{ + g_manager.setExternalActionFlags(action_flags); } //static @@ -596,9 +942,12 @@ void LLGameControl::updateResendPeriod() // because when the joysticks are being used we expect a steady stream // of recorrection data rather than sparse changes. // + // (The above assumption is not necessarily true for "Actions" input + // (e.g. keyboard events). TODO: figure out what to do about this.) + // // In other words: we want to include changed axes in the first resend - // so we only overrite g_gameControlState.mPrevAxes on higher resends. - g_gameControlState.mPrevAxes = g_gameControlState.mAxes; + // so we only overwrite g_finalState.mPrevAxes on higher resends. + g_finalState.mPrevAxes = g_finalState.mAxes; g_nextResendPeriod *= RESEND_EXPANSION_RATE; } } -- cgit v1.2.3 From ec39ac89e8529da206dafd519d75ad5944888076 Mon Sep 17 00:00:00 2001 From: leviathan Date: Fri, 1 Mar 2024 14:04:54 -0800 Subject: more GameControl prefs UI --- indra/llwindow/llgamecontrol.cpp | 524 ++++++++++++++++++++++++--------------- 1 file changed, 321 insertions(+), 203 deletions(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 9853eb763a..a1ab2dd52d 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -35,10 +35,51 @@ #include "SDL2/SDL_joystick.h" #include "indra_constants.h" +#include "llfile.h" #include "llgamecontroltranslator.h" constexpr size_t NUM_AXES = 6; +// util for dumping SDL_GameController info +std::ostream& operator<<(std::ostream& out, SDL_GameController* c) +{ + if (!c) + { + return out << "nullptr"; + } + out << "{"; + out << " name='" << SDL_GameControllerName(c) << "'"; + out << " type='" << SDL_GameControllerGetType(c) << "'"; + out << " vendor='" << SDL_GameControllerGetVendor(c) << "'"; + out << " product='" << SDL_GameControllerGetProduct(c) << "'"; + out << " version='" << SDL_GameControllerGetProductVersion(c) << "'"; + //CRASH! out << " serial='" << SDL_GameControllerGetSerial(c) << "'"; + out << " }"; + return out; +} + +// util for dumping SDL_Joystick info +std::ostream& operator<<(std::ostream& out, SDL_Joystick* j) +{ + if (!j) + { + return out << "nullptr"; + } + out << "{"; + out << " p=0x" << (void*)(j); + out << " name='" << SDL_JoystickName(j) << "'"; + out << " type='" << SDL_JoystickGetType(j) << "'"; + out << " instance='" << SDL_JoystickInstanceID(j) << "'"; + out << " product='" << SDL_JoystickGetProduct(j) << "'"; + out << " version='" << SDL_JoystickGetProductVersion(j) << "'"; + out << " num_axes=" << SDL_JoystickNumAxes(j); + out << " num_balls=" << SDL_JoystickNumBalls(j); + out << " num_hats=" << SDL_JoystickNumHats(j); + out << " num_buttons=" << SDL_JoystickNumHats(j); + out << " }"; + return out; +} + std::string LLGameControl::InputChannel::getLocalName() const { // HACK: we hard-code English channel names, but @@ -192,34 +233,42 @@ public: void onAxis(SDL_JoystickID id, U8 axis, S16 value); void onButton(SDL_JoystickID id, U8 button, bool pressed); - void clearAllState(); - size_t getControllerIndex(SDL_JoystickID id) const; + void clearAllStates(); void accumulateInternalState(); void computeFinalState(LLGameControl::State& state); - LLGameControl::InputChannel getChannelByName(const std::string& name) const; LLGameControl::InputChannel getChannelByActionName(const std::string& action_name) const; + LLGameControl::InputChannel getFlycamChannelByActionName(const std::string& action_name) const; bool updateActionMap(const std::string& name, LLGameControl::InputChannel channel); U32 computeInternalActionFlags(); - void getCameraInputs(std::vector& inputs_out); - void setExternalActionFlags(U32 action_flags); + void getFlycamInputs(std::vector& inputs_out); + void setExternalInput(U32 action_flags, U32 buttons); void clear(); private: - std::vector mControllerIDs; - std::vector mControllers; - std::vector mStates; // one state per device + bool updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel); + + std::list mStates; // one state per device + using state_it = std::list::iterator; + state_it findState(SDL_JoystickID id) + { + return std::find_if(mStates.begin(), mStates.end(), + [id](LLGameControl::State& state) + { + return state.getJoystickID() == id; + }); + } LLGameControl::State mExternalState; LLGameControlTranslator mActionTranslator; - ActionToChannelMap mCameraChannelMap; + std::vector mFlycamChannels; std::vector mAxesAccumulator; U32 mButtonAccumulator { 0 }; - U32 mLastActionFlags { 0 }; - U32 mLastCameraActionFlags { 0 }; + U32 mLastActiveFlags { 0 }; + U32 mLastFlycamActionFlags { 0 }; }; // local globals @@ -235,7 +284,7 @@ namespace // // To reduce the likelihood of buttons being stuck "pressed" forever // on the receiving side (for lost final packet) we resend the last - // data state. However, to keep th ambient resend bandwidth low we + // data state. However, to keep the ambient resend bandwidth low we // expand the resend period at a geometric rate. // constexpr U64 MSEC_PER_NSEC = 1e6; @@ -267,6 +316,12 @@ LLGameControl::State::State() : mButtons(0) mPrevAxes.resize(NUM_AXES, 0); } +void LLGameControl::State::setDevice(int joystickID, void* controller) +{ + mJoystickID = joystickID; + mController = controller; +} + void LLGameControl::State::clear() { std::fill(mAxes.begin(), mAxes.end(), 0); @@ -299,7 +354,7 @@ LLGameControllerManager::LLGameControllerManager() { mAxesAccumulator.resize(NUM_AXES, 0); - // Here we build an invarient map between the named agent actions + // Here we build an invariant map between the named agent actions // and control bit sent to the server. This map will be used, // in combination with the action->InputChannel map below, // to maintain an inverse map from control bit masks to GameControl data. @@ -319,13 +374,12 @@ LLGameControllerManager::LLGameControllerManager() // Not a problem because these bits are only used internally. actions["toggle_run"] = AGENT_CONTROL_NUDGE_AT_POS; // HACK actions["toggle_fly"] = AGENT_CONTROL_FLY; // HACK - actions["toggle_sit"] = AGENT_CONTROL_SIT_ON_GROUND; // HACK actions["toggle_flycam"] = AGENT_CONTROL_NUDGE_AT_NEG; // HACK mActionTranslator.setAvailableActions(actions); // Here we build a list of pairs between named agent actions and // GameControl channels. Note: we only supply the non-signed names - // (e.g. "push" instead of "push+" and "push-") because mActionTranator + // (e.g. "push" instead of "push+" and "push-") because mActionTranslator // automatially expands action names as necessary. using type = LLGameControl::InputChannel::Type; std::vector< std::pair< std::string, LLGameControl::InputChannel> > agent_defaults = @@ -337,90 +391,50 @@ LLGameControllerManager::LLGameControllerManager() { "look", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), 1 } }, { "toggle_run", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSHOULDER) } }, { "toggle_fly", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_DPAD_UP) } }, - { "toggle_sit", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_DPAD_DOWN) } }, { "toggle_flycam", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_RIGHTSHOULDER) } }, { "stop", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSTICK) } } }; mActionTranslator.setMappings(agent_defaults); - // Camera actions don't need bitwise translation, so we maintain the map in - // here directly rather than using an LLGameControlTranslator. - // Note: there must NOT be duplicate names between avatar and camera actions - mCameraChannelMap = - { - { "move", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), -1 } }, - { "pan", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), -1 } }, - { "rise", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT), -1 } }, - { "pitch", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), -1 } }, - { "yaw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), -1 } }, - { "zoom", { type::TYPE_NONE, 0 } }, - // TODO?: allow flycam to roll - //{ "roll_ccw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT) } }, - //{ "roll_cw", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERLEFT) } } + // Flycam actions don't need bitwise translation, so we maintain the map + // of channels here directly rather than using an LLGameControlTranslator. + mFlycamChannels = { + { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), 1 }, // advance + { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), 1 }, // pan + { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT), 1 }, // rise + { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), -1 }, // pitch + { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), 1 }, // yaw + { type::TYPE_NONE, 0 } // zoom }; } void LLGameControllerManager::addController(SDL_JoystickID id, SDL_GameController* controller) { - if (controller) + LL_INFOS("GameController") << "joystick id: " << id << ", controller: " << controller << LL_ENDL; + + llassert(id >= 0); + llassert(controller); + + if (findState(id) != mStates.end()) { - size_t i = 0; - for (; i < mControllerIDs.size(); ++i) - { - if (id == mControllerIDs[i]) - { - break; - } - } - if (i == mControllerIDs.size()) - { - mControllerIDs.push_back(id); - mControllers.push_back(controller); - mStates.push_back(LLGameControl::State()); - LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec - << " controller=" << controller - << LL_ENDL; - } + LL_WARNS("GameController") << "device already added" << LL_ENDL; + return; } + + mStates.emplace_back().setDevice(id, controller); + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " controller=" << controller + << LL_ENDL; } void LLGameControllerManager::removeController(SDL_JoystickID id) { - size_t i = 0; - size_t num_controllers = mControllerIDs.size(); - for (; i < num_controllers; ++i) - { - if (id == mControllerIDs[i]) - { - LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec - << " controller=" << mControllers[i] - << LL_ENDL; - - mControllerIDs[i] = mControllerIDs[num_controllers - 1]; - mControllers[i] = mControllers[num_controllers - 1]; - mStates[i] = mStates[num_controllers - 1]; - - mControllerIDs.pop_back(); - mControllers.pop_back(); - mStates.pop_back(); - break; - } - } -} + LL_INFOS("GameController") << "joystick id: " << id << LL_ENDL; -size_t LLGameControllerManager::getControllerIndex(SDL_JoystickID id) const -{ - constexpr size_t UNREASONABLY_HIGH_INDEX = 1e6; - size_t index = UNREASONABLY_HIGH_INDEX; - for (size_t i = 0; i < mControllers.size(); ++i) - { - if (id == mControllerIDs[i]) + mStates.remove_if([id](LLGameControl::State& state) { - index = i; - break; - } - } - return index; + return state.getJoystickID() == id; + }); } void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) @@ -429,10 +443,11 @@ void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) { return; } - size_t index = getControllerIndex(id); - if (index < mControllers.size()) + + state_it it = findState(id); + if (it != mStates.end()) { - // Note: the RAW analog joystics provide NEGATIVE X,Y values for LEFT,FORWARD + // Note: the RAW analog joysticks provide NEGATIVE X,Y values for LEFT,FORWARD // whereas those directions are actually POSITIVE in SL's local right-handed // reference frame. Therefore we implicitly negate those axes here where // they are extracted from SDL, before being used anywhere. @@ -454,33 +469,33 @@ void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec << " axis=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; - mStates[index].mAxes[axis] = value; + it->mAxes[axis] = value; } } void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool pressed) { - size_t index = getControllerIndex(id); - if (index < mControllers.size()) + state_it it = findState(id); + if (it != mStates.end()) { - if (mStates[index].onButton(button, pressed)) + if (it->onButton(button, pressed)) { LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec << " button i=" << (S32)(button) - << " pressed=" << pressed << LL_ENDL; + << " pressed=" << pressed << LL_ENDL; } } } -void LLGameControllerManager::clearAllState() +void LLGameControllerManager::clearAllStates() { for (auto& state : mStates) { state.clear(); } mExternalState.clear(); - mLastActionFlags = 0; - mLastCameraActionFlags = 0; + mLastActiveFlags = 0; + mLastFlycamActionFlags = 0; } void LLGameControllerManager::accumulateInternalState() @@ -512,10 +527,6 @@ void LLGameControllerManager::computeFinalState(LLGameControl::State& final_stat { // accumulate from mExternalState final_state.mButtons |= mExternalState.mButtons; - for (size_t i = 0; i < NUM_AXES; ++i) - { - mAxesAccumulator[i] += (S32)(mExternalState.mAxes[i]); - } } if (old_buttons != final_state.mButtons) { @@ -525,17 +536,26 @@ void LLGameControllerManager::computeFinalState(LLGameControl::State& final_stat // clamp the accumulated axes for (size_t i = 0; i < NUM_AXES; ++i) { - S32 new_axis = (S16)(std::min(std::max(mAxesAccumulator[i], -32768), 32767)); + S32 axis = mAxesAccumulator[i]; + if (g_translateAgentActions) + { + // Note: we accumulate mExternalState onto local 'axis' variable + // rather than onto mAxisAccumulator[i] because the internal + // accumulated value is also used to drive the Flycam, and + // we don't want any external state leaking into that value. + axis += (S32)(mExternalState.mAxes[i]); + } + axis = (S16)(std::min(std::max(axis, -32768), 32767)); // check for change - if (final_state.mAxes[i] != new_axis) + if (final_state.mAxes[i] != axis) { // When axis changes we explicitly update the corresponding prevAxis - // prior to storing new_axis. The only other place where prevAxis + // prior to storing axis. The only other place where prevAxis // is updated in updateResendPeriod() which is explicitly called after // a packet is sent. The result is: unchanged axes are included in // first resend but not later ones. final_state.mPrevAxes[i] = final_state.mAxes[i]; - final_state.mAxes[i] = new_axis; + final_state.mAxes[i] = axis; g_nextResendPeriod = 0; // packet needs to go out ASAP } } @@ -546,12 +566,53 @@ LLGameControl::InputChannel LLGameControllerManager::getChannelByActionName(cons LLGameControl::InputChannel channel = mActionTranslator.getChannelByAction(name); if (channel.isNone()) { - //maybe we're looking for a camera action - ActionToChannelMap::const_iterator itr = mCameraChannelMap.find(name); - if (itr != mCameraChannelMap.end()) - { - channel = itr->second; - } + // maybe we're looking for a flycam action + channel = getFlycamChannelByActionName(name); + } + return channel; +} + +// helper +S32 get_flycam_index_by_name(const std::string& name) +{ + // the Flycam action<-->channel relationship + // is implicitly stored in std::vector in a known order + S32 index = -1; + if (name.rfind("advance", 0) == 0) + { + index = 0; + } + else if (name.rfind("pan", 0) == 0) + { + index = 1; + } + else if (name.rfind("rise", 0) == 0) + { + index = 2; + } + else if (name.rfind("pitch", 0) == 0) + { + index = 3; + } + else if (name.rfind("yaw", 0) == 0) + { + index = 4; + } + else if (name.rfind("zoom", 0) == 0) + { + index = 5; + } + return index; +} + +LLGameControl::InputChannel LLGameControllerManager::getFlycamChannelByActionName(const std::string& name) const +{ + // the Flycam channels are stored in a strict order + LLGameControl::InputChannel channel; + S32 index = get_flycam_index_by_name(name); + if (index != -1) + { + channel = mFlycamChannels[index]; } return channel; } @@ -561,17 +622,12 @@ bool LLGameControllerManager::updateActionMap(const std::string& action, LLGame bool success = mActionTranslator.updateMap(action, channel); if (success) { - mLastActionFlags = 0; + mLastActiveFlags = 0; } else { - // maybe we're looking for a camera action - ActionToChannelMap::iterator itr = mCameraChannelMap.find(action); - if (itr != mCameraChannelMap.end()) - { - itr->second = channel; - success = true; - } + // maybe we're looking for a flycam action + success = updateFlycamMap(action, channel); } if (!success) { @@ -580,6 +636,17 @@ bool LLGameControllerManager::updateActionMap(const std::string& action, LLGame return success; } +bool LLGameControllerManager::updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel) +{ + S32 index = get_flycam_index_by_name(action); + if (index != -1) + { + mFlycamChannels[index] = channel; + return true; + } + return false; +} + U32 LLGameControllerManager::computeInternalActionFlags() { // add up device inputs @@ -591,34 +658,85 @@ U32 LLGameControllerManager::computeInternalActionFlags() return 0; } -void LLGameControllerManager::getCameraInputs(std::vector& inputs_out) +void LLGameControllerManager::getFlycamInputs(std::vector& inputs) { - // TODO: fill inputs_out with real data - inputs_out.resize(6); - for (auto& value : inputs_out) + // The inputs are packed in the same order as they exist in mFlycamChannels: + // + // advance + // pan + // rise + // pitch + // yaw + // zoom + // + for (const auto& channel: mFlycamChannels) { - value = 0.0f; + S16 axis; + if (channel.mIndex == (U8)(LLGameControl::AXIS_TRIGGERLEFT) + || channel.mIndex == (U8)(LLGameControl::AXIS_TRIGGERRIGHT)) + { + // TIED TRIGGER HACK: we assume the two triggers are paired together + S32 total_axis = mAxesAccumulator[(U8)(LLGameControl::AXIS_TRIGGERLEFT)] + - mAxesAccumulator[(U8)(LLGameControl::AXIS_TRIGGERRIGHT)]; + if (channel.mIndex == (U8)(LLGameControl::AXIS_TRIGGERRIGHT)) + { + // negate previous math when TRIGGERRIGHT is positive channel + total_axis *= -1; + } + axis = S16(std::min(std::max(total_axis, -32768), 32767)); + } + else + { + axis = S16(std::min(std::max(mAxesAccumulator[channel.mIndex], -32768), 32767)); + } + // value arrives as S16 in range [-32768, 32767] + // so we scale positive and negative values by slightly different factors + // to try to map it to [-1, 1] + F32 input = F32(axis) * ((axis > 0.0f) ? 3.051850476e-5 : 3.0517578125e-5f) * channel.mSign; + inputs.push_back(input); } } -// static -void LLGameControllerManager::setExternalActionFlags(U32 action_flags) +void LLGameControllerManager::setExternalInput(U32 action_flags, U32 buttons) { if (g_translateAgentActions) { + // HACK: these are the bits we can safely translate from control flags to GameControl + // Extracting LLGameControl::InputChannels that are mapped to other bits is a WIP. + // TODO: translate other bits to GameControl, which might require measure of gAgent + // state changes (e.g. sitting <--> standing, flying <--> not-flying, etc) + const U32 BITS_OF_INTEREST = + AGENT_CONTROL_AT_POS | AGENT_CONTROL_AT_NEG + | AGENT_CONTROL_LEFT_POS | AGENT_CONTROL_LEFT_NEG + | AGENT_CONTROL_UP_POS | AGENT_CONTROL_UP_NEG + | AGENT_CONTROL_YAW_POS | AGENT_CONTROL_YAW_NEG + | AGENT_CONTROL_PITCH_POS | AGENT_CONTROL_PITCH_NEG + | AGENT_CONTROL_STOP + | AGENT_CONTROL_FAST_AT + | AGENT_CONTROL_FAST_LEFT + | AGENT_CONTROL_FAST_UP; + action_flags &= BITS_OF_INTEREST; + U32 active_flags = action_flags & mActionTranslator.getMappedFlags(); - if (active_flags != mLastActionFlags) + if (active_flags != mLastActiveFlags) { - mLastActionFlags = active_flags; + mLastActiveFlags = active_flags; mExternalState = mActionTranslator.computeStateFromFlags(action_flags); + mExternalState.mButtons |= buttons; + } + else + { + mExternalState.mButtons = buttons; } } + else + { + mExternalState.mButtons = buttons; + } } void LLGameControllerManager::clear() { - mControllerIDs.clear(); - mControllers.clear(); mStates.clear(); } @@ -628,57 +746,50 @@ U64 get_now_nsec() return (std::chrono::steady_clock::now() - t0).count(); } -// util for dumping SDL_GameController info -std::ostream& operator<<(std::ostream& out, SDL_GameController* c) +void onJoystickDeviceAdded(const SDL_Event& event) { - if (! c) + LL_INFOS("GameController") << "device index: " << event.cdevice.which << LL_ENDL; + + if (SDL_Joystick* joystick = SDL_JoystickOpen(event.cdevice.which)) { - return out << "nullptr"; + LL_INFOS("GameController") << "joystick: " << joystick << LL_ENDL; + } + else + { + LL_WARNS("GameController") << "Can't open joystick: " << SDL_GetError() << LL_ENDL; } - out << "{"; - out << " name='" << SDL_GameControllerName(c) << "'"; - out << " type='" << SDL_GameControllerGetType(c) << "'"; - out << " vendor='" << SDL_GameControllerGetVendor(c) << "'"; - out << " product='" << SDL_GameControllerGetProduct(c) << "'"; - out << " version='" << SDL_GameControllerGetProductVersion(c) << "'"; - //CRASH! out << " serial='" << SDL_GameControllerGetSerial(c) << "'"; - out << " }"; - return out; } -// util for dumping SDL_Joystick info -std::ostream& operator<<(std::ostream& out, SDL_Joystick* j) +void onJoystickDeviceRemoved(const SDL_Event& event) { - if (! j) - { - return out << "nullptr"; - } - out << "{"; - out << " p=0x" << (void*)(j); - out << " name='" << SDL_JoystickName(j) << "'"; - out << " type='" << SDL_JoystickGetType(j) << "'"; - out << " instance='" << SDL_JoystickInstanceID(j) << "'"; - out << " product='" << SDL_JoystickGetProduct(j) << "'"; - out << " version='" << SDL_JoystickGetProductVersion(j) << "'"; - out << " num_axes=" << SDL_JoystickNumAxes(j); - out << " num_balls=" << SDL_JoystickNumBalls(j); - out << " num_hats=" << SDL_JoystickNumHats(j); - out << " num_buttons=" << SDL_JoystickNumHats(j); - out << " }"; - return out; + LL_INFOS("GameController") << "joystick id: " << event.cdevice.which << LL_ENDL; } void onControllerDeviceAdded(const SDL_Event& event) { - int device_index = event.cdevice.which; - SDL_JoystickID id = SDL_JoystickGetDeviceInstanceID(device_index); - SDL_GameController* controller = SDL_GameControllerOpen(device_index); + LL_INFOS("GameController") << "device index: " << event.cdevice.which << LL_ENDL; + + SDL_JoystickID id = SDL_JoystickGetDeviceInstanceID(event.cdevice.which); + if (id < 0) + { + LL_WARNS("GameController") << "Can't get device instance ID: " << SDL_GetError() << LL_ENDL; + return; + } + + SDL_GameController* controller = SDL_GameControllerOpen(event.cdevice.which); + if (!controller) + { + LL_WARNS("GameController") << "Can't open game controller: " << SDL_GetError() << LL_ENDL; + return; + } g_manager.addController(id, controller); } void onControllerDeviceRemoved(const SDL_Event& event) { + LL_INFOS("GameController") << "joystick id=" << event.cdevice.which << LL_ENDL; + SDL_JoystickID id = event.cdevice.which; g_manager.removeController(id); } @@ -708,13 +819,39 @@ void sdl_logger(void *userdata, int category, SDL_LogPriority priority, const ch } // static -void LLGameControl::init() +void LLGameControl::init(const std::string& gamecontrollerdb_path) { if (!g_gameControl) { - g_gameControl = LLGameControl::getInstance(); - SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + int result = SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + if (result < 0) + { + // This error is critical, we stop working with SDL and return + LL_WARNS("GameController") << "Error initializing the subsystems : " << SDL_GetError() << LL_ENDL; + return; + } + SDL_LogSetOutputFunction(&sdl_logger, nullptr); + + // The inability to read this file is not critical, we can continue working + if (!LLFile::isfile(gamecontrollerdb_path.c_str())) + { + LL_WARNS("GameController") << "Device mapping db file not found: " << gamecontrollerdb_path << LL_ENDL; + } + else + { + int count = SDL_GameControllerAddMappingsFromFile(gamecontrollerdb_path.c_str()); + if (count < 0) + { + LL_WARNS("GameController") << "Error adding mappings from " << gamecontrollerdb_path << " : " << SDL_GetError() << LL_ENDL; + } + else + { + LL_INFOS("GameController") << "Total " << count << " mappings added from " << gamecontrollerdb_path << LL_ENDL; + } + } + + g_gameControl = LLGameControl::getInstance(); } } @@ -732,20 +869,21 @@ void LLGameControl::terminate() // or not. bool LLGameControl::computeFinalStateAndCheckForChanges() { - // Note: LLGameControllerManager::computeFinalState() can modify g_nextResendPeriod as a side-effect + // Note: LLGameControllerManager::computeFinalState() modifies g_nextResendPeriod as a side-effect g_manager.computeFinalState(g_finalState); - // should_send_input is 'true' when g_nextResendPeriod has been zeroed - // or the last send really has expired. - U64 now = get_now_nsec(); - bool should_send_input = (g_lastSend + g_nextResendPeriod < now); - return should_send_input; + // should send input when: + // sending is enabled and + // g_lastSend has "expired" + // either because g_nextResendPeriod has been zeroed + // or the last send really has expired. + return g_sendToServer && (g_lastSend + g_nextResendPeriod < get_now_nsec()); } // static -void LLGameControl::clearAllState() +void LLGameControl::clearAllStates() { - g_manager.clearAllState(); + g_manager.clearAllStates(); } // static @@ -759,7 +897,7 @@ void LLGameControl::processEvents(bool app_has_focus) { // do nothing: SDL_PollEvent() is the operator } - g_manager.clearAllState(); + g_manager.clearAllStates(); return; } @@ -767,6 +905,12 @@ void LLGameControl::processEvents(bool app_has_focus) { switch (event.type) { + case SDL_JOYDEVICEADDED: + onJoystickDeviceAdded(event); + break; + case SDL_JOYDEVICEREMOVED: + onJoystickDeviceRemoved(event); + break; case SDL_CONTROLLERDEVICEADDED: onControllerDeviceAdded(event); break; @@ -794,9 +938,9 @@ const LLGameControl::State& LLGameControl::getState() } // static -void LLGameControl::getCameraInputs(std::vector& inputs_out) +void LLGameControl::getFlycamInputs(std::vector& inputs_out) { - return g_manager.getCameraInputs(inputs_out); + return g_manager.getFlycamInputs(inputs_out); } // static @@ -822,38 +966,12 @@ void LLGameControl::setAgentControlMode(LLGameControl::AgentControlMode mode) g_agentControlMode = mode; } -// static -bool LLGameControl::willSendToServer() -{ - return g_sendToServer; -} - // static bool LLGameControl::willControlAvatar() { return g_controlAgent && g_agentControlMode == CONTROL_MODE_AVATAR; } -// static -bool LLGameControl::willControlFlycam() -{ - return g_controlAgent && g_agentControlMode == CONTROL_MODE_FLYCAM; -} - -// static -bool LLGameControl::willTranslateAgentActions() -{ - return g_translateAgentActions; -} - -/* -// static -LLGameControl::LocalControlMode LLGameControl::getLocalControlMode() -{ - return g_agentControlMode; -} -*/ - // static // // Given a name like "AXIS_1-" or "BUTTON_5" returns the corresponding InputChannel @@ -891,7 +1009,7 @@ LLGameControl::InputChannel LLGameControl::getChannelByName(const std::string& n // the BUTTON_ decimal postfix can be up to two characters wide size_t i = 6; U8 index = 0; - while (i < name.length() && i < 8 && name[i] <= '0') + while (i < name.length() && i < 8 && name[i] >= '0') { index = index * 10 + name[i] - '0'; } @@ -922,9 +1040,9 @@ U32 LLGameControl::computeInternalActionFlags() } // static -void LLGameControl::setExternalActionFlags(U32 action_flags) +void LLGameControl::setExternalInput(U32 action_flags, U32 buttons) { - g_manager.setExternalActionFlags(action_flags); + g_manager.setExternalInput(action_flags, buttons); } //static -- cgit v1.2.3 From 2daf175650cdda7cc8f820b6cb17b1475496e7ac Mon Sep 17 00:00:00 2001 From: Alexander Gavriliuk Date: Wed, 8 May 2024 23:32:58 +0200 Subject: Add GameControl UI for per device settings --- indra/llwindow/llgamecontrol.cpp | 1702 ++++++++++++++++++++++++++++++-------- 1 file changed, 1356 insertions(+), 346 deletions(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index a1ab2dd52d..23849aca66 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -37,80 +37,202 @@ #include "indra_constants.h" #include "llfile.h" #include "llgamecontroltranslator.h" +#include "llsd.h" -constexpr size_t NUM_AXES = 6; - -// util for dumping SDL_GameController info -std::ostream& operator<<(std::ostream& out, SDL_GameController* c) +namespace std { - if (!c) + string to_string(const char* text) + { + return text ? string(text) : LLStringUtil::null; + } + + string to_string(const SDL_JoystickGUID& guid) + { + char buffer[33] = { 0 }; + SDL_JoystickGetGUIDString(guid, buffer, sizeof(guid)); + return buffer; + } + + string to_string(SDL_JoystickType type) + { + switch (type) + { + case SDL_JOYSTICK_TYPE_GAMECONTROLLER: + return "GAMECONTROLLER"; + case SDL_JOYSTICK_TYPE_WHEEL: + return "WHEEL"; + case SDL_JOYSTICK_TYPE_ARCADE_STICK: + return "ARCADE_STICK"; + case SDL_JOYSTICK_TYPE_FLIGHT_STICK: + return "FLIGHT_STICK"; + case SDL_JOYSTICK_TYPE_DANCE_PAD: + return "DANCE_PAD"; + case SDL_JOYSTICK_TYPE_GUITAR: + return "GUITAR"; + case SDL_JOYSTICK_TYPE_DRUM_KIT: + return "DRUM_KIT"; + case SDL_JOYSTICK_TYPE_ARCADE_PAD: + return "ARCADE_PAD"; + case SDL_JOYSTICK_TYPE_THROTTLE: + return "THROTTLE"; + default:; + } + return "UNKNOWN"; + } + + string to_string(SDL_GameControllerType type) { - return out << "nullptr"; + switch (type) + { + case SDL_CONTROLLER_TYPE_XBOX360: + return "XBOX360"; + case SDL_CONTROLLER_TYPE_XBOXONE: + return "XBOXONE"; + case SDL_CONTROLLER_TYPE_PS3: + return "PS3"; + case SDL_CONTROLLER_TYPE_PS4: + return "PS4"; + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO: + return "NINTENDO_SWITCH_PRO"; + case SDL_CONTROLLER_TYPE_VIRTUAL: + return "VIRTUAL"; + case SDL_CONTROLLER_TYPE_PS5: + return "PS5"; + case SDL_CONTROLLER_TYPE_AMAZON_LUNA: + return "AMAZON_LUNA"; + case SDL_CONTROLLER_TYPE_GOOGLE_STADIA: + return "GOOGLE_STADIA"; + case SDL_CONTROLLER_TYPE_NVIDIA_SHIELD: + return "NVIDIA_SHIELD"; + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT: + return "NINTENDO_SWITCH_JOYCON_LEFT"; + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT: + return "NINTENDO_SWITCH_JOYCON_RIGHT"; + case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: + return "NINTENDO_SWITCH_JOYCON_PAIR"; + default:; + } + return "UNKNOWN"; } - out << "{"; - out << " name='" << SDL_GameControllerName(c) << "'"; - out << " type='" << SDL_GameControllerGetType(c) << "'"; - out << " vendor='" << SDL_GameControllerGetVendor(c) << "'"; - out << " product='" << SDL_GameControllerGetProduct(c) << "'"; - out << " version='" << SDL_GameControllerGetProductVersion(c) << "'"; - //CRASH! out << " serial='" << SDL_GameControllerGetSerial(c) << "'"; - out << " }"; - return out; } -// util for dumping SDL_Joystick info -std::ostream& operator<<(std::ostream& out, SDL_Joystick* j) +// Util for dumping SDL_JoystickGUID info +std::ostream& operator<<(std::ostream& out, SDL_JoystickGUID& guid) +{ + return out << std::to_string(guid); +} + +// Util for dumping SDL_JoystickType type name +std::ostream& operator<<(std::ostream& out, SDL_JoystickType type) +{ + return out << std::to_string(type); +} + +// Util for dumping SDL_GameControllerType type name +std::ostream& operator<<(std::ostream& out, SDL_GameControllerType type) +{ + return out << std::to_string(type); +} + +namespace std { - if (!j) + string to_string(SDL_Joystick* joystick) { - return out << "nullptr"; + if (!joystick) + { + return "nullptr"; + } + + std::stringstream ss; + + ss << "{id:" << SDL_JoystickInstanceID(joystick); + SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick); + ss << ",guid:'" << guid << "'"; + ss << ",type:'" << SDL_JoystickGetType(joystick) << "'"; + ss << ",name:'" << std::to_string(SDL_JoystickName(joystick)) << "'"; + ss << ",vendor:" << SDL_JoystickGetVendor(joystick); + ss << ",product:" << SDL_JoystickGetProduct(joystick); + if (U16 version = SDL_JoystickGetProductVersion(joystick)) + { + ss << ",version:" << version; + } + if (U16 firmware = SDL_JoystickGetFirmwareVersion(joystick)) + { + ss << ",firmware:" << firmware; + } + if (const char* serial = SDL_JoystickGetSerial(joystick)) + { + ss << ",serial:'" << serial << "'"; + } + ss << ",num_axes:" << SDL_JoystickNumAxes(joystick); + ss << ",num_balls:" << SDL_JoystickNumBalls(joystick); + ss << ",num_hats:" << SDL_JoystickNumHats(joystick); + ss << ",num_buttons:" << SDL_JoystickNumHats(joystick); + ss << "}"; + + return ss.str(); + } + + string to_string(SDL_GameController* controller) + { + if (!controller) + { + return "nullptr"; + } + + stringstream ss; + + ss << "{type:'" << SDL_GameControllerGetType(controller) << "'"; + ss << ",name:'" << std::to_string(SDL_GameControllerName(controller)) << "'"; + ss << ",vendor:" << SDL_GameControllerGetVendor(controller); + ss << ",product:" << SDL_GameControllerGetProduct(controller); + if (U16 version = SDL_GameControllerGetProductVersion(controller)) + { + ss << ",version:" << version; + } + if (U16 firmware = SDL_GameControllerGetFirmwareVersion(controller)) + { + ss << ",firmware:" << firmware; + } + if (const char* serial = SDL_GameControllerGetSerial(controller)) + { + ss << ",serial:'" << serial << "'"; + } + ss << "}"; + + return ss.str(); } - out << "{"; - out << " p=0x" << (void*)(j); - out << " name='" << SDL_JoystickName(j) << "'"; - out << " type='" << SDL_JoystickGetType(j) << "'"; - out << " instance='" << SDL_JoystickInstanceID(j) << "'"; - out << " product='" << SDL_JoystickGetProduct(j) << "'"; - out << " version='" << SDL_JoystickGetProductVersion(j) << "'"; - out << " num_axes=" << SDL_JoystickNumAxes(j); - out << " num_balls=" << SDL_JoystickNumBalls(j); - out << " num_hats=" << SDL_JoystickNumHats(j); - out << " num_buttons=" << SDL_JoystickNumHats(j); - out << " }"; - return out; +} + +// Util for dumping SDL_Joystick info +std::ostream& operator<<(std::ostream& out, SDL_Joystick* joystick) +{ + return out << std::to_string(joystick); +} + +// Util for dumping SDL_GameController info +std::ostream& operator<<(std::ostream& out, SDL_GameController* controller) +{ + return out << std::to_string(controller); } std::string LLGameControl::InputChannel::getLocalName() const { // HACK: we hard-code English channel names, but // they should be loaded from localized XML config files. - std::string name = " "; - if (mType == LLGameControl::InputChannel::TYPE_AXIS) + + if ((mType == LLGameControl::InputChannel::TYPE_AXIS) && (mIndex < NUM_AXES)) { - if (mIndex < (U8)(NUM_AXES)) - { - name = "AXIS_"; - name.append(std::to_string((S32)(mIndex))); - if (mSign < 0) - { - name.append("-"); - } - else if (mSign > 0) - { - name.append("+"); - } - } + return "AXIS_" + std::to_string((U32)mIndex) + + (mSign < 0 ? "-" : mSign > 0 ? "+" : ""); } - else if (mType == LLGameControl::InputChannel::TYPE_BUTTON) + + if ((mType == LLGameControl::InputChannel::TYPE_BUTTON) && (mIndex < NUM_BUTTONS)) { - constexpr U8 NUM_BUTTONS = 32; - if (mIndex < NUM_BUTTONS) - { - name = "BUTTON_"; - name.append(std::to_string((S32)(mIndex))); - } + return "BUTTON_" + std::to_string((U32)mIndex); } - return name; + + return "NONE"; } std::string LLGameControl::InputChannel::getRemoteName() const @@ -121,7 +243,7 @@ std::string LLGameControl::InputChannel::getRemoteName() const // GAME_CONTROL_AXIS_LEFTX, GAME_CONTROL_BUTTON_A, etc if (mType == LLGameControl::InputChannel::TYPE_AXIS) { - switch(mIndex) + switch (mIndex) { case 0: name = "GAME_CONTROL_AXIS_LEFTX"; @@ -227,7 +349,15 @@ public: using ActionToChannelMap = std::map< std::string, LLGameControl::InputChannel >; LLGameControllerManager(); - void addController(SDL_JoystickID id, SDL_GameController* controller); + static void getDefaultMappings(std::vector>& agent_channels, + std::vector& flycam_channels); + void getDefaultMappings(std::vector>& mappings); + void initializeMappingsByDefault(); + void resetDeviceOptionsToDefaults(); + void loadDeviceOptionsFromSettings(); + void saveDeviceOptionsToSettings() const; + + void addController(SDL_JoystickID id, const std::string& guid, const std::string& name); void removeController(SDL_JoystickID id); void onAxis(SDL_JoystickID id, U8 axis, S16 value); @@ -236,39 +366,56 @@ public: void clearAllStates(); void accumulateInternalState(); - void computeFinalState(LLGameControl::State& state); + void computeFinalState(); - LLGameControl::InputChannel getChannelByActionName(const std::string& action_name) const; - LLGameControl::InputChannel getFlycamChannelByActionName(const std::string& action_name) const; + LLGameControl::ActionNameType getActionNameType(const std::string& action) const; + LLGameControl::InputChannel getChannelByAction(const std::string& action) const; + LLGameControl::InputChannel getFlycamChannelByAction(const std::string& action) const; bool updateActionMap(const std::string& name, LLGameControl::InputChannel channel); U32 computeInternalActionFlags(); void getFlycamInputs(std::vector& inputs_out); void setExternalInput(U32 action_flags, U32 buttons); + U32 getMappedFlags() const { return mActionTranslator.getMappedFlags(); } + void clear(); + std::string getAnalogMappings() const; + std::string getBinaryMappings() const; + std::string getFlycamMappings() const; + + void setAnalogMappings(const std::string& mappings); + void setBinaryMappings(const std::string& mappings); + void setFlycamMappings(const std::string& mappings); + private: - bool updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel); + void updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel); - std::list mStates; // one state per device - using state_it = std::list::iterator; - state_it findState(SDL_JoystickID id) + std::list mDevices; // all connected devices + using device_it = std::list::iterator; + device_it findDevice(SDL_JoystickID id) { - return std::find_if(mStates.begin(), mStates.end(), - [id](LLGameControl::State& state) + return std::find_if(mDevices.begin(), mDevices.end(), + [id](LLGameControl::Device& device) { - return state.getJoystickID() == id; + return device.getJoystickID() == id; }); } LLGameControl::State mExternalState; LLGameControlTranslator mActionTranslator; + std::map mActions; + std::vector mAnalogActions; + std::vector mBinaryActions; + std::vector mFlycamActions; std::vector mFlycamChannels; std::vector mAxesAccumulator; U32 mButtonAccumulator { 0 }; U32 mLastActiveFlags { 0 }; U32 mLastFlycamActionFlags { 0 }; + + friend class LLGameControl; }; // local globals @@ -301,8 +448,52 @@ namespace bool g_translateAgentActions = false; LLGameControl::AgentControlMode g_agentControlMode = LLGameControl::CONTROL_MODE_AVATAR; - constexpr U8 MAX_AXIS = 5; - constexpr U8 MAX_BUTTON = 31; + std::map g_deviceOptions; + + std::function s_loadBoolean; + std::function s_saveBoolean; + std::function s_loadString; + std::function s_saveString; + std::function s_loadObject; + std::function s_saveObject; + + std::string SETTING_SENDTOSERVER("GameControlToServer"); + std::string SETTING_CONTROLAGENT("GameControlToAgent"); + std::string SETTING_TRANSLATEACTIONS("AgentToGameControl"); + std::string SETTING_AGENTCONTROLMODE("AgentControlMode"); + std::string SETTING_ANALOGMAPPINGS("AnalogChannelMappings"); + std::string SETTING_BINARYMAPPINGS("BinaryChannelMappings"); + std::string SETTING_FLYCAMMAPPINGS("FlycamChannelMappings"); + std::string SETTING_KNOWNCONTROLLERS("KnownGameControllers"); + + std::string ENUM_AGENTCONTROLMODE_FLYCAM("flycam"); + std::string ENUM_AGENTCONTROLMODE_NONE("none"); + + LLGameControl::AgentControlMode convertStringToAgentControlMode(const std::string& mode) + { + if (mode == ENUM_AGENTCONTROLMODE_NONE) + return LLGameControl::CONTROL_MODE_NONE; + if (mode == ENUM_AGENTCONTROLMODE_FLYCAM) + return LLGameControl::CONTROL_MODE_FLYCAM; + // All values except NONE and FLYCAM are treated as default (AVATAR) + return LLGameControl::CONTROL_MODE_AVATAR; + } + + const std::string& convertAgentControlModeToString(LLGameControl::AgentControlMode mode) + { + if (mode == LLGameControl::CONTROL_MODE_NONE) + return ENUM_AGENTCONTROLMODE_NONE; + if (mode == LLGameControl::CONTROL_MODE_FLYCAM) + return ENUM_AGENTCONTROLMODE_FLYCAM; + // All values except NONE and FLYCAM are treated as default (AVATAR) + return LLStringUtil::null; + } + + const std::string& getDeviceOptionsString(const std::string& guid) + { + const auto& it = g_deviceOptions.find(guid); + return it == g_deviceOptions.end() ? LLStringUtil::null : it->second; + } } LLGameControl::~LLGameControl() @@ -310,18 +501,13 @@ LLGameControl::~LLGameControl() terminate(); } -LLGameControl::State::State() : mButtons(0) +LLGameControl::State::State() +: mButtons(0) { mAxes.resize(NUM_AXES, 0); mPrevAxes.resize(NUM_AXES, 0); } -void LLGameControl::State::setDevice(int joystickID, void* controller) -{ - mJoystickID = joystickID; - mController = controller; -} - void LLGameControl::State::clear() { std::fill(mAxes.begin(), mAxes.end(), 0); @@ -335,7 +521,7 @@ void LLGameControl::State::clear() bool LLGameControl::State::onButton(U8 button, bool pressed) { U32 old_buttons = mButtons; - if (button <= MAX_BUTTON) + if (button < NUM_BUTTONS) { if (pressed) { @@ -346,152 +532,525 @@ bool LLGameControl::State::onButton(U8 button, bool pressed) mButtons &= ~(0x01 << button); } } - bool changed = (old_buttons != mButtons); - return changed; + return mButtons != old_buttons; +} + +LLGameControl::Device::Device(int joystickID, const std::string& guid, const std::string& name) +: mJoystickID(joystickID) +, mGUID(guid) +, mName(name) +{ +} + +LLGameControl::Options::Options() +{ + mAxisOptions.resize(NUM_AXES); + mAxisMap.resize(NUM_AXES); + mButtonMap.resize(NUM_BUTTONS); + + resetToDefaults(); +} + +void LLGameControl::Options::resetToDefaults() +{ + for (size_t i = 0; i < NUM_AXES; ++i) + { + mAxisOptions[i].resetToDefaults(); + mAxisMap[i] = (U8)i; + } + + for (size_t i = 0; i < NUM_BUTTONS; ++i) + { + mButtonMap[i] = (U8)i; + } +} + +U8 LLGameControl::Options::mapAxis(U8 axis) const +{ + if (axis >= NUM_AXES) + { + LL_WARNS("SDL2") << "Invalid input axis: " << axis << LL_ENDL; + return axis; + } + return mAxisMap[axis]; +} + +U8 LLGameControl::Options::mapButton(U8 button) const +{ + if (button >= NUM_BUTTONS) + { + LL_WARNS("SDL2") << "Invalid input button: " << button << LL_ENDL; + return button; + } + return mButtonMap[button]; +} + +S16 LLGameControl::Options::fixAxisValue(U8 axis, S16 value) const +{ + if (axis >= NUM_AXES) + { + LL_WARNS("SDL2") << "Invalid input axis: " << axis << LL_ENDL; + } + else + { + const AxisOptions& options = mAxisOptions[axis]; + S32 new_value = (S32)value + (S32)options.mOffset; + value = (S16)std::clamp(new_value , -32768, 32767); + if ((value > 0 && value < (S16)options.mDeadZone) || + (value < 0 && value > -(S16)options.mDeadZone)) + { + value = 0; + } + else if (options.mInvert) + { + value = -value; + } + } + return value; +} + +std::string LLGameControl::Options::AxisOptions::saveToString() const +{ + std::list options; + + if (mInvert) + { + options.push_back("invert:1"); + } + if (mDeadZone) + { + options.push_back(llformat("dead_zone:%u", mDeadZone)); + } + if (mOffset) + { + options.push_back(llformat("offset:%d", mOffset)); + } + + std::string result = LLStringUtil::join(options); + + return result.empty() ? result : "{" + result + "}"; +} + +// Parse string "{key:value,key:{key:value,key:value}}" and fill the map +static bool parse(std::map& result, std::string source) +{ + result.clear(); + + LLStringUtil::trim(source); + if (source.empty()) + return true; + + if (source.front() != '{' || source.back() != '}') + return false; + + source = source.substr(1, source.size() - 2); + + LLStringUtil::trim(source); + if (source.empty()) + return true; + + // Split the string "key:value" and add the pair to the map + auto split = [&](const std::string& pair) -> bool + { + size_t pos = pair.find(':'); + if (!pos || pos == std::string::npos) + return false; + std::string key = pair.substr(0, pos); + std::string value = pair.substr(pos + 1); + LLStringUtil::trim(key); + LLStringUtil::trim(value); + if (key.empty() || value.empty()) + return false; + result[key] = value; + return true; + }; + + U32 depth = 0; + size_t offset = 0; + while (true) + { + size_t pos = source.find_first_of(depth ? "{}" : ",{}", offset); + if (pos == std::string::npos) + { + return !depth && split(source); + } + if (source[pos] == ',') + { + if (!split(source.substr(0, pos))) + return false; + source = source.substr(pos + 1); + offset = 0; + } + else if (source[pos] == '{') + { + depth++; + offset = pos + 1; + } + else if (depth) // Assume '}' here + { + depth--; + offset = pos + 1; + } + else + { + return false; // Extra '}' found + } + } + + return true; +} + +void LLGameControl::Options::AxisOptions::loadFromString(std::string options) +{ + resetToDefaults(); + + if (options.empty()) + return; + + std::map pairs; + if (!parse(pairs, options)) + { + LL_WARNS("SDL2") << "Invalid axis options: '" << options << "'" << LL_ENDL; + } + + std::string invert = pairs["invert"]; + if (!invert.empty()) + { + if (invert != "1") + { + LL_WARNS("SDL2") << "Invalid invert value: '" << invert << "'" << LL_ENDL; + } + else + { + mInvert = true; + } + } + + std::string dead_zone = pairs["dead_zone"]; + if (!dead_zone.empty()) + { + size_t number = std::stoull(dead_zone); + if (number > MAX_AXIS_DEAD_ZONE || std::to_string(number) != dead_zone) + { + LL_WARNS("SDL2") << "Invalid dead_zone value: '" << dead_zone << "'" << LL_ENDL; + } + else + { + mDeadZone = (U16)number; + } + } + + std::string offset = pairs["offset"]; + if (!offset.empty()) + { + S32 number = std::stoi(offset); + if (abs(number) > MAX_AXIS_OFFSET || std::to_string(number) != offset) + { + LL_WARNS("SDL2") << "Invalid offset value: '" << offset << "'" << LL_ENDL; + } + else + { + mOffset = (S16)number; + } + } +} + +std::string LLGameControl::Options::saveToString(const std::string& name, bool force_empty) const +{ + return stringifyDeviceOptions(name, mAxisOptions, mAxisMap, mButtonMap, force_empty); +} + +bool LLGameControl::Options::loadFromString(std::string& name, std::string options) +{ + return LLGameControl::parseDeviceOptions(options, name, mAxisOptions, mAxisMap, mButtonMap); +} + +bool LLGameControl::Options::loadFromString(std::string options) +{ + std::string dummy_name; + return LLGameControl::parseDeviceOptions(options, dummy_name, mAxisOptions, mAxisMap, mButtonMap); } LLGameControllerManager::LLGameControllerManager() { - mAxesAccumulator.resize(NUM_AXES, 0); + mAxesAccumulator.resize(LLGameControl::NUM_AXES, 0); + + mAnalogActions = { "push", "slide", "jump", "turn", "look" }; + mBinaryActions = { "toggle_run", "toggle_fly", "toggle_flycam", "stop" }; + mFlycamActions = { "advance", "pan", "rise", "pitch", "yaw", "zoom" }; + + // Collect all known action names with their types in one container + for (const std::string& name : mAnalogActions) + { + mActions[name] = LLGameControl::ACTION_NAME_ANALOG; + mActions[name + "+"] = LLGameControl::ACTION_NAME_ANALOG_POS; + mActions[name + "-"] = LLGameControl::ACTION_NAME_ANALOG_NEG; + } + for (const std::string& name : mBinaryActions) + { + mActions[name] = LLGameControl::ACTION_NAME_BINARY; + } + for (const std::string& name : mFlycamActions) + { + mActions[name] = LLGameControl::ACTION_NAME_FLYCAM; + } // Here we build an invariant map between the named agent actions - // and control bit sent to the server. This map will be used, + // and control bit sent to the server. This map will be used, // in combination with the action->InputChannel map below, // to maintain an inverse map from control bit masks to GameControl data. - LLGameControlTranslator::ActionToMaskMap actions; - actions["push+"] = AGENT_CONTROL_AT_POS | AGENT_CONTROL_FAST_AT; - actions["push-"] = AGENT_CONTROL_AT_NEG | AGENT_CONTROL_FAST_AT; - actions["slide+"] = AGENT_CONTROL_LEFT_POS | AGENT_CONTROL_FAST_LEFT; - actions["slide-"] = AGENT_CONTROL_LEFT_NEG | AGENT_CONTROL_FAST_LEFT; - actions["jump+"] = AGENT_CONTROL_UP_POS | AGENT_CONTROL_FAST_UP; - actions["jump-"] = AGENT_CONTROL_UP_NEG | AGENT_CONTROL_FAST_UP; - actions["turn+"] = AGENT_CONTROL_YAW_POS; - actions["turn-"] = AGENT_CONTROL_YAW_NEG; - actions["look+"] = AGENT_CONTROL_PITCH_POS; - actions["look-"] = AGENT_CONTROL_PITCH_NEG; - actions["stop"] = AGENT_CONTROL_STOP; + LLGameControlTranslator::ActionToMaskMap actionMasks = + { + // Analog actions (pairs) + { "push+", AGENT_CONTROL_AT_POS | AGENT_CONTROL_FAST_AT }, + { "push-", AGENT_CONTROL_AT_NEG | AGENT_CONTROL_FAST_AT }, + { "slide+", AGENT_CONTROL_LEFT_POS | AGENT_CONTROL_FAST_LEFT }, + { "slide-", AGENT_CONTROL_LEFT_NEG | AGENT_CONTROL_FAST_LEFT }, + { "jump+", AGENT_CONTROL_UP_POS | AGENT_CONTROL_FAST_UP }, + { "jump-", AGENT_CONTROL_UP_NEG | AGENT_CONTROL_FAST_UP }, + { "turn+", AGENT_CONTROL_YAW_POS }, + { "turn-", AGENT_CONTROL_YAW_NEG }, + { "look+", AGENT_CONTROL_PITCH_POS }, + { "look-", AGENT_CONTROL_PITCH_NEG }, + // Button actions + { "stop", AGENT_CONTROL_STOP }, // These are HACKs. We borrow some AGENT_CONTROL bits for "unrelated" features. // Not a problem because these bits are only used internally. - actions["toggle_run"] = AGENT_CONTROL_NUDGE_AT_POS; // HACK - actions["toggle_fly"] = AGENT_CONTROL_FLY; // HACK - actions["toggle_flycam"] = AGENT_CONTROL_NUDGE_AT_NEG; // HACK - mActionTranslator.setAvailableActions(actions); + { "toggle_run", AGENT_CONTROL_NUDGE_AT_POS }, // HACK + { "toggle_fly", AGENT_CONTROL_FLY }, // HACK + { "toggle_flycam", AGENT_CONTROL_NUDGE_AT_NEG }, // HACK + }; + mActionTranslator.setAvailableActionMasks(actionMasks); + + initializeMappingsByDefault(); +} +// static +void LLGameControllerManager::getDefaultMappings( + std::vector>& agent_channels, + std::vector& flycam_channels) +{ // Here we build a list of pairs between named agent actions and // GameControl channels. Note: we only supply the non-signed names // (e.g. "push" instead of "push+" and "push-") because mActionTranslator // automatially expands action names as necessary. using type = LLGameControl::InputChannel::Type; - std::vector< std::pair< std::string, LLGameControl::InputChannel> > agent_defaults = - { - { "push", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), 1 } }, - { "slide", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), 1 } }, - { "jump", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERLEFT), 1 } }, - { "turn", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), 1 } }, - { "look", { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), 1 } }, - { "toggle_run", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSHOULDER) } }, - { "toggle_fly", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_DPAD_UP) } }, - { "toggle_flycam", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_RIGHTSHOULDER) } }, - { "stop", { type::TYPE_BUTTON, (U8)(LLGameControl::BUTTON_LEFTSTICK) } } + agent_channels = + { + // Analog actions (associated by common name - without '+' or '-') + { "push", { type::TYPE_AXIS, LLGameControl::AXIS_LEFTY, 1 } }, + { "slide", { type::TYPE_AXIS, LLGameControl::AXIS_LEFTX, 1 } }, + { "jump", { type::TYPE_AXIS, LLGameControl::AXIS_TRIGGERLEFT, 1 } }, + { "turn", { type::TYPE_AXIS, LLGameControl::AXIS_RIGHTX, 1 } }, + { "look", { type::TYPE_AXIS, LLGameControl::AXIS_RIGHTY, 1 } }, + // Button actions (associated by name) + { "toggle_run", { type::TYPE_BUTTON, LLGameControl::BUTTON_LEFTSHOULDER } }, + { "toggle_fly", { type::TYPE_BUTTON, LLGameControl::BUTTON_DPAD_UP } }, + { "toggle_flycam", { type::TYPE_BUTTON, LLGameControl::BUTTON_RIGHTSHOULDER } }, + { "stop", { type::TYPE_BUTTON, LLGameControl::BUTTON_LEFTSTICK } } }; - mActionTranslator.setMappings(agent_defaults); // Flycam actions don't need bitwise translation, so we maintain the map // of channels here directly rather than using an LLGameControlTranslator. - mFlycamChannels = { - { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTY), 1 }, // advance - { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_LEFTX), 1 }, // pan - { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_TRIGGERRIGHT), 1 }, // rise - { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTY), -1 }, // pitch - { type::TYPE_AXIS, (U8)(LLGameControl::AXIS_RIGHTX), 1 }, // yaw - { type::TYPE_NONE, 0 } // zoom + flycam_channels = + { + // Flycam actions (associated just by an order index) + { type::TYPE_AXIS, LLGameControl::AXIS_LEFTY, 1 }, // advance + { type::TYPE_AXIS, LLGameControl::AXIS_LEFTX, 1 }, // pan + { type::TYPE_AXIS, LLGameControl::AXIS_TRIGGERRIGHT, 1 }, // rise + { type::TYPE_AXIS, LLGameControl::AXIS_RIGHTY, -1 }, // pitch + { type::TYPE_AXIS, LLGameControl::AXIS_RIGHTX, 1 }, // yaw + { type::TYPE_NONE, 0 } // zoom }; } -void LLGameControllerManager::addController(SDL_JoystickID id, SDL_GameController* controller) +void LLGameControllerManager::getDefaultMappings(std::vector>& mappings) { - LL_INFOS("GameController") << "joystick id: " << id << ", controller: " << controller << LL_ENDL; + // Join two different data structures into the one + std::vector flycam_channels; + getDefaultMappings(mappings, flycam_channels); + for (size_t i = 0; i < flycam_channels.size(); ++i) + { + mappings.emplace_back(mFlycamActions[i], flycam_channels[i]); + } +} +void LLGameControllerManager::initializeMappingsByDefault() +{ + std::vector> agent_channels; + getDefaultMappings(agent_channels, mFlycamChannels); + mActionTranslator.setMappings(agent_channels); +} + +void LLGameControllerManager::resetDeviceOptionsToDefaults() +{ + for (LLGameControl::Device& device : mDevices) + { + device.resetOptionsToDefaults(); + } +} + +void LLGameControllerManager::loadDeviceOptionsFromSettings() +{ + for (LLGameControl::Device& device : mDevices) + { + device.loadOptionsFromString(getDeviceOptionsString(device.getGUID())); + } +} + +void LLGameControllerManager::saveDeviceOptionsToSettings() const +{ + for (const LLGameControl::Device& device : mDevices) + { + std::string options = device.saveOptionsToString(); + if (options.empty()) + { + g_deviceOptions.erase(device.getGUID()); + } + else + { + g_deviceOptions[device.getGUID()] = options; + } + } +} + +void LLGameControllerManager::addController(SDL_JoystickID id, const std::string& guid, const std::string& name) +{ llassert(id >= 0); - llassert(controller); - if (findState(id) != mStates.end()) + for (const LLGameControl::Device& device : mDevices) { - LL_WARNS("GameController") << "device already added" << LL_ENDL; - return; + if (device.getJoystickID() == id) + { + LL_WARNS("SDL2") << "device with id=" << id << " was already added" + << ", guid: '" << device.getGUID() << "'" + << ", name: '" << device.getName() << "'" + << LL_ENDL; + return; + } } - mStates.emplace_back().setDevice(id, controller); - LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec - << " controller=" << controller - << LL_ENDL; + mDevices.emplace_back(id, guid, name).loadOptionsFromString(getDeviceOptionsString(guid)); } void LLGameControllerManager::removeController(SDL_JoystickID id) { - LL_INFOS("GameController") << "joystick id: " << id << LL_ENDL; + LL_INFOS("SDL2") << "joystick id: " << id << LL_ENDL; - mStates.remove_if([id](LLGameControl::State& state) + mDevices.remove_if([id](LLGameControl::Device& device) { - return state.getJoystickID() == id; + return device.getJoystickID() == id; }); } void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) { - if (axis > MAX_AXIS) + device_it it = findDevice(id); + if (it == mDevices.end()) { + LL_WARNS("SDL2") << "Unknown device: joystick=0x" << std::hex << id << std::dec + << " axis=" << (S32)axis + << " value=" << (S32)value << LL_ENDL; return; } - state_it it = findState(id); - if (it != mStates.end()) + // Map axis using device-specific settings + // or leave the value unchanged + U8 mapped_axis = it->mOptions.mapAxis(axis); + if (mapped_axis != axis) { - // Note: the RAW analog joysticks provide NEGATIVE X,Y values for LEFT,FORWARD - // whereas those directions are actually POSITIVE in SL's local right-handed - // reference frame. Therefore we implicitly negate those axes here where - // they are extracted from SDL, before being used anywhere. - if (axis < SDL_CONTROLLER_AXIS_TRIGGERLEFT) - { - // Note: S16 value is in range [-32768, 32767] which means - // the negative range has an extra possible value. We need - // to add (or subtract) one during negation. - if (value < 0) - { - value = - (value + 1); - } - else if (value > 0) - { - value = (-value) - 1; - } - } + LL_DEBUGS("SDL2") << "Axis mapped: joystick=0x" << std::hex << id << std::dec + << " input axis i=" << (S32)axis + << " mapped axis i=" << (S32)mapped_axis << LL_ENDL; + axis = mapped_axis; + } - LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + if (axis >= LLGameControl::NUM_AXES) + { + LL_WARNS("SDL2") << "Unknown axis: joystick=0x" << std::hex << id << std::dec << " axis=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; - it->mAxes[axis] = value; + return; } + + // Fix value using device-specific settings + // or leave the value unchanged + S16 fixed_value = it->mOptions.fixAxisValue(axis, value); + if (fixed_value != value) + { + LL_DEBUGS("SDL2") << "Value fixed: joystick=0x" << std::hex << id << std::dec + << " axis i=" << (S32)axis + << " input value=" << (S32)value + << " fixed value=" << (S32)fixed_value << LL_ENDL; + value = fixed_value; + } + + // Note: the RAW analog joysticks provide NEGATIVE X,Y values for LEFT,FORWARD + // whereas those directions are actually POSITIVE in SL's local right-handed + // reference frame. Therefore we implicitly negate those axes here where + // they are extracted from SDL, before being used anywhere. + if (axis < SDL_CONTROLLER_AXIS_TRIGGERLEFT) + { + // Note: S16 value is in range [-32768, 32767] which means + // the negative range has an extra possible value. We need + // to add (or subtract) one during negation. + if (value < 0) + { + value = - (value + 1); + } + else if (value > 0) + { + value = (-value) - 1; + } + } + + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " axis=" << (S32)(axis) + << " value=" << (S32)(value) << LL_ENDL; + it->mState.mAxes[axis] = value; } void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool pressed) { - state_it it = findState(id); - if (it != mStates.end()) + device_it it = findDevice(id); + if (it == mDevices.end()) { - if (it->onButton(button, pressed)) - { - LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec - << " button i=" << (S32)(button) - << " pressed=" << pressed << LL_ENDL; - } + LL_WARNS("SDL2") << "Unknown device: joystick=0x" << std::hex << id << std::dec + << " button i=" << (S32)button << LL_ENDL; + return; + } + + // Map button using device-specific settings + // or leave the value unchanged + U8 mapped_button = it->mOptions.mapButton(button); + if (mapped_button != button) + { + LL_DEBUGS("SDL2") << "Button mapped: joystick=0x" << std::hex << id << std::dec + << " input button i=" << (S32)button + << " mapped button i=" << (S32)mapped_button << LL_ENDL; + button = mapped_button; + } + + if (button >= LLGameControl::NUM_BUTTONS) + { + LL_WARNS("SDL2") << "Unknown button: joystick=0x" << std::hex << id << std::dec + << " button i=" << (S32)button << LL_ENDL; + return; + } + + if (it->mState.onButton(button, pressed)) + { + LL_DEBUGS("SDL2") << "joystick=0x" << std::hex << id << std::dec + << " button i=" << (S32)button + << " pressed=" << pressed << LL_ENDL; } } void LLGameControllerManager::clearAllStates() { - for (auto& state : mStates) + for (auto& device : mDevices) { - state.clear(); + device.mState.clear(); } mExternalState.clear(); mLastActiveFlags = 0; @@ -505,146 +1064,249 @@ void LLGameControllerManager::accumulateInternalState() mButtonAccumulator = 0; // accumulate the controllers - for (const auto& state : mStates) + for (const auto& device : mDevices) { - mButtonAccumulator |= state.mButtons; - for (size_t i = 0; i < NUM_AXES; ++i) + mButtonAccumulator |= device.mState.mButtons; + for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i) { // Note: we don't bother to clamp the axes yet // because at this stage we haven't yet accumulated the "inner" state. - mAxesAccumulator[i] += (S32)(state.mAxes[i]); + mAxesAccumulator[i] += (S32)device.mState.mAxes[i]; } } } -void LLGameControllerManager::computeFinalState(LLGameControl::State& final_state) +void LLGameControllerManager::computeFinalState() +{ + // We assume accumulateInternalState() has already been called and we will + // finish by accumulating "external" state (if enabled) + U32 old_buttons = g_finalState.mButtons; + g_finalState.mButtons = mButtonAccumulator; + if (g_translateAgentActions) + { + // accumulate from mExternalState + g_finalState.mButtons |= mExternalState.mButtons; + } + if (old_buttons != g_finalState.mButtons) + { + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + + // clamp the accumulated axes + for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i) + { + S32 axis = mAxesAccumulator[i]; + if (g_translateAgentActions) + { + // Note: we accumulate mExternalState onto local 'axis' variable + // rather than onto mAxisAccumulator[i] because the internal + // accumulated value is also used to drive the Flycam, and + // we don't want any external state leaking into that value. + axis += (S32)mExternalState.mAxes[i]; + } + axis = (S16)std::min(std::max(axis, -32768), 32767); + // check for change + if (g_finalState.mAxes[i] != axis) + { + // When axis changes we explicitly update the corresponding prevAxis + // prior to storing axis. The only other place where prevAxis + // is updated in updateResendPeriod() which is explicitly called after + // a packet is sent. The result is: unchanged axes are included in + // first resend but not later ones. + g_finalState.mPrevAxes[i] = g_finalState.mAxes[i]; + g_finalState.mAxes[i] = axis; + g_nextResendPeriod = 0; // packet needs to go out ASAP + } + } +} + +LLGameControl::ActionNameType LLGameControllerManager::getActionNameType(const std::string& action) const +{ + auto it = mActions.find(action); + return it == mActions.end() ? LLGameControl::ACTION_NAME_UNKNOWN : it->second; +} + +LLGameControl::InputChannel LLGameControllerManager::getChannelByAction(const std::string& action) const +{ + LLGameControl::InputChannel channel; + auto action_it = mActions.find(action); + if (action_it != mActions.end()) + { + if (action_it->second == LLGameControl::ACTION_NAME_FLYCAM) + { + channel = getFlycamChannelByAction(action); + } + else + { + channel = mActionTranslator.getChannelByAction(action); + } + } + return channel; +} + +LLGameControl::InputChannel LLGameControllerManager::getFlycamChannelByAction(const std::string& action) const +{ + auto flycam_it = std::find(mFlycamActions.begin(), mFlycamActions.end(), action); + llassert(flycam_it != mFlycamActions.end()); + std::ptrdiff_t index = std::distance(mFlycamActions.begin(), flycam_it); + return mFlycamChannels[(std::size_t)index]; +} + +// Common implementation of getAnalogMappings(), getBinaryMappings() and getFlycamMappings() +static std::string getMappings(const std::vector& actions, LLGameControl::InputChannel::Type type, + std::function getChannel) +{ + std::list mappings; + + std::vector> default_mappings; + LLGameControl::getDefaultMappings(default_mappings); + + // Walk through the all known actions of the chosen type + for (const std::string& action : actions) + { + LLGameControl::InputChannel channel = getChannel(action); + // Only channels of the expected type should be stored + if (channel.mType == type) + { + bool mapping_differs = false; + for (const auto& pair : default_mappings) + { + if (pair.first == action) + { + mapping_differs = !channel.isEqual(pair.second); + break; + } + } + // Only mappings different from the default should be stored + if (mapping_differs) + { + mappings.push_back(action + ":" + channel.getLocalName()); + } + } + } + + std::string result = LLStringUtil::join(mappings); + + return result; +} + +std::string LLGameControllerManager::getAnalogMappings() const +{ + return getMappings(mAnalogActions, LLGameControl::InputChannel::TYPE_AXIS, + [&](const std::string& action) -> LLGameControl::InputChannel + { + return mActionTranslator.getChannelByAction(action + "+"); + }); +} + +std::string LLGameControllerManager::getBinaryMappings() const +{ + return getMappings(mBinaryActions, LLGameControl::InputChannel::TYPE_BUTTON, + [&](const std::string& action) -> LLGameControl::InputChannel + { + return mActionTranslator.getChannelByAction(action); + }); +} + +std::string LLGameControllerManager::getFlycamMappings() const +{ + return getMappings(mFlycamActions, LLGameControl::InputChannel::TYPE_AXIS, + [&](const std::string& action) -> LLGameControl::InputChannel + { + return getFlycamChannelByAction(action); + }); +} + +// Common implementation of setAnalogMappings(), setBinaryMappings() and setFlycamMappings() +static void setMappings(const std::string& mappings, + const std::vector& actions, LLGameControl::InputChannel::Type type, + std::function updateMap) { - // We assume accumulateInternalState() has already been called and we will - // finish by accumulating "external" state (if enabled) - U32 old_buttons = final_state.mButtons; - final_state.mButtons = mButtonAccumulator; - if (g_translateAgentActions) - { - // accumulate from mExternalState - final_state.mButtons |= mExternalState.mButtons; - } - if (old_buttons != final_state.mButtons) - { - g_nextResendPeriod = 0; // packet needs to go out ASAP - } + if (mappings.empty()) + return; - // clamp the accumulated axes - for (size_t i = 0; i < NUM_AXES; ++i) - { - S32 axis = mAxesAccumulator[i]; - if (g_translateAgentActions) + std::map pairs; + LLStringOps::splitString(mappings, ',', [&](const std::string& mapping) { - // Note: we accumulate mExternalState onto local 'axis' variable - // rather than onto mAxisAccumulator[i] because the internal - // accumulated value is also used to drive the Flycam, and - // we don't want any external state leaking into that value. - axis += (S32)(mExternalState.mAxes[i]); - } - axis = (S16)(std::min(std::max(axis, -32768), 32767)); - // check for change - if (final_state.mAxes[i] != axis) + std::size_t pos = mapping.find(':'); + if (pos > 0 && pos != std::string::npos) + { + pairs[mapping.substr(0, pos)] = mapping.substr(pos + 1); + } + }); + + static const LLGameControl::InputChannel channelNone; + + for (const std::string& action : actions) + { + auto it = pairs.find(action); + if (it != pairs.end()) { - // When axis changes we explicitly update the corresponding prevAxis - // prior to storing axis. The only other place where prevAxis - // is updated in updateResendPeriod() which is explicitly called after - // a packet is sent. The result is: unchanged axes are included in - // first resend but not later ones. - final_state.mPrevAxes[i] = final_state.mAxes[i]; - final_state.mAxes[i] = axis; - g_nextResendPeriod = 0; // packet needs to go out ASAP + LLGameControl::InputChannel channel = LLGameControl::getChannelByName(it->second); + if (channel.isNone() || channel.mType == type) + { + updateMap(action, channel); + continue; + } } + updateMap(action, channelNone); } } -LLGameControl::InputChannel LLGameControllerManager::getChannelByActionName(const std::string& name) const +void LLGameControllerManager::setAnalogMappings(const std::string& mappings) { - LLGameControl::InputChannel channel = mActionTranslator.getChannelByAction(name); - if (channel.isNone()) - { - // maybe we're looking for a flycam action - channel = getFlycamChannelByActionName(name); - } - return channel; + setMappings(mappings, mAnalogActions, LLGameControl::InputChannel::TYPE_AXIS, + [&](const std::string& action, LLGameControl::InputChannel channel) + { + mActionTranslator.updateMap(action, channel); + }); } -// helper -S32 get_flycam_index_by_name(const std::string& name) +void LLGameControllerManager::setBinaryMappings(const std::string& mappings) { - // the Flycam action<-->channel relationship - // is implicitly stored in std::vector in a known order - S32 index = -1; - if (name.rfind("advance", 0) == 0) - { - index = 0; - } - else if (name.rfind("pan", 0) == 0) - { - index = 1; - } - else if (name.rfind("rise", 0) == 0) - { - index = 2; - } - else if (name.rfind("pitch", 0) == 0) - { - index = 3; - } - else if (name.rfind("yaw", 0) == 0) - { - index = 4; - } - else if (name.rfind("zoom", 0) == 0) - { - index = 5; - } - return index; + setMappings(mappings, mBinaryActions, LLGameControl::InputChannel::TYPE_BUTTON, + [&](const std::string& action, LLGameControl::InputChannel channel) + { + mActionTranslator.updateMap(action, channel); + }); } -LLGameControl::InputChannel LLGameControllerManager::getFlycamChannelByActionName(const std::string& name) const +void LLGameControllerManager::setFlycamMappings(const std::string& mappings) { - // the Flycam channels are stored in a strict order - LLGameControl::InputChannel channel; - S32 index = get_flycam_index_by_name(name); - if (index != -1) - { - channel = mFlycamChannels[index]; - } - return channel; + setMappings(mappings, mFlycamActions, LLGameControl::InputChannel::TYPE_AXIS, + [&](const std::string& action, LLGameControl::InputChannel channel) + { + updateFlycamMap(action, channel); + }); } -bool LLGameControllerManager::updateActionMap(const std::string& action, LLGameControl::InputChannel channel) +bool LLGameControllerManager::updateActionMap(const std::string& action, LLGameControl::InputChannel channel) { - bool success = mActionTranslator.updateMap(action, channel); - if (success) + auto action_it = mActions.find(action); + if (action_it == mActions.end()) { - mLastActiveFlags = 0; + LL_WARNS("SDL2") << "unmappable action='" << action << "'" << LL_ENDL; + return false; } - else + + if (action_it->second == LLGameControl::ACTION_NAME_FLYCAM) { - // maybe we're looking for a flycam action - success = updateFlycamMap(action, channel); + updateFlycamMap(action, channel); } - if (!success) + else { - LL_WARNS("GameControl") << "unmappable action='" << action << "'" << LL_ENDL; + mActionTranslator.updateMap(action, channel); } - return success; + return true; } -bool LLGameControllerManager::updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel) +void LLGameControllerManager::updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel) { - S32 index = get_flycam_index_by_name(action); - if (index != -1) - { - mFlycamChannels[index] = channel; - return true; - } - return false; + auto flycam_it = std::find(mFlycamActions.begin(), mFlycamActions.end(), action); + llassert(flycam_it != mFlycamActions.end()); + std::ptrdiff_t index = std::distance(mFlycamActions.begin(), flycam_it); + llassert(index >= 0 && (std::size_t)index < mFlycamChannels.size()); + mFlycamChannels[(std::size_t)index] = channel; } U32 LLGameControllerManager::computeInternalActionFlags() @@ -672,13 +1334,13 @@ void LLGameControllerManager::getFlycamInputs(std::vector& inputs) for (const auto& channel: mFlycamChannels) { S16 axis; - if (channel.mIndex == (U8)(LLGameControl::AXIS_TRIGGERLEFT) - || channel.mIndex == (U8)(LLGameControl::AXIS_TRIGGERRIGHT)) + if (channel.mIndex == LLGameControl::AXIS_TRIGGERLEFT || + channel.mIndex == LLGameControl::AXIS_TRIGGERRIGHT) { // TIED TRIGGER HACK: we assume the two triggers are paired together - S32 total_axis = mAxesAccumulator[(U8)(LLGameControl::AXIS_TRIGGERLEFT)] - - mAxesAccumulator[(U8)(LLGameControl::AXIS_TRIGGERRIGHT)]; - if (channel.mIndex == (U8)(LLGameControl::AXIS_TRIGGERRIGHT)) + S32 total_axis = mAxesAccumulator[LLGameControl::AXIS_TRIGGERLEFT] + - mAxesAccumulator[LLGameControl::AXIS_TRIGGERRIGHT]; + if (channel.mIndex == LLGameControl::AXIS_TRIGGERRIGHT) { // negate previous math when TRIGGERRIGHT is positive channel total_axis *= -1; @@ -692,7 +1354,7 @@ void LLGameControllerManager::getFlycamInputs(std::vector& inputs) // value arrives as S16 in range [-32768, 32767] // so we scale positive and negative values by slightly different factors // to try to map it to [-1, 1] - F32 input = F32(axis) * ((axis > 0.0f) ? 3.051850476e-5 : 3.0517578125e-5f) * channel.mSign; + F32 input = F32(axis) / ((axis > 0.0f) ? 32767 : 32768) * channel.mSign; inputs.push_back(input); } } @@ -737,7 +1399,7 @@ void LLGameControllerManager::setExternalInput(U32 action_flags, U32 buttons) void LLGameControllerManager::clear() { - mStates.clear(); + mDevices.clear(); } U64 get_now_nsec() @@ -748,47 +1410,67 @@ U64 get_now_nsec() void onJoystickDeviceAdded(const SDL_Event& event) { - LL_INFOS("GameController") << "device index: " << event.cdevice.which << LL_ENDL; + SDL_JoystickGUID guid(SDL_JoystickGetDeviceGUID(event.cdevice.which)); + SDL_JoystickType type(SDL_JoystickGetDeviceType(event.cdevice.which)); + std::string name(std::to_string(SDL_JoystickNameForIndex(event.cdevice.which))); + std::string path(std::to_string(SDL_JoystickPathForIndex(event.cdevice.which))); + + LL_INFOS("SDL2") << "joystick {id:" << event.cdevice.which + << ",guid:'" << guid << "'" + << ",type:'" << type << "'" + << ",name:'" << name << "'" + << ",path:'" << path << "'" + << "}" << LL_ENDL; if (SDL_Joystick* joystick = SDL_JoystickOpen(event.cdevice.which)) { - LL_INFOS("GameController") << "joystick: " << joystick << LL_ENDL; + LL_INFOS("SDL2") << "joystick " << joystick << LL_ENDL; } else { - LL_WARNS("GameController") << "Can't open joystick: " << SDL_GetError() << LL_ENDL; + LL_WARNS("SDL2") << "Can't open joystick: " << SDL_GetError() << LL_ENDL; } } void onJoystickDeviceRemoved(const SDL_Event& event) { - LL_INFOS("GameController") << "joystick id: " << event.cdevice.which << LL_ENDL; + LL_INFOS("SDL2") << "joystick id: " << event.cdevice.which << LL_ENDL; } void onControllerDeviceAdded(const SDL_Event& event) { - LL_INFOS("GameController") << "device index: " << event.cdevice.which << LL_ENDL; + std::string guid(std::to_string(SDL_JoystickGetDeviceGUID(event.cdevice.which))); + SDL_GameControllerType type(SDL_GameControllerTypeForIndex(event.cdevice.which)); + std::string name(std::to_string(SDL_GameControllerNameForIndex(event.cdevice.which))); + std::string path(std::to_string(SDL_GameControllerPathForIndex(event.cdevice.which))); + + LL_INFOS("SDL2") << "controller {id:" << event.cdevice.which + << ",guid:'" << guid << "'" + << ",type:'" << type << "'" + << ",name:'" << name << "'" + << ",path:'" << path << "'" + << "}" << LL_ENDL; SDL_JoystickID id = SDL_JoystickGetDeviceInstanceID(event.cdevice.which); if (id < 0) { - LL_WARNS("GameController") << "Can't get device instance ID: " << SDL_GetError() << LL_ENDL; + LL_WARNS("SDL2") << "Can't get device instance ID: " << SDL_GetError() << LL_ENDL; return; } SDL_GameController* controller = SDL_GameControllerOpen(event.cdevice.which); if (!controller) { - LL_WARNS("GameController") << "Can't open game controller: " << SDL_GetError() << LL_ENDL; + LL_WARNS("SDL2") << "Can't open game controller: " << SDL_GetError() << LL_ENDL; return; } - g_manager.addController(id, controller); + g_manager.addController(id, guid, name); } void onControllerDeviceRemoved(const SDL_Event& event) { - LL_INFOS("GameController") << "joystick id=" << event.cdevice.which << LL_ENDL; + LL_INFOS("SDL2") << "joystick id=" << event.cdevice.which << LL_ENDL; SDL_JoystickID id = event.cdevice.which; g_manager.removeController(id); @@ -819,40 +1501,62 @@ void sdl_logger(void *userdata, int category, SDL_LogPriority priority, const ch } // static -void LLGameControl::init(const std::string& gamecontrollerdb_path) +void LLGameControl::init(const std::string& gamecontrollerdb_path, + std::function loadBoolean, + std::function saveBoolean, + std::function loadString, + std::function saveString, + std::function loadObject, + std::function saveObject) { - if (!g_gameControl) + if (g_gameControl) + return; + + llassert(loadBoolean); + llassert(saveBoolean); + llassert(loadString); + llassert(saveString); + llassert(loadObject); + llassert(saveObject); + + int result = SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + if (result < 0) { - int result = SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); - if (result < 0) - { - // This error is critical, we stop working with SDL and return - LL_WARNS("GameController") << "Error initializing the subsystems : " << SDL_GetError() << LL_ENDL; - return; - } + // This error is critical, we stop working with SDL and return + LL_WARNS("SDL2") << "Error initializing the subsystems : " << SDL_GetError() << LL_ENDL; + return; + } - SDL_LogSetOutputFunction(&sdl_logger, nullptr); + SDL_LogSetOutputFunction(&sdl_logger, nullptr); - // The inability to read this file is not critical, we can continue working - if (!LLFile::isfile(gamecontrollerdb_path.c_str())) + // The inability to read this file is not critical, we can continue working + if (!LLFile::isfile(gamecontrollerdb_path.c_str())) + { + LL_WARNS("SDL2") << "Device mapping db file not found: " << gamecontrollerdb_path << LL_ENDL; + } + else + { + int count = SDL_GameControllerAddMappingsFromFile(gamecontrollerdb_path.c_str()); + if (count < 0) { - LL_WARNS("GameController") << "Device mapping db file not found: " << gamecontrollerdb_path << LL_ENDL; + LL_WARNS("SDL2") << "Error adding mappings from " << gamecontrollerdb_path << " : " << SDL_GetError() << LL_ENDL; } else { - int count = SDL_GameControllerAddMappingsFromFile(gamecontrollerdb_path.c_str()); - if (count < 0) - { - LL_WARNS("GameController") << "Error adding mappings from " << gamecontrollerdb_path << " : " << SDL_GetError() << LL_ENDL; - } - else - { - LL_INFOS("GameController") << "Total " << count << " mappings added from " << gamecontrollerdb_path << LL_ENDL; - } + LL_INFOS("SDL2") << "Total " << count << " mappings added from " << gamecontrollerdb_path << LL_ENDL; } - - g_gameControl = LLGameControl::getInstance(); } + + g_gameControl = LLGameControl::getInstance(); + + s_loadBoolean = loadBoolean; + s_saveBoolean = saveBoolean; + s_loadString = loadString; + s_saveString = saveString; + s_loadObject = loadObject; + s_saveObject = saveObject; + + loadFromSettings(); } // static @@ -862,6 +1566,18 @@ void LLGameControl::terminate() SDL_Quit(); } +// static +const std::list& LLGameControl::getDevices() +{ + return g_manager.mDevices; +} + +//static +const std::map& LLGameControl::getDeviceOptions() +{ + return g_deviceOptions; +} + //static // returns 'true' if GameControlInput message needs to go out, // which will be the case for new data or resend. Call this right @@ -870,7 +1586,7 @@ void LLGameControl::terminate() bool LLGameControl::computeFinalStateAndCheckForChanges() { // Note: LLGameControllerManager::computeFinalState() modifies g_nextResendPeriod as a side-effect - g_manager.computeFinalState(g_finalState); + g_manager.computeFinalState(); // should send input when: // sending is enabled and @@ -937,6 +1653,48 @@ const LLGameControl::State& LLGameControl::getState() return g_finalState; } +// static +LLGameControl::InputChannel LLGameControl::getActiveInputChannel() +{ + InputChannel input; + + State state = g_finalState; + if (state.mButtons > 0) + { + // check buttons + input.mType = LLGameControl::InputChannel::TYPE_BUTTON; + for (U8 i = 0; i < 32; ++i) + { + if ((0x1 << i) & state.mButtons) + { + input.mIndex = i; + break; + } + } + } + else + { + // scan axes + S16 threshold = std::numeric_limits::max() / 2; + for (U8 i = 0; i < 6; ++i) + { + if (abs(state.mAxes[i]) > threshold) + { + input.mType = LLGameControl::InputChannel::TYPE_AXIS; + // input.mIndex ultimately translates to a LLGameControl::KeyboardAxis + // which distinguishes between negative and positive directions + // so we must translate to axis index "i" according to the sign + // of the axis value. + input.mIndex = i; + input.mSign = state.mAxes[i] > 0 ? 1 : -1; + break; + } + } + } + + return input; +} + // static void LLGameControl::getFlycamInputs(std::vector& inputs_out) { @@ -944,26 +1702,61 @@ void LLGameControl::getFlycamInputs(std::vector& inputs_out) } // static -void LLGameControl::enableSendToServer(bool enable) +void LLGameControl::setSendToServer(bool enable) { g_sendToServer = enable; + s_saveBoolean(SETTING_SENDTOSERVER, g_sendToServer); } // static -void LLGameControl::enableControlAgent(bool enable) +void LLGameControl::setControlAgent(bool enable) { g_controlAgent = enable; + s_saveBoolean(SETTING_CONTROLAGENT, g_controlAgent); } // static -void LLGameControl::enableTranslateAgentActions(bool enable) +void LLGameControl::setTranslateAgentActions(bool enable) { g_translateAgentActions = enable; + s_saveBoolean(SETTING_TRANSLATEACTIONS, g_translateAgentActions); } +// static void LLGameControl::setAgentControlMode(LLGameControl::AgentControlMode mode) { g_agentControlMode = mode; + s_saveString(SETTING_AGENTCONTROLMODE, convertAgentControlModeToString(mode)); +} + +// static +bool LLGameControl::getSendToServer() +{ + return g_sendToServer; +} + +// static +bool LLGameControl::getControlAgent() +{ + return g_controlAgent; +} + +// static +bool LLGameControl::getTranslateAgentActions() +{ + return g_translateAgentActions; +} + +// static +LLGameControl::AgentControlMode LLGameControl::getAgentControlMode() +{ + return g_agentControlMode; +} + +// static +LLGameControl::ActionNameType LLGameControl::getActionNameType(const std::string& action) +{ + return g_manager.getActionNameType(action); } // static @@ -979,58 +1772,39 @@ bool LLGameControl::willControlAvatar() LLGameControl::InputChannel LLGameControl::getChannelByName(const std::string& name) { LLGameControl::InputChannel channel; + // 'name' has two acceptable formats: AXIS_[sign] or BUTTON_ - if (name.length() < 6) - { - // name must be at least as long as 'AXIS_n' - return channel; - } - if (name.rfind("AXIS_", 0) == 0) + if (LLStringUtil::startsWith(name, "AXIS_")) { - char c = name[5]; - if (c >= '0') - { - channel.mType = LLGameControl::InputChannel::Type::TYPE_AXIS; - channel.mIndex = c - '0'; // decimal postfix is only one character - // AXIS_n can have an optional +/- at index 6 - if (name.length() >= 6) - { - channel.mSign = (name[6] == '-') ? -1 : 1; - } - else - { - // assume positive axis when sign not provided - channel.mSign = 1; - } - } + channel.mType = LLGameControl::InputChannel::Type::TYPE_AXIS; + // Decimal postfix is only one character + channel.mIndex = atoi(name.substr(5, 1).c_str()); + // AXIS_n can have an optional +/- at index 6 + // Assume positive axis when sign not provided + channel.mSign = name.back() == '-' ? -1 : 1; } - else if (name.rfind("BUTTON_", 0) == 0) + else if (LLStringUtil::startsWith(name, "BUTTON_")) { - // the BUTTON_ decimal postfix can be up to two characters wide - size_t i = 6; - U8 index = 0; - while (i < name.length() && i < 8 && name[i] >= '0') - { - index = index * 10 + name[i] - '0'; - } channel.mType = LLGameControl::InputChannel::Type::TYPE_BUTTON; - channel.mIndex = index; + // Decimal postfix is only one or two characters + channel.mIndex = atoi(name.substr(7).c_str()); } + return channel; } // static // Given an action_name like "push+", or "strafe-", returns the InputChannel // mapped to it if found, else channel.isNone() will be true. -LLGameControl::InputChannel LLGameControl::getChannelByActionName(const std::string& name) +LLGameControl::InputChannel LLGameControl::getChannelByAction(const std::string& action) { - return g_manager.getChannelByActionName(name); + return g_manager.getChannelByAction(action); } // static -bool LLGameControl::updateActionMap(const std::string& action_name, LLGameControl::InputChannel channel) +bool LLGameControl::updateActionMap(const std::string& action, LLGameControl::InputChannel channel) { - return g_manager.updateActionMap(action_name, channel); + return g_manager.updateActionMap(action, channel); } // static @@ -1070,3 +1844,239 @@ void LLGameControl::updateResendPeriod() } } +// static +std::string LLGameControl::stringifyAnalogMappings(getChannel_t getChannel) +{ + return getMappings(g_manager.mAnalogActions, InputChannel::TYPE_AXIS, getChannel); +} + +// static +std::string LLGameControl::stringifyBinaryMappings(getChannel_t getChannel) +{ + return getMappings(g_manager.mBinaryActions, InputChannel::TYPE_BUTTON, getChannel); +} + +// static +std::string LLGameControl::stringifyFlycamMappings(getChannel_t getChannel) +{ + return getMappings(g_manager.mFlycamActions, InputChannel::TYPE_AXIS, getChannel); +} + +// static +void LLGameControl::getDefaultMappings(std::vector>& mappings) +{ + g_manager.getDefaultMappings(mappings); +} + +// static +bool LLGameControl::parseDeviceOptions(const std::string& options, std::string& name, + std::vector& axis_options, + std::vector& axis_map, std::vector& button_map) +{ + if (options.empty()) + return false; + + name.clear(); + axis_options.resize(NUM_AXES); + axis_map.resize(NUM_AXES); + button_map.resize(NUM_BUTTONS); + + for (size_t i = 0; i < NUM_AXES; ++i) + { + axis_options[i].resetToDefaults(); + axis_map[i] = (U8)i; + } + + for (size_t i = 0; i < NUM_BUTTONS; ++i) + { + button_map[i] = (U8)i; + } + + std::map pairs; + if (!parse(pairs, options)) + { + LL_WARNS("SDL2") << "Invalid options: '" << options << "'" << LL_ENDL; + return false; + } + + std::map axis_string_options; + if (!parse(axis_string_options, pairs["axis_options"])) + { + LL_WARNS("SDL2") << "Invalid axis_options: '" << pairs["axis_options"] << "'" << LL_ENDL; + return false; + } + + std::map axis_string_map; + if (!parse(axis_string_map, pairs["axis_map"])) + { + LL_WARNS("SDL2") << "Invalid axis_map: '" << pairs["axis_map"] << "'" << LL_ENDL; + return false; + } + + std::map button_string_map; + if (!parse(button_string_map, pairs["button_map"])) + { + LL_WARNS("SDL2") << "Invalid button_map: '" << pairs["button_map"] << "'" << LL_ENDL; + return false; + } + + name = pairs["name"]; + + for (size_t i = 0; i < NUM_AXES; ++i) + { + std::string key = std::to_string(i); + + std::string one_axis_options = axis_string_options[key]; + if (!one_axis_options.empty()) + { + axis_options[i].loadFromString(one_axis_options); + } + + std::string value = axis_string_map[key]; + if (!value.empty()) + { + size_t number = std::stoull(value); + if (number >= NUM_AXES || std::to_string(number) != value) + { + LL_WARNS("SDL2") << "Invalid axis mapping: " << i << "->" << value << LL_ENDL; + } + else + { + axis_map[i] = (U8)number; + } + } + } + + for (size_t i = 0; i < NUM_BUTTONS; ++i) + { + std::string value = button_string_map[std::to_string(i)]; + if (!value.empty()) + { + size_t number = std::stoull(value); + if (number >= NUM_BUTTONS || std::to_string(number) != value) + { + LL_WARNS("SDL2") << "Invalid button mapping: " << i << "->" << value << LL_ENDL; + } + else + { + button_map[i] = (U8)number; + } + } + } + + return true; +} + +// static +std::string LLGameControl::stringifyDeviceOptions(const std::string& name, + const std::vector& axis_options, + const std::vector& axis_map, const std::vector& button_map, + bool force_empty) +{ + std::list options; + + auto opts2str = [](size_t i, const Options::AxisOptions& options) -> std::string + { + std::string string = options.saveToString(); + return string.empty() ? string : llformat("%u:%s", i, string.c_str()); + }; + + std::string axis_options_string = LLStringUtil::join, Options::AxisOptions>(axis_options, opts2str); + if (!axis_options_string.empty()) + { + options.push_back("axis_options:{" + axis_options_string + "}"); + } + + auto map2str = [](size_t index, const U8& value) -> std::string + { + return value == index ? LLStringUtil::null : llformat("%u:%u", index, value); + }; + + std::string axis_map_string = LLStringUtil::join, U8>(axis_map, map2str); + if (!axis_map_string.empty()) + { + options.push_back("axis_map:{" + axis_map_string + "}"); + } + + std::string button_map_string = LLStringUtil::join, U8>(button_map, map2str); + if (!button_map_string.empty()) + { + options.push_back("button_map:{" + button_map_string + "}"); + } + + if (!force_empty && options.empty()) + return LLStringUtil::null; + + // Remove control characters [',', '{', '}'] from name + std::string safe_name; + safe_name.reserve(name.size()); + for (char c : name) + { + if (c != ',' && c != '{' && c != '}') + { + safe_name.push_back(c); + } + } + options.push_front(llformat("name:%s", safe_name.c_str())); + + std::string result = LLStringUtil::join(options); + + return "{" + result + "}"; +} + +// static +void LLGameControl::initByDefault() +{ + g_sendToServer = false; + g_controlAgent = false; + g_translateAgentActions = false; + g_agentControlMode = CONTROL_MODE_AVATAR; + g_manager.initializeMappingsByDefault(); + g_manager.resetDeviceOptionsToDefaults(); + g_deviceOptions.clear(); +} + +// static +void LLGameControl::loadFromSettings() +{ + // In case of absence of the required setting the default value is assigned + g_sendToServer = s_loadBoolean(SETTING_SENDTOSERVER); + g_controlAgent = s_loadBoolean(SETTING_CONTROLAGENT); + g_translateAgentActions = s_loadBoolean(SETTING_TRANSLATEACTIONS); + g_agentControlMode = convertStringToAgentControlMode(s_loadString(SETTING_AGENTCONTROLMODE)); + + g_manager.initializeMappingsByDefault(); + + // Load action-to-channel mappings + std::string analogMappings = s_loadString(SETTING_ANALOGMAPPINGS); + std::string binaryMappings = s_loadString(SETTING_BINARYMAPPINGS); + std::string flycamMappings = s_loadString(SETTING_FLYCAMMAPPINGS); + g_manager.setAnalogMappings(analogMappings); + g_manager.setBinaryMappings(binaryMappings); + g_manager.setFlycamMappings(flycamMappings); + + // Load device-specific settings + g_deviceOptions.clear(); + LLSD options = s_loadObject(SETTING_KNOWNCONTROLLERS); + for (auto it = options.beginMap(); it != options.endMap(); ++it) + { + g_deviceOptions.emplace(it->first, it->second); + } + g_manager.loadDeviceOptionsFromSettings(); +} + +// static +void LLGameControl::saveToSettings() +{ + s_saveBoolean(SETTING_SENDTOSERVER, g_sendToServer); + s_saveBoolean(SETTING_CONTROLAGENT, g_controlAgent); + s_saveBoolean(SETTING_TRANSLATEACTIONS, g_translateAgentActions); + s_saveString(SETTING_AGENTCONTROLMODE, convertAgentControlModeToString(g_agentControlMode)); + s_saveString(SETTING_ANALOGMAPPINGS, g_manager.getAnalogMappings()); + s_saveString(SETTING_BINARYMAPPINGS, g_manager.getBinaryMappings()); + s_saveString(SETTING_FLYCAMMAPPINGS, g_manager.getFlycamMappings()); + + g_manager.saveDeviceOptionsToSettings(); + LLSD deviceOptions(g_deviceOptions, true); + s_saveObject(SETTING_KNOWNCONTROLLERS, deviceOptions); +} -- cgit v1.2.3 From 9c986bef6704ac07112e18dc82b870acf1847e41 Mon Sep 17 00:00:00 2001 From: leviathan Date: Thu, 27 Jun 2024 00:18:42 -0700 Subject: put GameControl behind a feature flag --- indra/llwindow/llgamecontrol.cpp | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 23849aca66..0e3782a10e 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -443,6 +443,7 @@ namespace U64 g_lastSend = 0; U64 g_nextResendPeriod = FIRST_RESEND_PERIOD; + bool g_enabled = false; bool g_sendToServer = false; bool g_controlAgent = false; bool g_translateAgentActions = false; @@ -457,6 +458,7 @@ namespace std::function s_loadObject; std::function s_saveObject; + std::string SETTING_ENABLE("EnableGameControl"); std::string SETTING_SENDTOSERVER("GameControlToServer"); std::string SETTING_CONTROLAGENT("GameControlToAgent"); std::string SETTING_TRANSLATEACTIONS("AgentToGameControl"); @@ -1489,6 +1491,21 @@ void onControllerAxis(const SDL_Event& event) g_manager.onAxis(event.caxis.which, event.caxis.axis, event.caxis.value); } +// static +bool LLGameControl::isEnabled() +{ + return g_enabled; +} + +void LLGameControl::setEnabled(bool enable) +{ + if (enable != g_enabled) + { + g_enabled = enable; + s_saveBoolean(SETTING_ENABLE, g_enabled); + } +} + // static bool LLGameControl::isInitialized() { @@ -1593,7 +1610,7 @@ bool LLGameControl::computeFinalStateAndCheckForChanges() // g_lastSend has "expired" // either because g_nextResendPeriod has been zeroed // or the last send really has expired. - return g_sendToServer && (g_lastSend + g_nextResendPeriod < get_now_nsec()); + return g_enabled && g_sendToServer && (g_lastSend + g_nextResendPeriod < get_now_nsec()); } // static @@ -1762,7 +1779,7 @@ LLGameControl::ActionNameType LLGameControl::getActionNameType(const std::string // static bool LLGameControl::willControlAvatar() { - return g_controlAgent && g_agentControlMode == CONTROL_MODE_AVATAR; + return g_enabled && g_controlAgent && g_agentControlMode == CONTROL_MODE_AVATAR; } // static @@ -2040,6 +2057,7 @@ void LLGameControl::initByDefault() void LLGameControl::loadFromSettings() { // In case of absence of the required setting the default value is assigned + g_enabled = s_loadBoolean(SETTING_ENABLE); g_sendToServer = s_loadBoolean(SETTING_SENDTOSERVER); g_controlAgent = s_loadBoolean(SETTING_CONTROLAGENT); g_translateAgentActions = s_loadBoolean(SETTING_TRANSLATEACTIONS); @@ -2068,6 +2086,7 @@ void LLGameControl::loadFromSettings() // static void LLGameControl::saveToSettings() { + s_saveBoolean(SETTING_ENABLE, g_enabled); s_saveBoolean(SETTING_SENDTOSERVER, g_sendToServer); s_saveBoolean(SETTING_CONTROLAGENT, g_controlAgent); s_saveBoolean(SETTING_TRANSLATEACTIONS, g_translateAgentActions); -- cgit v1.2.3 From 59ed92522f7b72731911825a831bab559f0c1c8b Mon Sep 17 00:00:00 2001 From: leviathan Date: Wed, 17 Jul 2024 15:25:22 -0700 Subject: even more correct GameControl feature-flag switch --- indra/llwindow/llgamecontrol.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 0e3782a10e..812c861370 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -434,7 +434,7 @@ namespace // data state. However, to keep the ambient resend bandwidth low we // expand the resend period at a geometric rate. // - constexpr U64 MSEC_PER_NSEC = 1e6; + constexpr U64 MSEC_PER_NSEC = 1000000; constexpr U64 FIRST_RESEND_PERIOD = 100 * MSEC_PER_NSEC; constexpr U64 RESEND_EXPANSION_RATE = 10; LLGameControl::State g_outerState; // from controller devices -- cgit v1.2.3 From 8213a0fb6b3149042d6833c8f0c4a3c1bfdc8bc9 Mon Sep 17 00:00:00 2001 From: leviathan Date: Tue, 27 Aug 2024 16:18:57 -0700 Subject: more correct application of GameControl prefs --- indra/llwindow/llgamecontrol.cpp | 68 ++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 23 deletions(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 812c861370..c7a27c5558 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -356,6 +356,7 @@ public: void resetDeviceOptionsToDefaults(); void loadDeviceOptionsFromSettings(); void saveDeviceOptionsToSettings() const; + void setDeviceOptions(const std::string& guid, const LLGameControl::Options& options); void addController(SDL_JoystickID id, const std::string& guid, const std::string& name); void removeController(SDL_JoystickID id); @@ -457,6 +458,7 @@ namespace std::function s_saveString; std::function s_loadObject; std::function s_saveObject; + std::function s_updateUI; std::string SETTING_ENABLE("EnableGameControl"); std::string SETTING_SENDTOSERVER("GameControlToServer"); @@ -549,7 +551,6 @@ LLGameControl::Options::Options() mAxisOptions.resize(NUM_AXES); mAxisMap.resize(NUM_AXES); mButtonMap.resize(NUM_BUTTONS); - resetToDefaults(); } @@ -560,7 +561,6 @@ void LLGameControl::Options::resetToDefaults() mAxisOptions[i].resetToDefaults(); mAxisMap[i] = (U8)i; } - for (size_t i = 0; i < NUM_BUTTONS; ++i) { mButtonMap[i] = (U8)i; @@ -595,18 +595,7 @@ S16 LLGameControl::Options::fixAxisValue(U8 axis, S16 value) const } else { - const AxisOptions& options = mAxisOptions[axis]; - S32 new_value = (S32)value + (S32)options.mOffset; - value = (S16)std::clamp(new_value , -32768, 32767); - if ((value > 0 && value < (S16)options.mDeadZone) || - (value < 0 && value > -(S16)options.mDeadZone)) - { - value = 0; - } - else if (options.mInvert) - { - value = -value; - } + value = mAxisOptions[axis].computeModifiedValue(value); } return value; } @@ -615,7 +604,7 @@ std::string LLGameControl::Options::AxisOptions::saveToString() const { std::list options; - if (mInvert) + if (mMultiplier == -1) { options.push_back("invert:1"); } @@ -715,16 +704,17 @@ void LLGameControl::Options::AxisOptions::loadFromString(std::string options) LL_WARNS("SDL2") << "Invalid axis options: '" << options << "'" << LL_ENDL; } + mMultiplier = 1; std::string invert = pairs["invert"]; if (!invert.empty()) { - if (invert != "1") + if (invert == "1") { - LL_WARNS("SDL2") << "Invalid invert value: '" << invert << "'" << LL_ENDL; + mMultiplier = -1; } else { - mInvert = true; + LL_WARNS("SDL2") << "Invalid invert value: '" << invert << "'" << LL_ENDL; } } @@ -732,13 +722,13 @@ void LLGameControl::Options::AxisOptions::loadFromString(std::string options) if (!dead_zone.empty()) { size_t number = std::stoull(dead_zone); - if (number > MAX_AXIS_DEAD_ZONE || std::to_string(number) != dead_zone) + if (number <= MAX_AXIS_DEAD_ZONE && std::to_string(number) == dead_zone) { - LL_WARNS("SDL2") << "Invalid dead_zone value: '" << dead_zone << "'" << LL_ENDL; + mDeadZone = (U16)number; } else { - mDeadZone = (U16)number; + LL_WARNS("SDL2") << "Invalid dead_zone value: '" << dead_zone << "'" << LL_ENDL; } } @@ -764,11 +754,13 @@ std::string LLGameControl::Options::saveToString(const std::string& name, bool f bool LLGameControl::Options::loadFromString(std::string& name, std::string options) { + resetToDefaults(); return LLGameControl::parseDeviceOptions(options, name, mAxisOptions, mAxisMap, mButtonMap); } bool LLGameControl::Options::loadFromString(std::string options) { + resetToDefaults(); std::string dummy_name; return LLGameControl::parseDeviceOptions(options, dummy_name, mAxisOptions, mAxisMap, mButtonMap); } @@ -916,6 +908,19 @@ void LLGameControllerManager::saveDeviceOptionsToSettings() const } } +void LLGameControllerManager::setDeviceOptions(const std::string& guid, const LLGameControl::Options& options) +{ + // find Device by name + for (LLGameControl::Device& device : mDevices) + { + if (device.getGUID() == guid) + { + device.mOptions = options; + return; + } + } +} + void LLGameControllerManager::addController(SDL_JoystickID id, const std::string& guid, const std::string& name) { llassert(id >= 0); @@ -1468,6 +1473,10 @@ void onControllerDeviceAdded(const SDL_Event& event) } g_manager.addController(id, guid, name); + + // this event could happen while the preferences UI is open + // in which case we need to force it to update + s_updateUI(); } void onControllerDeviceRemoved(const SDL_Event& event) @@ -1476,6 +1485,10 @@ void onControllerDeviceRemoved(const SDL_Event& event) SDL_JoystickID id = event.cdevice.which; g_manager.removeController(id); + + // this event could happen while the preferences UI is open + // in which case we need to force it to update + s_updateUI(); } void onControllerButton(const SDL_Event& event) @@ -1524,7 +1537,8 @@ void LLGameControl::init(const std::string& gamecontrollerdb_path, std::function loadString, std::function saveString, std::function loadObject, - std::function saveObject) + std::function saveObject, + std::function updateUI) { if (g_gameControl) return; @@ -1535,6 +1549,7 @@ void LLGameControl::init(const std::string& gamecontrollerdb_path, llassert(saveString); llassert(loadObject); llassert(saveObject); + llassert(updateUI); int result = SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); if (result < 0) @@ -1572,6 +1587,7 @@ void LLGameControl::init(const std::string& gamecontrollerdb_path, s_saveString = saveString; s_loadObject = loadObject; s_saveObject = saveObject; + s_updateUI = updateUI; loadFromSettings(); } @@ -1692,7 +1708,7 @@ LLGameControl::InputChannel LLGameControl::getActiveInputChannel() else { // scan axes - S16 threshold = std::numeric_limits::max() / 2; + constexpr S16 threshold = std::numeric_limits::max() / 2; for (U8 i = 0; i < 6; ++i) { if (abs(state.mAxes[i]) > threshold) @@ -2099,3 +2115,9 @@ void LLGameControl::saveToSettings() LLSD deviceOptions(g_deviceOptions, true); s_saveObject(SETTING_KNOWNCONTROLLERS, deviceOptions); } + +// static +void LLGameControl::setDeviceOptions(const std::string& guid, const Options& options) +{ + g_manager.setDeviceOptions(guid, options); +} -- cgit v1.2.3 From 0617923ae7f450ece7288f8a73446c45a8ed32db Mon Sep 17 00:00:00 2001 From: leviathan Date: Tue, 3 Sep 2024 15:38:35 -0700 Subject: remove crashy LLSD ctor used by GameControl --- indra/llwindow/llgamecontrol.cpp | 95 +++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 46 deletions(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index c7a27c5558..01c6f91d25 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -156,10 +156,6 @@ namespace std { ss << ",version:" << version; } - if (U16 firmware = SDL_JoystickGetFirmwareVersion(joystick)) - { - ss << ",firmware:" << firmware; - } if (const char* serial = SDL_JoystickGetSerial(joystick)) { ss << ",serial:'" << serial << "'"; @@ -190,10 +186,6 @@ namespace std { ss << ",version:" << version; } - if (U16 firmware = SDL_GameControllerGetFirmwareVersion(controller)) - { - ss << ",firmware:" << firmware; - } if (const char* serial = SDL_GameControllerGetSerial(controller)) { ss << ",serial:'" << serial << "'"; @@ -1420,13 +1412,11 @@ void onJoystickDeviceAdded(const SDL_Event& event) SDL_JoystickGUID guid(SDL_JoystickGetDeviceGUID(event.cdevice.which)); SDL_JoystickType type(SDL_JoystickGetDeviceType(event.cdevice.which)); std::string name(std::to_string(SDL_JoystickNameForIndex(event.cdevice.which))); - std::string path(std::to_string(SDL_JoystickPathForIndex(event.cdevice.which))); LL_INFOS("SDL2") << "joystick {id:" << event.cdevice.which << ",guid:'" << guid << "'" << ",type:'" << type << "'" << ",name:'" << name << "'" - << ",path:'" << path << "'" << "}" << LL_ENDL; if (SDL_Joystick* joystick = SDL_JoystickOpen(event.cdevice.which)) @@ -1449,13 +1439,11 @@ void onControllerDeviceAdded(const SDL_Event& event) std::string guid(std::to_string(SDL_JoystickGetDeviceGUID(event.cdevice.which))); SDL_GameControllerType type(SDL_GameControllerTypeForIndex(event.cdevice.which)); std::string name(std::to_string(SDL_GameControllerNameForIndex(event.cdevice.which))); - std::string path(std::to_string(SDL_GameControllerPathForIndex(event.cdevice.which))); LL_INFOS("SDL2") << "controller {id:" << event.cdevice.which << ",guid:'" << guid << "'" << ",type:'" << type << "'" << ",name:'" << name << "'" - << ",path:'" << path << "'" << "}" << LL_ENDL; SDL_JoystickID id = SDL_JoystickGetDeviceInstanceID(event.cdevice.which); @@ -1525,11 +1513,6 @@ bool LLGameControl::isInitialized() return g_gameControl != nullptr; } -void sdl_logger(void *userdata, int category, SDL_LogPriority priority, const char *message) -{ - LL_DEBUGS("SDL2") << "log='" << message << "'" << LL_ENDL; -} - // static void LLGameControl::init(const std::string& gamecontrollerdb_path, std::function loadBoolean, @@ -1551,16 +1534,14 @@ void LLGameControl::init(const std::string& gamecontrollerdb_path, llassert(saveObject); llassert(updateUI); - int result = SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER); + int result = SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR); if (result < 0) { // This error is critical, we stop working with SDL and return - LL_WARNS("SDL2") << "Error initializing the subsystems : " << SDL_GetError() << LL_ENDL; + LL_WARNS("SDL2") << "Error initializing GameController subsystems : " << SDL_GetError() << LL_ENDL; return; } - SDL_LogSetOutputFunction(&sdl_logger, nullptr); - // The inability to read this file is not critical, we can continue working if (!LLFile::isfile(gamecontrollerdb_path.c_str())) { @@ -1596,7 +1577,6 @@ void LLGameControl::init(const std::string& gamecontrollerdb_path, void LLGameControl::terminate() { g_manager.clear(); - SDL_Quit(); } // static @@ -1638,6 +1618,7 @@ void LLGameControl::clearAllStates() // static void LLGameControl::processEvents(bool app_has_focus) { + // This method used by non-linux platforms which only use SDL for GameController input SDL_Event event; if (!app_has_focus) { @@ -1652,31 +1633,42 @@ void LLGameControl::processEvents(bool app_has_focus) while (g_gameControl && SDL_PollEvent(&event)) { - switch (event.type) - { - case SDL_JOYDEVICEADDED: - onJoystickDeviceAdded(event); - break; - case SDL_JOYDEVICEREMOVED: - onJoystickDeviceRemoved(event); - break; - case SDL_CONTROLLERDEVICEADDED: - onControllerDeviceAdded(event); - break; - case SDL_CONTROLLERDEVICEREMOVED: - onControllerDeviceRemoved(event); - break; - case SDL_CONTROLLERBUTTONDOWN: - /* FALLTHROUGH */ - case SDL_CONTROLLERBUTTONUP: + handleEvent(event, app_has_focus); + } +} + +void LLGameControl::handleEvent(const SDL_Event& event, bool app_has_focus) +{ + switch (event.type) + { + case SDL_JOYDEVICEADDED: + onJoystickDeviceAdded(event); + break; + case SDL_JOYDEVICEREMOVED: + onJoystickDeviceRemoved(event); + break; + case SDL_CONTROLLERDEVICEADDED: + onControllerDeviceAdded(event); + break; + case SDL_CONTROLLERDEVICEREMOVED: + onControllerDeviceRemoved(event); + break; + case SDL_CONTROLLERBUTTONDOWN: + /* FALLTHROUGH */ + case SDL_CONTROLLERBUTTONUP: + if (app_has_focus) + { onControllerButton(event); - break; - case SDL_CONTROLLERAXISMOTION: + } + break; + case SDL_CONTROLLERAXISMOTION: + if (app_has_focus) + { onControllerAxis(event); - break; - default: - break; - } + } + break; + default: + break; } } @@ -2112,7 +2104,18 @@ void LLGameControl::saveToSettings() s_saveString(SETTING_FLYCAMMAPPINGS, g_manager.getFlycamMappings()); g_manager.saveDeviceOptionsToSettings(); - LLSD deviceOptions(g_deviceOptions, true); + + // construct LLSD version of g_deviceOptions but only include non-empty values + LLSD deviceOptions = LLSD::emptyMap(); + for (const auto& data_pair : g_deviceOptions) + { + if (!data_pair.second.empty()) + { + LLSD value(data_pair.second); + deviceOptions.insert(data_pair.first, value); + } + } + s_saveObject(SETTING_KNOWNCONTROLLERS, deviceOptions); } -- cgit v1.2.3 From ea650ac073aeb03ab99b88c41f51ce636ce37982 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Thu, 3 Oct 2024 12:08:26 -0700 Subject: fix GameControl save settings, fix linux build --- indra/llwindow/llgamecontrol.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) (limited to 'indra/llwindow/llgamecontrol.cpp') diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 01c6f91d25..9d3c854ca2 100644 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -41,11 +41,6 @@ namespace std { - string to_string(const char* text) - { - return text ? string(text) : LLStringUtil::null; - } - string to_string(const SDL_JoystickGUID& guid) { char buffer[33] = { 0 }; @@ -149,7 +144,7 @@ namespace std SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick); ss << ",guid:'" << guid << "'"; ss << ",type:'" << SDL_JoystickGetType(joystick) << "'"; - ss << ",name:'" << std::to_string(SDL_JoystickName(joystick)) << "'"; + ss << ",name:'" << ll_safe_string(SDL_JoystickName(joystick)) << "'"; ss << ",vendor:" << SDL_JoystickGetVendor(joystick); ss << ",product:" << SDL_JoystickGetProduct(joystick); if (U16 version = SDL_JoystickGetProductVersion(joystick)) @@ -179,7 +174,7 @@ namespace std stringstream ss; ss << "{type:'" << SDL_GameControllerGetType(controller) << "'"; - ss << ",name:'" << std::to_string(SDL_GameControllerName(controller)) << "'"; + ss << ",name:'" << ll_safe_string(SDL_GameControllerName(controller)) << "'"; ss << ",vendor:" << SDL_GameControllerGetVendor(controller); ss << ",product:" << SDL_GameControllerGetProduct(controller); if (U16 version = SDL_GameControllerGetProductVersion(controller)) @@ -1411,7 +1406,7 @@ void onJoystickDeviceAdded(const SDL_Event& event) { SDL_JoystickGUID guid(SDL_JoystickGetDeviceGUID(event.cdevice.which)); SDL_JoystickType type(SDL_JoystickGetDeviceType(event.cdevice.which)); - std::string name(std::to_string(SDL_JoystickNameForIndex(event.cdevice.which))); + std::string name(ll_safe_string(SDL_JoystickNameForIndex(event.cdevice.which))); LL_INFOS("SDL2") << "joystick {id:" << event.cdevice.which << ",guid:'" << guid << "'" @@ -1438,7 +1433,7 @@ void onControllerDeviceAdded(const SDL_Event& event) { std::string guid(std::to_string(SDL_JoystickGetDeviceGUID(event.cdevice.which))); SDL_GameControllerType type(SDL_GameControllerTypeForIndex(event.cdevice.which)); - std::string name(std::to_string(SDL_GameControllerNameForIndex(event.cdevice.which))); + std::string name(ll_safe_string(SDL_GameControllerNameForIndex(event.cdevice.which))); LL_INFOS("SDL2") << "controller {id:" << event.cdevice.which << ",guid:'" << guid << "'" @@ -1514,6 +1509,7 @@ bool LLGameControl::isInitialized() } // static +// TODO: find a cleaner way to provide callbacks to LLGameControl void LLGameControl::init(const std::string& gamecontrollerdb_path, std::function loadBoolean, std::function saveBoolean, -- cgit v1.2.3