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