From 16002ec0c82a59082f670f9364834764f627f835 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sat, 5 Oct 2024 15:25:48 +0200 Subject: add GameControl feature --- indra/newview/llagent.cpp | 244 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 226 insertions(+), 18 deletions(-) (limited to 'indra/newview/llagent.cpp') diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 178a14f470..b05b4d437c 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -1433,7 +1433,7 @@ LLVector3 LLAgent::getReferenceUpVector() return up_vector; } -// Radians, positive is forward into ground +// Radians, positive is downward toward ground //----------------------------------------------------------------------------- // pitch() //----------------------------------------------------------------------------- @@ -1447,27 +1447,23 @@ void LLAgent::pitch(F32 angle) LLVector3 skyward = getReferenceUpVector(); + // SL-19286 Avatar is upside down when viewed from below + // after left-clicking the mouse on the avatar and dragging down + // + // The issue is observed on angle below 10 degrees + const F32 look_down_limit = 179.f * DEG_TO_RAD; + const F32 look_up_limit = 10.f * DEG_TO_RAD; + + F32 angle_from_skyward = acos(mFrameAgent.getAtAxis() * skyward); + // clamp pitch to limits - if (angle >= 0.f) + if ((angle >= 0.f) && (angle_from_skyward + angle > look_down_limit)) { - const F32 look_down_limit = 179.f * DEG_TO_RAD; - F32 angle_from_skyward = acos(mFrameAgent.getAtAxis() * skyward); - if (angle_from_skyward + angle > look_down_limit) - { - angle = look_down_limit - angle_from_skyward; - } + angle = look_down_limit - angle_from_skyward; } - else if (angle < 0.f) + else if ((angle < 0.f) && (angle_from_skyward + angle < look_up_limit)) { - const F32 look_up_limit = 5.f * DEG_TO_RAD; - const LLVector3& viewer_camera_pos = LLViewerCamera::getInstance()->getOrigin(); - LLVector3 agent_focus_pos = getPosAgentFromGlobal(gAgentCamera.calcFocusPositionTargetGlobal()); - LLVector3 look_dir = agent_focus_pos - viewer_camera_pos; - F32 angle_from_skyward = angle_between(look_dir, skyward); - if (angle_from_skyward + angle < look_up_limit) - { - angle = look_up_limit - angle_from_skyward; - } + angle = look_up_limit - angle_from_skyward; } if (fabs(angle) > 1e-4) @@ -4989,6 +4985,218 @@ void LLAgent::renderAutoPilotTarget() } } +static U64 g_lastUpdateTime { 0 }; +static F32 g_deltaTime { 0.0f }; +static S32 g_lastUpdateFrame { 0 }; +static S32 g_deltaFrame { 0 }; + +void LLAgent::applyExternalActionFlags(U32 outer_flags) +{ + llassert(LLCoros::on_main_thread_main_coro()); + assert(LLGameControl::isEnabled() && LLGameControl::willControlAvatar()); + mExternalActionFlags = outer_flags; + + // HACK: AGENT_CONTROL_NUDGE_AT_NEG is used to toggle Flycam + if ((mExternalActionFlags & AGENT_CONTROL_NUDGE_AT_NEG) > 0) + { + if (mToggleFlycam) + { + mUsingFlycam = !mUsingFlycam; + if (mUsingFlycam) + { + // copy main camera transform to flycam + LLViewerCamera* camera = LLViewerCamera::getInstance(); + mFlycam.setTransform(camera->getOrigin(), camera->getQuaternion()); + mFlycam.setView(camera->getView()); + mLastFlycamUpdate = LLFrameTimer::getTotalTime(); + } + } + mToggleFlycam = false; + } + else + { + mToggleFlycam = true; + } + + // measure delta time and frame + // Note: it is possible for the deltas to be very large + // and it is the duty of the code that uses them to clamp as necessary + U64 now = LLFrameTimer::getTotalTime(); + g_deltaTime = F32(now - g_lastUpdateTime) / (F32)(USEC_PER_SEC); + g_lastUpdateTime = now; + + S32 frame_count = LLFrameTimer::getFrameCount(); + g_deltaFrame = frame_count - g_lastUpdateFrame; + g_lastUpdateFrame = frame_count; + + if (mUsingFlycam) + { + // Flycam will be updated later by exernal context + return; + } + + S32 direction = (S32)(mExternalActionFlags & AGENT_CONTROL_AT_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_AT_NEG) >> 1); + if (direction != 0) + { + moveAt(direction); + } + + static U32 last_non_fly_frame = 0; + static U64 last_non_fly_time = 0; + direction = (S32)(mExternalActionFlags & AGENT_CONTROL_UP_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_UP_NEG) >> 1); + if (direction != 0) + { + // HACK: this auto-fly logic based on original code still extant in llviewerinput.cpp::agent_jump() + // but has been cleaned up. + // TODO?: DRY this logic + if (direction > 0) + { + if (!getFlying() + && !upGrabbed() + && gSavedSettings.getBOOL("AutomaticFly")) + { + constexpr F32 FLY_TIME = 0.5f; + constexpr U32 FLY_FRAMES = 4; + F32 delta_time = (F32)(now - last_non_fly_time) / (F32)(USEC_PER_SEC); + U32 delta_frames = frame_count - last_non_fly_frame; + if( delta_time > FLY_TIME + && delta_frames > FLY_FRAMES) + { + setFlying(TRUE); + } + } + } + else + { + last_non_fly_frame = frame_count; + last_non_fly_time = now; + } + + moveUp(direction); + } + else if (!getFlying()) + { + last_non_fly_frame = frame_count; + last_non_fly_time = now; + } + + direction = (S32)(mExternalActionFlags & AGENT_CONTROL_LEFT_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_LEFT_NEG) >> 1); + if (direction != 0) + { + moveLeft(direction); + } + + direction = (S32)(mExternalActionFlags & AGENT_CONTROL_YAW_POS) + - (S32)((mExternalActionFlags & AGENT_CONTROL_YAW_NEG) >> 1); + if (direction != 0) + { + F32 sign = (direction < 0 ? -1.0f : 1.0f); + // HACK: hard-code 3.0 seconds for YawRate measure. It is simpler, + // and the missing variable yaw rate is unnoticeable. + moveYaw(sign * LLFloaterMove::getYawRate(3.0f)); + } + + { + F32 pitch = ((mExternalActionFlags & AGENT_CONTROL_PITCH_POS) > 0 ? 1.0f : 0.0f) + - ((mExternalActionFlags & AGENT_CONTROL_PITCH_NEG) > 0 ? 1.0f : 0.0f); + movePitch(pitch); + } + + if ((mExternalActionFlags & AGENT_CONTROL_FLY) > 0) + { + if (mToggleFly) + { + setFlying(!getFlying()); + } + mToggleFly = false; + } + else + { + mToggleFly = true; + } + + if (mExternalActionFlags & AGENT_CONTROL_STOP) + { + setControlFlags(AGENT_CONTROL_STOP); + } + + // HACK: AGENT_CONTROL_NUDGE_AT_POS is used to toggle running + if ((mExternalActionFlags & AGENT_CONTROL_NUDGE_AT_POS) > 0) + { + if (mToggleRun) + { + if (getRunning()) + { + clearRunning(); + sendWalkRun(false); + } + else + { + setRunning(); + sendWalkRun(true); + } + } + mToggleRun = false; + } + else + { + mToggleRun = true; + } +} + +void LLAgent::pressGameControlButton(U8 button_index) +{ + mGameControlButtonsFromKeys |= (1U << button_index); +} + +void LLAgent::releaseGameControlButton(U8 button_index) +{ + mGameControlButtonsFromKeys &= !(1U << button_index); +} + +void LLAgent::updateFlycam() +{ + // Note: flycam_inputs arrive in range [-1,1] + std::vector flycam_inputs; + LLGameControl::getFlycamInputs(flycam_inputs); + + // Note: no matter how flycam_inputs are mapped to the controller + // they arrive in the following order: + constexpr S32 FLYCAM_ADVANCE = 0; + constexpr S32 FLYCAM_PAN = 1; + constexpr S32 FLYCAM_RISE = 2; + constexpr S32 FLYCAM_PITCH = 3; + constexpr S32 FLYCAM_YAW = 4; + constexpr S32 FLYCAM_ZOOM = 5; + + LLVector3 linear_velocity( + flycam_inputs[FLYCAM_ADVANCE], + flycam_inputs[FLYCAM_PAN], + flycam_inputs[FLYCAM_RISE]); + constexpr F32 MAX_FLYCAM_SPEED = 10.0f; + mFlycam.setLinearVelocity(MAX_FLYCAM_SPEED * linear_velocity); + + mFlycam.setPitchRate(flycam_inputs[FLYCAM_PITCH]); + mFlycam.setYawRate(flycam_inputs[FLYCAM_YAW]); + mFlycam.setZoomRate(flycam_inputs[FLYCAM_ZOOM]); + + mFlycam.integrate(g_deltaTime); + + LLVector3 pos; + LLQuaternion rot; + mFlycam.getTransform(pos, rot); + LLMatrix3 mat(rot); + LLViewerCamera::getInstance()->setOrigin(pos); + LLViewerCamera::getInstance()->mXAxis = LLVector3(mat.mMatrix[0]); + LLViewerCamera::getInstance()->mYAxis = LLVector3(mat.mMatrix[1]); + LLViewerCamera::getInstance()->mZAxis = LLVector3(mat.mMatrix[2]); + + LLViewerCamera::getInstance()->setView(mFlycam.getView()); +} + /********************************************************************************/ //----------------------------------------------------------------------------- -- cgit v1.3 From 72f22ebbdde44d783cd1d3667e9d40867132e090 Mon Sep 17 00:00:00 2001 From: Rye Date: Wed, 31 Dec 2025 07:01:54 -0500 Subject: Address feedback from Copilot Fix releaseGameControlButton to use bitwise op correctly Fix gamecontrol_stub comment and SDL3 include Signed-off-by: Rye --- indra/newview/llagent.cpp | 2 +- indra/newview/tests/llgamecontrol_stub.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'indra/newview/llagent.cpp') diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index b05b4d437c..83809a0bd3 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -5154,7 +5154,7 @@ void LLAgent::pressGameControlButton(U8 button_index) void LLAgent::releaseGameControlButton(U8 button_index) { - mGameControlButtonsFromKeys &= !(1U << button_index); + mGameControlButtonsFromKeys &= ~(1U << button_index); } void LLAgent::updateFlycam() diff --git a/indra/newview/tests/llgamecontrol_stub.cpp b/indra/newview/tests/llgamecontrol_stub.cpp index b06a7fd132..68f507c2dc 100644 --- a/indra/newview/tests/llgamecontrol_stub.cpp +++ b/indra/newview/tests/llgamecontrol_stub.cpp @@ -1,5 +1,5 @@ /** - * @file llgamecontrol_stub.h + * @file llgamecontrol_stub.cpp * @brief Stubbery for LLGameControl * * $LicenseInfo:firstyear=2023&license=viewerlgpl$ @@ -28,7 +28,7 @@ #include "llgamecontrol.h" -#include "SDL2/SDL_events.h" +#include "SDL3/SDL_events.h" LLGameControl::State g_state; -- cgit v1.3 From 5f9fc17e90f3d5a219c52dc3cecf449275174ffc Mon Sep 17 00:00:00 2001 From: leviathan Date: Thu, 7 May 2026 22:52:05 -0700 Subject: overhaul game-control config and UI --- indra/llcommon/llkeybind.cpp | 256 ++- indra/llcommon/llkeybind.h | 3 + indra/llui/llscrolllistctrl.cpp | 3 +- indra/llui/llscrolllistctrl.h | 1 + indra/llui/llurlentry.cpp | 148 +- indra/llui/llurlentry.h | 36 + indra/llui/llurlregistry.cpp | 14 +- indra/llui/llurlregistry.h | 6 + indra/llwindow/llgamecontrol.cpp | 1830 ++++++++++++++------ indra/llwindow/llgamecontrol.h | 207 ++- indra/llwindow/llgamecontroltranslator.cpp | 86 +- indra/llwindow/llgamecontroltranslator.h | 7 +- indra/llwindow/llkeyboard.h | 47 +- indra/llwindow/llsdl.cpp | 12 +- indra/llwindow/llwindowsdl.cpp | 10 +- indra/llwindow/llwindowsdl.h | 2 +- indra/newview/CMakeLists.txt | 4 +- indra/newview/app_settings/gamecontrollerdb.txt | 12 +- indra/newview/app_settings/key_bindings.xml | 41 +- indra/newview/app_settings/settings.xml | 101 +- indra/newview/llagent.cpp | 68 +- indra/newview/llagent.h | 15 + indra/newview/llagentcamera.cpp | 101 +- indra/newview/llagentcamera.h | 14 +- indra/newview/llappviewer.cpp | 70 +- indra/newview/llfloaterjoystick.cpp | 5 +- indra/newview/llfloaterpreference.cpp | 1182 +------------ indra/newview/llfloaterpreference.h | 138 -- indra/newview/llflycam.cpp | 19 + indra/newview/llflycam.h | 2 + indra/newview/llkeyconflict.cpp | 26 +- indra/newview/llpanelpreferencegamecontrol.cpp | 1156 +++++++++++++ indra/newview/llpanelpreferencegamecontrol.h | 195 +++ indra/newview/lltoolfocus.cpp | 42 +- indra/newview/lltoolfocus.h | 10 +- indra/newview/llviewerinput.cpp | 660 +++++-- indra/newview/llviewerinput.h | 29 +- indra/newview/llviewerjoystick.cpp | 2 +- indra/newview/llviewermenu.cpp | 21 +- indra/newview/llviewerwindow.cpp | 45 + .../xui/en/control_table_contents_game_control.xml | 10 + .../xui/en/control_table_contents_movement.xml | 4 +- .../skins/default/xui/en/floater_preferences.xml | 2 +- .../default/xui/en/game_control_table_rows.xml | 12 +- indra/newview/skins/default/xui/en/menu_viewer.xml | 79 +- .../xui/en/panel_preferences_game_control.xml | 850 ++++++--- indra/newview/tests/llgamecontrol_stub.cpp | 4 +- 47 files changed, 5026 insertions(+), 2561 deletions(-) create mode 100755 indra/newview/llpanelpreferencegamecontrol.cpp create mode 100644 indra/newview/llpanelpreferencegamecontrol.h (limited to 'indra/newview/llagent.cpp') diff --git a/indra/llcommon/llkeybind.cpp b/indra/llcommon/llkeybind.cpp index 63ddf88821..8b633ad197 100644 --- a/indra/llcommon/llkeybind.cpp +++ b/indra/llcommon/llkeybind.cpp @@ -37,7 +37,9 @@ LLKeyData::LLKeyData() mMouse(CLICK_NONE), mKey(KEY_NONE), mMask(MASK_NONE), - mIgnoreMasks(false) + mIgnoreMasks(false), + mControllerActionType(255), + mControllerAction(0) { } @@ -46,7 +48,9 @@ LLKeyData::LLKeyData(EMouseClickType mouse, KEY key, MASK mask) mMouse(mouse), mKey(key), mMask(mask), - mIgnoreMasks(false) + mIgnoreMasks(false), + mControllerActionType(255), + mControllerAction(0) { } @@ -55,7 +59,9 @@ LLKeyData::LLKeyData(EMouseClickType mouse, KEY key, bool ignore_mask) mMouse(mouse), mKey(key), mMask(MASK_NONE), - mIgnoreMasks(ignore_mask) + mIgnoreMasks(ignore_mask), + mControllerActionType(255), + mControllerAction(0) { } @@ -64,10 +70,233 @@ LLKeyData::LLKeyData(EMouseClickType mouse, KEY key, MASK mask, bool ignore_mask mMouse(mouse), mKey(key), mMask(mask), - mIgnoreMasks(ignore_mask) + mIgnoreMasks(ignore_mask), + mControllerActionType(255), + mControllerAction(0) { } +LLKeyData::LLKeyData(U8 actionType, U8 action) + : + mMouse(CLICK_NONE), + mKey(KEY_NONE), + mMask(MASK_NONE), + mIgnoreMasks(true), + mControllerActionType(actionType), + mControllerAction(action) +{ + +} + +static std::string controllerStringFromKeyData(const U8 actionType, const U8 action) +{ + switch (actionType) + { + case 0: + { + switch(action) + { + case 0: return "axis_left"; + case 1: return "axis_right"; + case 2: return "axis_forward"; + case 3: return "axis_backward"; + case 4: return "axis_turn_left"; + case 5: return "axis_turn_right"; + case 6: return "axis_look_up"; + case 7: return "axis_look_down"; + case 8: return "axis_up"; + case 9: return "axis_down"; + case 10: return "axis_roll_left"; + case 11: return "axis_roll_right"; + } + } + case 1: + { + switch(action) + { + case 0: return "button_a"; + case 1: return "button_b"; + case 2: return "button_x"; + case 3: return "button_y"; + case 4: return "button_back"; + case 5: return "button_start"; + case 6: return "button_guide"; + case 7: return "button_leftstick"; + case 8: return "button_rightstick"; + case 9: return "button_leftshoulder"; + case 10: return "button_rightshoulder"; + case 11: return "button_dpad_up"; + case 12: return "button_dpad_down"; + case 13: return "button_dpad_left"; + case 14: return "button_dpad_right"; + case 15: return "button_misc1"; + case 16: return "button_paddle1"; + case 17: return "button_paddle2"; + case 18: return "button_paddle3"; + case 19: return "button_paddle4"; + case 20: return "button_touchpad"; + } + } + } + return "NONE"; +} + +static bool controllerActionFromString(const std::string& string, U8& actionType, U8& action) +{ + actionType = 255; + if(LLStringUtil::startsWith(string, "axis_")) { + actionType = 0; + if(string == "axis_left") + { + action = 0; + } + else if(string == "axis_right") + { + action = 1; + } + else if(string == "axis_forward") + { + action = 2; + } + else if(string == "axis_backward") + { + action = 3; + } + else if(string == "axis_turn_left") + { + action = 4; + } + else if(string == "axis_turn_right") + { + action = 5; + } + else if(string == "axis_look_up") + { + action = 6; + } + else if(string == "axis_look_down") + { + action = 7; + } + else if(string == "axis_up") + { + action = 8; + } + else if(string == "axis_down") + { + action = 9; + } + else if(string == "axis_roll_left") + { + action = 10; + } + else if(string == "axis_roll_right") + { + action = 11; + } + else + { + actionType = 255; + return false; + } + return true; + } + else if(LLStringUtil::startsWith(string, "button_")) { + actionType = 1; + if(string == "button_a") + { + action = 9; + } + else if(string == "button_b") + { + action = 1; + } + else if(string == "button_x") + { + action = 2; + } + else if(string == "button_y") + { + action = 3; + } + else if(string == "button_back") + { + action = 4; + } + else if(string == "button_start") + { + action = 5; + } + else if(string == "button_guide") + { + action = 6; + } + else if(string == "button_leftstick") + { + action = 7; + } + else if(string == "button_rightstick") + { + action = 8; + } + else if(string == "button_leftshoulder") + { + action = 9; + } + else if(string == "button_rightshoulder") + { + action = 10; + } + else if(string == "button_dpad_up") + { + action = 11; + } + else if(string == "button_dpad_down") + { + action = 12; + } + else if(string == "button_dpad_left") + { + action = 13; + } + else if(string == "button_dpad_right") + { + action = 14; + } + else if(string == "button_misc1") + { + action = 15; + } + else if(string == "button_paddle1") + { + action = 16; + } + else if(string == "button_paddle2") + { + action = 17; + } + else if(string == "button_paddle3") + { + action = 18; + } + else if(string == "button_paddle4") + { + action = 19; + } + else if(string == "button_touchpad") + { + action = 20; + } + else + { + actionType = 255; + return false; + } + return true; + } + return false; +} + LLKeyData::LLKeyData(const LLSD &key_data) { if (key_data.has("mouse")) @@ -86,6 +315,14 @@ LLKeyData::LLKeyData(const LLSD &key_data) { mMask = key_data["mask"].asInteger(); } + if (key_data.has("controller")) + { + U8 actionType = 255; + U8 action = 0; + controllerActionFromString(key_data["controller"],actionType, action); + mControllerActionType = actionType; + mControllerAction = action; + } } LLSD LLKeyData::asLLSD() const @@ -98,6 +335,7 @@ LLSD LLKeyData::asLLSD() const { data["ignore_accelerators"] = (LLSD::Boolean)mIgnoreMasks; } + data["controller"] = (LLSD::String)controllerStringFromKeyData(mControllerActionType, mControllerAction); return data; } @@ -112,6 +350,8 @@ void LLKeyData::reset() mKey = KEY_NONE; mMask = MASK_NONE; mIgnoreMasks = false; + mControllerActionType = 255; + mControllerAction = 0; } bool LLKeyData::operator==(const LLKeyData& rhs) const @@ -120,6 +360,8 @@ bool LLKeyData::operator==(const LLKeyData& rhs) const if (mKey != rhs.mKey) return false; if (mMask != rhs.mMask) return false; if (mIgnoreMasks != rhs.mIgnoreMasks) return false; + if (mControllerActionType != rhs.mControllerActionType) return false; + if (mControllerAction != rhs.mControllerAction) return false; return true; } @@ -129,6 +371,8 @@ bool LLKeyData::operator!=(const LLKeyData& rhs) const if (mKey != rhs.mKey) return true; if (mMask != rhs.mMask) return true; if (mIgnoreMasks != rhs.mIgnoreMasks) return true; + if (mControllerActionType != rhs.mControllerActionType) return true; + if (mControllerAction != rhs.mControllerAction) return true; return false; } @@ -136,7 +380,9 @@ bool LLKeyData::canHandle(const LLKeyData& data) const { if (data.mKey == mKey && data.mMouse == mMouse - && ((mIgnoreMasks && (data.mMask & mMask) == mMask) || data.mMask == mMask)) + && ((mIgnoreMasks && (data.mMask & mMask) == mMask) || data.mMask == mMask) + && (mControllerActionType == data.mControllerActionType && mControllerAction == data.mControllerAction) + ) { return true; } diff --git a/indra/llcommon/llkeybind.h b/indra/llcommon/llkeybind.h index 620b859e8c..2e623c4dd7 100644 --- a/indra/llcommon/llkeybind.h +++ b/indra/llcommon/llkeybind.h @@ -37,6 +37,7 @@ public: LLKeyData(EMouseClickType mouse, KEY key, MASK mask); LLKeyData(EMouseClickType mouse, KEY key, bool ignore_mask); LLKeyData(EMouseClickType mouse, KEY key, MASK mask, bool ignore_mask); + LLKeyData(U8 actionType, U8 action); LLKeyData(const LLSD &key_data); LLSD asLLSD() const; @@ -52,6 +53,8 @@ public: EMouseClickType mMouse; KEY mKey; MASK mMask; + U8 mControllerActionType; + U8 mControllerAction; // Either to expect exact match or ignore not expected masks as long as expected mask-bit is present bool mIgnoreMasks; }; diff --git a/indra/llui/llscrolllistctrl.cpp b/indra/llui/llscrolllistctrl.cpp index a2f16f5fa9..2dc74ccb49 100644 --- a/indra/llui/llscrolllistctrl.cpp +++ b/indra/llui/llscrolllistctrl.cpp @@ -2155,9 +2155,10 @@ bool LLScrollListCtrl::handleClick(S32 x, S32 y, MASK mask) for (item_list::iterator iter = mItemList.begin(); iter != mItemList.end(); iter++) { LLScrollListItem* item = *iter; - if (item->getSelected()) + if (item && item->getSelected()) { LLScrollListCell* cellp = item->getColumn(column_index); + if (!cellp) continue; cellp->setValue(item_value); cellp->onCommit(); if (mLastSelected == NULL) diff --git a/indra/llui/llscrolllistctrl.h b/indra/llui/llscrolllistctrl.h index 3bb91c528f..1640fcf1f0 100644 --- a/indra/llui/llscrolllistctrl.h +++ b/indra/llui/llscrolllistctrl.h @@ -178,6 +178,7 @@ public: virtual LLScrollListColumn* getColumn(S32 index); virtual LLScrollListColumn* getColumn(const std::string& name); virtual S32 getNumColumns() const { return static_cast(mColumnsIndexed.size()); } + virtual S32 getPageLines() const { return mPageLines; } // Adds a single element, from an array of: // "columns" => [ "column" => column name, "value" => value, "type" => type, "font" => font, "font-style" => style ], "id" => uuid diff --git a/indra/llui/llurlentry.cpp b/indra/llui/llurlentry.cpp index 06dac54cf9..fb6d90f86f 100644 --- a/indra/llui/llurlentry.cpp +++ b/indra/llui/llurlentry.cpp @@ -34,6 +34,7 @@ #include "llavatarnamecache.h" #include "llcachename.h" +#include "llgamecontrol.h" #include "llkeyboard.h" #include "llregex.h" #include "llscrolllistctrl.h" // for LLUrlEntryKeybinding file parsing @@ -403,9 +404,9 @@ bool LLUrlEntryInvalidSLURL::isSLURLvalid(const std::string &url) const if (path_parts == actual_parts) { // handle slurl with (X,Y,Z) coordinates - LLStringUtil::convertToS32(path_array[path_parts-3],x); - LLStringUtil::convertToS32(path_array[path_parts-2],y); - LLStringUtil::convertToS32(path_array[path_parts-1],z); + LLStringUtil::convertToS32(path_array[path_parts - 3].asStringRef(), x); + LLStringUtil::convertToS32(path_array[path_parts - 2].asStringRef(), y); + LLStringUtil::convertToS32(path_array[path_parts - 1].asStringRef(), z); if((x>= 0 && x<= 256) && (y>= 0 && y<= 256) && (z>= 0)) { @@ -416,8 +417,8 @@ bool LLUrlEntryInvalidSLURL::isSLURLvalid(const std::string &url) const { // handle slurl with (X,Y) coordinates - LLStringUtil::convertToS32(path_array[path_parts-2],x); - LLStringUtil::convertToS32(path_array[path_parts-1],y); + LLStringUtil::convertToS32(path_array[path_parts - 2].asStringRef(), x); + LLStringUtil::convertToS32(path_array[path_parts - 1].asStringRef(), y); ; if((x>= 0 && x<= 256) && (y>= 0 && y<= 256)) { @@ -427,7 +428,7 @@ bool LLUrlEntryInvalidSLURL::isSLURLvalid(const std::string &url) const else if (path_parts == (actual_parts-2)) { // handle slurl with (X) coordinate - LLStringUtil::convertToS32(path_array[path_parts-1],x); + LLStringUtil::convertToS32(path_array[path_parts - 1].asStringRef(), x); if(x>= 0 && x<= 256) { return true; @@ -1785,3 +1786,138 @@ void LLUrlEntryKeybinding::initLocalizationFromFile(const std::string& filename) } } } + + +// +// LLUrlEntryGameController Displays currently assigned game control binding +// +LLUrlEntryGameController::LLUrlEntryGameController() + : LLUrlEntryBase() + , pKeyHandler(NULL) + , pControllerHandler(NULL) +{ + mPattern = boost::regex(APP_HEADER_REGEX "/gamecontroller/[\\w+-]+(\\?mode=\\w+)?$", + boost::regex::perl | boost::regex::icase); + mMenuName = "menu_url_experience.xml"; + + initLocalization(); +} + +std::string LLUrlEntryGameController::getLabel(const std::string& url, const LLUrlLabelCallback& cb) +{ + std::string control = getControlName(url); + + std::string keybind = ""; + if (pControllerHandler && pControllerHandler->hasHandlingDevice()) + { + keybind = pControllerHandler->getBindingAsString(control); + LL_WARNS() << "Game Control Translate:" << control << " = " << keybind << LL_ENDL; + if(!keybind.empty()) + { + std::map::iterator iter = mLocalizations.find(keybind); + if(iter != mLocalizations.end()) { + keybind = iter->second.mLocalization; + } + // Debug text change for testing + keybind = keybind + " (Game Control)"; + return keybind; + } + } + // Fallback to key binding if game control binding is not found + if (pKeyHandler && keybind.empty()) + { + LLStringUtil::toLower(control); + keybind = pKeyHandler->getKeyBindingAsString(getMode(url), "game_control_" + control); + LL_WARNS() << "Key Binding Translate:" << control << " = " << keybind << LL_ENDL; + if(!keybind.empty()) + { + // Debug text change for testing + keybind = keybind + " (Key Binding)"; + return keybind; + } + } + + return "Unbound Control: " + control; +} + +std::string LLUrlEntryGameController::getTooltip(const std::string& url) const +{ + std::string control = getControlName(url); + + std::map::const_iterator iter = mLocalizations.find(control); + if (iter != mLocalizations.end()) + { + return iter->second.mTooltip; + } + return url; +} + +std::string LLUrlEntryGameController::getControlName(const std::string& url) const +{ + std::string search = "/gamecontroller/"; + size_t pos_start = url.find(search); + if (pos_start == std::string::npos) + { + return std::string(); + } + pos_start += search.size(); + + size_t pos_end = url.find("?mode="); + if (pos_end == std::string::npos) + { + pos_end = url.size(); + } + return url.substr(pos_start, pos_end - pos_start); +} + +std::string LLUrlEntryGameController::getMode(const std::string& url) const +{ + std::string search = "?mode="; + size_t pos_start = url.find(search); + if (pos_start == std::string::npos) + { + return std::string(); + } + pos_start += search.size(); + return url.substr(pos_start, url.size() - pos_start); +} + +void LLUrlEntryGameController::initLocalization() +{ + initLocalizationFromFile("control_table_contents_game_control.xml"); + // TODO : Need to add localization for other game control bindings specifically axes +} + +void LLUrlEntryGameController::initLocalizationFromFile(const std::string& filename) +{ + LLXMLNodePtr xmlNode; + LLScrollListCtrl::Contents contents; + if (!LLUICtrlFactory::getLayeredXMLNode(filename, xmlNode)) + { + LL_WARNS() << "Failed to load " << filename << LL_ENDL; + return; + } + LLXUIParser parser; + parser.readXUI(xmlNode, contents, filename); + + if (!contents.validateBlock()) + { + LL_WARNS() << "Failed to validate " << filename << LL_ENDL; + return; + } + + for (LLInitParam::ParamIterator::const_iterator row_it = contents.rows.begin(); + row_it != contents.rows.end(); + ++row_it) + { + std::string control = row_it->value.getValue().asString(); + if (!control.empty() && control != "menu_separator") + { + mLocalizations[control] = + LLLocalizationData( + row_it->columns.begin()->value.getValue().asString(), + row_it->columns.begin()->tool_tip.getValue() + ); + } + } +} diff --git a/indra/llui/llurlentry.h b/indra/llui/llurlentry.h index efb5081103..4ac6624bc4 100644 --- a/indra/llui/llurlentry.h +++ b/indra/llui/llurlentry.h @@ -610,4 +610,40 @@ private: LLKeyBindingToStringHandler* pHandler; }; + +class LLGameControllerBindingToStringHandler; +/// LLUrlEntryGameController A way to access game controller bindings and show +/// currently used one in text. +/// secondlife:///app/gamecontroller/{control_name} +class LLUrlEntryGameController: public LLUrlEntryBase +{ +public: + LLUrlEntryGameController(); + /*virtual*/ std::string getLabel(const std::string& url, const LLUrlLabelCallback& cb); + /*virtual*/ std::string getTooltip(const std::string& url) const; + void setKeyHandler(LLKeyBindingToStringHandler* handler) {pKeyHandler = handler;} + void setControllerHandler(LLGameControllerBindingToStringHandler* handler) {pControllerHandler = handler;} + +private: + std::string getControlName(const std::string& url) const; + std::string getMode(const std::string& url) const; + void initLocalization(); + void initLocalizationFromFile(const std::string& filename); + + struct LLLocalizationData + { + LLLocalizationData() {} + LLLocalizationData(const std::string& localization, const std::string& tooltip) + : mLocalization(localization) + , mTooltip(tooltip) + {} + std::string mLocalization; + std::string mTooltip; + }; + + std::map mLocalizations; + LLKeyBindingToStringHandler* pKeyHandler; + LLGameControllerBindingToStringHandler* pControllerHandler; +}; + #endif diff --git a/indra/llui/llurlregistry.cpp b/indra/llui/llurlregistry.cpp index 8a21222b3c..8a2f959a42 100644 --- a/indra/llui/llurlregistry.cpp +++ b/indra/llui/llurlregistry.cpp @@ -77,6 +77,8 @@ LLUrlRegistry::LLUrlRegistry() registerUrl(new LLUrlEntryExperienceProfile()); mUrlEntryKeybinding = new LLUrlEntryKeybinding(); registerUrl(mUrlEntryKeybinding); + mUrlEntryGameController = new LLUrlEntryGameController(); + registerUrl(mUrlEntryGameController); //LLUrlEntrySL and LLUrlEntrySLLabel have more common pattern, //so it should be registered in the end of list registerUrl(new LLUrlEntrySL()); @@ -389,8 +391,16 @@ bool LLUrlRegistry::isUrl(const LLWString &text) void LLUrlRegistry::setKeybindingHandler(LLKeyBindingToStringHandler* handler) { - LLUrlEntryKeybinding *entry = (LLUrlEntryKeybinding*)mUrlEntryKeybinding; - entry->setHandler(handler); + LLUrlEntryKeybinding *keyEntry = (LLUrlEntryKeybinding*)mUrlEntryKeybinding; + keyEntry->setHandler(handler); + LLUrlEntryGameController *controllerEntry = (LLUrlEntryGameController*)mUrlEntryGameController; + controllerEntry->setKeyHandler(handler); +} + +void LLUrlRegistry::setGameControllerHandler(LLGameControllerBindingToStringHandler* handler) +{ + LLUrlEntryGameController *controllerEntry = (LLUrlEntryGameController*)mUrlEntryGameController; + controllerEntry->setControllerHandler(handler); } bool LLUrlRegistry::containsAgentMention(const std::string& text) diff --git a/indra/llui/llurlregistry.h b/indra/llui/llurlregistry.h index 592e422487..fa1e743569 100644 --- a/indra/llui/llurlregistry.h +++ b/indra/llui/llurlregistry.h @@ -36,6 +36,7 @@ #include class LLKeyBindingToStringHandler; +class LLGameControllerBindingToStringHandler; /// This default callback for findUrl() simply ignores any label updates void LLUrlRegistryNullCallback(const std::string &url, @@ -92,6 +93,10 @@ public: // Set handler for url registry to be capable of parsing and populating keybindings void setKeybindingHandler(LLKeyBindingToStringHandler* handler); + // Set handler for url registry to be capable of parsing and populating + // game-controller bindings. + void setGameControllerHandler(LLGameControllerBindingToStringHandler* handler); + bool containsAgentMention(const std::string& text); private: @@ -103,6 +108,7 @@ private: LLUrlEntryBase* mUrlEntrySLLabel; LLUrlEntryBase* mUrlEntryNoLink; LLUrlEntryBase* mUrlEntryKeybinding; + LLUrlEntryBase* mUrlEntryGameController; LLUrlEntryBase* mUrlEntryAgentMention; }; diff --git a/indra/llwindow/llgamecontrol.cpp b/indra/llwindow/llgamecontrol.cpp index 06501e28a1..9d22fe4628 100755 --- a/indra/llwindow/llgamecontrol.cpp +++ b/indra/llwindow/llgamecontrol.cpp @@ -104,6 +104,41 @@ namespace std } return "UNKNOWN"; } + + string to_string(SDL_GamepadButton button) + { + return SDL_GetGamepadStringForButton(button); + } + + string to_string(SDL_GamepadAxis axis) + { + return SDL_GetGamepadStringForAxis(axis); + } + + string to_string(SDL_GamepadButtonLabel label) + { + switch (label) + { + case SDL_GAMEPAD_BUTTON_LABEL_A: + return "a"; + case SDL_GAMEPAD_BUTTON_LABEL_B: + return "b"; + case SDL_GAMEPAD_BUTTON_LABEL_X: + return "x"; + case SDL_GAMEPAD_BUTTON_LABEL_Y: + return "y"; + case SDL_GAMEPAD_BUTTON_LABEL_CROSS: + return "cross"; + case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: + return "circle"; + case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: + return "square"; + case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: + return "triangle"; + default: + return "UNKOWN"; + } + } } // Util for dumping SDL_JoystickGUID info @@ -205,8 +240,7 @@ std::string LLGameControl::InputChannel::getLocalName() const if (isAxis() && mIndex < NUM_AXES) { - return "AXIS_" + std::to_string((U32)mIndex) + - (mSign < 0 ? "-" : mSign > 0 ? "+" : ""); + return "AXIS_" + std::to_string((U32)mIndex); } if (isButton() && mIndex < NUM_BUTTONS) @@ -217,33 +251,43 @@ std::string LLGameControl::InputChannel::getLocalName() const return "NONE"; } +std::string LLGameControl::InputChannel::getSignedLocalName() const +{ + std::string name = getLocalName(); + if (isAxis() && mIndex < NUM_AXES) + { + name.append(mSign < 0 ? "-" : mSign > 0 ? "+" : ""); + } + 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 + // AXIS_LEFTX, BUTTON_SOUTH, etc if (isAxis()) { switch (mIndex) { case 0: - name = "GAME_CONTROL_AXIS_LEFTX"; + name = "AXIS_LEFTX"; break; case 1: - name = "GAME_CONTROL_AXIS_LEFTY"; + name = "AXIS_LEFTY"; break; case 2: - name = "GAME_CONTROL_AXIS_RIGHTX"; + name = "AXIS_RIGHTX"; break; case 3: - name = "GAME_CONTROL_AXIS_RIGHTY"; + name = "AXIS_RIGHTY"; break; case 4: - name = "GAME_CONTROL_AXIS_PADDLELEFT"; + name = "AXIS_LEFT_TRIGGER"; break; case 5: - name = "GAME_CONTROL_AXIS_PADDLERIGHT"; + name = "AXIS_RIGHT_TRIGGER"; break; default: break; @@ -254,67 +298,67 @@ std::string LLGameControl::InputChannel::getRemoteName() const switch(mIndex) { case 0: - name = "GAME_CONTROL_BUTTON_A"; + name = "BUTTON_SOUTH"; break; case 1: - name = "GAME_CONTROL_BUTTON_B"; + name = "BUTTON_EAST"; break; case 2: - name = "GAME_CONTROL_BUTTON_X"; + name = "BUTTON_WEST"; break; case 3: - name = "GAME_CONTROL_BUTTON_Y"; + name = "BUTTON_NORTH"; break; case 4: - name = "GAME_CONTROL_BUTTON_BACK"; + name = "BUTTON_BACK"; break; case 5: - name = "GAME_CONTROL_BUTTON_GUIDE"; + name = "BUTTON_GUIDE"; break; case 6: - name = "GAME_CONTROL_BUTTON_START"; + name = "BUTTON_START"; break; case 7: - name = "GAME_CONTROL_BUTTON_LEFTSTICK"; + name = "BUTTON_LEFT_STICK"; break; case 8: - name = "GAME_CONTROL_BUTTON_RIGHTSTICK"; + name = "BUTTON_RIGHT_STICK"; break; case 9: - name = "GAME_CONTROL_BUTTON_LEFTSHOULDER"; + name = "BUTTON_LEFT_SHOULDER"; break; case 10: - name = "GAME_CONTROL_BUTTON_RIGHTSHOULDER"; + name = "BUTTON_RIGHT_SHOULDER"; break; case 11: - name = "GAME_CONTROL_BUTTON_DPAD_UP"; + name = "BUTTON_DPAD_UP"; break; case 12: - name = "GAME_CONTROL_BUTTON_DPAD_DOWN"; + name = "BUTTON_DPAD_DOWN"; break; case 13: - name = "GAME_CONTROL_BUTTON_DPAD_LEFT"; + name = "BUTTON_DPAD_LEFT"; break; case 14: - name = "GAME_CONTROL_BUTTON_DPAD_RIGHT"; + name = "BUTTON_DPAD_RIGHT"; break; case 15: - name = "GAME_CONTROL_BUTTON_MISC1"; + name = "BUTTON_MISC1"; break; case 16: - name = "GAME_CONTROL_BUTTON_PADDLE1"; + name = "BUTTON_PADDLE1"; break; case 17: - name = "GAME_CONTROL_BUTTON_PADDLE2"; + name = "BUTTON_PADDLE2"; break; case 18: - name = "GAME_CONTROL_BUTTON_PADDLE3"; + name = "BUTTON_PADDLE3"; break; case 19: - name = "GAME_CONTROL_BUTTON_PADDLE4"; + name = "BUTTON_PADDLE4"; break; case 20: - name = "GAME_CONTROL_BUTTON_TOUCHPAD"; + name = "BUTTON_TOUCHPAD"; break; default: break; @@ -331,18 +375,16 @@ public: using ActionToChannelMap = std::map< std::string, LLGameControl::InputChannel >; LLGameControllerManager(); - 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 applyRememberedDeviceOptions(); + void rememberDeviceOptions() 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); + const LLGameControl::Device* getLastActiveDevice() const; + void onAxis(SDL_JoystickID id, U8 axis, S16 value); void onButton(SDL_JoystickID id, U8 button, bool pressed); @@ -352,28 +394,19 @@ public: void computeFinalState(); LLGameControl::ActionNameType getActionNameType(const std::string& action) const; - LLGameControl::InputChannel getChannelByAction(const std::string& action) const; - LLGameControl::InputChannel getFlycamChannelByAction(const std::string& action) const; U32 computeInternalActionFlags(); void getFlycamInputs(std::vector& inputs_out); void setExternalInput(U32 action_flags, U32 buttons); - U32 getMappedFlags() const { return mActionTranslator.getMappedFlags(); } + // Rebuild mAxisActionLabels/mButtonActionLabels from the global ModeMappings + // for the currently-active AgentControlMode. Cheap; called on settings change + // and on mode change. Safe to call every frame (early-outs when up to date). + void rebuildActionLookup(bool force = false); 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: - void updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel); - std::list mDevices; // all connected devices using device_it = std::list::iterator; device_it findDevice(SDL_JoystickID id) @@ -388,14 +421,24 @@ private: LLGameControl::State mExternalState; LLGameControlTranslator mActionTranslator; std::map mActions; - std::vector mAnalogActions; - std::vector mBinaryActions; - std::vector mFlycamActions; - std::vector mFlycamChannels; - std::vector mAxesAccumulator; + + // Runtime action lookup for the active AgentControlMode, rebuilt from the + // global ModeMappings whenever settings or the active mode change. Each entry + // holds the UI action label (e.g. "Move forward/back") bound to that canonical + // input, or "" if unbound. mAxisActionLabels is indexed by physical axis + // (KeyboardAxis, NUM_AXES); mButtonActionLabels by Button (NUM_BUTTONS). + std::vector mAxisActionLabels; + std::vector mButtonActionLabels; + LLGameControl::AgentControlMode mLookupMode { LLGameControl::CONTROL_MODE_NONE }; + + // std::vector mAxesAccumulator; + // std::vector mAxesMappedAccumulator; + // LLGameControl::State mAccumulatedState; + // LLGameControl::State mMappedAccumulatedState; U32 mButtonAccumulator { 0 }; U32 mLastActiveFlags { 0 }; U32 mLastFlycamActionFlags { 0 }; + SDL_JoystickID mlastActiveControllerID { 0 }; friend class LLGameControl; }; @@ -419,38 +462,53 @@ namespace 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 - LLGameControl::State g_innerState; // state from gAgent - LLGameControl::State g_finalState; // sum of inner and outer + + LLGameControl::State g_innerState; // state from all game controllers + LLGameControl::State g_mappedState; // state after user mapping is applied + LLGameControl::State g_flycamMappedState; // state for flycam after user mapping is applied + LLGameControl::ServerState g_finalState; // sum of inner and outer + + LLTimer g_buttonLevelTimer[LLGameControl::Button::NUM_BUTTONS]; + LLTimer g_axisHeldTimer[LLGameControl::MovementDirection::NUM_MOVE_DIRS]; + S32 g_buttonLevelFrames[LLGameControl::Button::NUM_BUTTONS]; + S32 g_axisHeldFrames[LLGameControl::MovementDirection::NUM_MOVE_DIRS]; + 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; LLGameControl::AgentControlMode g_agentControlMode = LLGameControl::CONTROL_MODE_AVATAR; + // g_gameControlSettings is the nested GameControl structure stored under the + // single "GameControl" setting key: global per-mode action mappings plus + // per-device hardware config. See buildDefaultGameControlSettings() below. + LLSD g_gameControlSettings; + + // g_deviceOptions is a map of [guid,deviceOptions] pairs for known devices + // its values are expected to agree with connected device 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; + LLGameControl::LoadSettingsFn s_loadSettings; + LLGameControl::SaveSettingsFn s_saveSettings; std::function s_updateUI; - std::string SETTING_ENABLE("EnableGameControl"); - 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 SETTING_GAMECONTROL("GameControl"); + + // Keys used within the nested GameControl LLSD structure + // (see buildDefaultGameControlSettings()). + const std::string GC_COMMENT("Comment"); + const std::string GC_SENDTOSERVER("SendDataToServer"); + const std::string GC_DEVICES("Devices"); + const std::string GC_DEFAULT_DEVICE("Default"); + const std::string GC_CONFIG("Config"); + const std::string GC_MODEMAPPINGS("ModeMappings"); + const std::string GC_AXES("Axes"); + const std::string GC_BUTTONS("Buttons"); + const std::string GC_MODE_AVATAR("Avatar"); + const std::string GC_MODE_FLYCAM("FlyCam"); + const std::string GC_MODE_CAPTIVE("Captive"); + +#ifdef TEMPORARILY_DISABLED std::string ENUM_AGENTCONTROLMODE_FLYCAM("flycam"); std::string ENUM_AGENTCONTROLMODE_NONE("none"); @@ -473,12 +531,137 @@ namespace // All values except NONE and FLYCAM are treated as default (AVATAR) return LLStringUtil::null; } +#endif // TEMPORARILY_DISABLED const std::string& getDeviceOptionsString(const std::string& guid) { const auto& it = g_deviceOptions.find(guid); return it == g_deviceOptions.end() ? LLStringUtil::null : it->second; } + + // Map an AgentControlMode to the mode key used in the GameControl LLSD. + // Returns empty for modes without a mapping (e.g. CONTROL_MODE_NONE). + const std::string& modeToString(LLGameControl::AgentControlMode mode) + { + switch (mode) + { + case LLGameControl::CONTROL_MODE_AVATAR: return GC_MODE_AVATAR; + case LLGameControl::CONTROL_MODE_FLYCAM: return GC_MODE_FLYCAM; + case LLGameControl::CONTROL_MODE_CAPTIVE: return GC_MODE_CAPTIVE; + default: return LLStringUtil::null; + } + } + + // Build the default global per-mode mapping structure. Layout: + // { : { "Axes": {action:input...}, "Buttons": {action:input...} } } + // Action keys match the combo-box labels in panel_preferences_game_control.xml; + // input values match the axis_input_selector / button_input_selector values. + LLSD buildDefaultModeMappings() + { + // Axes: action label -> axis input + LLSD avatar_axes; + avatar_axes["Strafe left/right"] = "AXIS_LEFTX"; + avatar_axes["Move forward/back"] = "AXIS_LEFTY"; + avatar_axes["Turn left/right"] = "AXIS_RIGHTX"; + avatar_axes["Look up/down"] = "AXIS_RIGHTY"; + avatar_axes["Rise up"] = "AXIS_LEFT_TRIGGER"; + avatar_axes["Drop down"] = "AXIS_RIGHT_TRIGGER"; + + // Buttons: action label -> button input + LLSD avatar_buttons; + avatar_buttons["Jump"] = "BUTTON_SOUTH"; + avatar_buttons["Crouch"] = "BUTTON_EAST"; + avatar_buttons["Toggle sit"] = "BUTTON_WEST"; + avatar_buttons["Interact"] = "BUTTON_NORTH"; + avatar_buttons["Toggle 3rd person view"] = "BUTTON_BACK"; + avatar_buttons["Toggle menu"] = "BUTTON_GUIDE"; + avatar_buttons["Toggle mouselook"] = "BUTTON_START"; + avatar_buttons["Toggle fly"] = "BUTTON_LEFT_STICK"; + avatar_buttons["Toggle flycam"] = "BUTTON_RIGHT_STICK"; + avatar_buttons["Mouse click left"] = "BUTTON_LEFT_SHOULDER"; + avatar_buttons["Mouse click right"] = "BUTTON_RIGHT_SHOULDER"; + avatar_buttons["Move forward"] = "BUTTON_DPAD_UP"; + avatar_buttons["Move back"] = "BUTTON_DPAD_DOWN"; + avatar_buttons["Strafe left"] = "BUTTON_DPAD_LEFT"; + avatar_buttons["Strafe right"] = "BUTTON_DPAD_RIGHT"; + + // FlyCam adds a Roll axis and uses a distinct button set. + LLSD flycam_axes = avatar_axes; + flycam_axes["Roll left/right"] = "AXIS_NONE"; + + LLSD flycam_buttons; + flycam_buttons["Select"] = "BUTTON_SOUTH"; + flycam_buttons["Toggle AltZoom"] = "BUTTON_EAST"; + flycam_buttons["Interact"] = "BUTTON_NORTH"; + flycam_buttons["Toggle menu"] = "BUTTON_GUIDE"; + flycam_buttons["Toggle flycam"] = "BUTTON_RIGHT_STICK"; + flycam_buttons["Roll left"] = "BUTTON_LEFT_SHOULDER"; + flycam_buttons["Roll right"] = "BUTTON_RIGHT_SHOULDER"; + flycam_buttons["Move forward"] = "BUTTON_DPAD_UP"; + flycam_buttons["Move back"] = "BUTTON_DPAD_DOWN"; + flycam_buttons["Strafe left"] = "BUTTON_DPAD_LEFT"; + flycam_buttons["Strafe right"] = "BUTTON_DPAD_RIGHT"; + + auto makeMode = [](const LLSD& axes, const LLSD& buttons) + { + LLSD mode; + mode[GC_AXES] = axes; + mode[GC_BUTTONS] = buttons; + return mode; + }; + + LLSD mappings; + mappings[GC_MODE_AVATAR] = makeMode(avatar_axes, avatar_buttons); + mappings[GC_MODE_FLYCAM] = makeMode(flycam_axes, flycam_buttons); + mappings[GC_MODE_CAPTIVE] = makeMode(avatar_axes, avatar_buttons); + return mappings; + } + + // Build the full default GameControl structure: global per-mode action mappings + // plus a single empty "Default" device template. ModeMappings are global (not + // per-device): the Devices layer normalizes hardware to canonical inputs, while + // the global ModeMappings layer binds canonical inputs to logical actions per mode. + LLSD buildDefaultGameControlSettings() + { + LLSD device; + device[GC_CONFIG] = ""; + + LLSD settings; + settings[GC_COMMENT] = "GameControl settings"; + settings[GC_SENDTOSERVER] = false; + settings[GC_MODEMAPPINGS] = buildDefaultModeMappings(); + settings[GC_DEVICES][GC_DEFAULT_DEVICE] = device; + return settings; + } + + // Ensure g_gameControlSettings holds a usable structure; seed from defaults + // if it is empty or malformed. The global ModeMappings block is seeded + // independently so an older config that lacks it still gets sane defaults. + void ensureGameControlSettings() + { + if (!g_gameControlSettings.isMap() || !g_gameControlSettings.has(GC_DEVICES)) + { + g_gameControlSettings = buildDefaultGameControlSettings(); + } + if (!g_gameControlSettings[GC_MODEMAPPINGS].isMap()) + { + g_gameControlSettings[GC_MODEMAPPINGS] = buildDefaultModeMappings(); + } + } + + // Ensure a per-device entry exists before mutating it. Devices now hold only + // their hardware Config; the action mappings live in the global ModeMappings. + void ensureDeviceEntry(const std::string& guid) + { + ensureGameControlSettings(); + LLSD& devices = g_gameControlSettings[GC_DEVICES]; + if (!devices.has(guid)) + { + LLSD device; + device[GC_CONFIG] = ""; + devices[guid] = device; + } + } } LLGameControl::~LLGameControl() @@ -487,20 +670,34 @@ LLGameControl::~LLGameControl() } LLGameControl::State::State() -: mButtons(0) + : + mButtons(0), + mPrevButtons(0) + { - mAxes.resize(NUM_AXES, 0); - mPrevAxes.resize(NUM_AXES, 0); + mAxes.resize(NUM_MOVE_DIRS, 0); + mRawAxes.resize(NUM_MOVE_DIRS, 0); + mPrevAxes.resize(NUM_MOVE_DIRS, 0); } void LLGameControl::State::clear() { std::fill(mAxes.begin(), mAxes.end(), 0); + std::fill(mRawAxes.begin(), mRawAxes.end(), 0); + mButtons = 0; - // DO NOT clear mPrevAxes because those are managed by external logic. - //std::fill(mPrevAxes.begin(), mPrevAxes.end(), 0); + // // DO NOT clear mPrevAxes because those are managed by external logic. + // std::fill(mPrevAxes.begin(), mPrevAxes.end(), 0); + // mPrevButtons = 0; +} - mButtons = 0; +void LLGameControl::State::storePrevious() +{ + mPrevButtons = mButtons; + for(size_t i = 0; i < mAxes.size(); i++) + { + mPrevAxes[i] = mAxes[i]; + } } bool LLGameControl::State::onButton(U8 button, bool pressed) @@ -520,6 +717,24 @@ bool LLGameControl::State::onButton(U8 button, bool pressed) return mButtons != old_buttons; } +LLGameControl::ServerState::ServerState() +: mButtons(0) +, mPrevButtons(0) +{ + mAxes.resize(NUM_AXES,0); + mPrevAxes.resize(NUM_AXES,0); +} + +void LLGameControl::ServerState::clear() +{ + std::fill(mAxes.begin(), mAxes.end(), 0); + mButtons = 0; + + // // DO NOT clear mPrevAxes because those are managed by external logic. + // std::fill(mPrevAxes.begin(), mPrevAxes.end(), 0); + // mPrevButtons = 0; +} + LLGameControl::Device::Device(int joystickID, const std::string& guid, const std::string& name) : mJoystickID(joystickID) , mGUID(guid) @@ -527,6 +742,23 @@ LLGameControl::Device::Device(int joystickID, const std::string& guid, const std { } +S16 LLGameControl::Options::AxisOptions::computeModifiedValue(S16 raw_value) const +{ + S16 new_value = (S16)(std::clamp(((S32)raw_value + S32(mOffset)) * mMultiplier, -32768, 32767)); + if (abs(new_value) < mDeadZone) + { + new_value = 0; + } + return new_value; +} + +void LLGameControl::Options::AxisOptions::resetToDefaults() +{ + mMultiplier = 1; + mDeadZone = 0; + mOffset = 0; +} + LLGameControl::Options::Options() { mAxisOptions.resize(NUM_AXES); @@ -568,6 +800,42 @@ U8 LLGameControl::Options::mapButton(U8 button) const return mButtonMap[button]; } +U8 LLGameControl::Options::unmapAxis(U8 axis) const +{ + if (axis >= NUM_AXES) + { + LL_WARNS("SDL3") << "Invalid unmap axis: " << axis << LL_ENDL; + return axis; + } + for(size_t i=0; i < mAxisMap.size(); i++) + { + if(mAxisMap[i] == axis) + { + return (U8)i; + } + } + return axis; +} + +U8 LLGameControl::Options::unmapButton(U8 button) const +{ + + if (button >= NUM_BUTTONS) + { + LL_WARNS("SDL3") << "Invalid unmap button: " << button << LL_ENDL; + return button; + } + + for(size_t i=0; i < mButtonMap.size(); i++) + { + if(mButtonMap[i] == button) + { + return (U8)i; + } + } + return button; +} + S16 LLGameControl::Options::fixAxisValue(U8 axis, S16 value) const { if (axis >= NUM_AXES) @@ -748,113 +1016,6 @@ bool LLGameControl::Options::loadFromString(std::string options) LLGameControllerManager::LLGameControllerManager() { - 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, - // in combination with the action->InputChannel map below, - // to maintain an inverse map from control bit masks to GameControl data. - 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. - { "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; - 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 } } - }; - - // Flycam actions don't need bitwise translation, so we maintain the map - // of channels here directly rather than using an LLGameControlTranslator. - 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::getDefaultMappings(std::vector>& mappings) -{ - // 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() @@ -865,7 +1026,7 @@ void LLGameControllerManager::resetDeviceOptionsToDefaults() } } -void LLGameControllerManager::loadDeviceOptionsFromSettings() +void LLGameControllerManager::applyRememberedDeviceOptions() { for (LLGameControl::Device& device : mDevices) { @@ -873,7 +1034,7 @@ void LLGameControllerManager::loadDeviceOptionsFromSettings() } } -void LLGameControllerManager::saveDeviceOptionsToSettings() const +void LLGameControllerManager::rememberDeviceOptions() const { for (const LLGameControl::Device& device : mDevices) { @@ -891,12 +1052,23 @@ 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; + + // remember the options + std::string options_str = device.saveOptionsToString(true); + auto itr = g_deviceOptions.find(guid); + if (itr == g_deviceOptions.end()) + { + g_deviceOptions.insert({guid, options_str}); + } + else + { + itr->second = options_str; + } return; } } @@ -931,6 +1103,61 @@ void LLGameControllerManager::removeController(SDL_JoystickID id) }); } +const LLGameControl::Device* LLGameControllerManager::getLastActiveDevice() const +{ + if (mDevices.empty()) + { + return nullptr; + } + + if (mlastActiveControllerID != 0) + { + auto it = std::find_if(mDevices.begin(), mDevices.end(), + [this](const LLGameControl::Device& device) + { + return device.getJoystickID() == mlastActiveControllerID; + }); + if (it != mDevices.end()) + { + return &(*it); + } + } + + return &mDevices.front(); +} + +// Negates a signed axis value, accounting for the asymmetric S16 range +// [-32768, 32767] by shifting one during negation. +static S16 negateAxisValue(S16 value) +{ + if (value < 0) + { + return (S16)(-(value + 1)); + } + if (value > 0) + { + return (S16)((-value) - 1); + } + return 0; +} + +// Splits a signed axis value into the +/- half-axis pair used by State::mAxes / +// State::mRawAxes: the positive magnitude lands in half_axes[base], the negative +// magnitude in half_axes[base + 1]. +static void storeHalfAxes(std::vector& half_axes, U8 base, S16 value) +{ + half_axes[base] = 0; + half_axes[base + 1] = 0; + if (value > 0) + { + half_axes[base] = value; + } + else + { + half_axes[base + 1] = (U16)abs(value); + } +} + void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) { device_it it = findDevice(id); @@ -961,6 +1188,10 @@ void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 value) return; } + // Keep the pre-fix (raw) value so the UI can display it alongside the + // post-fix value; see LLGameControl::Device::State::mRawAxes. + S16 raw_value = value; + // Fix value using device-specific settings // or leave the value unchanged S16 fixed_value = it->mOptions.fixAxisValue(axis, value); @@ -976,26 +1207,22 @@ void LLGameControllerManager::onAxis(SDL_JoystickID id, U8 axis, S16 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. + // they are extracted from SDL, before being used anywhere. The raw value is + // negated the same way so both share one sign convention and differ only by + // the fix transform (dead zone / offset / invert). if (axis < SDL_GAMEPAD_AXIS_LEFT_TRIGGER) { - // 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; - } + value = negateAxisValue(value); + raw_value = negateAxisValue(raw_value); } LL_DEBUGS("SDL3") << "joystick=0x" << std::hex << id << std::dec << " axis=" << (S32)(axis) << " value=" << (S32)(value) << LL_ENDL; - it->mState.mAxes[axis] = value; + + U8 base = axis * 2; + storeHalfAxes(it->mState.mAxes, base, value); + storeHalfAxes(it->mState.mRawAxes, base, raw_value); } void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool pressed) @@ -1008,6 +1235,8 @@ void LLGameControllerManager::onButton(SDL_JoystickID id, U8 button, bool presse return; } + mlastActiveControllerID = id; + // Map button using device-specific settings // or leave the value unchanged U8 mapped_button = it->mOptions.mapButton(button); @@ -1047,64 +1276,186 @@ void LLGameControllerManager::clearAllStates() void LLGameControllerManager::accumulateInternalState() { + g_innerState.storePrevious(); // clear the old state - std::fill(mAxesAccumulator.begin(), mAxesAccumulator.end(), 0); - mButtonAccumulator = 0; + g_innerState.clear(); + // accumulate the controllers for (const auto& device : mDevices) { - mButtonAccumulator |= device.mState.mButtons; - for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i) + g_innerState.mButtons |= device.mState.mButtons; + for(U8 i = 0; i < LLGameControl::Button::NUM_BUTTONS; i++) + { + U32 button = 1 << i; + if(g_innerState.mButtons & button && ~(g_innerState.mPrevButtons) & button) + { + g_buttonLevelTimer[i].reset(); + g_buttonLevelFrames[i] = 0; + } + else + { + g_buttonLevelFrames[i]++; + } + } + for (size_t i = 0; i < LLGameControl::NUM_MOVE_DIRS; ++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)device.mState.mAxes[i]; + if (device.mState.mAxes[i] > g_innerState.mAxes[i]) + { + g_innerState.mAxes[i] = (S32)device.mState.mAxes[i]; + if(g_innerState.mPrevAxes[i] < 1) + { + g_axisHeldTimer[i].reset(); + g_axisHeldFrames[i] = 0; + } + else + { + g_axisHeldFrames[i]++; + } + } } } } +F32 LLGameControl::getControllerHeldTime(ActionType actionType, U8 action) +{ + switch (actionType) + { + case ActionType::DOF: + return g_axisHeldTimer[action].getElapsedTimeF32(); + case ActionType::BUTTON: + return g_buttonLevelTimer[action].getElapsedTimeF32(); + default: + return 0.0; + } +} + +S32 LLGameControl::getControllerHeldFrames(ActionType actionType, U8 action) +{ + switch (actionType) + { + case ActionType::DOF: + return g_axisHeldFrames[action]; + case ActionType::BUTTON: + return g_buttonLevelFrames[action]; + default: + return 0.0; + } +} + void LLGameControllerManager::computeFinalState() { + static const LLGameControlTranslator::ControllerMappings axis_mappings = { + // Axes + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_STRAFE_LEFT}, LLGameControl::MovementDirection::MOVE_DIR_STRAFE_LEFT}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_STRAFE_RIGHT}, LLGameControl::MovementDirection::MOVE_DIR_STRAFE_RIGHT}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_PUSH_FORWARD}, LLGameControl::MovementDirection::MOVE_DIR_PUSH_FORWARD}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_PUSH_BACKWARD}, LLGameControl::MovementDirection::MOVE_DIR_PUSH_BACKWARD}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_TURN_LEFT}, LLGameControl::MovementDirection::MOVE_DIR_TURN_LEFT}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_TURN_RIGHT}, LLGameControl::MovementDirection::MOVE_DIR_TURN_RIGHT}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_LOOK_UP}, LLGameControl::MovementDirection::MOVE_DIR_LOOK_UP}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_LOOK_DOWN}, LLGameControl::MovementDirection::MOVE_DIR_LOOK_DOWN}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_RISE_UP}, LLGameControl::MovementDirection::MOVE_DIR_RISE_UP}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_DROP_DOWN}, LLGameControl::MovementDirection::MOVE_DIR_DROP_DOWN}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_ROLL_LEFT}, LLGameControl::MovementDirection::MOVE_DIR_ROLL_LEFT}, + {{LLGameControl::ActionType::DOF, LLGameControl::MovementDirection::MOVE_DIR_ROLL_RIGHT}, LLGameControl::MovementDirection::MOVE_DIR_ROLL_RIGHT} + }; + + static const LLGameControlTranslator::ControllerMappings button_mappings = { + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_SOUTH}, (U32)1 << LLGameControl::Button::BUTTON_SOUTH}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_EAST}, (U32)1 << LLGameControl::Button::BUTTON_EAST}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_WEST}, (U32)1 << LLGameControl::Button::BUTTON_WEST}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_NORTH}, (U32)1 << LLGameControl::Button::BUTTON_NORTH}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_BACK}, (U32)1 << LLGameControl::Button::BUTTON_BACK}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_GUIDE}, (U32)1 << LLGameControl::Button::BUTTON_GUIDE}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_START}, (U32)1 << LLGameControl::Button::BUTTON_START}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_LEFT_STICK}, (U32)1 << LLGameControl::Button::BUTTON_LEFT_STICK}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_RIGHT_STICK}, (U32)1 << LLGameControl::Button::BUTTON_RIGHT_STICK}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_LEFT_SHOULDER}, (U32)1 << LLGameControl::Button::BUTTON_LEFT_SHOULDER}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_RIGHT_SHOULDER}, (U32)1 << LLGameControl::Button::BUTTON_RIGHT_SHOULDER}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_DPAD_UP}, (U32)1 << LLGameControl::Button::BUTTON_DPAD_UP}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_DPAD_DOWN}, (U32)1 << LLGameControl::Button::BUTTON_DPAD_DOWN}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_DPAD_LEFT}, (U32)1 << LLGameControl::Button::BUTTON_DPAD_LEFT}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_DPAD_RIGHT}, (U32)1 << LLGameControl::Button::BUTTON_DPAD_RIGHT}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_MISC1}, (U32)1 << LLGameControl::Button::BUTTON_MISC1}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_PADDLE1}, (U32)1 << LLGameControl::Button::BUTTON_PADDLE1}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_PADDLE2}, (U32)1 << LLGameControl::Button::BUTTON_PADDLE2}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_PADDLE3}, (U32)1 << LLGameControl::Button::BUTTON_PADDLE3}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_PADDLE4}, (U32)1 << LLGameControl::Button::BUTTON_PADDLE4}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_TOUCHPAD}, (U32)1 << LLGameControl::Button::BUTTON_TOUCHPAD}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_21}, (U32)1 << LLGameControl::Button::BUTTON_21}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_22}, (U32)1 << LLGameControl::Button::BUTTON_22}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_23}, (U32)1 << LLGameControl::Button::BUTTON_23}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_24}, (U32)1 << LLGameControl::Button::BUTTON_24}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_25}, (U32)1 << LLGameControl::Button::BUTTON_25}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_26}, (U32)1 << LLGameControl::Button::BUTTON_26}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_27}, (U32)1 << LLGameControl::Button::BUTTON_27}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_28}, (U32)1 << LLGameControl::Button::BUTTON_28}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_29}, (U32)1 << LLGameControl::Button::BUTTON_29}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_30}, (U32)1 << LLGameControl::Button::BUTTON_30}, + {{LLGameControl::ActionType::BUTTON, LLGameControl::Button::BUTTON_31}, (U32)1 << LLGameControl::Button::BUTTON_31} + }; + // 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; - } + g_mappedState.clear(); + g_mappedState.mButtons = mActionTranslator.calculateTranslatedButtons(button_mappings, g_innerState); + + mActionTranslator.calculateTranslatedAxes(axis_mappings, g_innerState, g_mappedState.mAxes); + + g_finalState.mPrevButtons = g_finalState.mButtons; + g_finalState.mButtons = g_mappedState.mButtons; + + // TODO: accumulate from mExternalState in a different way + g_finalState.mButtons |= mExternalState.mButtons; + if (old_buttons != g_finalState.mButtons) { g_nextResendPeriod = 0; // packet needs to go out ASAP } + size_t final_i = 0; // clamp the accumulated axes - for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i) + for(size_t i = 0; i < LLGameControl::NUM_MOVE_DIRS; i+=2) { - S32 axis = mAxesAccumulator[i]; - if (g_translateAgentActions) + S16 axis_pos = g_mappedState.mAxes[i]; + S16 axis_neg = g_mappedState.mAxes[i+1]; + if (true) { // 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]; + // TODO: FIX TO greater not sum + if(mExternalState.mAxes[i] > axis_pos) + { + axis_pos = mExternalState.mAxes[i]; + } + if(mExternalState.mAxes[i+1] > axis_neg) + { + axis_neg = mExternalState.mAxes[i+1]; + } } - axis = (S16)std::min(std::max(axis, -32768), 32767); + // axis = (S16)std::min(std::max(axis, -32768), 32767); + S16 axis = axis_pos + (axis_neg * -1); // check for change - if (g_finalState.mAxes[i] != axis) + // Note: g_mappedState uses NUM_MOVE_DIRS split half-axes (indexed by 'i'), + // while g_finalState uses NUM_AXES combined signed axes (indexed by 'final_i'). + if (g_finalState.mAxes[final_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_finalState.mPrevAxes[final_i] = g_finalState.mAxes[final_i]; + g_finalState.mAxes[final_i] = axis; g_nextResendPeriod = 0; // packet needs to go out ASAP } + ++final_i; } } @@ -1114,236 +1465,311 @@ LLGameControl::ActionNameType LLGameControllerManager::getActionNameType(const s return it == mActions.end() ? LLGameControl::ACTION_NAME_UNKNOWN : it->second; } -LLGameControl::InputChannel LLGameControllerManager::getChannelByAction(const std::string& action) const +namespace { - LLGameControl::InputChannel channel; - auto action_it = mActions.find(action); - if (action_it != mActions.end()) - { - if (action_it->second == LLGameControl::ACTION_NAME_FLYCAM) + // Resolve a symbolic input name as stored in ModeMappings ("AXIS_LEFTX", + // "BUTTON_SOUTH", "AXIS_NONE", ...) to a canonical InputChannel. This is the + // inverse of InputChannel::getRemoteName(); axis channels are returned with + // sign 0 (bidirectional) because ModeMappings do not store a sign. Anything + // unrecognized (including the "none" sentinels) yields an isNone() channel. + LLGameControl::InputChannel channelFromInputName(const std::string& name) + { + static const std::map s_axis_names = { + { "AXIS_LEFTX", LLGameControl::AXIS_LEFTX }, + { "AXIS_LEFTY", LLGameControl::AXIS_LEFTY }, + { "AXIS_RIGHTX", LLGameControl::AXIS_RIGHTX }, + { "AXIS_RIGHTY", LLGameControl::AXIS_RIGHTY }, + { "AXIS_LEFT_TRIGGER", LLGameControl::AXIS_LEFT_TRIGGER }, + { "AXIS_RIGHT_TRIGGER", LLGameControl::AXIS_RIGHT_TRIGGER }, + }; + static const std::map s_button_names = { + { "BUTTON_SOUTH", LLGameControl::BUTTON_SOUTH }, + { "BUTTON_EAST", LLGameControl::BUTTON_EAST }, + { "BUTTON_WEST", LLGameControl::BUTTON_WEST }, + { "BUTTON_NORTH", LLGameControl::BUTTON_NORTH }, + { "BUTTON_BACK", LLGameControl::BUTTON_BACK }, + { "BUTTON_GUIDE", LLGameControl::BUTTON_GUIDE }, + { "BUTTON_START", LLGameControl::BUTTON_START }, + { "BUTTON_LEFT_STICK", LLGameControl::BUTTON_LEFT_STICK }, + { "BUTTON_RIGHT_STICK", LLGameControl::BUTTON_RIGHT_STICK }, + { "BUTTON_LEFT_SHOULDER", LLGameControl::BUTTON_LEFT_SHOULDER }, + { "BUTTON_RIGHT_SHOULDER", LLGameControl::BUTTON_RIGHT_SHOULDER }, + { "BUTTON_DPAD_UP", LLGameControl::BUTTON_DPAD_UP }, + { "BUTTON_DPAD_DOWN", LLGameControl::BUTTON_DPAD_DOWN }, + { "BUTTON_DPAD_LEFT", LLGameControl::BUTTON_DPAD_LEFT }, + { "BUTTON_DPAD_RIGHT", LLGameControl::BUTTON_DPAD_RIGHT }, + { "BUTTON_MISC1", LLGameControl::BUTTON_MISC1 }, + { "BUTTON_PADDLE1", LLGameControl::BUTTON_PADDLE1 }, + { "BUTTON_PADDLE2", LLGameControl::BUTTON_PADDLE2 }, + { "BUTTON_PADDLE3", LLGameControl::BUTTON_PADDLE3 }, + { "BUTTON_PADDLE4", LLGameControl::BUTTON_PADDLE4 }, + { "BUTTON_TOUCHPAD", LLGameControl::BUTTON_TOUCHPAD }, + }; + + auto ait = s_axis_names.find(name); + if (ait != s_axis_names.end()) { - channel = getFlycamChannelByAction(action); + return LLGameControl::InputChannel(LLGameControl::InputChannel::TYPE_AXIS, ait->second, 0); } - else + auto bit = s_button_names.find(name); + if (bit != s_button_names.end()) + { + return LLGameControl::InputChannel(LLGameControl::InputChannel::TYPE_BUTTON, bit->second); + } + // Numeric fallback for the higher buttons (e.g. "BUTTON_21"). + if (LLStringUtil::startsWith(name, "BUTTON_")) { - channel = mActionTranslator.getChannelByAction(action); + S32 index = atoi(name.substr(7).c_str()); + if (index > 0 && index < LLGameControl::NUM_BUTTONS) + { + return LLGameControl::InputChannel(LLGameControl::InputChannel::TYPE_BUTTON, (U8)index); + } } + return LLGameControl::InputChannel(); // isNone() + } + + // Step-0 bridge (approach A): UI action label -> engine effect for the + // Avatar/Captive modes. Analog axis labels expand to a positive-half and + // negative-half AGENT_CONTROL bit; unidirectional trigger labels ("Rise up", + // "Drop down") use only the positive half. Button labels expand to a single + // AGENT_CONTROL bit (the fly/flycam entries reuse the HACK bits consumed by + // LLAgent::applyExternalActionFlags). Labels that map to viewer *commands* + // rather than movement bits (Interact, Toggle sit/menu/mouselook/3rd-person, + // Mouse clicks) are intentionally absent here and are a separate follow-up. + struct AxisActionEffect { U32 posFlag; U32 negFlag; }; + + const std::map& avatarAxisBridge() + { + // Note: g_innerState half-axes are pre-negated so LEFT/FORWARD/UP land in + // the positive half (axis*2); see LLGameControllerManager::onAxis(). + static const std::map bridge = { + { "Strafe left/right", { AGENT_CONTROL_LEFT_POS, AGENT_CONTROL_LEFT_NEG } }, + { "Move forward/back", { AGENT_CONTROL_AT_POS, AGENT_CONTROL_AT_NEG } }, + { "Turn left/right", { AGENT_CONTROL_YAW_POS, AGENT_CONTROL_YAW_NEG } }, + { "Look up/down", { AGENT_CONTROL_PITCH_POS, AGENT_CONTROL_PITCH_NEG } }, + { "Rise up", { AGENT_CONTROL_UP_POS, 0 } }, + { "Drop down", { AGENT_CONTROL_UP_NEG, 0 } }, + }; + return bridge; } - 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]; + const std::map& avatarButtonBridge() + { + static const std::map bridge = { + { "Jump", AGENT_CONTROL_UP_POS }, + { "Crouch", AGENT_CONTROL_UP_NEG }, + { "Move forward", AGENT_CONTROL_AT_POS }, + { "Move back", AGENT_CONTROL_AT_NEG }, + { "Strafe left", AGENT_CONTROL_LEFT_POS }, + { "Strafe right", AGENT_CONTROL_LEFT_NEG }, + // HACK bits consumed by LLAgent::applyExternalActionFlags for toggles. + { "Toggle fly", AGENT_CONTROL_FLY }, // HACK + { "Toggle flycam", AGENT_CONTROL_NUDGE_AT_NEG }, // HACK + }; + return bridge; + } } -// Common implementation of getAnalogMappings(), getBinaryMappings() and getFlycamMappings() -static std::string getMappings( - const std::vector& actions, - LLGameControl::InputChannel::Type type, - std::function getChannel) +void LLGameControllerManager::rebuildActionLookup(bool force) { - std::list mappings; + LLGameControl::AgentControlMode mode = g_agentControlMode; + if (!force && mode == mLookupMode && !mAxisActionLabels.empty()) + { + return; // already current + } + mLookupMode = mode; + mAxisActionLabels.assign(LLGameControl::NUM_AXES, std::string()); + mButtonActionLabels.assign(LLGameControl::NUM_BUTTONS, std::string()); - std::vector> default_mappings; - LLGameControl::getDefaultMappings(default_mappings); + const std::string& mode_name = modeToString(mode); + if (mode_name.empty()) + { + return; // CONTROL_MODE_NONE has no mappings + } - // Walk through all known actions of the chosen type - for (const std::string& action : actions) + // Invert ModeMappings[mode][Axes|Buttons] (action label -> input name) into + // canonical input index -> action label. Last binding wins on collision. + LLSD axes = LLGameControl::getModeMapping(mode_name, GC_AXES); + for (auto it = axes.beginMap(); it != axes.endMap(); ++it) { - LLGameControl::InputChannel channel = getChannel(action); - // Only channels of the expected type should be stored (or NONE) - if (channel.mType == type || channel.isNone()) + LLGameControl::InputChannel channel = channelFromInputName(it->second.asString()); + if (channel.isAxis() && channel.mIndex < LLGameControl::NUM_AXES) { - bool mapping_differs = false; - for (const auto& [name, default_channel] : default_mappings) - { - if (name == action) - { - mapping_differs = !channel.isEqual(default_channel); - break; - } - } - // Only mappings different from the default should be stored - if (mapping_differs) - { - mappings.push_back(action + ":" + channel.getLocalName()); - } + mAxisActionLabels[channel.mIndex] = it->first; } } - - 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 + LLSD buttons = LLGameControl::getModeMapping(mode_name, GC_BUTTONS); + for (auto it = buttons.beginMap(); it != buttons.endMap(); ++it) + { + LLGameControl::InputChannel channel = channelFromInputName(it->second.asString()); + if (channel.isButton() && channel.mIndex < LLGameControl::NUM_BUTTONS) { - return mActionTranslator.getChannelByAction(action + "+"); - }); + mButtonActionLabels[channel.mIndex] = it->first; + } + } } -std::string LLGameControllerManager::getBinaryMappings() const +U32 LLGameControllerManager::computeInternalActionFlags() { - return getMappings(mBinaryActions, LLGameControl::InputChannel::TYPE_BUTTON, - [&](const std::string& action) -> LLGameControl::InputChannel - { - return mActionTranslator.getChannelByAction(action); - }); -} + // Ensure the lookup matches the active mode (cheap no-op when up to date). + rebuildActionLookup(); -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& known_actions, - LLGameControl::InputChannel::Type type, - std::function updateMap) -{ - // 'known_actions' is a list of all available actions ('forward', 'jump', etc) + // Threshold matching LLGameControlTranslator's ON/OFF zone for analog->digital. + constexpr U16 AXIS_THRESHOLD = 32768 / 8; - // 'mappings' is a comma-separated list of action:input pairs - // where 'action' is the name of an avatar behavior ('jump', 'fly') - // and 'input' is a game controller input ('AXIS_4+', 'BUTTON_5') + U32 flags = 0; - if (mappings.empty()) + // Axes: each physical axis' two half-magnitudes (positive at axis*2, negative + // at axis*2+1 in g_innerState) drive the bound label's positive/negative bits. + const auto& axis_bridge = avatarAxisBridge(); + for (U8 axis = 0; axis < LLGameControl::NUM_AXES; ++axis) { - // an empty mappings means all actions are using default inputs - return; - } - - // convert the CSV mappings string a real map - std::map action_to_input_map; - LLStringOps::splitString(mappings, ',', [&](const std::string& mapping) + const std::string& label = mAxisActionLabels[axis]; + if (label.empty()) { - std::size_t pos = mapping.find(':'); - if (pos > 0 && pos != std::string::npos) - { - action_to_input_map[mapping.substr(0, pos)] = mapping.substr(pos + 1); - } - }); - - // only update actions listed in the map - // (any action not itemized will use the default input) - for (auto [action_name, input_name] : action_to_input_map) - { - auto it = std::find(known_actions.begin(), known_actions.end(), action_name); - if (it != known_actions.end()) + continue; + } + auto it = axis_bridge.find(label); + if (it == axis_bridge.end()) { - LLGameControl::InputChannel channel = LLGameControl::getChannelByName(input_name); - if (channel.isNone() || channel.mType == type) - { - updateMap(action_name, channel); - continue; - } + continue; + } + if (g_innerState.mAxes[axis * 2] > AXIS_THRESHOLD) + { + flags |= it->second.posFlag; + } + if (g_innerState.mAxes[axis * 2 + 1] > AXIS_THRESHOLD) + { + flags |= it->second.negFlag; } } -} -void LLGameControllerManager::setAnalogMappings(const std::string& mappings) -{ - setMappings(mappings, mAnalogActions, LLGameControl::InputChannel::TYPE_AXIS, - [&](const std::string& action, LLGameControl::InputChannel channel) + // Buttons: each pressed button contributes its bound label's bit. + const auto& button_bridge = avatarButtonBridge(); + for (U8 btn = 0; btn < LLGameControl::NUM_BUTTONS; ++btn) + { + if (!(g_innerState.mButtons & (1U << btn))) { - mActionTranslator.updateMap(action, channel); - }); -} - -void LLGameControllerManager::setBinaryMappings(const std::string& mappings) -{ - setMappings(mappings, mBinaryActions, LLGameControl::InputChannel::TYPE_BUTTON, - [&](const std::string& action, LLGameControl::InputChannel channel) + continue; + } + const std::string& label = mButtonActionLabels[btn]; + if (label.empty()) { - mActionTranslator.updateMap(action, channel); - }); -} - -void LLGameControllerManager::setFlycamMappings(const std::string& mappings) -{ - setMappings(mappings, mFlycamActions, LLGameControl::InputChannel::TYPE_AXIS, - [&](const std::string& action, LLGameControl::InputChannel channel) + continue; + } + auto it = button_bridge.find(label); + if (it != button_bridge.end()) { - updateFlycamMap(action, channel); - }); + flags |= it->second; + } + } + + return flags; } -void LLGameControllerManager::updateFlycamMap(const std::string& action, LLGameControl::InputChannel channel) +namespace { - auto flycam_it = std::find(mFlycamActions.begin(), mFlycamActions.end(), action); - if (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; + // Flycam channel packing order is defined by LLGameControl::FlycamChannel + // (shared with LLAgent::updateFlycam()). + struct FlycamAxisEffect { U8 channel; F32 polarity; }; + + // FlyCam-mode axis label -> flycam channel + polarity applied to the axis' + // signed value (positive half minus negative half). "Rise up"/"Drop down" + // are two labels on the paired triggers that accumulate into RISE with + // opposite sign, giving the tied-trigger behavior for free. + const std::map& flycamAxisBridge() + { + static const std::map bridge = { + { "Move forward/back", { LLGameControl::FLYCAM_ADVANCE, 1.f } }, + { "Strafe left/right", { LLGameControl::FLYCAM_PAN, 1.f } }, + { "Rise up", { LLGameControl::FLYCAM_RISE, 1.f } }, + { "Drop down", { LLGameControl::FLYCAM_RISE, -1.f } }, + { "Look up/down", { LLGameControl::FLYCAM_PITCH, 1.f } }, + { "Turn left/right", { LLGameControl::FLYCAM_YAW, 1.f } }, + { "Roll left/right", { LLGameControl::FLYCAM_ROLL, 1.f } }, + }; + return bridge; + } + + // FlyCam-mode button label -> flycam channel + full-deflection contribution. + // A gamepad has no free axis for roll (all six are used for translate/look), + // so roll is driven by the shoulder buttons by default; the dpad likewise + // provides digital advance/pan. Command-type buttons (Select, Interact, + // Toggle AltZoom/menu) and the flycam on/off toggle are handled elsewhere. + const std::map& flycamButtonBridge() + { + static const std::map bridge = { + { "Roll left", { LLGameControl::FLYCAM_ROLL, 1.f } }, + { "Roll right", { LLGameControl::FLYCAM_ROLL, -1.f } }, + { "Move forward", { LLGameControl::FLYCAM_ADVANCE, 1.f } }, + { "Move back", { LLGameControl::FLYCAM_ADVANCE, -1.f } }, + { "Strafe left", { LLGameControl::FLYCAM_PAN, 1.f } }, + { "Strafe right", { LLGameControl::FLYCAM_PAN, -1.f } }, + }; + return bridge; } } -U32 LLGameControllerManager::computeInternalActionFlags() +void LLGameControllerManager::getFlycamInputs(std::vector& inputs) { - // add up device inputs - accumulateInternalState(); - if (g_controlAgent) + // Ensure the runtime lookup matches the active (FlyCam) mode. + rebuildActionLookup(); + + // Accumulate each flycam channel from the bound FlyCam-mode axes, reading the + // canonical device state. Values are normalized to [-1, 1] and packed in the + // fixed order consumed by LLAgent::updateFlycam(). + std::vector dof(LLGameControl::FLYCAM_NUM_CHANNELS, 0.f); + const auto& bridge = flycamAxisBridge(); + for (U8 axis = 0; axis < LLGameControl::NUM_AXES; ++axis) { - return mActionTranslator.computeFlagsFromState(mAxesAccumulator, mButtonAccumulator); + const std::string& label = mAxisActionLabels[axis]; + if (label.empty()) + { + continue; + } + auto it = bridge.find(label); + if (it == bridge.end()) + { + continue; + } + // g_innerState half-axes: positive magnitude at axis*2, negative at axis*2+1. + F32 pos = (F32)g_innerState.mAxes[axis * 2] / 32767.f; + F32 neg = (F32)g_innerState.mAxes[axis * 2 + 1] / 32767.f; + dof[it->second.channel] += it->second.polarity * (pos - neg); } - return 0; -} -void LLGameControllerManager::getFlycamInputs(std::vector& inputs) -{ - // The inputs are packed in the same order as they exist in mFlycamChannels: - // - // advance - // pan - // rise - // pitch - // yaw - // zoom - // - for (const auto& channel: mFlycamChannels) + // Button-driven flycam motion (roll on the shoulders, dpad advance/pan): + // each pressed button contributes full deflection to its channel. + const auto& button_bridge = flycamButtonBridge(); + for (U8 btn = 0; btn < LLGameControl::NUM_BUTTONS; ++btn) { - S16 axis; - if (channel.mIndex == LLGameControl::AXIS_TRIGGERLEFT || - channel.mIndex == LLGameControl::AXIS_TRIGGERRIGHT) + if (!(g_innerState.mButtons & (1U << btn))) { - // TIED TRIGGER HACK: we assume the two triggers are paired together - 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; - } - axis = S16(std::min(std::max(total_axis, -32768), 32767)); + continue; } - else + const std::string& label = mButtonActionLabels[btn]; + if (label.empty()) { - axis = S16(std::min(std::max(mAxesAccumulator[channel.mIndex], -32768), 32767)); + continue; } - // 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) ? 32767 : 32768) * channel.mSign; - inputs.push_back(input); + auto it = button_bridge.find(label); + if (it != button_bridge.end()) + { + dof[it->second.channel] += it->second.polarity; + } + } + + for (F32& v : dof) + { + v = std::clamp(v, -1.f, 1.f); } + inputs = dof; } void LLGameControllerManager::setExternalInput(U32 action_flags, U32 buttons) { - if (g_translateAgentActions) + return; + if (true) { // 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. @@ -1470,18 +1896,225 @@ void onControllerAxis(const SDL_Event& event) } // static -bool LLGameControl::isEnabled() +bool LLGameControl::actionFromString(const std::string& string, ActionType& actionType, U8& action) { - return g_enabled; + actionType = ActionType::NONE; + if(LLStringUtil::startsWith(string, "axis_")) { + actionType = ActionType::DOF; + if(string == "axis_left") + { + action = MOVE_DIR_STRAFE_LEFT; + } + else if(string == "axis_right") + { + action = MOVE_DIR_STRAFE_RIGHT; + } + else if(string == "axis_forward") + { + action = MOVE_DIR_PUSH_FORWARD; + } + else if(string == "axis_backward") + { + action = MOVE_DIR_PUSH_BACKWARD; + } + else if(string == "axis_turn_left") + { + action = MOVE_DIR_TURN_LEFT; + } + else if(string == "axis_turn_right") + { + action = MOVE_DIR_TURN_RIGHT; + } + else if(string == "axis_look_up") + { + action = MOVE_DIR_LOOK_UP; + } + else if(string == "axis_look_down") + { + action = MOVE_DIR_LOOK_DOWN; + } + else if(string == "axis_up") + { + action = MOVE_DIR_RISE_UP; + } + else if(string == "axis_down") + { + action = MOVE_DIR_DROP_DOWN; + } + else if(string == "axis_roll_left") + { + action = MOVE_DIR_ROLL_LEFT; + } + else if(string == "axis_roll_right") + { + action = MOVE_DIR_ROLL_RIGHT; + } + else + { + actionType = ActionType::NONE; + return false; + } + return true; + } + else if(LLStringUtil::startsWith(string, "button_")) { + actionType = ActionType::BUTTON; + if(string == "button_south") + { + action = BUTTON_SOUTH; + } + else if(string == "button_east") + { + action = BUTTON_EAST; + } + else if(string == "button_west") + { + action = BUTTON_WEST; + } + else if(string == "button_north") + { + action = BUTTON_NORTH; + } + else if(string == "button_back") + { + action = BUTTON_BACK; + } + else if(string == "button_start") + { + action = BUTTON_START; + } + else if(string == "button_guide") + { + action = BUTTON_GUIDE; + } + else if(string == "button_leftstick") + { + action = BUTTON_LEFT_STICK; + } + else if(string == "button_rightstick") + { + action = BUTTON_RIGHT_STICK; + } + else if(string == "button_leftshoulder") + { + action = BUTTON_LEFT_SHOULDER; + } + else if(string == "button_rightshoulder") + { + action = BUTTON_RIGHT_SHOULDER; + } + else if(string == "button_dpad_up") + { + action = BUTTON_DPAD_UP; + } + else if(string == "button_dpad_down") + { + action = BUTTON_DPAD_DOWN; + } + else if(string == "button_dpad_left") + { + action = BUTTON_DPAD_LEFT; + } + else if(string == "button_dpad_right") + { + action = BUTTON_DPAD_RIGHT; + } + else if(string == "button_misc1") + { + action = BUTTON_MISC1; + } + else if(string == "button_paddle1") + { + action = BUTTON_PADDLE1; + } + else if(string == "button_paddle2") + { + action = BUTTON_PADDLE2; + } + else if(string == "button_paddle3") + { + action = BUTTON_PADDLE3; + } + else if(string == "button_paddle4") + { + action = BUTTON_PADDLE4; + } + else if(string == "button_touchpad") + { + action = BUTTON_TOUCHPAD; + } + else + { + actionType = ActionType::NONE; + return false; + } + return true; + } + return false; } -void LLGameControl::setEnabled(bool enable) +std::string LLGameControl::stringFromAction(const ActionType actionType, const U8 action) { - if (enable != g_enabled) + switch (actionType) { - g_enabled = enable; - s_saveBoolean(SETTING_ENABLE, g_enabled); + case ActionType::DOF: + { + switch(action) + { + case MovementDirection::MOVE_DIR_STRAFE_LEFT: return "axis_left"; + case MovementDirection::MOVE_DIR_STRAFE_RIGHT: return "axis_right"; + case MovementDirection::MOVE_DIR_PUSH_FORWARD: return "axis_forward"; + case MovementDirection::MOVE_DIR_PUSH_BACKWARD: return "axis_backward"; + case MovementDirection::MOVE_DIR_TURN_LEFT: return "axis_turn_left"; + case MovementDirection::MOVE_DIR_TURN_RIGHT: return "axis_turn_right"; + case MovementDirection::MOVE_DIR_LOOK_UP: return "axis_look_up"; + case MovementDirection::MOVE_DIR_LOOK_DOWN: return "axis_look_down"; + case MovementDirection::MOVE_DIR_RISE_UP: return "axis_up"; + case MovementDirection::MOVE_DIR_DROP_DOWN: return "axis_down"; + case MovementDirection::MOVE_DIR_ROLL_LEFT: return "axis_roll_left"; + case MovementDirection::MOVE_DIR_ROLL_RIGHT: return "axis_roll_right"; + } + } + case ActionType::BUTTON: + { + switch(action) + { + case BUTTON_SOUTH: return "button_south"; + case BUTTON_EAST: return "button_east"; + case BUTTON_WEST: return "button_west"; + case BUTTON_NORTH: return "button_north"; + case BUTTON_BACK: return "button_back"; + case BUTTON_START: return "button_start"; + case BUTTON_GUIDE: return "button_guide"; + case BUTTON_LEFT_STICK: return "button_leftstick"; + case BUTTON_RIGHT_STICK: return "button_rightstick"; + case BUTTON_LEFT_SHOULDER: return "button_leftshoulder"; + case BUTTON_RIGHT_SHOULDER: return "button_rightshoulder"; + case BUTTON_DPAD_UP: return "button_dpad_up"; + case BUTTON_DPAD_DOWN: return "button_dpad_down"; + case BUTTON_DPAD_LEFT: return "button_dpad_left"; + case BUTTON_DPAD_RIGHT: return "button_dpad_right"; + case BUTTON_MISC1: return "button_misc1"; + case BUTTON_PADDLE1: return "button_paddle1"; + case BUTTON_PADDLE2: return "button_paddle2"; + case BUTTON_PADDLE3: return "button_paddle3"; + case BUTTON_PADDLE4: return "button_paddle4"; + case BUTTON_TOUCHPAD: return "button_touchpad"; + } + } + default: + return ""; } + return ""; +} + +std::string LLGameControl::controllerInputStringFromAction(const ActionType actionType, const U8 action) +{ + //TODO map to recent controller + std::string aa = stringFromAction(actionType, action); + if(actionType < 2) { + LL_WARNS() << "MAP KEYBIND " << (S32)actionType << ":" << (S32)action << " = " << aa << LL_ENDL; + } + return aa; } // static @@ -1491,25 +2124,16 @@ 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, - std::function loadString, - std::function saveString, - std::function loadObject, - std::function saveObject, + LoadSettingsFn loadSettings, + SaveSettingsFn saveSettings, std::function updateUI) { if (g_gameControl) return; - llassert(loadBoolean); - llassert(saveBoolean); - llassert(loadString); - llassert(saveString); - llassert(loadObject); - llassert(saveObject); + llassert(loadSettings); + llassert(saveSettings); llassert(updateUI); bool result = SDL_InitSubSystem(SDL_INIT_GAMEPAD | SDL_INIT_SENSOR); @@ -1540,12 +2164,8 @@ void LLGameControl::init(const std::string& gamecontrollerdb_path, g_gameControl = LLGameControl::getInstance(); - s_loadBoolean = loadBoolean; - s_saveBoolean = saveBoolean; - s_loadString = loadString; - s_saveString = saveString; - s_loadObject = loadObject; - s_saveObject = saveObject; + s_loadSettings = loadSettings; + s_saveSettings = saveSettings; s_updateUI = updateUI; loadFromSettings(); @@ -1569,6 +2189,14 @@ const std::map& LLGameControl::getDeviceOptions() return g_deviceOptions; } +//static +void LLGameControl::computeFinalState() +{ + g_manager.accumulateInternalState(); + // Note: LLGameControllerManager::computeFinalState() modifies g_nextResendPeriod as a side-effect + g_manager.computeFinalState(); +} + //static // returns 'true' if GameControlInput message needs to go out, // which will be the case for new data or resend. Call this right @@ -1576,15 +2204,14 @@ const std::map& LLGameControl::getDeviceOptions() // or not. bool LLGameControl::computeFinalStateAndCheckForChanges() { - // Note: LLGameControllerManager::computeFinalState() modifies g_nextResendPeriod as a side-effect - g_manager.computeFinalState(); + computeFinalState(); // 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_enabled && g_sendToServer && (g_lastSend + g_nextResendPeriod < get_now_nsec()); + return g_sendToServer && (g_lastSend + g_nextResendPeriod < get_now_nsec()); } // static @@ -1648,24 +2275,29 @@ void LLGameControl::handleEvent(const SDL_Event& event, bool app_has_focus) } // static -const LLGameControl::State& LLGameControl::getState() +const LLGameControl::ServerState& LLGameControl::getServerState() { return g_finalState; } +const LLGameControl::State& LLGameControl::getState() +{ + return g_innerState; +} + // static LLGameControl::InputChannel LLGameControl::getActiveInputChannel() { InputChannel input; - State state = g_finalState; + ServerState 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) + if ((0x1u << i) & state.mButtons) { input.mIndex = i; break; @@ -1705,64 +2337,145 @@ void LLGameControl::getFlycamInputs(std::vector& inputs_out) void LLGameControl::setSendToServer(bool enable) { g_sendToServer = enable; - s_saveBoolean(SETTING_SENDTOSERVER, g_sendToServer); + // SendDataToServer lives inside the GameControl map; it is persisted along with + // the rest of that map (mode mappings, device config) when settings are saved. + ensureGameControlSettings(); + g_gameControlSettings[GC_SENDTOSERVER] = g_sendToServer; } // static -void LLGameControl::setControlAgent(bool enable) +bool LLGameControl::sendToServer() { - g_controlAgent = enable; - s_saveBoolean(SETTING_CONTROLAGENT, g_controlAgent); + return g_sendToServer; } // static -void LLGameControl::setTranslateAgentActions(bool enable) +void LLGameControl::setAgentControlMode(AgentControlMode mode) { - g_translateAgentActions = enable; - s_saveBoolean(SETTING_TRANSLATEACTIONS, g_translateAgentActions); + // AgentControlMode is purely runtime state, auto-derived from avatar state + // every frame (see LLAgent::updateGameControlMode). It is intentionally not + // persisted: the panel's "action_mode" selector only chooses which mode's + // mappings are shown for editing, not the live mode. + g_agentControlMode = mode; } // static -void LLGameControl::setAgentControlMode(LLGameControl::AgentControlMode mode) +LLGameControl::AgentControlMode LLGameControl::getAgentControlMode() { - g_agentControlMode = mode; - s_saveString(SETTING_AGENTCONTROLMODE, convertAgentControlModeToString(mode)); + return g_agentControlMode; } // static -bool LLGameControl::getSendToServer() +bool LLGameControl::isEnabled() { - return g_sendToServer; + // The feature no longer has a separate on/off toggle; it is "enabled" + // whenever at least one controller is connected. + return !g_manager.mDevices.empty(); } // static -bool LLGameControl::getControlAgent() +bool LLGameControl::willControlAvatar() { - return g_controlAgent; + return isEnabled() + && (g_agentControlMode == CONTROL_MODE_AVATAR + || g_agentControlMode == CONTROL_MODE_CAPTIVE); } // static -bool LLGameControl::getTranslateAgentActions() +bool LLGameControl::willControlFlycam() { - return g_translateAgentActions; + return isEnabled() && g_agentControlMode == CONTROL_MODE_FLYCAM; } // static -LLGameControl::AgentControlMode LLGameControl::getAgentControlMode() +std::string LLGameControl::getModeName(AgentControlMode mode) { - return g_agentControlMode; + return modeToString(mode); } // static -LLGameControl::ActionNameType LLGameControl::getActionNameType(const std::string& action) +LLSD LLGameControl::getDefaultModeMappings() { - return g_manager.getActionNameType(action); + return buildDefaultModeMappings(); } // static -bool LLGameControl::willControlAvatar() +LLSD LLGameControl::getDefaultGameControlSettings() +{ + return buildDefaultGameControlSettings(); +} + +// static +const LLSD& LLGameControl::getGameControlSettings() +{ + ensureGameControlSettings(); + return g_gameControlSettings; +} + +// static +void LLGameControl::setGameControlSettings(const LLSD& settings) +{ + g_gameControlSettings = settings; + ensureGameControlSettings(); + // Mappings may have changed: refresh the runtime action lookup immediately. + g_manager.rebuildActionLookup(true); +} + +// static +LLSD LLGameControl::getModeMapping(const std::string& mode, const std::string& kind) +{ + ensureGameControlSettings(); + LLSD mapping = g_gameControlSettings[GC_MODEMAPPINGS][mode][kind]; + if (!mapping.isMap()) + { + // Fall back to the built-in defaults for this mode/kind. + mapping = buildDefaultModeMappings()[mode][kind]; + } + return mapping; +} + +// static +void LLGameControl::setModeMapping(const std::string& mode, const std::string& kind, + const LLSD& mapping) +{ + ensureGameControlSettings(); + g_gameControlSettings[GC_MODEMAPPINGS][mode][kind] = mapping; + // Take effect immediately (e.g. panel "reset to defaults"), without waiting + // for OK: refresh the runtime action lookup for the active mode. + g_manager.rebuildActionLookup(true); +} + +// static +void LLGameControl::updateModeMapping(const std::string& mode, const std::string& kind, + const std::string& action, const std::string& input) +{ + ensureGameControlSettings(); + g_gameControlSettings[GC_MODEMAPPINGS][mode][kind][action] = input; + // Take effect immediately (live panel edit), without waiting for OK: refresh + // the runtime action lookup for the active mode. + g_manager.rebuildActionLookup(true); +} + +// static +std::string LLGameControl::getDeviceConfig(const std::string& guid) +{ + ensureGameControlSettings(); + const LLSD& devices = g_gameControlSettings[GC_DEVICES]; + return devices.has(guid) ? devices[guid][GC_CONFIG].asString() : LLStringUtil::null; +} + +// static +void LLGameControl::setDeviceConfig(const std::string& guid, const std::string& config) +{ + ensureDeviceEntry(guid); + g_gameControlSettings[GC_DEVICES][guid][GC_CONFIG] = config; +} + + +// static +LLGameControl::ActionNameType LLGameControl::getActionNameType(const std::string& action) { - return g_enabled && g_controlAgent && g_agentControlMode == CONTROL_MODE_AVATAR; + return g_manager.getActionNameType(action); } // static @@ -1793,14 +2506,6 @@ LLGameControl::InputChannel LLGameControl::getChannelByName(const std::string& n 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::getChannelByAction(const std::string& action) -{ - return g_manager.getChannelByAction(action); -} - // static U32 LLGameControl::computeInternalActionFlags() { @@ -1838,30 +2543,6 @@ 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, @@ -2022,70 +2703,84 @@ std::string LLGameControl::stringifyDeviceOptions(const std::string& name, void LLGameControl::initByDefault() { g_sendToServer = false; - g_controlAgent = false; - g_translateAgentActions = false; g_agentControlMode = CONTROL_MODE_AVATAR; - g_manager.initializeMappingsByDefault(); + g_gameControlSettings = buildDefaultGameControlSettings(); g_manager.resetDeviceOptionsToDefaults(); g_deviceOptions.clear(); } // static -void LLGameControl::loadFromSettings() +const std::vector& LLGameControl::getSettingKeys() { - // 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); - g_agentControlMode = convertStringToAgentControlMode(s_loadString(SETTING_AGENTCONTROLMODE)); - - g_manager.initializeMappingsByDefault(); + static const std::vector keys = { + SETTING_GAMECONTROL + }; + return keys; +} - // 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); +// static +LLSD LLGameControl::getSettingsAsLLSD() +{ + ensureGameControlSettings(); - // Load device-specific settings - g_deviceOptions.clear(); - LLSD options = s_loadObject(SETTING_KNOWNCONTROLLERS); - for (auto it = options.beginMap(); it != options.endMap(); ++it) + // Fold each device's serialized options into GameControl/Devices//Config. + g_manager.rememberDeviceOptions(); // refresh g_deviceOptions from current device state + for (const auto& [guid, options] : g_deviceOptions) { - g_deviceOptions.emplace(it->first, it->second); + if (!options.empty()) + { + g_gameControlSettings[GC_DEVICES][guid][GC_CONFIG] = options; + } } - g_manager.loadDeviceOptionsFromSettings(); + + g_gameControlSettings[GC_SENDTOSERVER] = g_sendToServer; + + LLSD result = LLSD::emptyMap(); + result[SETTING_GAMECONTROL] = g_gameControlSettings; + return result; } // static -void LLGameControl::saveToSettings() +void LLGameControl::applySettingsFromLLSD(const LLSD& settings) { - s_saveBoolean(SETTING_ENABLE, g_enabled); - 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()); + if (settings.has(SETTING_GAMECONTROL)) g_gameControlSettings = settings[SETTING_GAMECONTROL]; + ensureGameControlSettings(); - g_manager.saveDeviceOptionsToSettings(); + // SendDataToServer is nested inside the GameControl map. + if (g_gameControlSettings.has(GC_SENDTOSERVER)) + g_sendToServer = g_gameControlSettings[GC_SENDTOSERVER].asBoolean(); - // construct LLSD version of g_deviceOptions but only include non-empty values - LLSD deviceOptions = LLSD::emptyMap(); - for (const auto& [guid, options] : g_deviceOptions) + // Rebuild the device-options cache from GameControl/Devices//Config and + // apply it to the connected devices. + g_deviceOptions.clear(); + const LLSD& devices = g_gameControlSettings[GC_DEVICES]; + for (auto it = devices.beginMap(); it != devices.endMap(); ++it) { - if (!options.empty()) + if (it->first == GC_DEFAULT_DEVICE) + continue; // the "Default" template is not a real device + std::string config = it->second[GC_CONFIG].asString(); + if (!config.empty()) { - LLSD value(options); - deviceOptions.insert(guid, value); + g_deviceOptions.emplace(it->first, config); } } + g_manager.applyRememberedDeviceOptions(); - s_saveObject(SETTING_KNOWNCONTROLLERS, deviceOptions); + // ModeMappings may have changed (e.g. live edits in the preferences panel); + // force the runtime action lookup to rebuild for the active mode. + g_manager.rebuildActionLookup(true); +} + +// static +void LLGameControl::loadFromSettings() +{ + applySettingsFromLLSD(s_loadSettings(getSettingKeys())); +} + +// static +void LLGameControl::saveToSettings() +{ + s_saveSettings(getSettingsAsLLSD()); } // static @@ -2093,3 +2788,80 @@ void LLGameControl::setDeviceOptions(const std::string& guid, const Options& opt { g_manager.setDeviceOptions(guid, options); } + +static bool mapLocalStringToTypeAndIndex(const std::string control, LLGameControl::InputChannel::Type& cType, U8& cIndex) +{ + // HACK: needs proper method to map strings to Input Types and Indexes + for(U8 i = 0;i < 8;i++) + { + if(control == ("AXIS_" + std::to_string((U32)i))) + { + cIndex = i; + cType = LLGameControl::InputChannel::Type::TYPE_AXIS; + return true; + } + } + + for(U8 i = 0;i < 32;i++) + { + if(control == ("BUTTON_" + std::to_string((U32)i))) + { + cIndex = i; + cType = LLGameControl::InputChannel::Type::TYPE_BUTTON; + return true; + } + } + // /HACK + return false; +} + +// virtual, from LLGameControllerBindingToStringHandler +std::string LLGameControl::getBindingAsString(const std::string& control) const +{ + if(!hasHandlingDevice()) { + return std::string(); + } + + InputChannel::Type cType = InputChannel::Type::TYPE_NONE; + U8 cIndex = 255; + if(!mapLocalStringToTypeAndIndex(control, cType, cIndex)) + { + return std::string(); + } + + if(cType == InputChannel::Type::TYPE_NONE) + { + return std::string(); + } + + const LLGameControl::Device* device = g_manager.getLastActiveDevice(); + + if(!device) + { + return std::string(); + } + + SDL_Gamepad* gp = SDL_GetGamepadFromID(device->getJoystickID()); + + if(cType == InputChannel::Type::TYPE_AXIS) + { + U8 axis = device->getOptions().unmapAxis(cIndex); + return std::to_string((SDL_GamepadAxis)axis); + } + else + { + U8 button = device->getOptions().unmapButton(cIndex); + if(button < 4) + { + return std::to_string(SDL_GetGamepadButtonLabel(gp, (SDL_GamepadButton)button)); + } + return std::to_string((SDL_GamepadButton)button); + + } +} + +// virtual, from LLGameControllerBindingToStringHandler +bool LLGameControl::hasHandlingDevice() const +{ + return !g_manager.mDevices.empty(); +} diff --git a/indra/llwindow/llgamecontrol.h b/indra/llwindow/llgamecontrol.h index 3ea60afa93..0f15045ca4 100644 --- a/indra/llwindow/llgamecontrol.h +++ b/indra/llwindow/llgamecontrol.h @@ -44,9 +44,9 @@ // leftshoulder _(9)_________'-.____ ____.-'_________(10) rightshoulder // / _________ \_________/ \. // / / 1- \ (3) \. -// | | | (4) (5) (6) Y | -// | |0- (7) 0+| _________ (2)X B(1) | -// | | | / 3- \ A | +// | | | (4) (5) (6) N | +// | |0- (7) 0+| _________ (2)W E(1) | +// | | | / 3- \ S | // | | 1+ | | | (0) | // | \_________/ |2- (8) 2+| | // | leftstick (11) | | | @@ -65,8 +65,16 @@ // they are extracted from SDL, before being used anywhere. See the // implementation in LLGameControllerManager::onAxis(). +// Interface to get controller binding from assigned command +class LLGameControllerBindingToStringHandler +{ +public: + virtual std::string getBindingAsString(const std::string& control) const = 0; + virtual bool hasHandlingDevice() const = 0; +}; + // LLGameControl is a singleton with pure static public interface -class LLGameControl : public LLSingleton +class LLGameControl : public LLSingleton, public LLGameControllerBindingToStringHandler { LLSINGLETON_EMPTY_CTOR(LLGameControl); virtual ~LLGameControl(); @@ -77,16 +85,39 @@ public: { CONTROL_MODE_AVATAR, CONTROL_MODE_FLYCAM, + CONTROL_MODE_CAPTIVE, // Avatar is sitting, or controls have been taken CONTROL_MODE_NONE }; + enum ActionType: U8 + { + DOF, + BUTTON, + NONE = 255, + }; + + enum MovementDirection: U8 + { + MOVE_DIR_STRAFE_LEFT, + MOVE_DIR_STRAFE_RIGHT, + MOVE_DIR_PUSH_FORWARD, + MOVE_DIR_PUSH_BACKWARD, + MOVE_DIR_TURN_LEFT, + MOVE_DIR_TURN_RIGHT, + MOVE_DIR_LOOK_UP, + MOVE_DIR_LOOK_DOWN, + MOVE_DIR_RISE_UP, + MOVE_DIR_DROP_DOWN, + MOVE_DIR_ROLL_LEFT, + MOVE_DIR_ROLL_RIGHT, + NUM_MOVE_DIRS, + }; + enum ActionNameType { ACTION_NAME_UNKNOWN, - ACTION_NAME_ANALOG, // E.g., "push" - ACTION_NAME_ANALOG_POS, // E.g., "push+" - ACTION_NAME_ANALOG_NEG, // E.g., "push-" - ACTION_NAME_BINARY, // E.g., "stop" + ACTION_NAME_ANALOG, // avatar movement, e.g. "push" + ACTION_NAME_BINARY, // maps to button, e.g. "stop" ACTION_NAME_FLYCAM // E.g., "zoom" }; @@ -96,24 +127,24 @@ public: AXIS_LEFTY, AXIS_RIGHTX, AXIS_RIGHTY, - AXIS_TRIGGERLEFT, - AXIS_TRIGGERRIGHT, + AXIS_LEFT_TRIGGER, + AXIS_RIGHT_TRIGGER, NUM_AXES }; - enum Button + enum Button : U8 { - BUTTON_A, - BUTTON_B, - BUTTON_X, - BUTTON_Y, + BUTTON_SOUTH, + BUTTON_EAST, + BUTTON_WEST, + BUTTON_NORTH, BUTTON_BACK, BUTTON_GUIDE, BUTTON_START, - BUTTON_LEFTSTICK, - BUTTON_RIGHTSTICK, - BUTTON_LEFTSHOULDER, - BUTTON_RIGHTSHOULDER, // 10 + BUTTON_LEFT_STICK, + BUTTON_RIGHT_STICK, + BUTTON_LEFT_SHOULDER, + BUTTON_RIGHT_SHOULDER, // 10 BUTTON_DPAD_UP, BUTTON_DPAD_DOWN, BUTTON_DPAD_LEFT, @@ -138,6 +169,21 @@ public: NUM_BUTTONS }; + // Order in which flycam channel values are packed by getFlycamInputs() and + // consumed by LLAgent::updateFlycam(). Zoom currently has no FlyCam axis + // label (stays 0); Roll is driven by buttons by default. + enum FlycamChannel : U8 + { + FLYCAM_ADVANCE = 0, + FLYCAM_PAN, + FLYCAM_RISE, + FLYCAM_PITCH, + FLYCAM_YAW, + FLYCAM_ROLL, + FLYCAM_ZOOM, + FLYCAM_NUM_CHANNELS + }; + static const U16 MAX_AXIS_DEAD_ZONE = 16384; static const U16 MAX_AXIS_OFFSET = 16384; @@ -165,8 +211,9 @@ public: return mType == other.mType && mSign == other.mSign && mIndex == other.mIndex; } - std::string getLocalName() const; // AXIS_0-, AXIS_0+, BUTTON_0, NONE etc. - std::string getRemoteName() const; // GAME_CONTROL_AXIS_LEFTX, GAME_CONTROL_BUTTON_A, etc + std::string getLocalName() const; // AXIS_0, AXIS_1, BUTTON_0, NONE etc. + std::string getSignedLocalName() const; // AXIS_0-, AXIS_0+, BUTTON_0, NONE etc. + std::string getRemoteName() const; // AXIS_LEFTX, BUTTON_SOUTH, etc Type mType { TYPE_NONE }; S32 mSign { 0 }; @@ -183,18 +230,24 @@ public: U16 mDeadZone { 0 }; S16 mOffset { 0 }; + void resetToDefaults(); + /* void resetToDefaults() { mMultiplier = 1; mDeadZone = 0; mOffset = 0; } + */ + S16 computeModifiedValue(S16 raw_value) const; + /* S16 computeModifiedValue(S16 raw_value) const { S32 new_value = ((S32)raw_value + S32(mOffset)) * mMultiplier; return (S16)(std::clamp(new_value, -32768, 32767)); } + */ std::string saveToString() const; void loadFromString(std::string options); @@ -207,6 +260,9 @@ public: U8 mapAxis(U8 axis) const; U8 mapButton(U8 button) const; + U8 unmapAxis(U8 axis) const; + U8 unmapButton(U8 button) const; + S16 fixAxisValue(U8 axis, S16 value) const; std::string saveToString(const std::string& name, bool force_empty = false) const; @@ -232,10 +288,25 @@ public: public: State(); void clear(); + void storePrevious(); bool onButton(U8 button, bool pressed); + std::vector mAxes; // [ 0 , 32767 ] post-fix (dead zone/offset/invert applied), split into +/- half-axes + std::vector mRawAxes; // [ 0 , 32767 ] pre-fix values, same +/- half-axis layout as mAxes + U32 mButtons; + + std::vector mPrevAxes; + U32 mPrevButtons; + }; + + class ServerState + { + public: + ServerState(); + void clear(); std::vector mAxes; // [ -32768, 32767 ] std::vector mPrevAxes; // value in last outgoing packet U32 mButtons; + U32 mPrevButtons; }; // Device is a data structure for describing any detected controller @@ -262,17 +333,27 @@ public: friend class LLGameControllerManager; }; - static bool isEnabled(); - static void setEnabled(bool enabled); + static bool actionFromString(const std::string& string, ActionType& actionType, U8& action); + static std::string stringFromAction(const ActionType actionType, U8 action); + static std::string controllerInputStringFromAction(const ActionType actionType, U8 action); + + static F32 getControllerHeldTime(ActionType actionType, U8 action); + static S32 getControllerHeldFrames(ActionType actionType, U8 action); static bool isInitialized(); + + // Bulk settings I/O is delegated to the host so this library does not + // need to know about LLControlGroup at compile time. + // loadSettings(keys) -- returns an LLSD map of the requested keys. + // Keys absent from the host's store are + // omitted from the returned map. + // saveSettings(key_values) -- writes every entry of the LLSD map. + using LoadSettingsFn = std::function&)>; + using SaveSettingsFn = std::function; + static void init(const std::string& gamecontrollerdb_path, - std::function loadBoolean, - std::function saveBoolean, - std::function loadString, - std::function saveString, - std::function loadObject, - std::function saveObject, + LoadSettingsFn loadSettings, + SaveSettingsFn saveSettings, std::function updateUI); static void terminate(); @@ -284,38 +365,37 @@ public: // before deciding to put a GameControlInput packet on the wire // or not. static bool computeFinalStateAndCheckForChanges(); + static void computeFinalState(); static void clearAllStates(); static void processEvents(bool app_has_focus = true); static void handleEvent(const SDL_Event& event, bool app_has_focus); + static const ServerState& getServerState(); static const State& getState(); static InputChannel getActiveInputChannel(); static void getFlycamInputs(std::vector& inputs_out); // these methods for accepting input from keyboard static void setSendToServer(bool enable); - static void setControlAgent(bool enable); - static void setTranslateAgentActions(bool enable); static void setAgentControlMode(AgentControlMode mode); - static bool getSendToServer(); - static bool getControlAgent(); - static bool getTranslateAgentActions(); + static bool sendToServer(); static AgentControlMode getAgentControlMode(); - static ActionNameType getActionNameType(const std::string& action); - static bool willControlAvatar(); + // Runtime gating for the local-control path. "Enabled" now means simply + // "a controller is connected" -- the feature no longer has a separate on/off + // toggle (see the "GameControl always enabled" change). willControlAvatar() + // and willControlFlycam() additionally gate on the active AgentControlMode. + static bool isEnabled(); // a controller is connected + static bool willControlAvatar();// enabled AND mode is Avatar/Captive + static bool willControlFlycam();// enabled AND mode is FlyCam + static ActionNameType getActionNameType(const std::string& action); // Given a name like "AXIS_1-" or "BUTTON_5" returns the corresponding InputChannel // If the axis name lacks the +/- postfix it assumes '+' postfix. static LLGameControl::InputChannel getChannelByName(const std::string& name); - // action_name = push+, strafe-, etc - static LLGameControl::InputChannel getChannelByAction(const std::string& action); - - static bool updateActionMap(const std::string& action_name, LLGameControl::InputChannel channel); - // Keyboard presses produce action_flags which can be translated into State // and game_control devices produce State which can be translated into action_flags. // These methods help exchange such translations. @@ -325,12 +405,6 @@ public: // call this after putting a GameControlInput packet on the wire static void updateResendPeriod(); - using getChannel_t = std::function; - static std::string stringifyAnalogMappings(getChannel_t getChannel); - static std::string stringifyBinaryMappings(getChannel_t getChannel); - static std::string stringifyFlycamMappings(getChannel_t getChannel); - static void getDefaultMappings(std::vector>& mappings); - static bool parseDeviceOptions(const std::string& options, std::string& name, std::vector& axis_options, std::vector& axis_map, std::vector& button_map); @@ -342,6 +416,43 @@ public: static void initByDefault(); static void loadFromSettings(); static void saveToSettings(); + + // Settings serialization helpers used by loadFromSettings/saveToSettings + // and exposed for callers that need to snapshot or restore module state + // (e.g. preference panel cancel-restore). + // getSettingKeys() -- the host setting names this module reads/writes. + // getSettingsAsLLSD() -- snapshot of current in-memory state, keyed by the same names. + // applySettingsFromLLSD(settings) -- apply 'settings' to in-memory state. + // Keys not present in 'settings' are left unchanged. + static const std::vector& getSettingKeys(); + static LLSD getSettingsAsLLSD(); + static void applySettingsFromLLSD(const LLSD& settings); + + // GameControl settings, stored under the single "GameControl" setting key: + // ModeMappings//Axes|Buttons -- GLOBAL action -> canonical-input maps + // Devices//Config -- per-device serialized hardware options + // 'mode' is "Avatar"/"FlyCam"/"Captive"; 'kind' is "Axes"/"Buttons". + static LLSD getDefaultModeMappings(); // { : { Axes, Buttons } } + static LLSD getDefaultGameControlSettings(); // full default GameControl map + static const LLSD& getGameControlSettings(); + static void setGameControlSettings(const LLSD& settings); + + // Global per-mode action mapping accessors. getModeMapping falls back to the + // built-in defaults when an entry is missing. + static LLSD getModeMapping(const std::string& mode, const std::string& kind); + static void setModeMapping(const std::string& mode, const std::string& kind, + const LLSD& mapping); + static void updateModeMapping(const std::string& mode, const std::string& kind, + const std::string& action, const std::string& input); + static std::string getDeviceConfig(const std::string& guid); + static void setDeviceConfig(const std::string& guid, const std::string& config); + + // "Avatar"/"FlyCam"/"Captive" for the given mode (empty for CONTROL_MODE_NONE). + static std::string getModeName(AgentControlMode mode); + static void setDeviceOptions(const std::string& guid, const Options& options); -}; + // inherited from LLGameControllerBindingToStringHandler + virtual std::string getBindingAsString(const std::string& control) const override; + virtual bool hasHandlingDevice() const override; +}; diff --git a/indra/llwindow/llgamecontroltranslator.cpp b/indra/llwindow/llgamecontroltranslator.cpp index 8085fa6dbd..4ed76606eb 100644 --- a/indra/llwindow/llgamecontroltranslator.cpp +++ b/indra/llwindow/llgamecontroltranslator.cpp @@ -31,6 +31,7 @@ */ #include "llgamecontroltranslator.h" +#include "llgamecontrol.h" #include "llsd.h" @@ -40,6 +41,81 @@ LLGameControlTranslator::LLGameControlTranslator() { } + + +const S32 LLGameControlTranslator::calculateTranslatedButtons( + const ControllerMappings& mappings, + const LLGameControl::State state +) +{ + // HACK: supply hard-coded threshold for ON/OFF zones + constexpr S32 AXIS_THRESHOLD = 32768 / 8; + + S32 result = 0; + + + + size_t axesSize = state.mAxes.size(); + for(const ControllerMapping& mapping : mappings) + { + const ControllerActionTypeIndex& from = mapping.first; + const U32 to = mapping.second; + + U8 btn = 0; + + if(from.first == LLGameControl::ActionType::BUTTON) + { + btn = (state.mButtons & (1 << from.second)) > 0; + } + else if(from.first == LLGameControl::ActionType::DOF) + { + if(from.second < axesSize) { + btn = state.mAxes[from.second] > AXIS_THRESHOLD; + } + } + + result |= to * btn; + } + return result; +} + +void LLGameControlTranslator::calculateTranslatedAxes( + const ControllerMappings& mappings, + const LLGameControl::State& state, + std::vector& outDOF +) +{ + std::fill(outDOF.begin(), outDOF.end(), 0); + + size_t outDOFSize = outDOF.size(); + size_t dofSize = state.mAxes.size(); + for(const ControllerMapping& mapping : mappings) + { + const ControllerActionTypeIndex& from = mapping.first; + const U32 to = mapping.second; + + if(to >= outDOFSize) + { + continue; + } + + S16 dof = 0; + + if(from.first == LLGameControl::ActionType::BUTTON) + { + dof = 1.0 * ((state.mButtons & (1 << from.second)) > 0); + } + else if(from.first == LLGameControl::ActionType::DOF) + { + if(from.second < dofSize) { + dof = state.mAxes[from.second]; + } + } + + outDOF[to] = dof; + } +} + void LLGameControlTranslator::setAvailableActionMasks(ActionToMaskMap& action_to_mask) { mActionToMask = std::move(action_to_mask); @@ -136,18 +212,18 @@ void LLGameControlTranslator::updateMap(const std::string& action, const LLGameC // and those are pretty much the only supported devices right now // however TODO: figure out how to do this better. // - // AXIS_TRIGGERLEFT and AXIS_TRIGGERRIGHT are separate axes and most devices + // AXIS_LEFT_TRIGGER and AXIS_RIGHT_TRIGGER are separate axes and most devices // only allow them to read positive, not negative. When used for motion control // they are typically paired together. We assume as much here when computing // the other_channel. - if (channel.mIndex == LLGameControl::AXIS_TRIGGERLEFT) + if (channel.mIndex == LLGameControl::AXIS_LEFT_TRIGGER) { - other_channel.mIndex = LLGameControl::AXIS_TRIGGERRIGHT; + other_channel.mIndex = LLGameControl::AXIS_RIGHT_TRIGGER; other_channel.mSign = 1; } - else if (channel.mIndex == LLGameControl::AXIS_TRIGGERRIGHT) + else if (channel.mIndex == LLGameControl::AXIS_RIGHT_TRIGGER) { - other_channel.mIndex = LLGameControl::AXIS_TRIGGERLEFT; + other_channel.mIndex = LLGameControl::AXIS_LEFT_TRIGGER; other_channel.mSign = 1; } updateMapInternal(action + "-", other_channel); diff --git a/indra/llwindow/llgamecontroltranslator.h b/indra/llwindow/llgamecontroltranslator.h index 533408014c..8647fbca1d 100644 --- a/indra/llwindow/llgamecontroltranslator.h +++ b/indra/llwindow/llgamecontroltranslator.h @@ -47,13 +47,18 @@ class LLGameControlTranslator { public: + using ControllerActionTypeIndex = std::pair; + using ControllerMapping = std::pair; + using ControllerMappings = std::vector; + using ActionToMaskMap = std::map< std::string, U32 >; // < action : mask > using MaskToChannelMap = std::map< U32, LLGameControl::InputChannel >; // < mask : channel > using NamedChannel = std::pair < std::string , LLGameControl::InputChannel >; using NamedChannels = std::vector< NamedChannel >; - LLGameControlTranslator(); + const S32 calculateTranslatedButtons(const ControllerMappings& mapping, const LLGameControl::State state); + void calculateTranslatedAxes(const ControllerMappings& mapping, const LLGameControl::State& state, std::vector& outDOF); void setAvailableActionMasks(ActionToMaskMap& action_to_mask); LLGameControl::InputChannel getChannelByAction(const std::string& action) const; void setMappings(NamedChannels& named_channels); diff --git a/indra/llwindow/llkeyboard.h b/indra/llwindow/llkeyboard.h index 5964051899..db6b3aaade 100644 --- a/indra/llwindow/llkeyboard.h +++ b/indra/llwindow/llkeyboard.h @@ -40,7 +40,47 @@ enum EKeystate KEYSTATE_UP }; -typedef std::function LLKeyFunc; + +struct LLKeyPressState +{ + LLKeyPressState(EKeystate state, F32 value) + : + mState(state), + mValue(value), + mElapsedTime(0.0), + mElapsedFrames(0.0), + mRepeat(false), + mAnalog(false) + { + mUp = mState == KEYSTATE_UP; + mLevel = mState == KEYSTATE_LEVEL; + mDown = mState == KEYSTATE_DOWN; + } + LLKeyPressState(bool up, bool level, bool down, F32 value) + : + mUp(up), + mLevel(level), + mDown(down), + mValue(value), + mElapsedTime(0.0), + mElapsedFrames(0.0), + mRepeat(false), + mAnalog(false) + { + mState = mDown ? KEYSTATE_DOWN : ( mLevel ? KEYSTATE_LEVEL : KEYSTATE_UP ); + } + EKeystate mState; + F32 mValue; + F32 mElapsedTime; + S32 mElapsedFrames; + bool mRepeat; + bool mUp; + bool mLevel; + bool mDown; + bool mAnalog; +}; + +typedef std::function LLKeyFunc; typedef std::string (LLKeyStringTranslatorFunc)(std::string_view); enum EKeyboardInsertMode @@ -55,7 +95,7 @@ class LLKeyboard { public: #ifdef LL_SDL_WINDOW - // linux relies on SDL3 which uses U32 for its native key type + // linux relies on SDL which uses U32 for its native key type typedef U32 NATIVE_KEY_TYPE; #else // on non-linux platforms we can get by with a smaller native key type @@ -69,7 +109,8 @@ public: F32 getCurKeyElapsedTime() { return getKeyDown(mCurScanKey) ? getKeyElapsedTime( mCurScanKey ) : 0.f; } - F32 getCurKeyElapsedFrameCount() { return getKeyDown(mCurScanKey) ? (F32)getKeyElapsedFrameCount( mCurScanKey ) : 0.f; } + S32 getCurKeyElapsedFrameCount() { return getKeyDown(mCurScanKey) ? getKeyElapsedFrameCount( mCurScanKey ) : 0.0; } + KEY getCurScanKey() { return mCurScanKey; } bool getKeyDown(const KEY key) { return mKeyLevel[key]; } bool getKeyRepeated(const KEY key) { return mKeyRepeated[key]; } diff --git a/indra/llwindow/llsdl.cpp b/indra/llwindow/llsdl.cpp index 54323c9329..c562c32f88 100644 --- a/indra/llwindow/llsdl.cpp +++ b/indra/llwindow/llsdl.cpp @@ -93,7 +93,6 @@ void init_sdl(const std::string& app_name) #endif #if LL_SDL_WINDOW - // For linux we SDL_INIT_VIDEO and _AUDIO std::initializer_list > hintList = { {SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR,"0"}, @@ -136,5 +135,14 @@ void quit_sdl() #if LL_WINDOWS && defined(LL_SDL_WINDOW) SDL_UnregisterApp(); #endif - SDL_Quit(); + + // When SDL_MAIN_USE_CALLBACKS is in effect (gSDLMainHandled=true), the + // SDL framework owns SDL_Init/SDL_Quit around the SDL_App* callbacks. + // Calling SDL_Quit() here would run before SDL's own SDL_Quit at process + // exit — SDL3 makes this idempotent, but it's still the wrong owner. + // Only quit ourselves when we initialised SDL ourselves. + if (!gSDLMainHandled) + { + SDL_Quit(); + } } diff --git a/indra/llwindow/llwindowsdl.cpp b/indra/llwindow/llwindowsdl.cpp index 66b032f06c..a63dc771c7 100644 --- a/indra/llwindow/llwindowsdl.cpp +++ b/indra/llwindow/llwindowsdl.cpp @@ -98,8 +98,6 @@ LLWindowSDL::LLWindowSDL(LLWindowCallbacks* callbacks, : LLWindow(callbacks, fullscreen, flags), mGamma(1.0f), mFlashing(false) { - SDL_GL_LoadLibrary(nullptr); - // Initialize the keyboard gKeyboard = new LLKeyboardSDL(); gKeyboard->setCallbacks(callbacks); @@ -212,7 +210,7 @@ bool LLWindowSDL::createContext(int x, int y, int width, int height, int bits, b if (width == 0) width = 1024; if (height == 0) - width = 768; + height = 768; if (x == 0) x = SDL_WINDOWPOS_UNDEFINED; if (y == 0) @@ -998,9 +996,11 @@ LLWindow::LLWindowResolution* LLWindowSDL::getSupportedResolutions(S32 &num_reso if ((w >= 800) && (h >= 600)) { // make sure we don't add the same resolution multiple times! + // A row is "the same" only when both width AND height match the previous row; + // if either dimension differs we keep it. if ( (mNumSupportedResolutions == 0) || - ((mSupportedResolutions[mNumSupportedResolutions-1].mWidth != w) && - (mSupportedResolutions[mNumSupportedResolutions-1].mHeight != h)) ) + (mSupportedResolutions[mNumSupportedResolutions-1].mWidth != w) || + (mSupportedResolutions[mNumSupportedResolutions-1].mHeight != h) ) { mSupportedResolutions[mNumSupportedResolutions].mWidth = w; mSupportedResolutions[mNumSupportedResolutions].mHeight = h; diff --git a/indra/llwindow/llwindowsdl.h b/indra/llwindow/llwindowsdl.h index f13bda3d70..650f27fae9 100644 --- a/indra/llwindow/llwindowsdl.h +++ b/indra/llwindow/llwindowsdl.h @@ -211,7 +211,7 @@ protected: U32 mGrabbyKeyFlags = 0; S32 mReallyCapturedCount = 0; SDL_Window *mWindow = nullptr; - SDL_GLContext mContext; + SDL_GLContext mContext = nullptr; SDL_Cursor *mSDLCursors[UI_CURSOR_COUNT]; std::string mWindowTitle; diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 1a28a2fd24..74bd3f3c1f 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -506,6 +506,7 @@ set(viewer_SOURCE_FILES llpanelplaceprofile.cpp llpanelplaces.cpp llpanelplacestab.cpp + llpanelpreferencegamecontrol.cpp llpanelpresetscamerapulldown.cpp llpanelpresetspulldown.cpp llpanelprimmediacontrols.cpp @@ -1188,6 +1189,7 @@ set(viewer_HEADER_FILES llpanelplaceprofile.h llpanelplaces.h llpanelplacestab.h + llpanelpreferencegamecontrol.h llpanelpresetscamerapulldown.h llpanelpresetspulldown.h llpanelprimmediacontrols.h @@ -2465,7 +2467,7 @@ if (LL_TESTS) set_property(SOURCE ${viewer_TEST_SOURCE_FILES} PROPERTY - LL_TEST_ADDITIONAL_LIBRARIES + LL_TEST_ADDITIONAL_LIBRARIES ${test_libs} ) diff --git a/indra/newview/app_settings/gamecontrollerdb.txt b/indra/newview/app_settings/gamecontrollerdb.txt index cdfbc16a8e..e7a236ea53 100644 --- a/indra/newview/app_settings/gamecontrollerdb.txt +++ b/indra/newview/app_settings/gamecontrollerdb.txt @@ -1480,12 +1480,12 @@ xinput,XInput Controller,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2, 030000005e0400008e02000004010000,Microsoft Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, 030000005e0400008e02000056210000,Microsoft Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, 030000005e0400008e02000062230000,Microsoft Xbox 360,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000d102000001010000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000d102000003020000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000dd02000003020000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000ea02000008040000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -030000005e040000ea0200000f050000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, -060000005e040000120b000009050000,Microsoft Xbox One,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e040000d102000001010000,Microsoft Xbox One a,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e040000d102000003020000,Microsoft Xbox One b,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e040000dd02000003020000,Microsoft Xbox One c,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e040000ea02000008040000,Microsoft Xbox One d,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +030000005e040000ea0200000f050000,Microsoft Xbox One e,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, +060000005e040000120b000009050000,Microsoft Xbox One d,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,misc1:b11,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, 030000005e040000e302000003020000,Microsoft Xbox One Elite,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, 030000005e040000000b000007040000,Microsoft Xbox One Elite 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b12,paddle2:b14,paddle3:b13,paddle4:b15,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, 030000005e040000000b000008040000,Microsoft Xbox One Elite 2,a:b0,b:b1,back:b6,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,paddle1:b12,paddle2:b14,paddle3:b13,paddle4:b15,rightshoulder:b5,rightstick:b10,righttrigger:a5,rightx:a3,righty:a4,start:b7,x:b2,y:b3,platform:Linux, diff --git a/indra/newview/app_settings/key_bindings.xml b/indra/newview/app_settings/key_bindings.xml index 8d5349550f..237dc225d7 100644 --- a/indra/newview/app_settings/key_bindings.xml +++ b/indra/newview/app_settings/key_bindings.xml @@ -7,6 +7,7 @@ + @@ -21,9 +22,20 @@ - + + + + + + + + + + + + @@ -34,6 +46,7 @@ + @@ -51,6 +64,20 @@ + + + + + + + + + + + + + + @@ -87,6 +114,11 @@ + + + + + @@ -160,6 +192,11 @@ + + + + + @@ -178,7 +215,7 @@ - + diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 86027115f8..720f22b21c 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -102,7 +102,7 @@ Type S32 Value - 300 + 300 AckCollectTime @@ -294,7 +294,7 @@ AudioLevelMedia Comment - Audio level of Quicktime movies + Audio level of parcel Media Persist 1 Type @@ -2678,7 +2678,7 @@ Value 0 - DoubleClickTeleport + DoubleClickTeleport Comment Enable double-click to teleport where allowed (afects minimap and people panel) @@ -2777,10 +2777,10 @@ Value 1 - EnableGameControl + DebugShowControllerState Comment - Enable GameControl feature + Display internal controller state values Persist 1 Type @@ -2788,87 +2788,10 @@ Value 0 - GameControlToServer - - Comment - Transmit game controller data to server - Persist - 1 - Type - Boolean - Value - 0 - - GameControlToAgent - - Comment - GameControl data moves avatar/flycam - Persist - 1 - Type - Boolean - Value - 0 - - AgentToGameControl - - Comment - Avatar/flycam movement produces GameControl data - Persist - 1 - Type - Boolean - Value - 0 - - AgentControlMode + GameControl Comment - GameControl current agent control mode - Persist - 1 - Type - String - Value - - - AnalogChannelMappings - - Comment - GameControl analog button to channel mappings - Persist - 1 - Type - String - Value - push:AXIS_1+,slide:AXIS_0+,jump:AXIS_4+,turn:AXIS_2+,look:AXIS_3+ - - BinaryChannelMappings - - Comment - GameControl binary button to channel mappings - Persist - 1 - Type - String - Value - toggle_run:BUTTON_9,toggle_fly:BUTTON_11,toggle_flycam:BUTTON_10,stop:BUTTON_7 - - FlycamChannelMappings - - Comment - GameControl flycam button to channel mappings - Persist - 1 - Type - String - Value - advance:AXIS_1+,pan:AXIS_0+,rise:AXIS_5+,pitch:AXIS_3-,yaw:AXIS_2+ - - KnownGameControllers - - Comment - Map of controller device-specific options. + Game controller settings: global per-mode action mappings plus per-device hardware config. Empty means use built-in defaults. Persist 1 Type @@ -9570,7 +9493,7 @@ Value 1.0 - + RenderReflectionProbeDrawDistance Comment @@ -9580,7 +9503,7 @@ Type F32 Value - 64 + 64.0 RenderReflectionProbeAmbiance @@ -9769,7 +9692,7 @@ Value 1.0 - + RenderReflectionProbeMaxLocalLightAmbiance Comment @@ -11326,7 +11249,7 @@ Boolean Value 0 - + NearbyListShowMap Comment @@ -16537,7 +16460,7 @@ Type Boolean Value - 1 + 1 UpdateAppWindowTitleBar diff --git a/indra/newview/llagent.cpp b/indra/newview/llagent.cpp index 83809a0bd3..b7e0bd3b91 100644 --- a/indra/newview/llagent.cpp +++ b/indra/newview/llagent.cpp @@ -454,7 +454,7 @@ LLAgent::LLAgent() : mCurrentFidget(0), mFirstLogin(false), mOutfitChosen(false), - + mCrouch(false), mVoiceConnected(false), mMouselookModeInSignal(nullptr), @@ -790,13 +790,17 @@ void LLAgent::moveUp(S32 direction) mLastJumpInputTime = LLTimer::getTotalSeconds(); } setControlFlags(AGENT_CONTROL_UP_POS | AGENT_CONTROL_FAST_UP); + mCrouch = false; } else if (direction < 0) { setControlFlags(AGENT_CONTROL_UP_NEG | AGENT_CONTROL_FAST_UP); } - gAgentCamera.resetView(); + if (!mCrouch) + { + gAgentCamera.resetView(); + } } //----------------------------------------------------------------------------- @@ -847,6 +851,11 @@ void LLAgent::movePitch(F32 mag) } } +bool LLAgent::isCrouching() const +{ + return mCrouch && !getFlying(); +} + // Does this parcel allow you to fly? bool LLAgent::canFly() { @@ -914,6 +923,7 @@ void LLAgent::setFlying(bool fly, bool fail_sound) { add(LLStatViewer::FLY, 1); } + mCrouch = false; setControlFlags(AGENT_CONTROL_FLY); } else @@ -4990,10 +5000,34 @@ static F32 g_deltaTime { 0.0f }; static S32 g_lastUpdateFrame { 0 }; static S32 g_deltaFrame { 0 }; +void LLAgent::updateGameControlMode() +{ + // Auto-derive the active mode from avatar state. Flycam takes precedence + // (it is toggled via a game-control binding), then sitting/controls-taken + // maps to Captive, otherwise the normal Avatar mode. + LLGameControl::AgentControlMode mode; + if (mUsingFlycam) + { + mode = LLGameControl::CONTROL_MODE_FLYCAM; + } + else if (isSitting()) + { + mode = LLGameControl::CONTROL_MODE_CAPTIVE; + } + else + { + mode = LLGameControl::CONTROL_MODE_AVATAR; + } + LLGameControl::setAgentControlMode(mode); +} + void LLAgent::applyExternalActionFlags(U32 outer_flags) { llassert(LLCoros::on_main_thread_main_coro()); - assert(LLGameControl::isEnabled() && LLGameControl::willControlAvatar()); + // Valid whenever game control is driving avatar or flycam: in FlyCam mode this + // only services the flycam on/off toggle (movement bits are ignored below). + assert(LLGameControl::isEnabled() + && (LLGameControl::willControlAvatar() || LLGameControl::willControlFlycam())); mExternalActionFlags = outer_flags; // HACK: AGENT_CONTROL_NUDGE_AT_NEG is used to toggle Flycam @@ -5163,25 +5197,25 @@ void LLAgent::updateFlycam() std::vector flycam_inputs; LLGameControl::getFlycamInputs(flycam_inputs); - // Note: no matter how flycam_inputs are mapped to the controller - // they arrive in the following order: - constexpr S32 FLYCAM_ADVANCE = 0; - constexpr S32 FLYCAM_PAN = 1; - constexpr S32 FLYCAM_RISE = 2; - constexpr S32 FLYCAM_PITCH = 3; - constexpr S32 FLYCAM_YAW = 4; - constexpr S32 FLYCAM_ZOOM = 5; + // The channel order is defined by LLGameControl::FlycamChannel. + + // Defensive: getFlycamInputs() should return one value per channel. + if ((S32)flycam_inputs.size() < LLGameControl::FLYCAM_NUM_CHANNELS) + { + return; + } LLVector3 linear_velocity( - flycam_inputs[FLYCAM_ADVANCE], - flycam_inputs[FLYCAM_PAN], - flycam_inputs[FLYCAM_RISE]); + flycam_inputs[LLGameControl::FLYCAM_ADVANCE], + flycam_inputs[LLGameControl::FLYCAM_PAN], + flycam_inputs[LLGameControl::FLYCAM_RISE]); constexpr F32 MAX_FLYCAM_SPEED = 10.0f; mFlycam.setLinearVelocity(MAX_FLYCAM_SPEED * linear_velocity); - mFlycam.setPitchRate(flycam_inputs[FLYCAM_PITCH]); - mFlycam.setYawRate(flycam_inputs[FLYCAM_YAW]); - mFlycam.setZoomRate(flycam_inputs[FLYCAM_ZOOM]); + mFlycam.setPitchRate(flycam_inputs[LLGameControl::FLYCAM_PITCH]); + mFlycam.setYawRate(flycam_inputs[LLGameControl::FLYCAM_YAW]); + mFlycam.setRollRate(flycam_inputs[LLGameControl::FLYCAM_ROLL]); + mFlycam.setZoomRate(flycam_inputs[LLGameControl::FLYCAM_ZOOM]); mFlycam.integrate(g_deltaTime); diff --git a/indra/newview/llagent.h b/indra/newview/llagent.h index 82545eae6a..14f81c57f1 100644 --- a/indra/newview/llagent.h +++ b/indra/newview/llagent.h @@ -350,6 +350,15 @@ private: F32 mNextFidgetTime; S32 mCurrentFidget; + //-------------------------------------------------------------------- + // Crouch + //-------------------------------------------------------------------- +public: + bool isCrouching() const; + void toggleCrouch() { mCrouch = !mCrouch; } +private: + bool mCrouch; + //-------------------------------------------------------------------- // Fly //-------------------------------------------------------------------- @@ -509,6 +518,12 @@ public: void applyExternalActionFlags(U32 flags); void updateFlycam(); + // Auto-derive the active game-control AgentControlMode from current avatar + // state (flycam active -> FlyCam, sitting -> Captive, else Avatar) and push + // it to LLGameControl. Called once per frame before game-control input is + // processed so the runtime mapping/gating tracks avatar state automatically. + void updateGameControlMode(); + void pressGameControlButton(U8 button); void releaseGameControlButton(U8 button); U32 getGameControlButtonsFromKeys() const { return mGameControlButtonsFromKeys; } diff --git a/indra/newview/llagentcamera.cpp b/indra/newview/llagentcamera.cpp index 0fe5f622e3..ef269be0be 100644 --- a/indra/newview/llagentcamera.cpp +++ b/indra/newview/llagentcamera.cpp @@ -185,7 +185,9 @@ LLAgentCamera::LLAgentCamera() : mPanLeftKey(0.f), mPanRightKey(0.f), mPanInKey(0.f), - mPanOutKey(0.f) + mPanOutKey(0.f), + mRollLeftKey(0.f), + mRollRightKey(0.f) { mFollowCam.setMaxCameraDistantFromSubject( MAX_CAMERA_DISTANCE_FROM_AGENT ); @@ -195,6 +197,7 @@ LLAgentCamera::LLAgentCamera() : resetPanDiff(); resetOrbitDiff(); + resetCameraRoll(); } // Requires gSavedSettings to be initialized. @@ -331,7 +334,8 @@ void LLAgentCamera::resetView(bool reset_camera, bool change_camera) //Camera Tool is needed for Free Camera Control Mode if (!LLFloaterCamera::inFreeCameraMode()) { - LLFloaterReg::hideInstance("build"); + if (LLFloaterReg::instanceVisible("build")) + LLFloaterReg::hideInstance("build"); // Switch back to basic toolset LLToolMgr::getInstance()->setCurrentToolset(gBasicToolset); @@ -355,6 +359,8 @@ void LLAgentCamera::resetView(bool reset_camera, bool change_camera) setFocusOnAvatar(true, ANIMATE); mCameraFOVZoomFactor = 0.f; + + resetCameraRoll(); } resetPanDiff(); resetOrbitDiff(); @@ -905,6 +911,19 @@ void LLAgentCamera::cameraOrbitOver(const F32 angle) } } +//----------------------------------------------------------------------------- +// cameraRollOver() +//----------------------------------------------------------------------------- +void LLAgentCamera::cameraRollOver(const F32 angle) +{ + mRollAngle += fmod(angle, F_TWO_PI); +} + +void LLAgentCamera::resetCameraRoll() +{ + mRollAngle = 0.f; +} + void LLAgentCamera::resetCameraOrbit() { LLVector3 camera_offset_unit(mCameraFocusOffsetTarget); @@ -918,6 +937,7 @@ void LLAgentCamera::resetCameraOrbit() cameraZoomIn(1.f); resetOrbitDiff(); + resetCameraRoll(); } void LLAgentCamera::resetOrbitDiff() @@ -1282,6 +1302,7 @@ void LLAgentCamera::updateCamera() const F32 ORBIT_OVER_RATE = 90.f * DEG_TO_RAD; // radians per second const F32 ORBIT_AROUND_RATE = 90.f * DEG_TO_RAD; // radians per second const F32 PAN_RATE = 5.f; // meters per second + const F32 ROLL_RATE = 45.f * DEG_TO_RAD; // radians per second if (gAgentCamera.getOrbitUpKey() || gAgentCamera.getOrbitDownKey()) { @@ -1323,6 +1344,12 @@ void LLAgentCamera::updateCamera() cameraPanUp(input_rate * PAN_RATE / gFPSClamped ); } + if (getRollLeftKey() || getRollRightKey()) + { + F32 input_rate = getRollRightKey() - getRollLeftKey(); + cameraRollOver(input_rate * ROLL_RATE / gFPSClamped); + } + // Clear camera keyboard keys. gAgentCamera.clearOrbitKeys(); gAgentCamera.clearPanKeys(); @@ -1552,6 +1579,7 @@ void LLAgentCamera::updateCamera() LLVector3 chest_scale = chest_joint->getScale(); // shorten avatar skeleton to avoid foot interpenetration +#if 0 // This screws up mouselook attachments if (!gAgentAvatarp->mInAir) { LLVector3 chest_offset = LLVector3(0.f, 0.f, chest_joint->getPosition().mV[VZ]) * torso_joint->getWorldRotation(); @@ -1565,9 +1593,37 @@ void LLAgentCamera::updateCamera() chest_joint->setScale(LLVector3(1.f, 1.f, scale_factor)); diff.mV[VZ] = 0.f; } +#endif + static LLCachedControl useRealisticMouselook(gSavedSettings, "AlchemyRealisticMouselook", false); + if (useRealisticMouselook) + { + auto frame_agent = gAgent.getFrameAgent(); + LLQuaternion agent_rot(frame_agent.getQuaternion()); + auto avatarp_parent = (LLViewerObject*)gAgentAvatarp->getParent(); + if(avatarp_parent) + { + auto avatarp_root = (LLViewerObject*)gAgentAvatarp->getRoot(); + if(avatarp_root) + { + auto decoupled = avatarp_root->flagCameraDecoupled(); + if(decoupled) + { + agent_rot *= avatarp_parent->getRenderRotation(); + } - // SL-315 - gAgentAvatarp->mPelvisp->setPosition(gAgentAvatarp->mPelvisp->getPosition() + diff); + } + } + + LLViewerCamera::getInstance()->updateCameraLocation(head_pos, + mCameraUpVector, + gAgentAvatarp->mHeadp->getWorldPosition() + + LLVector3(1.0, 0.0, 0.0) * agent_rot); + } + else + { + // SL-315 + gAgentAvatarp->mPelvisp->setPosition(gAgentAvatarp->mPelvisp->getPosition() + diff); + } gAgentAvatarp->mRoot->updateWorldMatrixChildren(); @@ -1594,6 +1650,18 @@ void LLAgentCamera::updateCamera() torso_joint->setScale(torso_scale); chest_joint->setScale(chest_scale); } + + // We have do this at the very end to make sure it takes all previous calculations into + // account and then applies our roll on top of it, besides it wouldn't even work otherwise. + LLQuaternion rot_quat = LLViewerCamera::instance().getQuaternion(); + LLMatrix3 rot_mat(mRollAngle, 0.f, 0.f); + rot_quat = LLQuaternion(rot_mat)*rot_quat; + + LLMatrix3 mat(rot_quat); + + LLViewerCamera::instance().mXAxis = LLVector3(mat.mMatrix[0]); + LLViewerCamera::instance().mYAxis = LLVector3(mat.mMatrix[1]); + LLViewerCamera::instance().mZAxis = LLVector3(mat.mMatrix[2]); } void LLAgentCamera::updateLastCamera() @@ -1697,7 +1765,6 @@ LLVector3d LLAgentCamera::calcFocusPositionTargetGlobal() LLVector3d LLAgentCamera::calcThirdPersonFocusOffset() { // ...offset from avatar - LLVector3d focus_offset; LLQuaternion agent_rot = gAgent.getFrameAgent().getQuaternion(); if (isAgentAvatarValid() && gAgentAvatarp->getParent()) { @@ -2124,6 +2191,22 @@ void LLAgentCamera::handleScrollWheel(S32 clicks) } else if (mFocusOnAvatar && (mCameraMode == CAMERA_MODE_THIRD_PERSON)) { + MASK mask = gKeyboard->currentMask(true); + if (mask & MASK_SHIFT) + { + LLVector3d offset = gSavedSettings.getVector3d("FocusOffsetRearView"); + offset.mdV[VZ] += 0.1f * (F32)clicks; + gSavedSettings.setVector3d("FocusOffsetRearView", offset); + return; + } + else if (mask & MASK_CONTROL) + { + LLVector3 offset = gSavedSettings.getVector3("CameraOffsetRearView"); + offset.mV[VZ] += 0.1f * (F32)clicks; + gSavedSettings.setVector3("CameraOffsetRearView", offset); + return; + } + F32 camera_offset_initial_mag = getCameraOffsetInitial().magVec(); F32 current_zoom_fraction = mTargetCameraDistance / (camera_offset_initial_mag * gSavedSettings.getF32("CameraOffsetScale")); @@ -2216,7 +2299,6 @@ void LLAgentCamera::changeCameraToMouselook(bool animate) updateLastCamera(); mCameraMode = CAMERA_MODE_MOUSELOOK; - const U32 old_flags = gAgent.getControlFlags(); gAgent.setControlFlags(AGENT_CONTROL_MOUSELOOK); if (animate) @@ -2471,6 +2553,7 @@ void LLAgentCamera::switchCameraPreset(ECameraPreset preset) resetPanDiff(); resetOrbitDiff(); + resetCameraRoll(); gSavedSettings.setU32("CameraPresetType", mCameraPreset); } @@ -2675,9 +2758,9 @@ void LLAgentCamera::setCameraPosAndFocusGlobal(const LLVector3d& camera_pos, con if (mCameraAnimating) { - const F64 ANIM_METERS_PER_SECOND = 10.0; + const F64 ANIM_METERS_PER_SECOND = 25.0; const F64 MIN_ANIM_SECONDS = 0.5; - const F64 MAX_ANIM_SECONDS = 10.0; + const F64 MAX_ANIM_SECONDS = 1.0; F64 anim_duration = llmax( MIN_ANIM_SECONDS, sqrt(focus_delta_squared) / ANIM_METERS_PER_SECOND ); anim_duration = llmin( anim_duration, MAX_ANIM_SECONDS ); setAnimationDuration( (F32)anim_duration ); @@ -2950,6 +3033,8 @@ void LLAgentCamera::clearOrbitKeys() mOrbitDownKey = 0.f; mOrbitInKey = 0.f; mOrbitOutKey = 0.f; + mRollLeftKey = 0.f; + mRollRightKey = 0.f; } void LLAgentCamera::clearPanKeys() diff --git a/indra/newview/llagentcamera.h b/indra/newview/llagentcamera.h index 4b5afb544e..ca82667426 100644 --- a/indra/newview/llagentcamera.h +++ b/indra/newview/llagentcamera.h @@ -270,8 +270,10 @@ public: void cameraOrbitAround(const F32 radians); // Rotate camera CCW radians about build focus point void cameraOrbitOver(const F32 radians); // Rotate camera forward radians over build focus point void cameraOrbitIn(const F32 meters); // Move camera in toward build focus point + void cameraRollOver(const F32 radians); // Roll the camera void resetCameraOrbit(); void resetOrbitDiff(); + void resetCameraRoll(); //-------------------------------------------------------------------- // Zoom //-------------------------------------------------------------------- @@ -364,6 +366,8 @@ public: F32 getOrbitDownKey() const { return mOrbitDownKey; } F32 getOrbitInKey() const { return mOrbitInKey; } F32 getOrbitOutKey() const { return mOrbitOutKey; } + F32 getRollLeftKey() const { return mRollLeftKey; } + F32 getRollRightKey() const { return mRollRightKey; } void setOrbitLeftKey(F32 mag) { mOrbitLeftKey = mag; } void setOrbitRightKey(F32 mag) { mOrbitRightKey = mag; } @@ -371,6 +375,8 @@ public: void setOrbitDownKey(F32 mag) { mOrbitDownKey = mag; } void setOrbitInKey(F32 mag) { mOrbitInKey = mag; } void setOrbitOutKey(F32 mag) { mOrbitOutKey = mag; } + void setRollLeftKey(F32 mag) { mRollLeftKey = mag; } + void setRollRightKey(F32 mag) { mRollRightKey = mag; } void clearOrbitKeys(); private: @@ -384,6 +390,10 @@ private: F32 mOrbitAroundRadians; F32 mOrbitOverAngle; + F32 mRollLeftKey; + F32 mRollRightKey; + F32 mRollAngle = 0.f; + //-------------------------------------------------------------------- // Pan //-------------------------------------------------------------------- @@ -416,7 +426,9 @@ private: /** Keys ** ** *******************************************************************************/ - +public: + void storeCameraPosition(); + void loadCameraPosition(); }; extern LLAgentCamera gAgentCamera; diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index bbbce1e20b..3b0f0d9a45 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -56,6 +56,7 @@ #include "llmarketplacenotifications.h" #include "llmd5.h" #include "llmeshrepository.h" +#include "llpanelpreferencegamecontrol.h" #include "llpumpio.h" #include "llmimetypes.h" #include "llslurl.h" @@ -196,6 +197,7 @@ #include "lldebugview.h" #include "llconsole.h" #include "llcontainerview.h" +#include "lltoolfocus.h" #include "lltooltip.h" #include "llsdutil.h" @@ -1142,13 +1144,29 @@ bool LLAppViewer::init() } LLGameControl::init(gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "gamecontrollerdb.txt"), - [&](const std::string& name) -> bool { return gSavedSettings.getBOOL(name); }, - [&](const std::string& name, bool value) { gSavedSettings.setBOOL(name, value); }, - [&](const std::string& name) -> std::string { return gSavedSettings.getString(name); }, - [&](const std::string& name, const std::string& value) { gSavedSettings.setString(name, value); }, - [&](const std::string& name) -> LLSD { return gSavedSettings.getLLSD(name); }, - [&](const std::string& name, const LLSD& value) { gSavedSettings.setLLSD(name, value); }, - [&]() { LLPanelPreferenceGameControl::updateDeviceList(); }); + [](const std::vector& keys) -> LLSD + { + LLSD result = LLSD::emptyMap(); + for (const std::string& key : keys) + { + if (gSavedSettings.controlExists(key)) + { + result[key] = gSavedSettings.getLLSD(key); + } + } + return result; + }, + [](const LLSD& key_values) + { + for (auto it = key_values.beginMap(); it != key_values.endMap(); ++it) + { + if (gSavedSettings.controlExists(it->first)) + { + gSavedSettings.setLLSD(it->first, it->second); + } + } + }, + []() { LLPanelPreferenceGameControl::updateDeviceList(); }); try { @@ -1259,6 +1277,7 @@ bool LLAppViewer::init() LLWorld::createInstance(); LLViewerStatsRecorder::createInstance(); LLSelectMgr::createInstance(); + LLToolCamera::createInstance(); LLViewerCamera::createInstance(); LL::GLTFSceneManager::createInstance(); @@ -1349,7 +1368,7 @@ bool LLAppViewer::frame() void sendGameControlInput() { LLMessageSystem* msg = gMessageSystem; - const LLGameControl::State& state = LLGameControl::getState(); + const LLGameControl::ServerState& state = LLGameControl::getServerState(); msg->newMessageFast(_PREHASH_GameControlInput); msg->nextBlock("AgentData"); @@ -1374,7 +1393,7 @@ void sendGameControlInput() std::vector buttons; for (U8 i = 0; i < LLGameControl::NUM_BUTTONS; i++) { - if (button_flags & (0x1 << i)) + if (button_flags & (0x1u << i)) { buttons.push_back(i); } @@ -4909,6 +4928,7 @@ void LLAppViewer::loadKeyBindings() } } LLUrlRegistry::instance().setKeybindingHandler(&gViewerInput); + LLUrlRegistry::instance().setGameControllerHandler(LLGameControl::getInstance()); } // As per GHI #4498, remove old, stale CEF cache folders from previous sessions @@ -5381,28 +5401,38 @@ void LLAppViewer::idle() gAgent.autoPilot(&yaw); } + // Auto-derive the active game-control mode (Avatar/Captive/FlyCam) from + // current avatar state so the runtime mappings and gating track it. + gAgent.updateGameControlMode(); + // get control flags from each side U32 control_flags = gAgent.getControlFlags(); - U32 game_control_action_flags = LLGameControl::computeInternalActionFlags(); // apply to GameControl LLGameControl::setExternalInput(control_flags, gAgent.getGameControlButtonsFromKeys()); - bool should_send_game_control = LLGameControl::computeFinalStateAndCheckForChanges(); if (LLPanelPreferenceGameControl::isWaitingForInputChannel()) { LLPanelPreferenceGameControl::applyGameControlInput(); - // skip this send because input is being used to set preferences - should_send_game_control = false; } - if (should_send_game_control) + else { - sendGameControlInput(); - } + // Recompute canonical controller state (also populates the internal + // state consumed below) and send it to the server when it changed. + if (LLGameControl::computeFinalStateAndCheckForChanges()) + { + sendGameControlInput(); + } - // apply to AvatarControl - if (LLGameControl::isEnabled() && LLGameControl::willControlAvatar()) - { - gAgent.applyExternalActionFlags(game_control_action_flags); + // Drive the local avatar from the controller. In Avatar/Captive + // modes this moves the avatar; in FlyCam mode applyExternalActionFlags + // ignores movement bits (it early-returns while flycam is active) but + // still services the flycam on/off toggle, so we run it either way. + // Flycam *motion* is driven separately below via gAgent.updateFlycam(). + if (LLGameControl::willControlAvatar() || LLGameControl::willControlFlycam()) + { + U32 game_control_action_flags = LLGameControl::computeInternalActionFlags(); + gAgent.applyExternalActionFlags(game_control_action_flags); + } } send_agent_update(false); diff --git a/indra/newview/llfloaterjoystick.cpp b/indra/newview/llfloaterjoystick.cpp index 177b3afe2c..00e6aee234 100644 --- a/indra/newview/llfloaterjoystick.cpp +++ b/indra/newview/llfloaterjoystick.cpp @@ -82,7 +82,7 @@ BOOL CALLBACK di8_list_devices_callback(LPCDIDEVICEINSTANCE device_instance_ptr, // We always include the device if it is from 3DConnexion, otherwise // if the GameControl feature is enabled then we perfer to route other devices to it. std::string product_name = ll_convert(std::wstring(device_instance_ptr->tszProductName)); - bool include_device = LLViewerJoystick::is3DConnexionDevice(product_name) || !LLGameControl::isEnabled(); + bool include_device = LLViewerJoystick::is3DConnexionDevice(product_name); if (include_device) { @@ -314,8 +314,7 @@ void LLFloaterJoystick::refreshListOfDevices() // We always include the device if it is from 3DConnexion, otherwise we only // support the device in this context when GameControl feature is disabled. std::string desc = LLViewerJoystick::getInstance()->getDescription(); - bool include_device = LLViewerJoystick::is3DConnexionDevice(desc) || - (!LLGameControl::isEnabled() && !desc.empty()); + bool include_device = LLViewerJoystick::is3DConnexionDevice(desc); if (include_device) { diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp index 2f51e47046..466ee2da83 100755 --- a/indra/newview/llfloaterpreference.cpp +++ b/indra/newview/llfloaterpreference.cpp @@ -64,6 +64,7 @@ #include "llnotificationsutil.h" #include "llnotificationtemplate.h" #include "llpanellogin.h" +#include "llpanelpreferencegamecontrol.h" #include "llpanelvoicedevicesettings.h" #include "llradiogroup.h" #include "llsearchcombobox.h" @@ -289,6 +290,39 @@ public: }; LLKeybindingHandler gKeybindHandler; +// TODO: move this out of llfloaterpreference.cpp +class LLGameControlBindingHandler: public LLCommandHandler +{ +public: + // requires trusted browser to trigger + LLGameControlBindingHandler(): LLCommandHandler("gamecontroller", UNTRUSTED_CLICK_ONLY) + { + } + + bool handle(const LLSD& params, const LLSD& query_map, + const std::string& grid, LLMediaCtrl* web) + { + if (params.size() < 1) return false; + + LLFloaterPreference* prefsfloater = dynamic_cast + (LLFloaterReg::showInstance("preferences")); + + if (prefsfloater) + { + // find 'controls' panel and bring it the front + LLTabContainer* tabcontainer = prefsfloater->getChild("pref core"); + LLPanel* panel = prefsfloater->getChild("game_control"); + if (tabcontainer && panel) + { + tabcontainer->selectTabPanel(panel); + } + } + + return true; + } +}; +LLGameControlBindingHandler gGameControlBindingHandler; + ////////////////////////////////////////////// // LLFloaterPreference @@ -447,7 +481,6 @@ bool LLFloaterPreference::postBuild() mDisabledPopups = getChild("disabled_popups"); mEnablePopupBtn = getChild("enable_this_popup"); mDisablePopupBtn = getChild("disable_this_popup"); - setPanelVisibility("game_control", LLGameControl::isEnabled()); gSavedSettings.getControl("ChatFontSize")->getSignal()->connect(boost::bind(&LLFloaterIMSessionTab::processChatHistoryStyleUpdate, false)); @@ -1344,7 +1377,6 @@ void LLAvatarComplexityControls::setIndirectMaxArc() void LLFloaterPreference::refresh() { - setPanelVisibility("game_control", LLGameControl::isEnabled()); LLTabContainer* tabcontainer = getChild("pref core"); for (LLView* view : *tabcontainer->getChildList()) { @@ -2835,6 +2867,22 @@ void LLPanelPreferenceControls::addControlTableSeparator() void LLPanelPreferenceControls::populateControlTable() { + // The mode combo is the source of truth for which bindings to display. + // Keep mEditingMode in sync with it and make sure that mode's bindings are + // loaded before we render; otherwise the table shows empty cells on first + // open (refresh() populates before any load has happened, and mEditingMode + // may not match the mode currently selected in the combo). + if (pKeyModeBox) + { + mEditingMode = pKeyModeBox->getValue().asInteger(); + } + if (mEditingMode >= 0 + && mEditingMode < (S32)LLKeyConflictHandler::MODE_COUNT + && mConflictHandler[mEditingMode].empty()) + { + mConflictHandler[mEditingMode].loadFromSettings((LLKeyConflictHandler::ESourceMode)mEditingMode); + } + pControlsTable->clearRows(); pControlsTable->clearColumns(); @@ -2874,10 +2922,7 @@ void LLPanelPreferenceControls::populateControlTable() addControlTableSeparator(); addControlTableRows("control_table_contents_media.xml"); addControlTableSeparator(); - if (LLGameControl::isEnabled()) - { - addControlTableRows("control_table_contents_game_control.xml"); - } + addControlTableRows("control_table_contents_game_control.xml"); } // MODE_THIRD_PERSON; MODE_EDIT_AVATAR; MODE_SITTING else if (mEditingMode < LLKeyConflictHandler::MODE_SAVED_SETTINGS) @@ -2896,10 +2941,7 @@ void LLPanelPreferenceControls::populateControlTable() addControlTableRows("control_table_contents_media.xml"); addControlTableSeparator(); - if (LLGameControl::isEnabled()) - { - addControlTableRows("control_table_contents_game_control.xml"); - } + addControlTableRows("control_table_contents_game_control.xml"); } else { @@ -3294,1126 +3336,6 @@ void LLPanelPreferenceControls::onCancelKeyBind() pControlsTable->deselectAllItems(); } -//------------------------LLPanelPreferenceGameControl-------------------------------- -// -// Preference panel for configuring game controller (gamepad) input. -// -// This panel provides two main tabs: -// 1. Channel Mappings - Maps avatar actions (move forward, jump, etc.) to controller inputs -// 2. Device Settings - Per-device configuration including: -// - Axis Options: Invert, deadzone, and offset for each axis -// - Axis Mappings: Remap physical axes to different logical axes -// - Button Mappings: Remap physical buttons to different logical buttons -// -// Settings are stored in: -// - AnalogChannelMappings, BinaryChannelMappings, FlycamChannelMappings (action mappings) -// - KnownGameControllers (per-device options keyed by device GUID) -// -// The panel supports live input capture: when a combobox is open, pressing a controller -// button or moving an axis will automatically select that input channel. -// - -namespace -{ - // Singleton instance pointer - only one game control panel exists at a time - static LLPanelPreferenceGameControl* sGameControlPanel { nullptr }; - - // Track current UI selection state for input channel assignment. - // When user clicks a cell to change its mapping, these track which cell is being edited. - static LLScrollListCtrl* sSelectedGrid { nullptr }; - static LLScrollListItem* sSelectedItem { nullptr }; - static LLScrollListCell* sSelectedCell { nullptr }; -} - -// Static entry point called when device list changes (device connected/disconnected). -// Delegates to the singleton instance if it exists. -void LLPanelPreferenceGameControl::updateDeviceList() -{ - if (sGameControlPanel) - { - sGameControlPanel->updateDeviceListInternal(); - } -} - -// Constructor - registers this instance as the singleton -LLPanelPreferenceGameControl::LLPanelPreferenceGameControl() -{ - sGameControlPanel = this; -} - -// Destructor - clears singleton pointer -LLPanelPreferenceGameControl::~LLPanelPreferenceGameControl() -{ - sGameControlPanel = nullptr; -} - -static LLPanelInjector t_pref_game_control("panel_preference_game_control"); - -// Saves current UI state to settings. -// Extracts channel mappings from the action table and converts to string format. -// Also saves per-device options (axis settings, remappings) to KnownGameControllers. -void LLPanelPreferenceGameControl::saveSettings() -{ - LLPanelPreference::saveSettings(); - - std::vector items = mActionTable->getAllData(); - - // Lambda to find the channel associated with an action by looking it up in the UI table - LLGameControl::getChannel_t getChannel = - [&](const std::string& action) -> LLGameControl::InputChannel - { - for (LLScrollListItem* item : items) - { - if (action == item->getValue() && (item->getNumColumns() >= 2)) - { - return LLGameControl::getChannelByName(item->getColumn(1)->getValue()); - } - } - return LLGameControl::InputChannel(); - }; - - if (mOrigSettings.isEmpty()) - { - rememberOriginalSettings(); - } - - // Save action->channel mappings as strings (analog=axes, binary=buttons, flycam=camera) - if (LLControlVariable* analogMappings = gSavedSettings.getControl("AnalogChannelMappings")) - { - analogMappings->set(LLGameControl::stringifyAnalogMappings(getChannel)); - mSavedValues[analogMappings] = analogMappings->getValue(); - } - - if (LLControlVariable* binaryMappings = gSavedSettings.getControl("BinaryChannelMappings")) - { - binaryMappings->set(LLGameControl::stringifyBinaryMappings(getChannel)); - mSavedValues[binaryMappings] = binaryMappings->getValue(); - } - - if (LLControlVariable* flycamMappings = gSavedSettings.getControl("FlycamChannelMappings")) - { - flycamMappings->set(LLGameControl::stringifyFlycamMappings(getChannel)); - mSavedValues[flycamMappings] = flycamMappings->getValue(); - } - - if (LLControlVariable* knownControllers = gSavedSettings.getControl("KnownGameControllers")) - { - LLSD deviceOptions(LLSD::emptyMap()); - for (auto& [guid, device] : mDeviceOptions) - { - device.settings = device.options.saveToString(device.name); - if (!device.settings.empty()) - { - deviceOptions.insert(guid, device.settings); - } - } - knownControllers->set(deviceOptions); - mSavedValues[knownControllers] = deviceOptions; - } -} - -// Handles row selection in any of the mapping tables (action, axis, button). -// If user clicked the editable channel column, shows the appropriate combobox for editing. -void LLPanelPreferenceGameControl::onGridSelect(LLUICtrl* ctrl) -{ - clearSelectionState(); - - LLScrollListCtrl* table = dynamic_cast(ctrl); - if (!table || !table->getEnabled()) - return; - - if (LLScrollListItem* item = table->getFirstSelected()) - { - // Try to show combobox for editing; if not applicable, deselect - if (initCombobox(item, table)) - return; - - table->deselectAllItems(); - } -} - -// Initializes and displays the appropriate combobox over the selected cell. -// Determines which combobox to use based on the table and action type. -// Returns true if a combobox was shown, false if the click wasn't on an editable cell. -bool LLPanelPreferenceGameControl::initCombobox(LLScrollListItem* item, LLScrollListCtrl* grid) -{ - // Only column 1 (the channel/mapping column) is editable - if (item->getSelectedCell() != 1) - return false; - - LLScrollListText* cell = dynamic_cast(item->getColumn(1)); - if (!cell) - return false; - - // Select combobox based on table type and action category - LLComboBox* combobox = nullptr; - if (grid == mActionTable) - { - std::string action = item->getValue(); - LLGameControl::ActionNameType actionNameType = LLGameControl::getActionNameType(action); - combobox = - actionNameType == LLGameControl::ACTION_NAME_ANALOG ? mAnalogChannelSelector : - actionNameType == LLGameControl::ACTION_NAME_BINARY ? mBinaryChannelSelector : - actionNameType == LLGameControl::ACTION_NAME_FLYCAM ? mAnalogChannelSelector : - nullptr; - } - else if (grid == mAxisMappings) - { - combobox = mAxisSelector; - } - else if (grid == mButtonMappings) - { - combobox = mBinaryChannelSelector; - } - if (!combobox) - return false; - - // compute new rect for combobox - S32 row_index = grid->getItemIndex(item); - fitInRect(combobox, grid, row_index, 1); - - std::string channel_name = "NONE"; - std::string cell_value = cell->getValue(); - std::vector items = combobox->getAllData(); - for (const LLScrollListItem* item : items) - { - if (item->getColumn(0)->getValue().asString() == cell_value) - { - channel_name = item->getValue().asString(); - break; - } - } - - std::string value; - LLGameControl::InputChannel channel = LLGameControl::getChannelByName(channel_name); - if (!channel.isNone()) - { - std::string channel_name = channel.getLocalName(); - std::string channel_label = getChannelLabel(channel_name, combobox->getAllData()); - if (combobox->itemExists(channel_label)) - { - value = channel_name; - } - } - if (value.empty()) - { - // Assign the last element in the dropdown list which is "NONE" - value = combobox->getAllData().back()->getValue().asString(); - } - - combobox->setValue(value); - combobox->setVisible(true); - combobox->showList(); - - sSelectedGrid = grid; - sSelectedItem = item; - sSelectedCell = cell; - - return true; -} - -// Called when user selects a channel from the dropdown combobox. -// Updates the cell display and, for device mappings, updates the internal options. -void LLPanelPreferenceGameControl::onCommitInputChannel(LLUICtrl* ctrl) -{ - if (!sSelectedGrid || !sSelectedItem || !sSelectedCell) - return; - - LLComboBox* combobox = dynamic_cast(ctrl); - llassert(combobox); - if (!combobox) - return; - - if (sSelectedGrid == mActionTable) - { - std::string value = combobox->getValue(); - std::string label = (value == "NONE") ? - LLStringUtil::null : combobox->getSelectedItemLabel(); - sSelectedCell->setValue(label); - } - else - { - S32 chosen_index = combobox->getCurrentIndex(); - if (chosen_index >= 0) - { - int row_index = sSelectedGrid->getItemIndex(sSelectedItem); - llassert(row_index >= 0); - LLGameControl::Options& deviceOptions = getSelectedDeviceOptions(); - std::vector& map = sSelectedGrid == mAxisMappings ? - deviceOptions.getAxisMap() : deviceOptions.getButtonMap(); - if (chosen_index >= map.size()) - { - chosen_index = row_index; - } - std::string label = chosen_index == row_index ? - LLStringUtil::null : combobox->getSelectedItemLabel(); - sSelectedCell->setValue(label); - map[row_index] = chosen_index; - } - } - sSelectedGrid->deselectAllItems(); - clearSelectionState(); -} - -// Returns true if a cell is currently selected and waiting for input channel assignment. -// Used to determine whether to capture live controller input. -bool LLPanelPreferenceGameControl::isWaitingForInputChannel() -{ - return sSelectedCell != nullptr; -} - -// Static method called when controller input is detected while a channel selector is open. -// Automatically assigns the detected input channel to the selected cell. -void LLPanelPreferenceGameControl::applyGameControlInput() -{ - if (!sGameControlPanel || !sSelectedGrid || !sSelectedCell) - return; - - LLComboBox* combobox; - LLGameControl::InputChannel::Type expectedType; - if (sGameControlPanel->mAnalogChannelSelector->getVisible()) - { - combobox = sGameControlPanel->mAnalogChannelSelector; - expectedType = LLGameControl::InputChannel::TYPE_AXIS; - } - else if (sGameControlPanel->mBinaryChannelSelector->getVisible()) - { - combobox = sGameControlPanel->mBinaryChannelSelector; - expectedType = LLGameControl::InputChannel::TYPE_BUTTON; - } - else - { - return; - } - - LLGameControl::InputChannel channel = LLGameControl::getActiveInputChannel(); - if (channel.mType == expectedType) - { - std::string channel_name = channel.getLocalName(); - std::string channel_label = LLPanelPreferenceGameControl::getChannelLabel(channel_name, combobox->getAllData()); - - // Before assigning this channel, remove it from any other action row in the same - // category that already holds it. Avatar movement and flycam are independent - // no-duplicate sets, so a channel may appear in one row of each set simultaneously. - // Device remapping tables allow duplicate targets, so this only applies to mActionTable. - if (!channel_label.empty() && sSelectedGrid == sGameControlPanel->mActionTable) - { - bool selected_is_flycam = - LLGameControl::getActionNameType(sSelectedItem->getValue().asString()) - == LLGameControl::ACTION_NAME_FLYCAM; - - for (LLScrollListItem* item : sGameControlPanel->mActionTable->getAllData()) - { - if (item != sSelectedItem && item->getNumColumns() >= 2) - { - bool item_is_flycam = - LLGameControl::getActionNameType(item->getValue().asString()) - == LLGameControl::ACTION_NAME_FLYCAM; - if (item_is_flycam != selected_is_flycam) - continue; - - LLScrollListCell* cell = item->getColumn(1); - if (cell && cell->getValue().asString() == channel_label) - { - cell->setValue(LLStringUtil::null); - } - } - } - } - - sSelectedCell->setValue(channel_label); - sSelectedGrid->deselectAllItems(); - sGameControlPanel->clearSelectionState(); - sGameControlPanel->saveSettings(); - LLGameControl::loadFromSettings(); - } -} - -// Handles selection in the axis options table (invert, deadzone, offset). -// Shows numeric editor for deadzone/offset columns, updates invert flag immediately. -void LLPanelPreferenceGameControl::onAxisOptionsSelect() -{ - clearSelectionState(); - - if (LLScrollListItem* row = mAxisOptions->getFirstSelected()) - { - LLGameControl::Options& options = getSelectedDeviceOptions(); - S32 row_index = mAxisOptions->getItemIndex(row); - - { - // Always sync invert checkbox - clicking the checkbox selects the row - // but doesn't automatically update the underlying option - constexpr S32 invert_checkbox_column = 1; - bool invert = row->getColumn(invert_checkbox_column)->getValue().asBoolean(); - options.getAxisOptions()[row_index].mMultiplier = invert ? -1 : 1; - } - - S32 column_index = row->getSelectedCell(); - if (column_index == 2 || column_index == 3) - { - fitInRect(mNumericValueEditor, mAxisOptions, row_index, column_index); - if (column_index == 2) - { - mNumericValueEditor->setMinValue(0); - mNumericValueEditor->setMaxValue(LLGameControl::MAX_AXIS_DEAD_ZONE); - mNumericValueEditor->setValue(options.getAxisOptions()[row_index].mDeadZone); - } - else // column_index == 3 - { - mNumericValueEditor->setMinValue(-LLGameControl::MAX_AXIS_OFFSET); - mNumericValueEditor->setMaxValue(LLGameControl::MAX_AXIS_OFFSET); - mNumericValueEditor->setValue(options.getAxisOptions()[row_index].mOffset); - } - mNumericValueEditor->setVisible(true); - } - - initCombobox(row, mAxisOptions); - - LLGameControl::setDeviceOptions(mSelectedDeviceGUID, options); - } -} - -// Called when user commits a numeric value (deadzone or offset) in the spin control. -// Validates and clamps the value, then updates both the UI and device options. -void LLPanelPreferenceGameControl::onCommitNumericValue() -{ - if (LLScrollListItem* row = mAxisOptions->getFirstSelected()) - { - LLGameControl::Options& deviceOptions = getSelectedDeviceOptions(); - S32 value = mNumericValueEditor->getValue().asInteger(); - S32 row_index = mAxisOptions->getItemIndex(row); - S32 column_index = row->getSelectedCell(); - llassert(column_index == 2 || column_index == 3); // 2=deadzone, 3=offset - if (column_index < 2 || column_index > 3) - return; - - if (column_index == 2) - { - value = std::clamp(value, 0, LLGameControl::MAX_AXIS_DEAD_ZONE); - deviceOptions.getAxisOptions()[row_index].mDeadZone = (U16)value; - } - else // column_index == 3 - { - value = std::clamp(value, -LLGameControl::MAX_AXIS_OFFSET, LLGameControl::MAX_AXIS_OFFSET); - deviceOptions.getAxisOptions()[row_index].mOffset = (S16)value; - } - setNumericLabel(row->getColumn(column_index), value); - LLGameControl::setDeviceOptions(mSelectedDeviceGUID, deviceOptions); - } -} - -// Initializes all UI controls and sets up callbacks. -// Called once when the panel is first built from XML. -bool LLPanelPreferenceGameControl::postBuild() -{ - // Main checkboxes that control how game input is used - mCheckGameControlToServer = getChild("game_control_to_server"); - mCheckGameControlToAgent = getChild("game_control_to_agent"); - mCheckAgentToGameControl = getChild("agent_to_game_control"); - - mCheckGameControlToServer->setCommitCallback([this](LLUICtrl*, const LLSD&) - { - LLGameControl::setSendToServer(mCheckGameControlToServer->getValue()); - updateActionTableState(); - }); - mCheckGameControlToAgent->setCommitCallback([this](LLUICtrl*, const LLSD&) - { - LLGameControl::setControlAgent(mCheckGameControlToAgent->getValue()); - updateActionTableState(); - }); - mCheckAgentToGameControl->setCommitCallback([this](LLUICtrl*, const LLSD&) - { - LLGameControl::setTranslateAgentActions(mCheckAgentToGameControl->getValue()); - updateActionTableState(); - }); - - getChild("game_control_tabs")->setCommitCallback([this](LLUICtrl*, const LLSD&) { clearSelectionState(); }); - getChild("device_settings_tabs")->setCommitCallback([this](LLUICtrl*, const LLSD&) { clearSelectionState(); }); - - // 1st tab "Channel mappings" - mTabChannelMappings = getChild("tab_channel_mappings"); - mActionTable = getChild("action_table"); - mActionTable->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onGridSelect(ctrl); }); - - // 2nd tab "Device settings" - mTabDeviceSettings = getChild("tab_device_settings"); - mNoDeviceMessage = getChild("nodevice_message"); - mDevicePrompt = getChild("device_prompt"); - mSingleDevice = getChild("single_device"); - mDeviceList = getChild("device_list"); - mCheckShowAllDevices = getChild("show_all_known_devices"); - mPanelDeviceSettings = getChild("device_settings"); - - mCheckShowAllDevices->setCommitCallback([this](LLUICtrl*, const LLSD&) { populateDeviceTitle(); }); - mDeviceList->setCommitCallback([this](LLUICtrl*, const LLSD& value) { populateDeviceSettings(value); }); - - // Device settings sub-tabs: Axis Options, Axis Mappings, Button Mappings - mTabAxisOptions = getChild("tab_axis_options"); - mAxisOptions = getChild("axis_options"); - mAxisOptions->setCommitCallback([this](LLUICtrl*, const LLSD&) { onAxisOptionsSelect(); }); - - mTabAxisMappings = getChild("tab_axis_mappings"); - mAxisMappings = getChild("axis_mappings"); - mAxisMappings->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onGridSelect(ctrl); }); - - mTabButtonMappings = getChild("tab_button_mappings"); - mButtonMappings = getChild("button_mappings"); - mButtonMappings->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onGridSelect(ctrl); }); - - mResetToDefaults = getChild("reset_to_defaults"); - mResetToDefaults->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onResetToDefaults(); }); - - // Spin control for editing deadzone/offset values inline - mNumericValueEditor = getChild("numeric_value_editor"); - mNumericValueEditor->setCommitCallback([this](LLUICtrl*, const LLSD&) { onCommitNumericValue(); }); - - // Dropdown selectors shown inline when editing channel assignments. - // These are positioned over table cells dynamically by fitInRect(). - mAnalogChannelSelector = getChild("analog_channel_selector"); // For axis actions - mAnalogChannelSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); }); - - mBinaryChannelSelector = getChild("binary_channel_selector"); // For button actions - mBinaryChannelSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); }); - - mAxisSelector = getChild("axis_selector"); // For axis remapping - mAxisSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); }); - - // Populate action table with rows from XML config files - populateActionTableRows("game_control_table_rows.xml"); // Avatar movement actions - addActionTableSeparator(); - populateActionTableRows("game_control_table_camera_rows.xml"); // Flycam movement actions - - // Populate device settings tables with empty rows - populateOptionsTableRows(); - populateMappingTableRows(mAxisMappings, mAxisSelector, LLGameControl::NUM_AXES); - populateMappingTableRows(mButtonMappings, mBinaryChannelSelector, LLGameControl::NUM_BUTTONS); - - // Workaround for the common bug: - // LLScrollListCtrl with draw_heading="true" initially has incorrect mTop (17 px higher) - LLRect rect = mAxisOptions->getRect(); - rect.mTop = mAxisOptions->getParent()->getRect().getHeight() - 1; - mAxisOptions->setRect(rect); - mAxisOptions->updateLayout(); - - return true; -} - -// Called when the preferences floater is opened. -// Loads current LLGameControl state into UI controls and refreshes all tables. -void LLPanelPreferenceGameControl::onOpen(const LLSD& key) -{ - // Sync checkboxes with current LLGameControl state - mCheckGameControlToServer->setValue(LLGameControl::getSendToServer()); - mCheckGameControlToAgent->setValue(LLGameControl::getControlAgent()); - mCheckAgentToGameControl->setValue(LLGameControl::getTranslateAgentActions()); - - clearSelectionState(); - - // Refresh action mappings table with current channel assignments - populateActionTableCells(); - updateActionTableState(); - - // Refresh device list and settings - updateDeviceListInternal(); - updateEnable(); - - // Clear original settings - will be populated on first saveSettings() call - mOrigSettings = LLSD::emptyMap(); -} - -// Called when user clicks OK. Clears selections and prepares for onClose() -void LLPanelPreferenceGameControl::apply() -{ - LLPanelPreference::apply(); - clearSelectionState(); - - // Clear mOrigSettings to prevent cancel() from reverting changes. - // Note: cancel() is called in onClose() even after OK, so we must clear this. - mOrigSettings = LLSD::emptyMap(); - - // Note: any settings changes have already been written to global-settings - // via the LLControlVariables assigned to the UI elements. LLGameControl has - // already loaded from global-settings in saveSettings(). -} - -// Called when user clicks Cancel. Restores all settings to their original values. -void LLPanelPreferenceGameControl::cancel(const std::vector settings_to_skip) -{ - LLPanelPreference::cancel(settings_to_skip); - clearSelectionState(); - - // Skip restore if original settings were cleared in apply(). - if (mOrigSettings.isEmpty()) - { - return; - } - - // Restore original settings to the control variables - if (LLControlVariable* analogMappings = gSavedSettings.getControl("AnalogChannelMappings")) - { - analogMappings->set(mOrigSettings["AnalogChannelMappings"]); - mSavedValues[analogMappings] = analogMappings->getValue(); - } - - if (LLControlVariable* binaryMappings = gSavedSettings.getControl("BinaryChannelMappings")) - { - binaryMappings->set(mOrigSettings["BinaryChannelMappings"]); - mSavedValues[binaryMappings] = binaryMappings->getValue(); - } - - if (LLControlVariable* flycamMappings = gSavedSettings.getControl("FlycamChannelMappings")) - { - flycamMappings->set(mOrigSettings["FlycamChannelMappings"]); - mSavedValues[flycamMappings] = flycamMappings->getValue(); - } - - if (LLControlVariable* knownControllers = gSavedSettings.getControl("KnownGameControllers")) - { - LLSD deviceOptions = mOrigSettings["KnownGameControllers"]; - knownControllers->set(deviceOptions); - mSavedValues[knownControllers] = deviceOptions; - } - - // load from global settings after values have been restored - LLGameControl::loadFromSettings(); -} - -// Rebuilds the internal device options map from LLGameControl state. -// Merges saved options with currently connected devices. -void LLPanelPreferenceGameControl::updateDeviceListInternal() -{ - mDeviceOptions.clear(); - - // Load saved device options from settings - for (const auto& [guid, options] : LLGameControl::getDeviceOptions()) - { - DeviceOptions deviceOptions = { LLStringUtil::null, options, LLGameControl::Options() }; - deviceOptions.options.loadFromString(deviceOptions.name, deviceOptions.settings); - mDeviceOptions.emplace(guid, deviceOptions); - } - - // Add currently connected devices that don't have saved settings yet - for (const auto& device : LLGameControl::getDevices()) - { - if (mDeviceOptions.find(device.getGUID()) == mDeviceOptions.end()) - { - mDeviceOptions[device.getGUID()] = { device.getName(), device.saveOptionsToString(true), device.getOptions() }; - } - } - - mCheckShowAllDevices->setValue(false); - populateDeviceTitle(); -} - -// Loads action definitions from an XML file and adds them as rows to the action table. -// Each row gets a second column for the channel assignment (populated later). -void LLPanelPreferenceGameControl::populateActionTableRows(const std::string& filename) -{ - LLScrollListCtrl::Contents contents; - if (!parseXmlFile(contents, filename, "rows")) - return; - - // Setup params for the channel column (column 1) - LLScrollListCell::Params second_cell_params; - second_cell_params.font = LLFontGL::getFontSansSerif(); - second_cell_params.font_halign = LLFontGL::LEFT; - second_cell_params.column = mActionTable->getColumn(1)->mName; - second_cell_params.value = ""; // Actual value is assigned in populateActionTableCells - - for (const LLScrollListItem::Params& row_params : contents.rows) - { - std::string name = row_params.value.getValue().asString(); - if (!name.empty() && name != "menu_separator") - { - LLScrollListItem::Params new_params(row_params); - new_params.enabled.setValue(true); - // item_params should already have one column that was defined - // in XUI config file, and now we want to add one more - if (new_params.columns.size() == 1) - { - new_params.columns.add(second_cell_params); - } - mActionTable->addRow(new_params, EAddPosition::ADD_BOTTOM); - } - else - { - mActionTable->addRow(row_params, EAddPosition::ADD_BOTTOM); - } - } -} - -// Fills in the channel column for each action row based on current LLGameControl mappings. -void LLPanelPreferenceGameControl::populateActionTableCells() -{ - std::vector rows = mActionTable->getAllData(); - std::vector axes = mAnalogChannelSelector->getAllData(); - std::vector btns = mBinaryChannelSelector->getAllData(); - - for (LLScrollListItem* row : rows) - { - if (row->getNumColumns() >= 2) // Skip title and separator rows - { - std::string name = row->getValue().asString(); - if (!name.empty() && name != "menu_separator") - { - LLGameControl::InputChannel channel = LLGameControl::getChannelByAction(name); - std::string channel_name = channel.getLocalName(); - std::string channel_label = - channel.isAxis() ? getChannelLabel(channel_name, axes) : - channel.isButton() ? getChannelLabel(channel_name, btns) : - LLStringUtil::null; - row->getColumn(1)->setValue(channel_label); - } - } - } -} - -// Parses an XML file containing scroll list row definitions. -// Used to load action table rows from external configuration files. -bool LLPanelPreferenceGameControl::parseXmlFile(LLScrollListCtrl::Contents& contents, - const std::string& filename, const std::string& what) -{ - LLXMLNodePtr xmlNode; - if (!LLUICtrlFactory::getLayeredXMLNode(filename, xmlNode)) - { - LL_WARNS("Preferences") << "Failed to populate " << what << " from '" << filename << "'" << LL_ENDL; - return false; - } - - LLXUIParser parser; - parser.readXUI(xmlNode, contents, filename); - if (!contents.validateBlock()) - { - LL_WARNS("Preferences") << "Failed to parse " << what << " from '" << filename << "'" << LL_ENDL; - return false; - } - - return true; -} - -// Updates the device selection UI based on available devices. -// Shows single device name, dropdown list, or "no device" message as appropriate. -void LLPanelPreferenceGameControl::populateDeviceTitle() -{ - mSelectedDeviceGUID.clear(); - - // "Show all devices" includes saved settings for disconnected devices - bool showAllDevices = mCheckShowAllDevices->getValue().asBoolean(); - std::size_t deviceCount = showAllDevices ? mDeviceOptions.size() : LLGameControl::getDevices().size(); - - mNoDeviceMessage->setVisible(!deviceCount); - mDevicePrompt->setVisible(deviceCount); - mSingleDevice->setVisible(deviceCount == 1); - mDeviceList->setVisible(deviceCount > 1); - mPanelDeviceSettings->setVisible(deviceCount); - - auto makeTitle = [](const std::string& guid, const std::string& name) -> std::string - { - return guid + ", " + name; - }; - - if (deviceCount == 1) - { - if (showAllDevices) - { - const auto& [guid, device] = *mDeviceOptions.begin(); - mSingleDevice->setValue(makeTitle(guid, device.name)); - populateDeviceSettings(guid); - } - else - { - const LLGameControl::Device& device = LLGameControl::getDevices().front(); - mSingleDevice->setValue(makeTitle(device.getGUID(), device.getName())); - populateDeviceSettings(device.getGUID()); - } - } - else if (deviceCount) - { - mDeviceList->clear(); - mDeviceList->clearRows(); - - auto makeListItem = [](const std::string& guid, const std::string& title) - { - return LLSD().with("value", guid).with("columns", LLSD().with("label", title)); - }; - - if (showAllDevices) - { - for (const auto& [guid, device] : mDeviceOptions) - { - mDeviceList->addElement(makeListItem(guid, makeTitle(guid, device.name))); - } - } - else - { - for (const LLGameControl::Device& device : LLGameControl::getDevices()) - { - mDeviceList->addElement(makeListItem(device.getGUID(), makeTitle(device.getGUID(), device.getName()))); - } - } - - mDeviceList->selectNthItem(0); - populateDeviceSettings(mDeviceList->getValue()); - } -} - -// Loads settings for the specified device into the device settings sub-tabs. -void LLPanelPreferenceGameControl::populateDeviceSettings(const std::string& guid) -{ - mSelectedDeviceGUID = guid; - auto options_it = mDeviceOptions.find(guid); - llassert_always(options_it != mDeviceOptions.end()); - const DeviceOptions& deviceOptions = options_it->second; - - // Populate all three device settings sub-tabs - populateOptionsTableCells(); - populateMappingTableCells(mAxisMappings, deviceOptions.options.getAxisMap(), mAxisSelector); - populateMappingTableCells(mButtonMappings, deviceOptions.options.getButtonMap(), mBinaryChannelSelector); -} - -// Creates empty rows in the axis options table (one per axis). -// Columns: axis name, invert checkbox, deadzone, offset -void LLPanelPreferenceGameControl::populateOptionsTableRows() -{ - mAxisOptions->clearRows(); - - std::vector items = mAnalogChannelSelector->getAllData(); - - LLScrollListItem::Params row_params; - LLScrollListCell::Params cell_params; - cell_params.font = LLFontGL::getFontMonospace(); - for (size_t i = 0; i < mAxisOptions->getNumColumns(); ++i) - { - row_params.columns.add(cell_params); - } - - // Configure column types and alignment - row_params.columns(1).type = "checkbox"; // Invert - row_params.columns(2).font_halign = "right"; // Deadzone - row_params.columns(3).font_halign = "right"; // Offset - - for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i) - { - LLScrollListItem* row = mAxisOptions->addRow(row_params); - row->getColumn(0)->setValue(items[i]->getColumn(0)->getValue()); - } -} - -// Fills axis options table cells with current device settings. -void LLPanelPreferenceGameControl::populateOptionsTableCells() -{ - std::vector rows = mAxisOptions->getAllData(); - const auto& all_axis_options = getSelectedDeviceOptions().getAxisOptions(); - llassert(rows.size() == all_axis_options.size()); - - for (size_t i = 0; i < rows.size(); ++i) - { - LLScrollListItem* row = rows[i]; - const LLGameControl::Options::AxisOptions& axis_options = all_axis_options[i]; - row->getColumn(1)->setValue(axis_options.mMultiplier == -1); // Invert checkbox - setNumericLabel(row->getColumn(2), axis_options.mDeadZone); - setNumericLabel(row->getColumn(3), axis_options.mOffset); - } -} - -// Creates empty rows in a mapping table (axis or button remapping). -// Column 0 shows the physical input name, column 1 will show the mapped target. -void LLPanelPreferenceGameControl::populateMappingTableRows(LLScrollListCtrl* target, - const LLComboBox* source, size_t row_count) -{ - target->clearRows(); - - // Use the channel selector combobox as source for input names - std::vector items = source->getAllData(); - - LLScrollListItem::Params row_params; - LLScrollListCell::Params cell_params; - cell_params.font = LLFontGL::getFontMonospace(); - for (size_t i = 0; i < target->getNumColumns(); ++i) - { - row_params.columns.add(cell_params); - } - - for (size_t i = 0; i < row_count; ++i) - { - LLScrollListItem* row = target->addRow(row_params); - row->getColumn(0)->setValue(items[i]->getColumn(0)->getValue()); - } -} - -// Fills mapping table cells with current remapping settings. -// Only shows a value if the input is remapped to something different. -void LLPanelPreferenceGameControl::populateMappingTableCells(LLScrollListCtrl* target, - const std::vector& mappings, const LLComboBox* source) -{ - std::vector rows = target->getAllData(); - std::vector items = source->getAllData(); - llassert(rows.size() == mappings.size()); - - for (size_t i = 0; i < rows.size(); ++i) - { - U8 mapping = mappings[i]; - llassert(mapping < items.size()); - // Show empty for default (identity) mapping, otherwise show target name - rows[i]->getColumn(1)->setValue(mapping == i ? LLSD() : - items[mapping]->getColumn(0)->getValue()); - } -} - -// Returns the options struct for the currently selected device. -LLGameControl::Options& LLPanelPreferenceGameControl::getSelectedDeviceOptions() -{ - auto options_it = mDeviceOptions.find(mSelectedDeviceGUID); - llassert_always(options_it != mDeviceOptions.end()); - return options_it->second.options; -} - -// Looks up the display label for a channel name by searching the combobox items. -// Returns empty string if not found or if channel is "NONE". -std::string LLPanelPreferenceGameControl::getChannelLabel(const std::string& channel_name, - const std::vector& items) -{ - if (!channel_name.empty() && channel_name != "NONE") - { - for (LLScrollListItem* item : items) - { - if (item->getValue().asString() == channel_name) - { - if (item->getNumColumns()) - { - return item->getColumn(0)->getValue().asString(); - } - break; - } - } - } - return LLStringUtil::null; -} - -// Formats a numeric value for display in a table cell. -// Shows empty string for zero (default) values. -void LLPanelPreferenceGameControl::setNumericLabel(LLScrollListCell* cell, S32 value) -{ - cell->setValue(value ? llformat("%d ", value) : LLStringUtil::null); -} - -// Positions a UI control (combobox or spin control) to overlay a specific table cell. -// Used to show editors inline within the scroll list tables. -void LLPanelPreferenceGameControl::fitInRect(LLUICtrl* ctrl, LLScrollListCtrl* grid, S32 row_index, S32 col_index) -{ - LLRect rect(grid->getCellRect(row_index, col_index)); - LLView* parent = grid->getParent(); - while (parent && parent != ctrl->getParent()) - { - rect.translate(parent->getRect().mLeft, parent->getRect().mBottom); - parent = parent->getParent(); - } - - ctrl->setRect(rect); - rect.translate(-rect.mLeft, -rect.mBottom); - for (LLView* child : *ctrl->getChildList()) - { - LLRect childRect(child->getRect()); - childRect.intersectWith(rect); - if (childRect.mRight < rect.mRight && - childRect.mRight > (rect.mLeft + rect.mRight) / 2) - { - childRect.mRight = rect.mRight; - } - child->setRect(childRect); - } -} - -// Clears the current cell selection state and hides all popup editors. -void LLPanelPreferenceGameControl::clearSelectionState() -{ - sSelectedGrid = nullptr; - sSelectedItem = nullptr; - sSelectedCell = nullptr; - mNumericValueEditor->setVisible(false); - mAnalogChannelSelector->setVisible(false); - mBinaryChannelSelector->setVisible(false); - mAxisSelector->setVisible(false); -} - -// Adds a visual separator row to the action table. -void LLPanelPreferenceGameControl::addActionTableSeparator() -{ - LLScrollListItem::Params separator_params; - separator_params.enabled(false); - LLScrollListCell::Params column_params; - column_params.type = "icon"; - column_params.value = "menu_separator"; - column_params.column = "action"; - column_params.color = LLColor4(0.f, 0.f, 0.f, 0.7f); - column_params.font_halign = LLFontGL::HCENTER; - separator_params.columns.add(column_params); - mActionTable->addRow(separator_params, EAddPosition::ADD_BOTTOM); -} - -// Enables or disables UI controls based on whether game control is enabled globally. -void LLPanelPreferenceGameControl::updateEnable() -{ - bool enabled = LLGameControl::isEnabled(); - LLGameControl::setEnabled(enabled); - - mCheckGameControlToServer->setEnabled(enabled); - mCheckGameControlToAgent->setEnabled(enabled); - mCheckAgentToGameControl->setEnabled(enabled); - - mActionTable->setEnabled(enabled); - mAxisOptions->setEnabled(enabled); - mAxisMappings->setEnabled(enabled); - mButtonMappings->setEnabled(enabled); - mDeviceList->setEnabled(enabled); - - if (!enabled) - { - //mActionTable->deselectAllItems(); - mAnalogChannelSelector->setVisible(false); - mBinaryChannelSelector->setVisible(false); - clearSelectionState(); - } -} - -// Updates action table enabled state based on checkbox settings. -// Table is only editable if game control affects agent movement. -void LLPanelPreferenceGameControl::updateActionTableState() -{ - bool enable_table = LLGameControl::isEnabled() && - (mCheckGameControlToAgent->get() || mCheckAgentToGameControl->get()); - - mActionTable->deselectAllItems(); - mActionTable->setEnabled(enable_table); - mAnalogChannelSelector->setVisible(false); - mBinaryChannelSelector->setVisible(false); -} - -// Handles "Reset to Defaults" button click. -// Resets the appropriate settings based on which tab/sub-tab is currently visible. -void LLPanelPreferenceGameControl::onResetToDefaults() -{ - clearSelectionState(); - if (mTabChannelMappings->getVisible()) - { - resetChannelMappingsToDefaults(); - } - else if (mTabDeviceSettings->getVisible() && !mSelectedDeviceGUID.empty()) - { - if (mTabAxisOptions->getVisible()) - { - resetAxisOptionsToDefaults(); - } - else if (mTabAxisMappings->getVisible()) - { - resetAxisMappingsToDefaults(); - } - else if (mTabButtonMappings->getVisible()) - { - resetButtonMappingsToDefaults(); - } - } -} - -// Resets action-to-channel mappings to built-in defaults. -void LLPanelPreferenceGameControl::resetChannelMappingsToDefaults() -{ - std::vector> mappings; - LLGameControl::getDefaultMappings(mappings); - std::vector rows = mActionTable->getAllData(); - std::vector axes = mAnalogChannelSelector->getAllData(); - std::vector btns = mBinaryChannelSelector->getAllData(); - for (LLScrollListItem* row : rows) - { - if (row->getNumColumns() >= 2) // Skip separators - { - std::string action_name = row->getValue().asString(); - if (!action_name.empty() && action_name != "menu_separator") - { - std::string channel_label; - for (const auto& mapping : mappings) - { - if (mapping.first == action_name) - { - std::string channel_name = mapping.second.getLocalName(); - channel_label = - mapping.second.isAxis() ? getChannelLabel(channel_name, axes) : - mapping.second.isButton() ? getChannelLabel(channel_name, btns) : - LLStringUtil::null; - break; - } - } - row->getColumn(1)->setValue(channel_label); - } - } - } -} - -// Resets axis options (invert, deadzone, offset) to defaults for selected device. -void LLPanelPreferenceGameControl::resetAxisOptionsToDefaults() -{ - std::vector rows = mAxisOptions->getAllData(); - llassert(rows.size() == LLGameControl::NUM_AXES); - LLGameControl::Options& options = getSelectedDeviceOptions(); - llassert(options.getAxisOptions().size() == LLGameControl::NUM_AXES); - for (U8 i = 0; i < LLGameControl::NUM_AXES; ++i) - { - rows[i]->getColumn(1)->setValue(false); // Invert = false - rows[i]->getColumn(2)->setValue(LLStringUtil::null); // Deadzone = 0 - rows[i]->getColumn(3)->setValue(LLStringUtil::null); // Offset = 0 - options.getAxisOptions()[i].resetToDefaults(); - } -} - -// Resets axis remapping to identity (each axis maps to itself) for selected device. -void LLPanelPreferenceGameControl::resetAxisMappingsToDefaults() -{ - std::vector rows = mAxisMappings->getAllData(); - llassert(rows.size() == LLGameControl::NUM_AXES); - LLGameControl::Options& options = getSelectedDeviceOptions(); - llassert(options.getAxisMap().size() == LLGameControl::NUM_AXES); - for (U8 i = 0; i < LLGameControl::NUM_AXES; ++i) - { - rows[i]->getColumn(1)->setValue(LLStringUtil::null); - options.getAxisMap()[i] = i; // Identity mapping - } -} - -// Resets button remapping to identity (each button maps to itself) for selected device. -void LLPanelPreferenceGameControl::resetButtonMappingsToDefaults() -{ - std::vector rows = mButtonMappings->getAllData(); - llassert(rows.size() == LLGameControl::NUM_BUTTONS); - LLGameControl::Options& options = getSelectedDeviceOptions(); - llassert(options.getButtonMap().size() == LLGameControl::NUM_BUTTONS); - for (U8 i = 0; i < LLGameControl::NUM_BUTTONS; ++i) - { - rows[i]->getColumn(1)->setValue(LLStringUtil::null); - options.getButtonMap()[i] = i; // Identity mapping - } -} - -// Captures current settings values into mOrigSettings for later restoration upon cancel(). -void LLPanelPreferenceGameControl::rememberOriginalSettings() -{ - if (LLControlVariable* analogMappings = gSavedSettings.getControl("AnalogChannelMappings")) - { - mOrigSettings.insert("AnalogChannelMappings", gSavedSettings.getString("AnalogChannelMappings")); - } - if (LLControlVariable* binaryMappings = gSavedSettings.getControl("BinaryChannelMappings")) - { - mOrigSettings.insert("BinaryChannelMappings", gSavedSettings.getString("BinaryChannelMappings")); - } - if (LLControlVariable* flycamMappings = gSavedSettings.getControl("FlycamChannelMappings")) - { - mOrigSettings.insert("FlycamChannelMappings", gSavedSettings.getString("FlycamChannelMappings")); - } - if (LLControlVariable* knownControllers = gSavedSettings.getControl("KnownGameControllers")) - { - mOrigSettings.insert("KnownGameControllers", gSavedSettings.getLLSD("KnownGameControllers")); - } -} - //------------------------LLFloaterPreferenceProxy-------------------------------- LLFloaterPreferenceProxy::LLFloaterPreferenceProxy(const LLSD& key) diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h index 0ee0daacb4..4a967bc7d8 100644 --- a/indra/newview/llfloaterpreference.h +++ b/indra/newview/llfloaterpreference.h @@ -55,7 +55,6 @@ class LLScrollListCell; class LLSliderCtrl; class LLSD; class LLTextBox; -class LLPanelPreferenceGameControl; namespace ll { @@ -385,143 +384,6 @@ private: S32 mEditingMode; }; -// Preference panel for configuring game controller (gamepad) input. -// -// Provides two main tabs: -// 1. Channel Mappings - Maps viewer actions to controller inputs (axes/buttons) -// 2. Device Settings - Per-device configuration (axis options, remapping) -// -// Supports live input capture: pressing a controller input while editing -// automatically assigns that input to the selected action. -// -class LLPanelPreferenceGameControl : public LLPanelPreference -{ - LOG_CLASS(LLPanelPreferenceGameControl); -public: - - enum InputType - { - TYPE_AXIS, - TYPE_BUTTON, - TYPE_NONE - }; - - // Called when a device is connected/disconnected - static void updateDeviceList(); - - LLPanelPreferenceGameControl(); - ~LLPanelPreferenceGameControl(); - - void onOpen(const LLSD& key) override; - - void apply() override; - void cancel(const std::vector settings_to_skip = {}) override; - void saveSettings() override; - - void updateDeviceListInternal(); // Refresh device list and settings - - // UI event handlers - void onGridSelect(LLUICtrl* ctrl); // Handle table row selection - void onCommitInputChannel(LLUICtrl* ctrl); // Handle channel combobox selection - void onAxisOptionsSelect(); // Handle axis options table selection - void onCommitNumericValue(); // Handle deadzone/offset value changes - - // Live input capture support - static bool isWaitingForInputChannel(); // True if a cell is waiting for input - static void applyGameControlInput(); // Assign detected input to selected cell - -protected: - bool postBuild() override; - - // Action table population (Tab 1: Channel Mappings) - void populateActionTableRows(const std::string& filename); // Load action rows from XML - void populateActionTableCells(); // Fill channel column with current mappings - static bool parseXmlFile(LLScrollListCtrl::Contents& contents, - const std::string& filename, const std::string& what); - - // Device settings population (Tab 2: Device Settings) - void populateDeviceTitle(); // Update device selector UI - void populateDeviceSettings(const std::string& guid); // Load settings for device - void populateOptionsTableRows(); // Create axis options rows - void populateOptionsTableCells(); // Fill axis options with current values - void populateMappingTableRows(LLScrollListCtrl* target, - const LLComboBox* source, size_t row_count); // Create mapping table rows - void populateMappingTableCells(LLScrollListCtrl* target, - const std::vector& mappings, const LLComboBox* source); // Fill mapping cells - LLGameControl::Options& getSelectedDeviceOptions(); // Get options for selected device - - // Utility methods - static std::string getChannelLabel(const std::string& channelName, - const std::vector& items); // Look up display label - static void setNumericLabel(LLScrollListCell* cell, S32 value); // Format numeric cell - static void fitInRect(LLUICtrl* ctrl, LLScrollListCtrl* grid, S32 row_index, S32 col_index); // Position editor over cell - -private: - // Inline editing support - bool initCombobox(LLScrollListItem* item, LLScrollListCtrl* grid); // Show combobox over cell - void clearSelectionState(); // Hide editors and clear selection tracking - - // UI helpers - void addActionTableSeparator(); // Add visual separator to action table - void updateEnable(); // Enable/disable UI based on global setting - void updateActionTableState(); // Update action table enabled state - - // Reset to defaults handlers - void onResetToDefaults(); // Route to appropriate reset method - void resetChannelMappingsToDefaults(); // Reset action->channel mappings - void resetAxisOptionsToDefaults(); // Reset axis invert/deadzone/offset - void resetAxisMappingsToDefaults(); // Reset axis remapping to identity - void resetButtonMappingsToDefaults(); // Reset button remapping to identity - - void rememberOriginalSettings(); // Capture settings for cancel restoration - - // Main checkboxes controlling how game input is used - LLCheckBoxCtrl* mCheckGameControlToServer { nullptr }; // Send game_control data to server - LLCheckBoxCtrl* mCheckGameControlToAgent { nullptr }; // Use game_control data to move avatar - LLCheckBoxCtrl* mCheckAgentToGameControl { nullptr }; // Translate avatar actions to game_control - - // Tab 1: Channel Mappings - maps actions to controller inputs - LLPanel* mTabChannelMappings { nullptr }; - LLScrollListCtrl* mActionTable { nullptr }; // Action name | Channel assignment - - // Tab 2: Device Settings - per-device configuration - LLPanel* mTabDeviceSettings { nullptr }; - LLTextBox* mNoDeviceMessage { nullptr }; // Shown when no devices connected - LLTextBox* mDevicePrompt { nullptr }; // "Select device:" label - LLTextBox* mSingleDevice { nullptr }; // Shows single device name - LLComboBox* mDeviceList { nullptr }; // Dropdown for multiple devices - LLCheckBoxCtrl* mCheckShowAllDevices { nullptr }; // Include disconnected devices - LLPanel* mPanelDeviceSettings { nullptr }; - - // Device settings sub-tabs - LLPanel* mTabAxisOptions { nullptr }; - LLScrollListCtrl* mAxisOptions { nullptr }; // Axis | Invert | Deadzone | Offset - LLPanel* mTabAxisMappings { nullptr }; - LLScrollListCtrl* mAxisMappings { nullptr }; // Physical axis | Mapped to - LLPanel* mTabButtonMappings { nullptr }; - LLScrollListCtrl* mButtonMappings { nullptr }; // Physical button | Mapped to - - LLButton* mResetToDefaults { nullptr }; - - // Inline editors - positioned over table cells when editing - LLSpinCtrl* mNumericValueEditor { nullptr }; // For deadzone/offset values - LLComboBox* mAnalogChannelSelector { nullptr }; // For axis channel selection - LLComboBox* mBinaryChannelSelector { nullptr }; // For button channel selection - LLComboBox* mAxisSelector { nullptr }; // For axis remapping - - // Per-device options storage - struct DeviceOptions - { - std::string name; // Device display name - std::string settings; // Serialized settings string - LLGameControl::Options options; // Parsed options (axis settings, mappings) - }; - std::map mDeviceOptions; // Keyed by device GUID - std::string mSelectedDeviceGUID; // Currently selected device - - LLSD mOrigSettings; // Captured settings for cancel restoration -}; - class LLAvatarComplexityControls { public: diff --git a/indra/newview/llflycam.cpp b/indra/newview/llflycam.cpp index 35a89f79b6..155a703052 100644 --- a/indra/newview/llflycam.cpp +++ b/indra/newview/llflycam.cpp @@ -73,6 +73,14 @@ void LLFlycam::setYawRate(F32 yaw_rate) } +void LLFlycam::setRollRate(F32 roll_rate) +{ + // Note: this math expects roll_rate to be in range [-1.0, 1.0] + constexpr F32 ROLL_RATE_FACTOR = 0.90f; + mRollRate = roll_rate * ROLL_RATE_FACTOR; +} + + void LLFlycam::setZoomRate(F32 zoom_rate) { // Note: this math expects zoom_rate to be in range [-1.0, 1.0] @@ -103,6 +111,17 @@ void LLFlycam::integrate(F32 delta_time) needs_renormalization = true; } + // Roll about the camera's forward (local X) axis. Like pitch this is a + // body-frame rotation, so it pre-multiplies mRotation. + angle = delta_time * mRollRate * (mView / DEFAULT_FIELD_OF_VIEW); + if (fabsf(angle) > 0.0f) + { + LLQuaternion dQ; + dQ.setAngleAxis(angle, 1.0f, 0.0f, 0.0f); + mRotation = dQ * mRotation; + needs_renormalization = true; + } + angle = delta_time * mYawRate * (mView / DEFAULT_FIELD_OF_VIEW); if (fabsf(angle) > 0.0f) { diff --git a/indra/newview/llflycam.h b/indra/newview/llflycam.h index 27a7167786..193fa08991 100644 --- a/indra/newview/llflycam.h +++ b/indra/newview/llflycam.h @@ -45,6 +45,7 @@ public: void setLinearVelocity(const LLVector3& velocity); void setPitchRate(F32 pitch_rate); void setYawRate(F32 yaw_rate); + void setRollRate(F32 roll_rate); void setZoomRate(F32 zoom_rate); void integrate(F32 delta_time); @@ -55,6 +56,7 @@ protected: LLQuaternion mRotation; F32 mPitchRate { 0.0f }; F32 mYawRate { 0.0f }; + F32 mRollRate { 0.0f }; F32 mZoomRate { 0.0f }; F32 mView { DEFAULT_FIELD_OF_VIEW }; }; diff --git a/indra/newview/llkeyconflict.cpp b/indra/newview/llkeyconflict.cpp index 9547542e9b..2a7de963df 100644 --- a/indra/newview/llkeyconflict.cpp +++ b/indra/newview/llkeyconflict.cpp @@ -34,6 +34,7 @@ #include "llkeyconflict.h" +#include "llgamecontrol.h" #include "llinitparam.h" #include "llkeyboard.h" #include "lltrans.h" @@ -243,6 +244,8 @@ std::string LLKeyConflictHandler::getStringFromKeyData(const LLKeyData& keydata) result += LLKeyboard::stringFromMouse(keydata.mMouse); + result += LLGameControl::controllerInputStringFromAction((LLGameControl::ActionType)keydata.mControllerActionType, keydata.mControllerAction); + return result; } @@ -294,7 +297,20 @@ void LLKeyConflictHandler::loadFromSettings(const LLViewerInput::KeyMode& keymod // Assumes U32_MAX conflict mask, and is assignable by default, // but assignability might have been overriden by generatePlaceholders. LLKeyConflict &type_data = (*destination)[it->command]; - type_data.mKeyBind.addKeyData(mouse, key, mask, true); + + if (it->controller.isProvided() && !it->controller.getValue().empty()) + { + LLGameControl::ActionType actionType = LLGameControl::ActionType::NONE; + U8 action = 0; + if(LLGameControl::actionFromString(it->controller.getValue(),actionType, action)) + { + type_data.mKeyBind.addKeyData(LLKeyData((U8)actionType, action)); + } + } + else + { + type_data.mKeyBind.addKeyData(mouse, key, mask, true); + } } } @@ -518,6 +534,14 @@ void LLKeyConflictHandler::saveToSettings(bool temporary) // just copy old keys.xml and rename to key_bindings.xml, it should work binding.mouse.set(LLKeyboard::stringFromMouse(data.mMouse, false), true); } + if(data.mControllerActionType > LLGameControl::ActionType::BUTTON) + { + binding.controller.setProvided(false); + } + else + { + binding.controller.set(LLGameControl::stringFromAction((LLGameControl::ActionType)(data.mControllerActionType), data.mControllerAction)); + } binding.command = iter->first; mode.bindings.add(binding); } diff --git a/indra/newview/llpanelpreferencegamecontrol.cpp b/indra/newview/llpanelpreferencegamecontrol.cpp new file mode 100755 index 0000000000..7cab66c47b --- /dev/null +++ b/indra/newview/llpanelpreferencegamecontrol.cpp @@ -0,0 +1,1156 @@ +/** + * @file llfloaterpreference.cpp + * @brief Global preferences with and without persistence. + * + * $LicenseInfo:firstyear=2002&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, 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$ + */ + +/* + * App-wide preferences. Note that these are not per-user, + * because we need to load many preferences before we have + * a login name. + */ + +#include "llviewerprecompiledheaders.h" + +#include "llpanelpreferencegamecontrol.h" + +#include "llbutton.h" +#include "llcheckboxctrl.h" +#include "llcombobox.h" +#include "lltabcontainer.h" +#include "llspinctrl.h" +#include "lltextbox.h" + + +namespace +{ + // Singleton instance pointer - only one game control panel exists at a time + static LLPanelPreferenceGameControl* sGameControlPanel { nullptr }; + + // Track current UI selection state for input/output channel assignment. + // When user clicks a cell to change its mapping, these track which cell is being edited. + static LLScrollListCtrl* sSelectedGrid { nullptr }; + static LLScrollListItem* sSelectedItem { nullptr }; + static LLScrollListCell* sSelectedCell { nullptr }; + + // Selector items use the label "None" for the unmapped entry. + // Table cells should render that as a blank cell rather than the word. + const std::string NONE_LABEL("None"); + + // Kinds of action mapping, used both as the LLGameControl mapping key and as + // the block discriminator stored in each action-table row's value. + const std::string KIND_AXES("Axes"); + const std::string KIND_BUTTONS("Buttons"); + + LLSD blankIfNone(const LLSD& label) + { + return label.asString() == NONE_LABEL ? LLSD() : label; + } +} + +// Static entry point called when device list changes (device connected/disconnected). +// Delegates to the singleton instance if it exists. +void LLPanelPreferenceGameControl::updateDeviceList() +{ + if (sGameControlPanel) + { + sGameControlPanel->updateDeviceListInternal(); + } +} + +// Constructor - registers this instance as the singleton +LLPanelPreferenceGameControl::LLPanelPreferenceGameControl() +{ + sGameControlPanel = this; +} + +// Destructor - clears singleton pointer +LLPanelPreferenceGameControl::~LLPanelPreferenceGameControl() +{ + sGameControlPanel = nullptr; +} + +static LLPanelInjector t_pref_game_control("panel_preference_game_control"); + +// Snapshot the panel's UI state as the single "GameControl" setting. +// The returned map can be persisted (saveSettings) or applied directly to +// LLGameControl runtime state via LLGameControl::applySettingsFromLLSD(). +LLSD LLPanelPreferenceGameControl::getSettingsAsLLSD() +{ + // Fold each device's current options into GameControl/Devices//Config. + // The global per-mode action mappings are already live in LLGameControl (edited + // via its accessors), so snapshotting the whole GameControl structure captures both. + for (auto& [guid, device] : mDeviceOptions) + { + device.settings = device.options.saveToString(device.name); + LLGameControl::setDeviceConfig(guid, device.settings); + } + + LLSD result = LLSD::emptyMap(); + result["GameControl"] = LLGameControl::getGameControlSettings(); + return result; +} + +// Saves current UI state to settings by pushing the "GameControl" snapshot +// (global action mappings + per-device Config) into gSavedSettings. +void LLPanelPreferenceGameControl::saveSettings() +{ + LLPanelPreference::saveSettings(); + + if (mOrigSettings.isEmpty()) + { + rememberOriginalSettings(); + } + + // Push every entry of the snapshot into gSavedSettings, also tracking + // the new value in mSavedValues so the base panel's accounting stays in sync. + LLSD settings = getSettingsAsLLSD(); + for (auto it = settings.beginMap(); it != settings.endMap(); ++it) + { + if (LLControlVariable* control = gSavedSettings.getControl(it->first)) + { + control->set(it->second); + mSavedValues[control] = control->getValue(); + } + } +} + +// Handles row selection in the Actions table. If the user clicked the editable +// Input column of a real (non-spacer) row, shows the appropriate input selector. +void LLPanelPreferenceGameControl::onGridSelect(LLUICtrl* ctrl) +{ + clearSelectionState(); + + LLScrollListCtrl* table = dynamic_cast(ctrl); + if (!table || !table->getEnabled()) + return; + + if (LLScrollListItem* item = table->getFirstSelected()) + { + // Try to show combobox for editing; if not applicable, deselect + if (initCombobox(item, table)) + return; + + table->deselectAllItems(); + } +} + +// Initializes and displays the appropriate combobox over the selected cell. +// Chooses the combobox and editable column from the table and (for the Actions +// table) the row's block kind. Returns true if a combobox was shown, false if the +// click wasn't on an editable cell. +bool LLPanelPreferenceGameControl::initCombobox(LLScrollListItem* item, LLScrollListCtrl* grid) +{ + LLComboBox* combobox = nullptr; + S32 col = -1; + + if (grid == mActionMappingsAxes || grid == mActionMappingsButtons) + { + // Only the Input column (1) of a real action row is editable. + LLSD row_value = item->getValue(); + if (item->getSelectedCell() != 1 || !row_value.isMap()) + return false; + combobox = (grid == mActionMappingsAxes) ? mAxisInputSelector : mButtonInputSelector; + col = 1; + } + else if (grid == mAxisChannels) + { + // Only the Output column (4) uses a popup selector. + if (item->getSelectedCell() != 4) + return false; + combobox = mAxisInputSelector; + col = 4; + } + else if (grid == mButtonChannels) + { + // Only the Output column (1) uses a popup selector. + if (item->getSelectedCell() != 1) + return false; + combobox = mButtonInputSelector; + col = 1; + } + + if (!combobox) + return false; + + LLScrollListText* cell = dynamic_cast(item->getColumn(col)); + if (!cell) + return false; + + // compute new rect for combobox + S32 row_index = grid->getItemIndex(item); + fitInRect(combobox, grid, row_index, col); + + // Pre-select the dropdown item whose label matches the cell's current text. + // Match against the selector items directly (rather than parsing the item + // value) so this works regardless of the value naming scheme. + std::string value; + std::string cell_value = cell->getValue(); + std::vector items = combobox->getAllData(); + for (const LLScrollListItem* combo_item : items) + { + if (combo_item->getColumn(0)->getValue().asString() == cell_value) + { + value = combo_item->getValue().asString(); + break; + } + } + if (value.empty()) + { + // No match (e.g. a blank "None" cell): select the last item, which is "None". + value = items.back()->getValue().asString(); + } + + combobox->setValue(value); + combobox->setVisible(true); + combobox->showList(); + + sSelectedGrid = grid; + sSelectedItem = item; + sSelectedCell = cell; + + return true; +} + +// Called when the user selects a value from an input/output selector combobox. +// The originating grid determines the meaning: Actions table -> action mapping; +// axis/button channels table -> per-device physical-to-canonical remap. +void LLPanelPreferenceGameControl::onCommitInputChannel(LLUICtrl* ctrl) +{ + if (!sSelectedGrid || !sSelectedItem || !sSelectedCell) + return; + + LLComboBox* combobox = dynamic_cast(ctrl); + llassert(combobox); + if (!combobox) + return; + + if (sSelectedGrid == mActionMappingsAxes || sSelectedGrid == mActionMappingsButtons) + { + // The edited row identifies the action + block; the combobox supplies the input. + LLSD row_value = sSelectedItem->getValue(); + std::string mode = currentEditMode(); + std::string kind = row_value["kind"].asString(); + std::string action = row_value["action"].asString(); + const LLComboBox* input_selector = (kind == KIND_AXES) ? mAxisInputSelector : mButtonInputSelector; + std::string input_value = combobox->getValue().asString(); + std::string input_label = combobox->getSelectedItemLabel(); + + if (input_label != NONE_LABEL) + { + // An input can drive at most one action within a block: clear it elsewhere. + removeDuplicateActionInput(mode, kind, action, input_value, input_selector); + } + + // Store the mapping directly in LLGameControl's live GameControl settings. + // gSavedSettings is updated later via saveSettings() when the user clicks OK. + LLGameControl::updateModeMapping(mode, kind, action, input_value); + sSelectedCell->setValue(blankIfNone(input_label)); + } + else if (sSelectedGrid == mAxisChannels) + { + // The row is a physical axis; the selected item's index is the canonical + // axis it maps to. "None" (index >= NUM_AXES) is not a valid output. + S32 axis = mAxisChannels->getItemIndex(sSelectedItem); + S32 output = combobox->getCurrentIndex(); + LLGameControl::Options& options = getSelectedDeviceOptions(); + if (output >= 0 && output < (S32)LLGameControl::NUM_AXES) + { + options.getAxisMap()[axis] = (U8)output; + LLGameControl::setDeviceOptions(mSelectedDeviceGUID, options); + } + // Re-render the cell from the actual map (handles the ignored "None" case). + sSelectedCell->setValue(selectorLabelAt(mAxisInputSelector, options.getAxisMap()[axis])); + } + else if (sSelectedGrid == mButtonChannels) + { + // The row is a physical button; the selected item's index is the canonical + // button it maps to. "None" (index >= NUM_BUTTONS) is not a valid output. + S32 button = mButtonChannels->getItemIndex(sSelectedItem); + S32 output = combobox->getCurrentIndex(); + LLGameControl::Options& options = getSelectedDeviceOptions(); + if (output >= 0 && output < (S32)LLGameControl::NUM_BUTTONS) + { + options.getButtonMap()[button] = (U8)output; + LLGameControl::setDeviceOptions(mSelectedDeviceGUID, options); + } + sSelectedCell->setValue(selectorLabelAt(mButtonInputSelector, options.getButtonMap()[button])); + } + + sSelectedGrid->deselectAllItems(); + clearSelectionState(); +} + +// Returns true if a cell is currently selected and waiting for input channel assignment. +// Used to determine whether to capture live controller input. +bool LLPanelPreferenceGameControl::isWaitingForInputChannel() +{ + return sSelectedCell != nullptr; +} + +// Static method called when controller input is detected while a channel selector is open. +// Automatically assigns the detected input channel to the selected cell. +// NOTE: live-input capture is currently disabled (stub); the selector popups are the +// only way to assign an input. +void LLPanelPreferenceGameControl::applyGameControlInput() +{ +} + +// Handles selection in the axis-channels table (invert, deadzone, offset, output). +// Syncs the invert flag immediately; shows the numeric editor for deadzone/offset, +// or the axis selector popup for the Output column. +void LLPanelPreferenceGameControl::onAxisChannelsSelect() +{ + clearSelectionState(); + + if (LLScrollListItem* row = mAxisChannels->getFirstSelected()) + { + LLGameControl::Options& deviceOptions = getSelectedDeviceOptions(); + S32 row_index = mAxisChannels->getItemIndex(row); + + { + // Always sync invert checkbox - clicking the checkbox selects the row + // but doesn't automatically update the underlying option + constexpr S32 invert_checkbox_column = 1; + bool invert = row->getColumn(invert_checkbox_column)->getValue().asBoolean(); + S32 multiplier = invert ? -1 : 1; + if (multiplier != deviceOptions.getAxisOptions()[row_index].mMultiplier) + { + deviceOptions.getAxisOptions()[row_index].mMultiplier = multiplier; + + if (row_index == LLGameControl::AXIS_LEFT_TRIGGER + || row_index == LLGameControl::AXIS_RIGHT_TRIGGER) + { + // The two trigger axes act as one bidirectional axis under the hood, + // so their inversion must stay in sync -- update the sibling axis's + // option and reflect the new state in its checkbox cell. + S32 other_row_index = row_index == LLGameControl::AXIS_LEFT_TRIGGER + ? LLGameControl::AXIS_RIGHT_TRIGGER + : LLGameControl::AXIS_LEFT_TRIGGER; + deviceOptions.getAxisOptions()[other_row_index].mMultiplier = multiplier; + + if (LLScrollListItem* other_row = mAxisChannels->getItemByIndex(other_row_index)) + { + other_row->getColumn(invert_checkbox_column)->setValue(invert); + } + } + + LLGameControl::setDeviceOptions(mSelectedDeviceGUID, deviceOptions); + } + } + + S32 column_index = row->getSelectedCell(); + if (column_index == 2 || column_index == 3) + { + fitInRect(mNumericValueEditor, mAxisChannels, row_index, column_index); + if (column_index == 2) + { + mNumericValueEditor->setMinValue(0); + mNumericValueEditor->setMaxValue(LLGameControl::MAX_AXIS_DEAD_ZONE); + mNumericValueEditor->setValue(deviceOptions.getAxisOptions()[row_index].mDeadZone); + } + else // column_index == 3 + { + mNumericValueEditor->setMinValue(-LLGameControl::MAX_AXIS_OFFSET); + mNumericValueEditor->setMaxValue(LLGameControl::MAX_AXIS_OFFSET); + mNumericValueEditor->setValue(deviceOptions.getAxisOptions()[row_index].mOffset); + } + mNumericValueEditor->setVisible(true); + } + else + { + // Output column (4) shows the axis selector popup; other columns are no-ops. + initCombobox(row, mAxisChannels); + } + } +} + +// Handles selection in the button-channels table. Only the Output column uses a +// popup selector; the Input column is fixed (the physical button). +void LLPanelPreferenceGameControl::onButtonChannelsSelect() +{ + clearSelectionState(); + + if (LLScrollListItem* row = mButtonChannels->getFirstSelected()) + { + initCombobox(row, mButtonChannels); + } +} + +// Called when user commits a numeric value (deadzone or offset) in the spin control. +// Validates and clamps the value, then updates both the UI and device options. +void LLPanelPreferenceGameControl::onCommitNumericValue() +{ + if (LLScrollListItem* row = mAxisChannels->getFirstSelected()) + { + LLGameControl::Options& deviceOptions = getSelectedDeviceOptions(); + S32 value = mNumericValueEditor->getValue().asInteger(); + S32 row_index = mAxisChannels->getItemIndex(row); + S32 column_index = row->getSelectedCell(); + llassert(column_index == 2 || column_index == 3); // 2=deadzone, 3=offset + if (column_index < 2 || column_index > 3) + return; + + if (column_index == 2) + { + value = std::clamp(value, 0, LLGameControl::MAX_AXIS_DEAD_ZONE); + deviceOptions.getAxisOptions()[row_index].mDeadZone = (U16)value; + } + else // column_index == 3 + { + value = std::clamp(value, -LLGameControl::MAX_AXIS_OFFSET, LLGameControl::MAX_AXIS_OFFSET); + deviceOptions.getAxisOptions()[row_index].mOffset = (S16)value; + } + setNumericLabel(row->getColumn(column_index), value); + LLGameControl::setDeviceOptions(mSelectedDeviceGUID, deviceOptions); + } +} + +// Initializes all UI controls and sets up callbacks. +// Called once when the panel is first built from XML. +bool LLPanelPreferenceGameControl::postBuild() +{ + // Send-to-server checkbox (top-left of the main panel) + mCheckGameControlToServer = getChild("game_control_to_server"); + mCheckGameControlToServer->setCommitCallback([this](LLUICtrl*, const LLSD&) + { + LLGameControl::setSendToServer(mCheckGameControlToServer->getValue()); + }); + + getChild("game_control_tabs")->setCommitCallback([this](LLUICtrl*, const LLSD&) { clearSelectionState(); }); + + mTabActions = getChild("tab_actions"); + mTabDevices = getChild("tab_devices"); + + // Actions tab (global, per-mode) + mActionMode = getChild("action_mode"); + mActionMode->setCommitCallback([this](LLUICtrl*, const LLSD&) { onActionModeChanged(); }); + + mRestoreActionsDefaults = getChild("restore_actions_defaults"); + mRestoreActionsDefaults->setCommitCallback([this](LLUICtrl*, const LLSD&) { onResetActionsToDefaults(); }); + + mActionMappingsAxes = getChild("action_mappings_axes"); + mActionMappingsAxes->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onGridSelect(ctrl); }); + + mActionMappingsButtons = getChild("action_mappings_buttons"); + mActionMappingsButtons->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onGridSelect(ctrl); }); + + // Devices tab (per-device) + mNoDeviceMessage = getChild("nodevice_message"); + mDevicePrompt = getChild("device_prompt"); + mDeviceList = getChild("device_list"); + mCheckShowAllDevices = getChild("show_all_known_devices"); + mPanelDeviceSettings = getChild("device_settings"); + + mCheckShowAllDevices->setCommitCallback([this](LLUICtrl*, const LLSD&) { populateDeviceTitle(); }); + mDeviceList->setCommitCallback([this](LLUICtrl*, const LLSD& value) { populateDeviceSettings(value); }); + + mRestoreDeviceDefaults = getChild("restore_device_defaults"); + mRestoreDeviceDefaults->setCommitCallback([this](LLUICtrl*, const LLSD&) { onResetDeviceToDefaults(); }); + + mAxisChannels = getChild("axis_channels"); + mAxisChannels->setCommitCallback([this](LLUICtrl*, const LLSD&) { onAxisChannelsSelect(); }); + + mButtonChannels = getChild("button_channels"); + mButtonChannels->setCommitCallback([this](LLUICtrl*, const LLSD&) { onButtonChannelsSelect(); }); + + // Device State tab (per-device, live read-only) + mTabDeviceState = getChild("tab_device_state"); + mStateNoDeviceMessage = getChild("state_nodevice_message"); + mStateDevicePrompt = getChild("state_device_prompt"); + mStateRemapNote = getChild("state_remap_note"); + mStateDeviceList = getChild("state_device_list"); + mPanelDeviceState = getChild("device_state_settings"); + mStateDeviceList->setCommitCallback([this](LLUICtrl*, const LLSD& value) + { mStateSelectedDeviceGUID = value.asString(); }); + + mAxisState = getChild("axis_state"); + mButtonState = getChild("button_state"); + + // Spin control for editing deadzone/offset values inline + mNumericValueEditor = getChild("numeric_value_editor"); + mNumericValueEditor->setCommitCallback([this](LLUICtrl*, const LLSD&) { onCommitNumericValue(); }); + + // Action selectors: provide the "Action" column rows, chosen by edit mode. + mAnalogActionSelector = getChild("analog_action_selector"); + mBinaryActionSelector = getChild("binary_action_selector"); + mFlycamAnalogActionSelector = getChild("flycam_analog_action_selector"); + mFlycamBinaryActionSelector = getChild("flycam_binary_action_selector"); + + // Canonical input selectors, shown inline when editing an Input/Output cell. + mAxisInputSelector = getChild("axis_input_selector"); + mAxisInputSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); }); + + mButtonInputSelector = getChild("button_input_selector"); + mButtonInputSelector->setCommitCallback([this](LLUICtrl* ctrl, const LLSD&) { onCommitInputChannel(ctrl); }); + + // The device tables' rows are static (one per physical axis/button); their + // cells are filled per-device. The action table is mode-specific and built + // in populateActionMappings() on open / mode change. + populateAxisChannelsRows(); + populateButtonChannelsRows(); + + // The Device State tab's rows are static (one per physical axis/button); their + // Value cells are refreshed every frame in draw() from the selected device. + populateAxisStateRows(); + populateButtonStateRows(); + + // Workaround for the common bug: + // LLScrollListCtrl with draw_heading="true" initially has incorrect mTop (17 px higher). + // Each scroll list fills a dedicated wrapper panel, so the corrected top is its + // parent's height - 1. + auto fixHeadingTop = [](LLScrollListCtrl* grid) + { + LLRect rect = grid->getRect(); + rect.mTop = grid->getParent()->getRect().getHeight() - 1; + grid->setRect(rect); + grid->updateLayout(); + }; + fixHeadingTop(mActionMappingsAxes); + fixHeadingTop(mActionMappingsButtons); + fixHeadingTop(mAxisChannels); + fixHeadingTop(mButtonChannels); + fixHeadingTop(mAxisState); + fixHeadingTop(mButtonState); + + return true; +} + +// Called when the preferences floater is opened. +// Loads current LLGameControl state into UI controls and refreshes all tables. +void LLPanelPreferenceGameControl::onOpen(const LLSD& key) +{ + // Sync checkbox with current LLGameControl state + mCheckGameControlToServer->setValue(LLGameControl::sendToServer()); + + clearSelectionState(); + + // Default to editing the Avatar mode's mappings each time the panel opens + // (Avatar is the first action_mode item). + mActionMode->selectFirstItem(); + populateActionMappings(); + + // Refresh device list and settings + updateDeviceListInternal(); + + mCheckGameControlToServer->setEnabled(true); + mActionMappingsAxes->setEnabled(true); + mActionMappingsButtons->setEnabled(true); + mAxisChannels->setEnabled(true); + mButtonChannels->setEnabled(true); + mDeviceList->setEnabled(true); + mStateDeviceList->setEnabled(true); + + // Clear original settings - will be populated on first saveSettings() call + mOrigSettings = LLSD::emptyMap(); +} + +// Per-frame refresh of the Device State tab. Only touches the tables while that +// tab is actually the visible one, so it costs nothing on the other tabs. +void LLPanelPreferenceGameControl::draw() +{ + if (mTabDeviceState && mTabDeviceState->getVisible()) + { + populateDeviceStateValues(); + } + LLPanelPreference::draw(); +} + +// Called when user clicks OK. Clears selections and prepares for onClose() +void LLPanelPreferenceGameControl::apply() +{ + LLPanelPreference::apply(); + clearSelectionState(); + + // Clear mOrigSettings to prevent cancel() from reverting changes. + // Note: cancel() is called in onClose() even after OK, so we must clear this. + mOrigSettings = LLSD::emptyMap(); + + // Note: any settings changes have already been written to global-settings + // via the LLControlVariables assigned to the UI elements. LLGameControl has + // already loaded from global-settings in saveSettings(). +} + +// Called when user clicks Cancel. Restores all settings to their original values. +void LLPanelPreferenceGameControl::cancel(const std::vector settings_to_skip) +{ + LLPanelPreference::cancel(settings_to_skip); + clearSelectionState(); + + // Skip restore if original settings were cleared in apply(). + if (mOrigSettings.isEmpty()) + { + return; + } + + // Restore in-memory state from the snapshot, then push back to gSavedSettings + // via the bulk save path so persistence stays consistent. + LLGameControl::applySettingsFromLLSD(mOrigSettings); + LLGameControl::saveToSettings(); +} + +// Rebuilds the internal device options map from LLGameControl state. +// Merges saved options with currently connected devices. +void LLPanelPreferenceGameControl::updateDeviceListInternal() +{ + mDeviceOptions.clear(); + + // Load saved device options from settings + for (const auto& [guid, options] : LLGameControl::getDeviceOptions()) + { + DeviceOptions deviceOptions = { LLStringUtil::null, options, LLGameControl::Options() }; + deviceOptions.options.loadFromString(deviceOptions.name, deviceOptions.settings); + mDeviceOptions.emplace(guid, deviceOptions); + } + + // Add currently connected devices that don't have saved settings yet + for (const auto& device : LLGameControl::getDevices()) + { + if (mDeviceOptions.find(device.getGUID()) == mDeviceOptions.end()) + { + mDeviceOptions[device.getGUID()] = { device.getName(), device.saveOptionsToString(true), device.getOptions() }; + } + } + + mCheckShowAllDevices->setValue(false); + populateDeviceTitle(); + + // The Device State tab tracks the live set of connected devices too. + updateDeviceStateList(); +} + +// Updates the device selection UI based on available devices. +// Shows single device name, dropdown list, or "no device" message as appropriate. +void LLPanelPreferenceGameControl::populateDeviceTitle() +{ + mSelectedDeviceGUID.clear(); + + // "Show all devices" includes saved settings for disconnected devices + bool showAllDevices = mCheckShowAllDevices->getValue().asBoolean(); + std::size_t deviceCount = showAllDevices ? mDeviceOptions.size() : LLGameControl::getDevices().size(); + + mNoDeviceMessage->setVisible(!deviceCount); + mDevicePrompt->setVisible(deviceCount); + mDeviceList->setVisible(deviceCount); // always use the combo box, even for one device + mPanelDeviceSettings->setVisible(deviceCount); + mRestoreDeviceDefaults->setVisible(deviceCount); + + if (!deviceCount) + { + return; + } + + auto makeTitle = [](const std::string& guid, const std::string& name) -> std::string + { + return guid + ", " + name; + }; + + auto makeListItem = [](const std::string& guid, const std::string& title) + { + return LLSD().with("value", guid).with("columns", LLSD().with("label", title)); + }; + + mDeviceList->clear(); + mDeviceList->clearRows(); + + if (showAllDevices) + { + for (const auto& [guid, device] : mDeviceOptions) + { + mDeviceList->addElement(makeListItem(guid, makeTitle(guid, device.name))); + } + } + else + { + for (const LLGameControl::Device& device : LLGameControl::getDevices()) + { + mDeviceList->addElement(makeListItem(device.getGUID(), makeTitle(device.getGUID(), device.getName()))); + } + } + + mDeviceList->selectNthItem(0); + populateDeviceSettings(mDeviceList->getValue()); +} + +// Loads settings for the specified device into the device-channel tables. +void LLPanelPreferenceGameControl::populateDeviceSettings(const std::string& guid) +{ + mSelectedDeviceGUID = guid; + llassert_always(mDeviceOptions.find(guid) != mDeviceOptions.end()); + + // Device tables are per-device (mode-independent). + populateAxisChannelsCells(); + populateButtonChannelsCells(); +} + +// The mode string ("When moving Avatar"/"When moving FlyCam"/"When sitting") currently selected for editing. +std::string LLPanelPreferenceGameControl::currentEditMode() +{ + S32 ordinal = mActionMode->getValue().asInteger(); + return LLGameControl::getModeName((LLGameControl::AgentControlMode)ordinal); +} + +// Picks the action selector supplying the "Action" rows for a block + mode. +LLComboBox* LLPanelPreferenceGameControl::actionSelectorForMode(bool axis, const std::string& mode) const +{ + bool flycam = (mode == "FlyCam"); + if (axis) + return flycam ? mFlycamAnalogActionSelector : mAnalogActionSelector; + return flycam ? mFlycamBinaryActionSelector : mBinaryActionSelector; +} + +// Rebuilds the two Actions tables for the current mode: axis actions in the top +// table, button actions in the bottom table. Each row stores {kind, action} as its +// value and shows the action label + its currently-mapped input. +void LLPanelPreferenceGameControl::populateActionMappings() +{ + mActionMappingsAxes->clearRows(); + mActionMappingsButtons->clearRows(); + + std::string mode = currentEditMode(); + + auto addBlock = [&](LLScrollListCtrl* grid, const std::string& kind, + const LLComboBox* action_selector, const LLComboBox* input_selector) + { + LLSD mapping = LLGameControl::getModeMapping(mode, kind); + + for (LLScrollListItem* item : action_selector->getAllData()) + { + std::string action = item->getValue().asString(); + if (action == NONE_LABEL) // skip the "None" sentinel; not an action row + continue; + + LLScrollListItem::Params row_params; + LLScrollListCell::Params cell_params; + // Match the font size used by the "Controls" preferences panel + cell_params.font = LLFontGL::getFontSansSerif(); + for (S32 c = 0; c < grid->getNumColumns(); ++c) + { + cell_params.column = grid->getColumn(c)->mName; + row_params.columns.add(cell_params); + } + + // Row value carries the block kind and action key (used on commit/lookup). + LLSD row_value; + row_value["kind"] = kind; + row_value["action"] = action; + row_params.value = row_value; + + LLScrollListItem* row = grid->addRow(row_params); + row->getColumn(0)->setValue(item->getColumn(0)->getValue()); // action label + std::string input_value = mapping.has(action) ? mapping[action].asString() : LLStringUtil::null; + row->getColumn(1)->setValue(blankIfNone(inputLabel(input_selector, input_value))); + } + }; + + addBlock(mActionMappingsAxes, KIND_AXES, actionSelectorForMode(true, mode), mAxisInputSelector); + addBlock(mActionMappingsButtons, KIND_BUTTONS, actionSelectorForMode(false, mode), mButtonInputSelector); +} + +// Handles a change in the edit-mode selector: rebuild the action table. +void LLPanelPreferenceGameControl::onActionModeChanged() +{ + clearSelectionState(); + populateActionMappings(); +} + +// Enforces that an input drives at most one action within a mode's block. When a +// new (non-None) input is assigned to an action, any *other* action in the same +// mode+kind mapping that currently uses that same input is reset to "None" +// (its Input cell blanked and its mapping set to the selector's None value). +void LLPanelPreferenceGameControl::removeDuplicateActionInput(const std::string& mode, + const std::string& kind, const std::string& keep_action, const std::string& input_value, + const LLComboBox* input_selector) +{ + // The selector's last item is the "None" value (e.g. AXIS_NONE / BUTTON_NONE). + std::string none_value = input_selector->getAllData().back()->getValue().asString(); + LLSD mapping = LLGameControl::getModeMapping(mode, kind); + + LLScrollListCtrl* grid = (kind == KIND_AXES) ? mActionMappingsAxes : mActionMappingsButtons; + for (LLScrollListItem* row : grid->getAllData()) + { + LLSD row_value = row->getValue(); + if (!row_value.isMap()) + continue; + + std::string action = row_value["action"].asString(); + if (action == keep_action) + continue; + + if (mapping.has(action) && mapping[action].asString() == input_value) + { + LLGameControl::updateModeMapping(mode, kind, action, none_value); + row->getColumn(1)->setValue(LLSD()); // blank the Input cell + } + } +} + +// Creates one row per physical axis in the axis-channels table. +// Columns: Input (physical axis) | Invert | Dead Zone | Offset | Output. +void LLPanelPreferenceGameControl::populateAxisChannelsRows() +{ + mAxisChannels->clearRows(); + + // Column 0 shows the physical controller axis inputs (not actions) + std::vector items = mAxisInputSelector->getAllData(); + + LLScrollListItem::Params row_params; + LLScrollListCell::Params cell_params; + // Match the font size used by the "Controls" preferences panel + cell_params.font = LLFontGL::getFontSansSerif(); + for (S32 i = 0; i < (S32)(mAxisChannels->getNumColumns()); ++i) + { + cell_params.column = mAxisChannels->getColumn(i)->mName; + row_params.columns.add(cell_params); + } + + // Configure column types and alignment + row_params.columns(1).type = "checkbox"; // Invert + row_params.columns(2).font_halign = "right"; // Dead Zone + row_params.columns(3).font_halign = "right"; // Offset + + for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i) + { + LLScrollListItem* row = mAxisChannels->addRow(row_params); + row->getColumn(0)->setValue(items[i]->getColumn(0)->getValue()); // physical axis label + } +} + +// Fills axis-channel cells (invert / deadzone / offset / output) from the current device. +void LLPanelPreferenceGameControl::populateAxisChannelsCells() +{ + std::vector rows = mAxisChannels->getAllData(); + const LLGameControl::Options& options = getSelectedDeviceOptions(); + const auto& all_axis_options = options.getAxisOptions(); + const auto& axis_map = options.getAxisMap(); + llassert(rows.size() == all_axis_options.size()); + + for (size_t i = 0; i < rows.size(); ++i) + { + LLScrollListItem* row = rows[i]; + const LLGameControl::Options::AxisOptions& axis_options = all_axis_options[i]; + row->getColumn(1)->setValue(axis_options.mMultiplier == -1); // Invert checkbox + setNumericLabel(row->getColumn(2), axis_options.mDeadZone); + setNumericLabel(row->getColumn(3), axis_options.mOffset); + row->getColumn(4)->setValue(selectorLabelAt(mAxisInputSelector, axis_map[i])); // Output + } +} + +// Creates one row per physical button in the button-channels table. +// Columns: Input (physical button) | Output. +void LLPanelPreferenceGameControl::populateButtonChannelsRows() +{ + mButtonChannels->clearRows(); + + std::vector items = mButtonInputSelector->getAllData(); + + LLScrollListItem::Params row_params; + LLScrollListCell::Params cell_params; + cell_params.font = LLFontGL::getFontSansSerif(); + for (S32 i = 0; i < (S32)(mButtonChannels->getNumColumns()); ++i) + { + cell_params.column = mButtonChannels->getColumn(i)->mName; + row_params.columns.add(cell_params); + } + + for (size_t i = 0; i < LLGameControl::NUM_BUTTONS; ++i) + { + LLScrollListItem* row = mButtonChannels->addRow(row_params); + row->getColumn(0)->setValue(items[i]->getColumn(0)->getValue()); // physical button label + } +} + +// Fills the button-channel Output column from the current device's button map. +void LLPanelPreferenceGameControl::populateButtonChannelsCells() +{ + std::vector rows = mButtonChannels->getAllData(); + const auto& button_map = getSelectedDeviceOptions().getButtonMap(); + llassert(rows.size() == button_map.size()); + + for (size_t i = 0; i < rows.size(); ++i) + { + rows[i]->getColumn(1)->setValue(selectorLabelAt(mButtonInputSelector, button_map[i])); + } +} + +// Rebuilds the Device State selector from the currently connected devices. +// Only connected devices are listed here, since the tab shows live input and a +// disconnected device has no live state. Shows the "No device" message and hides +// the tables when nothing is connected. +void LLPanelPreferenceGameControl::updateDeviceStateList() +{ + mStateDeviceList->clear(); + mStateDeviceList->clearRows(); + + const std::list& devices = LLGameControl::getDevices(); + bool hasDevice = !devices.empty(); + + mStateNoDeviceMessage->setVisible(!hasDevice); + mStateDevicePrompt->setVisible(hasDevice); + mStateRemapNote->setVisible(hasDevice); + mStateDeviceList->setVisible(hasDevice); + mPanelDeviceState->setVisible(hasDevice); + + if (!hasDevice) + { + mStateSelectedDeviceGUID.clear(); + return; + } + + for (const LLGameControl::Device& device : devices) + { + std::string title = device.getGUID() + ", " + device.getName(); + mStateDeviceList->addElement(LLSD().with("value", device.getGUID()) + .with("columns", LLSD().with("label", title))); + } + + // Keep the current selection if that device is still connected; otherwise + // fall back to the first device. + if (mStateSelectedDeviceGUID.empty() || !mStateDeviceList->setSelectedByValue(mStateSelectedDeviceGUID, true)) + { + mStateDeviceList->selectNthItem(0); + } + mStateSelectedDeviceGUID = mStateDeviceList->getValue().asString(); +} + +// Creates one row per physical axis in the axis-state table. +// Columns: Axis (physical axis label) | Value (filled live in draw()). +void LLPanelPreferenceGameControl::populateAxisStateRows() +{ + mAxisState->clearRows(); + + std::vector items = mAxisInputSelector->getAllData(); + + LLScrollListItem::Params row_params; + LLScrollListCell::Params cell_params; + cell_params.font = LLFontGL::getFontSansSerif(); + for (S32 i = 0; i < (S32)(mAxisState->getNumColumns()); ++i) + { + cell_params.column = mAxisState->getColumn(i)->mName; + row_params.columns.add(cell_params); + } + row_params.columns(1).font_halign = "right"; // Raw Value + row_params.columns(2).font_halign = "right"; // Value + + for (size_t i = 0; i < LLGameControl::NUM_AXES; ++i) + { + LLScrollListItem* row = mAxisState->addRow(row_params); + row->getColumn(0)->setValue(items[i]->getColumn(0)->getValue()); // physical axis label + } +} + +// Creates one row per physical button in the button-state table. +// Columns: Button (physical button label) | Value (filled live in draw()). +void LLPanelPreferenceGameControl::populateButtonStateRows() +{ + mButtonState->clearRows(); + + std::vector items = mButtonInputSelector->getAllData(); + + LLScrollListItem::Params row_params; + LLScrollListCell::Params cell_params; + cell_params.font = LLFontGL::getFontSansSerif(); + for (S32 i = 0; i < (S32)(mButtonState->getNumColumns()); ++i) + { + cell_params.column = mButtonState->getColumn(i)->mName; + row_params.columns.add(cell_params); + } + row_params.columns(1).font_halign = "right"; // Value + + for (size_t i = 0; i < LLGameControl::NUM_BUTTONS; ++i) + { + LLScrollListItem* row = mButtonState->addRow(row_params); + row->getColumn(0)->setValue(items[i]->getColumn(0)->getValue()); // physical button label + } +} + +// Fills the Value columns of both state tables from the selected device's live +// input state. Axis values are the raw [0, 32767] readings; button values are +// 1 (pressed) or 0 (released). Called each frame while the tab is visible. +void LLPanelPreferenceGameControl::populateDeviceStateValues() +{ + if (mStateSelectedDeviceGUID.empty()) + { + return; + } + + // Locate the live state for the selected device by GUID. + const LLGameControl::State* state = nullptr; + for (const LLGameControl::Device& device : LLGameControl::getDevices()) + { + if (device.getGUID() == mStateSelectedDeviceGUID) + { + state = &device.getState(); + break; + } + } + if (!state) + { + return; + } + + // State stores each canonical axis as a +/- half-axis pair, so axis a's + // signed value is (positive half) - (negative half). mRawAxes is pre-fix, + // mAxes is post-fix; both share the same layout and sign convention. + std::vector axisRows = mAxisState->getAllData(); + for (size_t i = 0; i < axisRows.size(); ++i) + { + size_t base = i * 2; + if (base + 1 >= state->mAxes.size()) + { + break; + } + S32 raw = (S32)state->mRawAxes[base] - (S32)state->mRawAxes[base + 1]; + S32 fixed = (S32)state->mAxes[base] - (S32)state->mAxes[base + 1]; + axisRows[i]->getColumn(1)->setValue(llformat("%d ", raw)); // Raw Value (pre-fix) + axisRows[i]->getColumn(2)->setValue(llformat("%d ", fixed)); // Value (post-fix) + } + + std::vector buttonRows = mButtonState->getAllData(); + for (size_t i = 0; i < buttonRows.size(); ++i) + { + S32 value = (state->mButtons >> i) & 0x1; + buttonRows[i]->getColumn(1)->setValue(llformat("%d ", value)); + } +} + +// Returns the display label of the input selector item with the given value, +// or empty if there is no such item. +std::string LLPanelPreferenceGameControl::inputLabel(const LLComboBox* input_selector, const std::string& input_value) +{ + for (const LLScrollListItem* item : input_selector->getAllData()) + { + if (item->getValue().asString() == input_value) + { + return item->getColumn(0)->getValue().asString(); + } + } + return LLStringUtil::null; +} + +// Returns the display label of the selector item at the given index (the item +// position matches the canonical axis/button index), or empty if out of range. +std::string LLPanelPreferenceGameControl::selectorLabelAt(const LLComboBox* selector, S32 index) +{ + std::vector items = selector->getAllData(); + if (index < 0 || index >= (S32)items.size()) + { + return LLStringUtil::null; + } + return items[index]->getColumn(0)->getValue().asString(); +} + +// Returns the options struct for the currently selected device. +LLGameControl::Options& LLPanelPreferenceGameControl::getSelectedDeviceOptions() +{ + auto options_it = mDeviceOptions.find(mSelectedDeviceGUID); + llassert_always(options_it != mDeviceOptions.end()); + return options_it->second.options; +} + +// Formats a numeric value for display in a table cell. +// Always shows the value, including the zero (default) case. +void LLPanelPreferenceGameControl::setNumericLabel(LLScrollListCell* cell, S32 value) +{ + cell->setValue(llformat("%d ", value)); +} + +// Positions a UI control (combobox or spin control) to overlay a specific table cell. +// Used to show editors inline within the scroll list tables. +void LLPanelPreferenceGameControl::fitInRect(LLUICtrl* ctrl, LLScrollListCtrl* grid, S32 row_index, S32 col_index) +{ + LLRect rect(grid->getCellRect(row_index, col_index)); + LLView* parent = grid->getParent(); + while (parent && parent != ctrl->getParent()) + { + rect.translate(parent->getRect().mLeft, parent->getRect().mBottom); + parent = parent->getParent(); + } + + ctrl->setRect(rect); + rect.translate(-rect.mLeft, -rect.mBottom); + for (LLView* child : *ctrl->getChildList()) + { + LLRect childRect(child->getRect()); + childRect.intersectWith(rect); + if (childRect.mRight < rect.mRight && + childRect.mRight > (rect.mLeft + rect.mRight) / 2) + { + childRect.mRight = rect.mRight; + } + child->setRect(childRect); + } +} + +// Clears the current cell selection state and hides all popup editors. +void LLPanelPreferenceGameControl::clearSelectionState() +{ + sSelectedGrid = nullptr; + sSelectedItem = nullptr; + sSelectedCell = nullptr; + mNumericValueEditor->setVisible(false); + mAxisInputSelector->setVisible(false); + mButtonInputSelector->setVisible(false); +} + +// Resets the current mode's axis + button action mappings to the built-in defaults. +void LLPanelPreferenceGameControl::onResetActionsToDefaults() +{ + clearSelectionState(); + + std::string mode = currentEditMode(); + LLSD defaults = LLGameControl::getDefaultModeMappings(); + LLGameControl::setModeMapping(mode, KIND_AXES, defaults[mode][KIND_AXES]); + LLGameControl::setModeMapping(mode, KIND_BUTTONS, defaults[mode][KIND_BUTTONS]); + populateActionMappings(); + + // Push the pending UI state into LLGameControl's runtime so the effect is + // immediate. gSavedSettings is updated later via saveSettings() on OK. + LLGameControl::applySettingsFromLLSD(getSettingsAsLLSD()); +} + +// Resets the selected device's options (axis/button remap, invert, deadzone, offset) +// to defaults. +void LLPanelPreferenceGameControl::onResetDeviceToDefaults() +{ + clearSelectionState(); + if (mSelectedDeviceGUID.empty()) + { + return; + } + + LLGameControl::Options& options = getSelectedDeviceOptions(); + options.resetToDefaults(); + LLGameControl::setDeviceOptions(mSelectedDeviceGUID, options); + + populateAxisChannelsCells(); + populateButtonChannelsCells(); + + LLGameControl::applySettingsFromLLSD(getSettingsAsLLSD()); +} + +// Captures current settings values into mOrigSettings for later restoration upon cancel(). +void LLPanelPreferenceGameControl::rememberOriginalSettings() +{ + mOrigSettings = LLGameControl::getSettingsAsLLSD(); +} diff --git a/indra/newview/llpanelpreferencegamecontrol.h b/indra/newview/llpanelpreferencegamecontrol.h new file mode 100644 index 0000000000..8effbdc59a --- /dev/null +++ b/indra/newview/llpanelpreferencegamecontrol.h @@ -0,0 +1,195 @@ +/** + * @file llpanelpreferencegamecontrol.h + * @brief LLPanelPreferenceGameControl class definition + * + * $LicenseInfo:firstyear=2002&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, 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$ + */ + +#pragma once + +#include "llfloaterpreference.h" +#include "llgamecontrol.h" +#include "llscrolllistcell.h" +#include "llscrolllistctrl.h" + +class LLButton; +class LLCheckBoxCtrl; +class LLComboBox; +class LLPanel; +class LLSpinCtrl; +class LLTextBox; + +//------------------------LLPanelPreferenceGameControl-------------------------------- +// +// Preference panel for configuring game controller (gamepad) input. +// +// Two visible sub-tabs: +// - Actions (global, per mode Avatar/FlyCam/Captive): one table binding each +// mode's axis and button actions to a canonical input. Independent of device. +// - Devices (per device): normalizes physical hardware to canonical inputs -- +// an axis-channels table (Input | Invert | Dead Zone | Offset | Output) and a +// button-channels table (Input | Output). +// +// All settings are stored under a single "GameControl" key: +// GameControl/ModeMappings//{Axes,Buttons} - GLOBAL action -> input +// GameControl/Devices//Config - serialized per-device options +// +class LLPanelPreferenceGameControl : public LLPanelPreference +{ + LOG_CLASS(LLPanelPreferenceGameControl); +public: + + // Called when a device is connected/disconnected + static void updateDeviceList(); + + LLPanelPreferenceGameControl(); + ~LLPanelPreferenceGameControl(); + + void onOpen(const LLSD& key) override; + void draw() override; + + void apply() override; + void cancel(const std::vector settings_to_skip = {}) override; + void saveSettings() override; + + // Snapshots the panel's UI state as the single "GameControl" setting map. + // The result can be fed to LLGameControl::applySettingsFromLLSD() + // to push pending changes into the runtime without touching gSavedSettings. + LLSD getSettingsAsLLSD(); + + void updateDeviceListInternal(); // Refresh device list and settings + + // UI event handlers + void onGridSelect(LLUICtrl* ctrl); // Handle table row selection + void onCommitInputChannel(LLUICtrl* ctrl); // Handle input/output combobox selection + void onCommitNumericValue(); // Handle deadzone/offset value changes + + // Live input capture support + static bool isWaitingForInputChannel(); // True if a cell is waiting for input + static void applyGameControlInput(); // Assign detected input to selected cell + +protected: + bool postBuild() override; + + // Actions tab (global, per-mode) population. + void populateActionMappings(); // Rebuild the action table for the current mode + LLGameControl::Options& getSelectedDeviceOptions(); // Get options for selected device + + // Devices tab (per-device) population. + void populateDeviceTitle(); // Update device selector UI + void populateDeviceSettings(const std::string& guid); // Load settings for device + void populateAxisChannelsRows(); // Create axis-channel rows (one per physical axis) + void populateAxisChannelsCells(); // Fill axis-channel invert/deadzone/offset/output + void populateButtonChannelsRows(); // Create button-channel rows (one per physical button) + void populateButtonChannelsCells(); // Fill button-channel output column + + // Device State tab (per-device, live read-only) population. + void updateDeviceStateList(); // Rebuild the state device selector from connected devices + void populateAxisStateRows(); // Create axis-state rows (one per physical axis) + void populateButtonStateRows(); // Create button-state rows (one per physical button) + void populateDeviceStateValues(); // Fill the Value columns from the selected device's live state + + // Utility methods + static void setNumericLabel(LLScrollListCell* cell, S32 value); // Format numeric cell + static void fitInRect(LLUICtrl* ctrl, LLScrollListCtrl* grid, S32 row_index, S32 col_index); // Position editor over cell + +private: + // Inline editing support + bool initCombobox(LLScrollListItem* item, LLScrollListCtrl* grid); // Show combobox over cell + void clearSelectionState(); // Hide editors and clear selection tracking + + // Actions-tab helpers + std::string currentEditMode(); // mode string ("Avatar"/"FlyCam"/"Captive") from mActionMode + void onActionModeChanged(); // rebuild the action table when the edit-mode selector changes + LLComboBox* actionSelectorForMode(bool axis, const std::string& mode) const; // default vs flycam selector + void removeDuplicateActionInput(const std::string& mode, const std::string& kind, + const std::string& keep_action, const std::string& input_value, const LLComboBox* input_selector); + static std::string inputLabel(const LLComboBox* input_selector, const std::string& input_value); // value -> label + static std::string selectorLabelAt(const LLComboBox* selector, S32 index); // label of item at index + + // Handlers for individual table selections/commits + void onAxisChannelsSelect(); // invert checkbox / deadzone-offset editor / output popup + void onButtonChannelsSelect(); // output popup + + // Reset to defaults handlers + void onResetActionsToDefaults(); // Reset current mode's action mappings + void onResetDeviceToDefaults(); // Reset selected device's options (remap/invert/deadzone/offset) + + void rememberOriginalSettings(); // Capture settings for cancel restoration + + // Sends game_control data to server + LLCheckBoxCtrl* mCheckGameControlToServer { nullptr }; + + // Sub-tab panels + LLPanel* mTabActions { nullptr }; + LLPanel* mTabDevices { nullptr }; + + // Actions tab + LLComboBox* mActionMode { nullptr }; // Avatar / FlyCam / Captive + LLButton* mRestoreActionsDefaults { nullptr }; + LLScrollListCtrl* mActionMappingsAxes { nullptr }; // Action | Axis Input (axis action block) + LLScrollListCtrl* mActionMappingsButtons { nullptr }; // Action | Buttons Input (button action block) + + // Devices tab + LLTextBox* mNoDeviceMessage { nullptr }; // Shown when no devices connected + LLTextBox* mDevicePrompt { nullptr }; // "Device:" label + LLComboBox* mDeviceList { nullptr }; // Dropdown listing available devices + LLCheckBoxCtrl* mCheckShowAllDevices { nullptr }; // Include disconnected devices + LLPanel* mPanelDeviceSettings { nullptr }; + LLButton* mRestoreDeviceDefaults { nullptr }; + LLScrollListCtrl* mAxisChannels { nullptr }; // Input | Invert | Dead Zone | Offset | Output + LLScrollListCtrl* mButtonChannels { nullptr }; // Input | Output + + // Device State tab (per-device, live read-only) + LLPanel* mTabDeviceState { nullptr }; + LLTextBox* mStateNoDeviceMessage { nullptr }; // Shown when no devices connected + LLTextBox* mStateDevicePrompt { nullptr }; // "Device:" label + LLTextBox* mStateRemapNote { nullptr }; // Note that values are post-remap + LLComboBox* mStateDeviceList { nullptr }; // Dropdown listing connected devices + LLPanel* mPanelDeviceState { nullptr }; // Wrapper holding the two state tables + LLScrollListCtrl* mAxisState { nullptr }; // Axis | Value (live raw axis values) + LLScrollListCtrl* mButtonState { nullptr }; // Button | Value (live pressed state) + std::string mStateSelectedDeviceGUID; // GUID of the device shown in the state tab + + // Inline editors - positioned over table cells when editing + LLSpinCtrl* mNumericValueEditor { nullptr }; // For deadzone/offset values + LLComboBox* mAxisInputSelector { nullptr }; // Canonical axes (action Input + axis Output popups) + LLComboBox* mButtonInputSelector { nullptr }; // Canonical buttons (action Input + button Output popups) + + // Action selectors: source of the "Action" column rows, chosen by edit mode. + LLComboBox* mAnalogActionSelector { nullptr }; // Avatar/Captive axis actions + LLComboBox* mBinaryActionSelector { nullptr }; // Avatar/Captive button actions + LLComboBox* mFlycamAnalogActionSelector { nullptr }; // FlyCam axis actions + LLComboBox* mFlycamBinaryActionSelector { nullptr }; // FlyCam button actions + + // Per-device options storage + struct DeviceOptions + { + std::string name; // Device display name + std::string settings; // Serialized settings string + LLGameControl::Options options; // Parsed options (axis settings, mappings) + }; + std::map mDeviceOptions; // Keyed by device GUID + std::string mSelectedDeviceGUID; // Currently selected device + + LLSD mOrigSettings; // Captured settings for cancel restoration +}; diff --git a/indra/newview/lltoolfocus.cpp b/indra/newview/lltoolfocus.cpp index b8878b1e6f..a93f37e09a 100644 --- a/indra/newview/lltoolfocus.cpp +++ b/indra/newview/lltoolfocus.cpp @@ -62,6 +62,8 @@ bool gCameraBtnPan = false; const S32 SLOP_RANGE = 4; +extern bool agent_push_forward(const LLKeyPressState& s); + // // Camera - shared functionality // @@ -80,8 +82,10 @@ LLToolCamera::LLToolCamera() mMouseSteering(false), mMouseUpX(0), mMouseUpY(0), - mMouseUpMask(MASK_NONE) -{ } + mMouseUpMask(MASK_NONE), + mKeyState(LLKeyPressState(false,false,false,1.0)) +{ +} LLToolCamera::~LLToolCamera() @@ -337,6 +341,12 @@ bool LLToolCamera::handleMouseUp(S32 x, S32 y, MASK mask) bool LLToolCamera::handleHover(S32 x, S32 y, MASK mask) { + if (gViewerWindow->getRightMouseDown()) + { + mKeyState.mState = KEYSTATE_LEVEL; + agent_push_forward(mKeyState); + } + S32 dx = gViewerWindow->getCurrentMouseDX(); S32 dy = gViewerWindow->getCurrentMouseDY(); @@ -471,8 +481,36 @@ bool LLToolCamera::handleHover(S32 x, S32 y, MASK mask) return true; } +bool LLToolCamera::handleRightMouseDown(S32 x, S32 y, MASK mask) +{ + if(mMouseSteering) + { + mKeyState.mState = KEYSTATE_DOWN; + agent_push_forward(mKeyState); + return true; + } + else + { + return false; + } +} + +bool LLToolCamera::handleRightMouseUp(S32 x, S32 y, MASK mask) +{ + if (mMouseSteering) + { + mKeyState.mState = KEYSTATE_UP; + agent_push_forward(mKeyState); + return true; + } + else + { + return false; + } +} void LLToolCamera::onMouseCaptureLost() { releaseMouse(); + handleRightMouseUp(0, 0, 0); } diff --git a/indra/newview/lltoolfocus.h b/indra/newview/lltoolfocus.h index c2460e7aa4..56a7501f50 100644 --- a/indra/newview/lltoolfocus.h +++ b/indra/newview/lltoolfocus.h @@ -27,20 +27,23 @@ #ifndef LL_LLTOOLFOCUS_H #define LL_LLTOOLFOCUS_H +#include "llkeyboard.h" #include "lltool.h" class LLPickInfo; class LLToolCamera -: public LLTool, public LLSingleton +: public LLTool, public LLSimpleton { - LLSINGLETON(LLToolCamera); - virtual ~LLToolCamera(); public: + LLToolCamera(); + virtual ~LLToolCamera(); virtual bool handleMouseDown(S32 x, S32 y, MASK mask) override; virtual bool handleMouseUp(S32 x, S32 y, MASK mask) override; virtual bool handleHover(S32 x, S32 y, MASK mask) override; + virtual bool handleRightMouseDown(S32 x, S32 y, MASK mask) override; + virtual bool handleRightMouseUp(S32 x, S32 y, MASK mask) override; virtual void onMouseCaptureLost() override; @@ -72,6 +75,7 @@ protected: S32 mMouseUpX; // needed for releaseMouse() S32 mMouseUpY; MASK mMouseUpMask; + LLKeyPressState mKeyState; }; diff --git a/indra/newview/llviewerinput.cpp b/indra/newview/llviewerinput.cpp index 3a28790b3f..328c452b30 100644 --- a/indra/newview/llviewerinput.cpp +++ b/indra/newview/llviewerinput.cpp @@ -48,6 +48,7 @@ #include "llfloatercamera.h" #include "llinitparam.h" #include "llselectmgr.h" +#include "llviewercamera.h" // // Constants @@ -62,15 +63,18 @@ const F32 ORBIT_NUDGE_RATE = 0.05f; // fraction of normal speed const LLKeyData agent_control_lbutton(CLICK_LEFT, KEY_NONE, MASK_NONE, true); +static U64 g_lastUpdateTime { 0 }; +static F32 g_deltaTime { 0.0f }; + struct LLKeybindFunctionData { - LLKeybindFunctionData(std::function function, bool global) + LLKeybindFunctionData(LLKeyFunc function, bool global) : mFunction(function), mIsGlobal(global) { } - std::function mFunction; + LLKeyFunc mFunction; // todo: might be good idea to make this into enum, like: global/inworld/menu bool mIsGlobal; }; @@ -83,16 +87,16 @@ struct LLKeyboardActionRegistry LLViewerInput gViewerInput; -bool agent_jump( EKeystate s ) +bool agent_jump( const LLKeyPressState& s ) { static bool first_fly_attempt(true); - if (KEYSTATE_UP == s) + if (KEYSTATE_UP == s.mState) { first_fly_attempt = true; return true; } - F32 time = gKeyboard->getCurKeyElapsedTime(); - S32 frame_count = ll_round(gKeyboard->getCurKeyElapsedFrameCount()); + F32 time = s.mElapsedTime; + S32 frame_count = s.mElapsedFrames; if( time < FLY_TIME || frame_count <= FLY_FRAMES @@ -110,9 +114,22 @@ bool agent_jump( EKeystate s ) return true; } -bool agent_push_down( EKeystate s ) +bool agent_push_down( const LLKeyPressState& s ) +{ + if( KEYSTATE_UP == s.mState ) return true; + gAgent.moveUp(-1); + return true; +} + +bool agent_toggle_down( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; + + if (KEYSTATE_DOWN == s.mState + && !gAgent.getFlying()) + { + gAgent.toggleCrouch(); + } gAgent.moveUp(-1); return true; } @@ -129,16 +146,16 @@ static void agent_check_temporary_run(LLAgent::EDoubleTapRunMode mode) } } -static void agent_handle_doubletap_run(EKeystate s, LLAgent::EDoubleTapRunMode mode) +static void agent_handle_doubletap_run(const LLKeyPressState& s, LLAgent::EDoubleTapRunMode mode) { - if (KEYSTATE_UP == s) + if (KEYSTATE_UP == s.mState) { // Note: in case shift is already released, slide left/right run // will be released in agent_turn_left()/agent_turn_right() agent_check_temporary_run(mode); } else if (gSavedSettings.getBOOL("AllowTapTapHoldRun") && - KEYSTATE_DOWN == s && + KEYSTATE_DOWN == s.mState && !gAgent.getRunning()) { if (gAgent.mDoubleTapRunMode == mode && @@ -156,10 +173,13 @@ static void agent_handle_doubletap_run(EKeystate s, LLAgent::EDoubleTapRunMode m } } -static void agent_push_forwardbackward( EKeystate s, S32 direction, LLAgent::EDoubleTapRunMode mode ) +static void agent_push_forwardbackward( const LLKeyPressState& s, S32 direction, LLAgent::EDoubleTapRunMode mode ) { - F32 time = gKeyboard->getCurKeyElapsedTime(); - S32 frame_count = ll_round(gKeyboard->getCurKeyElapsedFrameCount()); + agent_handle_doubletap_run(s, mode); + if (KEYSTATE_UP == s.mState) return; + + F32 time = s.mElapsedTime; + S32 frame_count = s.mElapsedFrames; if( time < NUDGE_TIME || frame_count <= NUDGE_FRAMES) { @@ -171,9 +191,9 @@ static void agent_push_forwardbackward( EKeystate s, S32 direction, LLAgent::EDo } } -bool camera_move_forward( EKeystate s ); +bool camera_move_forward( const LLKeyPressState& s ); -bool agent_push_forward( EKeystate s ) +bool agent_push_forward( const LLKeyPressState& s ) { if(gAgent.isMovementLocked()) return true; @@ -189,9 +209,9 @@ bool agent_push_forward( EKeystate s ) return true; } -bool camera_move_backward( EKeystate s ); +bool camera_move_backward( const LLKeyPressState& s ); -bool agent_push_backward( EKeystate s ) +bool agent_push_backward( const LLKeyPressState& s ) { if(gAgent.isMovementLocked()) return true; @@ -211,12 +231,12 @@ bool agent_push_backward( EKeystate s ) return true; } -static void agent_slide_leftright( EKeystate s, S32 direction, LLAgent::EDoubleTapRunMode mode ) +static void agent_slide_leftright( const LLKeyPressState& s, S32 direction, LLAgent::EDoubleTapRunMode mode ) { agent_handle_doubletap_run(s, mode); - if( KEYSTATE_UP == s ) return; - F32 time = gKeyboard->getCurKeyElapsedTime(); - S32 frame_count = ll_round(gKeyboard->getCurKeyElapsedFrameCount()); + if( KEYSTATE_UP == s.mState ) return; + F32 time = s.mElapsedTime; + S32 frame_count = s.mElapsedFrames; if( time < NUDGE_TIME || frame_count <= NUDGE_FRAMES) { @@ -229,7 +249,7 @@ static void agent_slide_leftright( EKeystate s, S32 direction, LLAgent::EDoubleT } -bool agent_slide_left( EKeystate s ) +bool agent_slide_left( const LLKeyPressState& s ) { if(gAgent.isMovementLocked()) return true; agent_slide_leftright(s, 1, LLAgent::DOUBLETAP_SLIDELEFT); @@ -237,16 +257,16 @@ bool agent_slide_left( EKeystate s ) } -bool agent_slide_right( EKeystate s ) +bool agent_slide_right( const LLKeyPressState& s ) { if(gAgent.isMovementLocked()) return true; agent_slide_leftright(s, -1, LLAgent::DOUBLETAP_SLIDERIGHT); return true; } -bool camera_spin_around_cw( EKeystate s ); +bool camera_spin_around_cw( const LLKeyPressState& s ); -bool agent_turn_left(EKeystate s) +bool agent_turn_left(const LLKeyPressState& s) { //in free camera control mode we need to intercept keyboard events for avatar movements if (LLFloaterCamera::inFreeCameraMode()) @@ -263,21 +283,21 @@ bool agent_turn_left(EKeystate s) } else { - if (KEYSTATE_UP == s) + if (KEYSTATE_UP == s.mState) { // Check temporary running. In case user released 'left' key with shift already released. agent_check_temporary_run(LLAgent::DOUBLETAP_SLIDELEFT); return true; } - F32 time = gKeyboard->getCurKeyElapsedTime(); - gAgent.moveYaw( LLFloaterMove::getYawRate( time ) ); + F32 time = s.mElapsedTime; + gAgent.moveYaw( LLFloaterMove::getYawRate( time ) * s.mValue ); } return true; } -bool camera_spin_around_ccw( EKeystate s ); +bool camera_spin_around_ccw( const LLKeyPressState& s ); -bool agent_turn_right( EKeystate s ) +bool agent_turn_right( const LLKeyPressState& s ) { //in free camera control mode we need to intercept keyboard events for avatar movements if (LLFloaterCamera::inFreeCameraMode()) @@ -294,48 +314,48 @@ bool agent_turn_right( EKeystate s ) } else { - if (KEYSTATE_UP == s) + if (KEYSTATE_UP == s.mState) { // Check temporary running. In case user released 'right' key with shift already released. agent_check_temporary_run(LLAgent::DOUBLETAP_SLIDERIGHT); return true; } - F32 time = gKeyboard->getCurKeyElapsedTime(); - gAgent.moveYaw( -LLFloaterMove::getYawRate( time ) ); + F32 time = s.mElapsedTime; + gAgent.moveYaw( -LLFloaterMove::getYawRate( time ) * s.mValue ); } return true; } -bool agent_look_up( EKeystate s ) +bool agent_look_up( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgent.movePitch(-1); //gAgent.rotate(-2.f * DEG_TO_RAD, gAgent.getFrame().getLeftAxis() ); return true; } -bool agent_look_down( EKeystate s ) +bool agent_look_down( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgent.movePitch(1); //gAgent.rotate(2.f * DEG_TO_RAD, gAgent.getFrame().getLeftAxis() ); return true; } -bool agent_toggle_fly( EKeystate s ) +bool agent_toggle_fly( const LLKeyPressState& s ) { // Only catch the edge - if (KEYSTATE_DOWN == s ) + if (KEYSTATE_DOWN == s.mState ) { LLAgent::toggleFlying(); } return true; } -F32 get_orbit_rate() +F32 get_orbit_rate( const LLKeyPressState& s ) { - F32 time = gKeyboard->getCurKeyElapsedTime(); + F32 time = s.mElapsedTime; if( time < NUDGE_TIME ) { F32 rate = ORBIT_NUDGE_RATE + time * (1 - ORBIT_NUDGE_RATE)/ NUDGE_TIME; @@ -348,26 +368,26 @@ F32 get_orbit_rate() } } -bool camera_spin_around_ccw( EKeystate s ) +bool camera_spin_around_ccw( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setOrbitLeftKey( get_orbit_rate() ); + gAgentCamera.setOrbitLeftKey( get_orbit_rate(s) ); return true; } -bool camera_spin_around_cw( EKeystate s ) +bool camera_spin_around_cw( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setOrbitRightKey( get_orbit_rate() ); + gAgentCamera.setOrbitRightKey( get_orbit_rate(s) ); return true; } -bool camera_spin_around_ccw_sitting( EKeystate s ) +bool camera_spin_around_ccw_sitting( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_SLIDERIGHT ) return true; + if( KEYSTATE_UP == s.mState && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_SLIDERIGHT ) return true; if (gAgent.rotateGrabbed() || gAgentCamera.sitCameraEnabled() || gAgent.getRunning()) { //send keystrokes, but do not change camera @@ -377,15 +397,15 @@ bool camera_spin_around_ccw_sitting( EKeystate s ) { //change camera but do not send keystrokes gAgentCamera.unlockView(); - gAgentCamera.setOrbitLeftKey( get_orbit_rate() ); + gAgentCamera.setOrbitLeftKey( get_orbit_rate(s) ); } return true; } -bool camera_spin_around_cw_sitting( EKeystate s ) +bool camera_spin_around_cw_sitting( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_SLIDELEFT ) return true; + if( KEYSTATE_UP == s.mState && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_SLIDELEFT ) return true; if (gAgent.rotateGrabbed() || gAgentCamera.sitCameraEnabled() || gAgent.getRunning()) { //send keystrokes, but do not change camera @@ -395,32 +415,32 @@ bool camera_spin_around_cw_sitting( EKeystate s ) { //change camera but do not send keystrokes gAgentCamera.unlockView(); - gAgentCamera.setOrbitRightKey( get_orbit_rate() ); + gAgentCamera.setOrbitRightKey( get_orbit_rate(s) ); } return true; } -bool camera_spin_over( EKeystate s ) +bool camera_spin_over( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setOrbitUpKey( get_orbit_rate() ); + gAgentCamera.setOrbitUpKey( get_orbit_rate(s) ); return true; } -bool camera_spin_under( EKeystate s ) +bool camera_spin_under( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setOrbitDownKey( get_orbit_rate() ); + gAgentCamera.setOrbitDownKey( get_orbit_rate(s) ); return true; } -bool camera_spin_over_sitting( EKeystate s ) +bool camera_spin_over_sitting( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; if (gAgent.upGrabbed() || gAgentCamera.sitCameraEnabled()) { //send keystrokes, but do not change camera @@ -429,15 +449,15 @@ bool camera_spin_over_sitting( EKeystate s ) else { //change camera but do not send keystrokes - gAgentCamera.setOrbitUpKey( get_orbit_rate() ); + gAgentCamera.setOrbitUpKey( get_orbit_rate(s) ); } return true; } -bool camera_spin_under_sitting( EKeystate s ) +bool camera_spin_under_sitting( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; if (gAgent.downGrabbed() || gAgentCamera.sitCameraEnabled()) { //send keystrokes, but do not change camera @@ -446,45 +466,68 @@ bool camera_spin_under_sitting( EKeystate s ) else { //change camera but do not send keystrokes - gAgentCamera.setOrbitDownKey( get_orbit_rate() ); + gAgentCamera.setOrbitDownKey( get_orbit_rate(s) ); } return true; } -bool camera_move_forward( EKeystate s ) +bool camera_roll_left(const LLKeyPressState& s) +{ + if (KEYSTATE_UP == s.mState) return true; + gAgentCamera.unlockView(); + gAgentCamera.setRollLeftKey(get_orbit_rate(s)); + return true; +} + +bool camera_roll_right(const LLKeyPressState& s) +{ + if (KEYSTATE_UP == s.mState ) return true; + gAgentCamera.unlockView(); + gAgentCamera.setRollRightKey(get_orbit_rate(s)); + return true; +} + +bool camera_roll_reset(const LLKeyPressState& s) +{ + if (KEYSTATE_UP == s.mState ) return true; + gAgentCamera.resetCameraRoll(); + return true; +} + +bool camera_move_forward( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setOrbitInKey( get_orbit_rate() ); + gAgentCamera.setOrbitInKey( get_orbit_rate(s) ); return true; } -bool camera_move_backward( EKeystate s ) +bool camera_move_backward( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setOrbitOutKey( get_orbit_rate() ); + gAgentCamera.setOrbitOutKey( get_orbit_rate(s) ); return true; } -bool camera_move_forward_sitting( EKeystate s ) +bool camera_move_forward_sitting( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_FORWARD ) return true; + if( KEYSTATE_UP == s.mState && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_FORWARD ) return true; if (gAgent.forwardGrabbed() || gAgentCamera.sitCameraEnabled() || (gAgent.getRunning() && !gAgent.getAlwaysRun())) { agent_push_forward(s); } else { - gAgentCamera.setOrbitInKey( get_orbit_rate() ); + gAgentCamera.setOrbitInKey( get_orbit_rate(s) ); } return true; } -bool camera_move_backward_sitting( EKeystate s ) +bool camera_move_backward_sitting( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_BACKWARD ) return true; + if( KEYSTATE_UP == s.mState && gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_BACKWARD ) return true; if (gAgent.backwardGrabbed() || gAgentCamera.sitCameraEnabled() || (gAgent.getRunning() && !gAgent.getAlwaysRun())) { @@ -492,137 +535,137 @@ bool camera_move_backward_sitting( EKeystate s ) } else { - gAgentCamera.setOrbitOutKey( get_orbit_rate() ); + gAgentCamera.setOrbitOutKey( get_orbit_rate(s) ); } return true; } -bool camera_pan_up( EKeystate s ) +bool camera_pan_up( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setPanUpKey( get_orbit_rate() ); + gAgentCamera.setPanUpKey( get_orbit_rate(s) ); return true; } -bool camera_pan_down( EKeystate s ) +bool camera_pan_down( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setPanDownKey( get_orbit_rate() ); + gAgentCamera.setPanDownKey( get_orbit_rate(s) ); return true; } -bool camera_pan_left( EKeystate s ) +bool camera_pan_left( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setPanLeftKey( get_orbit_rate() ); + gAgentCamera.setPanLeftKey( get_orbit_rate(s) ); return true; } -bool camera_pan_right( EKeystate s ) +bool camera_pan_right( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setPanRightKey( get_orbit_rate() ); + gAgentCamera.setPanRightKey( get_orbit_rate(s) ); return true; } -bool camera_pan_in( EKeystate s ) +bool camera_pan_in( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setPanInKey( get_orbit_rate() ); + gAgentCamera.setPanInKey( get_orbit_rate(s) ); return true; } -bool camera_pan_out( EKeystate s ) +bool camera_pan_out( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); - gAgentCamera.setPanOutKey( get_orbit_rate() ); + gAgentCamera.setPanOutKey( get_orbit_rate(s) ); return true; } -bool camera_move_forward_fast( EKeystate s ) +bool camera_move_forward_fast( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); gAgentCamera.setOrbitInKey(2.5f); return true; } -bool camera_move_backward_fast( EKeystate s ) +bool camera_move_backward_fast( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gAgentCamera.unlockView(); gAgentCamera.setOrbitOutKey(2.5f); return true; } -bool edit_avatar_spin_ccw( EKeystate s ) +bool edit_avatar_spin_ccw( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gMorphView->setCameraDrivenByKeys( true ); - gAgentCamera.setOrbitLeftKey( get_orbit_rate() ); - //gMorphView->orbitLeft( get_orbit_rate() ); + gAgentCamera.setOrbitLeftKey( get_orbit_rate(s) ); + //gMorphView->orbitLeft( get_orbit_rate(s) ); return true; } -bool edit_avatar_spin_cw( EKeystate s ) +bool edit_avatar_spin_cw( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gMorphView->setCameraDrivenByKeys( true ); - gAgentCamera.setOrbitRightKey( get_orbit_rate() ); - //gMorphView->orbitRight( get_orbit_rate() ); + gAgentCamera.setOrbitRightKey( get_orbit_rate(s) ); + //gMorphView->orbitRight( get_orbit_rate(s) ); return true; } -bool edit_avatar_spin_over( EKeystate s ) +bool edit_avatar_spin_over( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gMorphView->setCameraDrivenByKeys( true ); - gAgentCamera.setOrbitUpKey( get_orbit_rate() ); - //gMorphView->orbitUp( get_orbit_rate() ); + gAgentCamera.setOrbitUpKey( get_orbit_rate(s) ); + //gMorphView->orbitUp( get_orbit_rate(s) ); return true; } -bool edit_avatar_spin_under( EKeystate s ) +bool edit_avatar_spin_under( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gMorphView->setCameraDrivenByKeys( true ); - gAgentCamera.setOrbitDownKey( get_orbit_rate() ); - //gMorphView->orbitDown( get_orbit_rate() ); + gAgentCamera.setOrbitDownKey( get_orbit_rate(s) ); + //gMorphView->orbitDown( get_orbit_rate(s) ); return true; } -bool edit_avatar_move_forward( EKeystate s ) +bool edit_avatar_move_forward( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gMorphView->setCameraDrivenByKeys( true ); - gAgentCamera.setOrbitInKey( get_orbit_rate() ); + gAgentCamera.setOrbitInKey( get_orbit_rate(s) ); //gMorphView->orbitIn(); return true; } -bool edit_avatar_move_backward( EKeystate s ) +bool edit_avatar_move_backward( const LLKeyPressState& s ) { - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; gMorphView->setCameraDrivenByKeys( true ); - gAgentCamera.setOrbitOutKey( get_orbit_rate() ); + gAgentCamera.setOrbitOutKey( get_orbit_rate(s) ); //gMorphView->orbitOut(); return true; } -bool stop_moving( EKeystate s ) +bool stop_moving( const LLKeyPressState& s ) { //it's supposed that 'stop moving' key will be held down for some time - if( KEYSTATE_UP == s ) return true; + if( KEYSTATE_UP == s.mState ) return true; // stop agent gAgent.setControlFlags(AGENT_CONTROL_STOP); @@ -631,23 +674,23 @@ bool stop_moving( EKeystate s ) return true; } -bool start_chat( EKeystate s ) +bool start_chat( const LLKeyPressState& s ) { if (LLAppViewer::instance()->quitRequested()) { return true; // can't talk, gotta go, kthxbye! } - if (KEYSTATE_DOWN != s) return true; + if (KEYSTATE_DOWN != s.mState) return true; // start chat LLFloaterIMNearbyChat::startChat(NULL); return true; } -bool start_gesture( EKeystate s ) +bool start_gesture( const LLKeyPressState& s ) { LLUICtrl* focus_ctrlp = dynamic_cast(gFocusMgr.getKeyboardFocus()); - if (KEYSTATE_UP == s && + if (KEYSTATE_UP == s.mState && ! (focus_ctrlp && focus_ctrlp->acceptsTextInput())) { if ((LLFloaterReg::getTypedInstance("nearby_chat"))->getCurrentChat().empty()) @@ -664,9 +707,9 @@ bool start_gesture( EKeystate s ) return true; } -bool run_forward(EKeystate s) +bool run_forward(const LLKeyPressState& s) { - if (KEYSTATE_UP != s) + if (KEYSTATE_UP != s.mState) { if (gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_FORWARD) { @@ -678,7 +721,7 @@ bool run_forward(EKeystate s) gAgent.sendWalkRun(true); } } - else if(KEYSTATE_UP == s) + else if(KEYSTATE_UP == s.mState) { if (gAgent.mDoubleTapRunMode == LLAgent::DOUBLETAP_FORWARD) gAgent.mDoubleTapRunMode = LLAgent::DOUBLETAP_NONE; @@ -689,9 +732,9 @@ bool run_forward(EKeystate s) return true; } -bool run_backward(EKeystate s) +bool run_backward(const LLKeyPressState& s) { - if (KEYSTATE_UP != s) + if (KEYSTATE_UP != s.mState) { if (gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_BACKWARD) { @@ -703,7 +746,7 @@ bool run_backward(EKeystate s) gAgent.sendWalkRun(true); } } - else if (KEYSTATE_UP == s) + else if (KEYSTATE_UP == s.mState) { if (gAgent.mDoubleTapRunMode == LLAgent::DOUBLETAP_BACKWARD) gAgent.mDoubleTapRunMode = LLAgent::DOUBLETAP_NONE; @@ -714,9 +757,9 @@ bool run_backward(EKeystate s) return true; } -bool run_left(EKeystate s) +bool run_left(const LLKeyPressState& s) { - if (KEYSTATE_UP != s) + if (KEYSTATE_UP != s.mState) { if (gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_SLIDELEFT) { @@ -728,7 +771,7 @@ bool run_left(EKeystate s) gAgent.sendWalkRun(true); } } - else if (KEYSTATE_UP == s) + else if (KEYSTATE_UP == s.mState) { if (gAgent.mDoubleTapRunMode == LLAgent::DOUBLETAP_SLIDELEFT) gAgent.mDoubleTapRunMode = LLAgent::DOUBLETAP_NONE; @@ -739,9 +782,9 @@ bool run_left(EKeystate s) return true; } -bool run_right(EKeystate s) +bool run_right(const LLKeyPressState& s) { - if (KEYSTATE_UP != s) + if (KEYSTATE_UP != s.mState) { if (gAgent.mDoubleTapRunMode != LLAgent::DOUBLETAP_SLIDERIGHT) { @@ -753,7 +796,7 @@ bool run_right(EKeystate s) gAgent.sendWalkRun(true); } } - else if (KEYSTATE_UP == s) + else if (KEYSTATE_UP == s.mState) { if (gAgent.mDoubleTapRunMode == LLAgent::DOUBLETAP_SLIDERIGHT) gAgent.mDoubleTapRunMode = LLAgent::DOUBLETAP_NONE; @@ -764,9 +807,9 @@ bool run_right(EKeystate s) return true; } -bool toggle_run(EKeystate s) +bool toggle_run(const LLKeyPressState& s) { - if (KEYSTATE_DOWN != s) return true; + if (KEYSTATE_DOWN != s.mState) return true; bool run = gAgent.getAlwaysRun(); if (run) { @@ -782,9 +825,9 @@ bool toggle_run(EKeystate s) return true; } -bool toggle_sit(EKeystate s) +bool toggle_sit(const LLKeyPressState& s) { - if (KEYSTATE_DOWN != s) return true; + if (KEYSTATE_DOWN != s.mState) return true; if (gAgent.isSitting()) { gAgent.standUp(); @@ -796,25 +839,25 @@ bool toggle_sit(EKeystate s) return true; } -bool toggle_pause_media(EKeystate s) // analogue of play/pause button in top bar +bool toggle_pause_media(const LLKeyPressState& s) // analogue of play/pause button in top bar { - if (KEYSTATE_DOWN != s) return true; + if (KEYSTATE_DOWN != s.mState) return true; bool pause = LLViewerMedia::getInstance()->isAnyMediaPlaying(); LLViewerMedia::getInstance()->setAllMediaPaused(pause); return true; } -bool toggle_enable_media(EKeystate s) +bool toggle_enable_media(const LLKeyPressState& s) { - if (KEYSTATE_DOWN != s) return true; + if (KEYSTATE_DOWN != s.mState) return true; bool pause = LLViewerMedia::getInstance()->isAnyMediaPlaying() || LLViewerMedia::getInstance()->isAnyMediaShowing(); LLViewerMedia::getInstance()->setAllMediaEnabled(!pause); return true; } -bool walk_to(EKeystate s) +bool walk_to(const LLKeyPressState& s) { - if (KEYSTATE_DOWN != s) + if (KEYSTATE_DOWN != s.mState) { // teleport/walk is usually on mouseclick, mouseclick needs // to let AGENT_CONTROL_LBUTTON_UP happen if teleport didn't, @@ -825,29 +868,29 @@ bool walk_to(EKeystate s) return LLToolPie::getInstance()->walkToClickedLocation(); } -bool teleport_to(EKeystate s) +bool teleport_to(const LLKeyPressState& s) { - if (KEYSTATE_DOWN != s) return false; + if (KEYSTATE_DOWN != s.mState) return false; return LLToolPie::getInstance()->teleportToClickedLocation(); } -bool toggle_voice(EKeystate s) +bool toggle_voice(const LLKeyPressState& s) { - if (KEYSTATE_DOWN != s) return true; + if (KEYSTATE_DOWN != s.mState) return true; if (!LLAgent::isActionAllowed("speak")) return false; LLVoiceClient::getInstance()->toggleUserPTTState(); return true; } -bool voice_follow_key(EKeystate s) +bool voice_follow_key(const LLKeyPressState& s) { - if (KEYSTATE_DOWN == s) + if (KEYSTATE_DOWN == s.mState) { if (!LLAgent::isActionAllowed("speak")) return false; LLVoiceClient::getInstance()->setUserPTTState(true); return true; } - else if (KEYSTATE_UP == s && LLVoiceClient::getInstance()->getUserPTTState()) + else if (KEYSTATE_UP == s.mState && LLVoiceClient::getInstance()->getUserPTTState()) { LLVoiceClient::getInstance()->setUserPTTState(false); return true; @@ -855,7 +898,7 @@ bool voice_follow_key(EKeystate s) return false; } -bool script_trigger_lbutton(EKeystate s) +bool script_trigger_lbutton(const LLKeyPressState& s) { // Check for script overriding/expecting left mouse button. // Note that this does not pass event further and depends onto mouselook. @@ -864,7 +907,7 @@ bool script_trigger_lbutton(EKeystate s) if (gAgent.leftButtonGrabbed()) { bool mouselook = gAgentCamera.cameraMouselook(); - switch (s) + switch (s.mState) { case KEYSTATE_DOWN: if (mouselook) @@ -893,11 +936,56 @@ bool script_trigger_lbutton(EKeystate s) return false; } +bool agent_pitch(const LLKeyPressState& s, F32 direction) +{ + if(!gAgentCamera.cameraMouselook() || KEYSTATE_UP == s.mState) + { + return false; + } + // const F32 NOMINAL_MOUSE_SENSITIVITY = 0.0025f; + + // static LLCachedControl mouse_sensitivity_setting(gSavedSettings, "MouseSensitivity"); + // F32 mouse_sensitivity = clamp_rescale(mouse_sensitivity_setting, 0.f, 15.f, 0.5f, 2.75f) * NOMINAL_MOUSE_SENSITIVITY; + + const F32 fov = LLViewerCamera::getInstance()->getView() / DEFAULT_FIELD_OF_VIEW; + // pitch (vertical) + gAgent.pitch(s.mValue * fov * direction * g_deltaTime); // or -dy if InvertMouse + return true; +} + +bool agent_pitch_forward(const LLKeyPressState& s) +{ + return agent_pitch(s,1); +} + +bool agent_pitch_backward(const LLKeyPressState& s) +{ + return agent_pitch(s,-1); +} + +bool agent_spin_over( const LLKeyPressState& s ) +{ + if( KEYSTATE_UP == s.mState ) return true; + //change camera but do not send keystrokes + gAgentCamera.setOrbitUpKey( get_orbit_rate(s) ); + return true; +} + + +bool agent_spin_under( const LLKeyPressState& s ) +{ + if( KEYSTATE_UP == s.mState ) return true; + //change camera but do not send keystrokes + gAgentCamera.setOrbitDownKey( get_orbit_rate(s) ); + return true; +} + + // Used by scripts, for overriding/handling left mouse button // see mControlsTakenCount -bool agent_control_lbutton_handle(EKeystate s) +bool agent_control_lbutton_handle(const LLKeyPressState& s) { - switch (s) + switch (s.mState) { case KEYSTATE_DOWN: gAgent.setControlFlags(AGENT_CONTROL_LBUTTON_DOWN); @@ -912,13 +1000,13 @@ bool agent_control_lbutton_handle(EKeystate s) } template -bool game_control_button(EKeystate s) +bool game_control_button(const LLKeyPressState& s) { - if (s == KEYSTATE_DOWN) + if ( KEYSTATE_DOWN == s.mState ) { gAgent.pressGameControlButton(N); } - else if ( s == KEYSTATE_UP) + else if ( KEYSTATE_UP == s.mState ) { gAgent.releaseGameControlButton(N); } @@ -948,6 +1036,10 @@ REGISTER_KEYBOARD_ACTION("spin_over", camera_spin_over); REGISTER_KEYBOARD_ACTION("spin_under", camera_spin_under); REGISTER_KEYBOARD_ACTION("spin_over_sitting", camera_spin_over_sitting); REGISTER_KEYBOARD_ACTION("spin_under_sitting", camera_spin_under_sitting); +REGISTER_KEYBOARD_ACTION("pitch_forward", agent_pitch_forward); +REGISTER_KEYBOARD_ACTION("pitch_backward", agent_pitch_backward); +REGISTER_KEYBOARD_ACTION("spin_over_agent", agent_spin_over); +REGISTER_KEYBOARD_ACTION("spin_under_agent", agent_spin_under); REGISTER_KEYBOARD_ACTION("move_forward", camera_move_forward); REGISTER_KEYBOARD_ACTION("move_backward", camera_move_backward); REGISTER_KEYBOARD_ACTION("move_forward_sitting", camera_move_forward_sitting); @@ -974,6 +1066,7 @@ REGISTER_KEYBOARD_ACTION("run_backward", run_backward); REGISTER_KEYBOARD_ACTION("run_left", run_left); REGISTER_KEYBOARD_ACTION("run_right", run_right); REGISTER_KEYBOARD_ACTION("toggle_run", toggle_run); +REGISTER_KEYBOARD_ACTION("toggle_down", agent_toggle_down); REGISTER_KEYBOARD_ACTION("toggle_sit", toggle_sit); REGISTER_KEYBOARD_ACTION("toggle_pause_media", toggle_pause_media); REGISTER_KEYBOARD_ACTION("toggle_enable_media", toggle_enable_media); @@ -982,6 +1075,9 @@ REGISTER_KEYBOARD_ACTION("walk_to", walk_to); REGISTER_KEYBOARD_GLOBAL_ACTION("toggle_voice", toggle_voice); REGISTER_KEYBOARD_GLOBAL_ACTION("voice_follow_key", voice_follow_key); REGISTER_KEYBOARD_ACTION(script_mouse_handler_name, script_trigger_lbutton); +REGISTER_KEYBOARD_ACTION("roll_left", camera_roll_left); +REGISTER_KEYBOARD_ACTION("roll_right", camera_roll_right); +REGISTER_KEYBOARD_ACTION("roll_reset", camera_roll_reset); REGISTER_KEYBOARD_ACTION("game_control_button_0", game_control_button<0>); REGISTER_KEYBOARD_ACTION("game_control_button_1", game_control_button<1>); REGISTER_KEYBOARD_ACTION("game_control_button_2", game_control_button<2>); @@ -1219,8 +1315,7 @@ bool LLViewerInput::handleGlobalBindsMouse(EMouseClickType clicktype, MASK mask, bool LLViewerInput::bindKey(const S32 mode, const KEY key, const MASK mask, const std::string& function_name) { size_t index; - typedef std::function function_t; - function_t function = nullptr; + LLKeyFunc function = nullptr; std::string name; // Allow remapping of F2-F12 @@ -1309,8 +1404,7 @@ bool LLViewerInput::bindKey(const S32 mode, const KEY key, const MASK mask, cons bool LLViewerInput::bindMouse(const S32 mode, const EMouseClickType mouse, const MASK mask, const std::string& function_name) { size_t index; - typedef std::function function_t; - function_t function = nullptr; + LLKeyFunc function = nullptr; if (mouse == CLICK_LEFT && mask == MASK_NONE @@ -1388,11 +1482,79 @@ bool LLViewerInput::bindMouse(const S32 mode, const EMouseClickType mouse, const return true; } +bool LLViewerInput::bindController(const S32 mode, const LLGameControl::ActionType actionType, U8 action, const std::string& function_name) +{ + size_t index; + LLKeyFunc function = nullptr; + + LLKeybindFunctionData* result = LLKeyboardActionRegistry::getValue(function_name); + if (result) + { + function = result->mFunction; + } + + if (!function) + { + LL_WARNS_ONCE() << "Can't bind mouse key to function " << function_name << ", no function with this name found" << LL_ENDL; + return false; + } + + if (mode >= MODE_COUNT) + { + LL_ERRS() << "LLKeyboard::bindKey() - unknown mode passed" << mode << LL_ENDL; + return false; + } + + // check for duplicate first and overwrite + if (result->mIsGlobal) + { + auto size = mGlobalControllerBindings[mode].size(); + for (index = 0; index < size; index++) + { + if (actionType == mGlobalControllerBindings[mode][index].mActionType && action == mGlobalControllerBindings[mode][index].mAction) + { + mGlobalControllerBindings[mode][index].mFunction = function; + return true; + } + } + } + else + { + auto size = mControllerBindings[mode].size(); + for (index = 0; index < size; index++) + { + if (actionType == mControllerBindings[mode][index].mActionType && action == mControllerBindings[mode][index].mAction) + { + mControllerBindings[mode][index].mFunction = function; + return true; + } + } + } + + LLControllerBinding bind; + bind.mActionType = actionType; + bind.mAction = action; + bind.mFunction = function; + bind.mFunctionName = function_name; + + if (result->mIsGlobal) + { + mGlobalControllerBindings[mode].push_back(bind); + } + else + { + mControllerBindings[mode].push_back(bind); + } + + return true; +} + LLViewerInput::KeyBinding::KeyBinding() : key("key"), mouse("mouse"), mask("mask"), - command("command") + command("command"), + controller("controller") {} LLViewerInput::KeyMode::KeyMode() @@ -1413,8 +1575,10 @@ void LLViewerInput::resetBindings() { mGlobalKeyBindings[i].clear(); mGlobalMouseBindings[i].clear(); + mGlobalControllerBindings[i].clear(); mKeyBindings[i].clear(); mMouseBindings[i].clear(); + mControllerBindings[i].clear(); mLMouseDefaultHandling[i] = false; } } @@ -1525,6 +1689,16 @@ bool compare_mouse_by_mask(LLMouseBinding i1, LLMouseBinding i2) return (count_masks(i1.mMask) > count_masks(i2.mMask)); } +void LLViewerInput::storeDeltaTime() +{ + // measure delta time and frame + // Note: it is possible for the deltas to be very large + // and it is the duty of the code that uses them to clamp as necessary + U64 now = LLFrameTimer::getTotalTime(); + g_deltaTime = F32(now - g_lastUpdateTime) / (F32)(USEC_PER_SEC); + g_lastUpdateTime = now; +} + S32 LLViewerInput::loadBindingMode(const LLViewerInput::KeyMode& keymode, S32 mode) { S32 binding_count = 0; @@ -1567,6 +1741,20 @@ S32 LLViewerInput::loadBindingMode(const LLViewerInput::KeyMode& keymode, S32 mo LL_WARNS_ONCE() << "There might be issues in keybindings' file" << LL_ENDL; } } + if(!processed && it->controller.isProvided() && !it->controller.getValue().empty()) + { + LLGameControl::ActionType actionType; + U8 action; + if(LLGameControl::actionFromString(it->controller.getValue(), actionType, action)) + { + bindController(mode, actionType, action, it->command); + processed = true; + } + else + { + LL_WARNS_ONCE() << "There might be issues in keybindings' file" << LL_ENDL; + } + } if (processed) { // total @@ -1610,29 +1798,38 @@ bool LLViewerInput::scanKey(const std::vector &binding, bool key_level, bool repeat) const { + // TODO implement time + LLKeyPressState state = LLKeyPressState(key_up, key_down, key_level, 1.0); + state.mRepeat = repeat; + for (S32 i = 0; i < binding_count; i++) { if (binding[i].mKey == key) { if ((binding[i].mMask & mask) == binding[i].mMask) { + state.mElapsedTime = gKeyboard->getCurKeyElapsedTime(); + state.mElapsedFrames = gKeyboard->getCurKeyElapsedFrameCount(); bool res = false; if (key_down && !repeat) { // ...key went down this frame, call function - res = binding[i].mFunction( KEYSTATE_DOWN ); + state.mState = KEYSTATE_DOWN; + res = binding[i].mFunction( state ); return true; } else if (key_up) { // ...key went down this frame, call function - res = binding[i].mFunction( KEYSTATE_UP ); + state.mState = KEYSTATE_UP; + res = binding[i].mFunction( state ); } else if (key_level) { // ...key held down from previous frame // Not windows, just call the function. - res = binding[i].mFunction( KEYSTATE_LEVEL ); + state.mState = KEYSTATE_LEVEL; + res = binding[i].mFunction( state ); }//if // Key+Mask combinations are supposed to be unique, so we won't find anything else return res; @@ -1667,6 +1864,82 @@ bool LLViewerInput::scanKey(KEY key, bool key_down, bool key_up, bool key_level) return res; } +bool LLViewerInput::scanControllerInput(const LLGameControl::ActionType actionType, const U8 action, const LLKeyPressState& state) const +{ + S32 mode = getMode(); + + for(auto binding : mControllerBindings[mode]) + { + if (binding.mActionType != actionType || binding.mAction != action) + { + continue; + } + + LL_WARNS() << "Controller Val " << LLGameControl::stringFromAction(actionType, action) << state.mValue << LL_ENDL; + + bool res = binding.mFunction(state); + if (state.mDown) + { + return true; + } + return res; + } + return false; +} + +void LLViewerInput::scanController() +{ + LLGameControl::computeFinalState(); + const LLGameControl::State& state = LLGameControl::getState(); + // HACK: GameControl: supply hard-coded threshold for ON/OFF zones + constexpr U16 AXIS_THRESHOLD = 32768 / 8; + + // TODO: GameControl: This is kind of inificient, it would be more efficient to + // loop the bindings and check if the keys/axes they want are active + // rather than looping all keys/axes then all bindings for each one + // But copying scanMouse pattern for now. + + LLKeyPressState keyState = LLKeyPressState(true, false, false, 0.0); + keyState.mAnalog = true; + + for(U8 i = 0;i < state.mAxes.size();i++) + { + U16 current = state.mAxes[i]; + U16 prev = state.mPrevAxes[i]; + keyState.mDown = prev < AXIS_THRESHOLD && current >= AXIS_THRESHOLD; + keyState.mLevel = prev >= AXIS_THRESHOLD && current >= AXIS_THRESHOLD; + keyState.mUp = prev >= AXIS_THRESHOLD && current < AXIS_THRESHOLD; + + if(keyState.mDown || keyState.mLevel || keyState.mUp) + { + keyState.mElapsedTime = LLGameControl::getControllerHeldTime(LLGameControl::ActionType::DOF, i); + keyState.mElapsedFrames = LLGameControl::getControllerHeldFrames(LLGameControl::ActionType::DOF, i); + keyState.mValue = std::clamp(((F32)current) / 32767.0,0.0,1.0); + keyState.mState = keyState.mDown ? KEYSTATE_DOWN : (keyState.mLevel ? KEYSTATE_LEVEL : KEYSTATE_UP); + scanControllerInput(LLGameControl::ActionType::DOF, i, keyState); + } + } + + keyState.mAnalog = false; + for(U8 i = 0; i < LLGameControl::Button::NUM_BUTTONS; i++) + { + U32 flag = 1 << i; + bool current = (state.mButtons & flag) == flag; + bool prev = (state.mPrevButtons & flag) == flag; + if(current || prev) + { + keyState.mDown = current && !prev; + keyState.mLevel = current && prev; + keyState.mUp = !current && prev; + keyState.mElapsedTime = LLGameControl::getControllerHeldTime(LLGameControl::ActionType::BUTTON, i); + keyState.mElapsedFrames = LLGameControl::getControllerHeldFrames(LLGameControl::ActionType::BUTTON, i); + keyState.mValue = current ? 1.0 : 0.0; + keyState.mState = keyState.mDown ? KEYSTATE_DOWN : (keyState.mLevel ? KEYSTATE_LEVEL : KEYSTATE_UP); + scanControllerInput(LLGameControl::ActionType::BUTTON, i, keyState); + } + } +} + bool LLViewerInput::handleMouse(LLWindow *window_impl, LLCoordGL pos, MASK mask, EMouseClickType clicktype, bool down) { bool is_toolmgr_action = false; @@ -1786,6 +2059,7 @@ bool LLViewerInput::scanMouse( bool ignore_additional_masks ) const { + LLKeyPressState keyState = LLKeyPressState(false,false,false,1.0); for (S32 i = 0; i < binding_count; i++) { if (binding[i].mMouse == mouse && (ignore_additional_masks ? (binding[i].mMask & mask) == binding[i].mMask : binding[i].mMask == mask)) @@ -1794,20 +2068,31 @@ bool LLViewerInput::scanMouse( switch (state) { case MOUSE_STATE_DOWN: - res = binding[i].mFunction(KEYSTATE_DOWN); + keyState.mDown = true; + keyState.mState = KEYSTATE_DOWN; + res = binding[i].mFunction(keyState); break; case MOUSE_STATE_CLICK: // Button went down and up in scope of single frame // might not work best with some functions, // but some function need specific states specifically - res = binding[i].mFunction(KEYSTATE_DOWN); - res |= binding[i].mFunction(KEYSTATE_UP); + keyState.mDown = true; + keyState.mState = KEYSTATE_DOWN; + res = binding[i].mFunction(keyState); + keyState.mDown = false; + keyState.mUp = true; + keyState.mState = KEYSTATE_UP; + res |= binding[i].mFunction(keyState); break; case MOUSE_STATE_LEVEL: - res = binding[i].mFunction(KEYSTATE_LEVEL); + keyState.mLevel = true; + keyState.mState = KEYSTATE_LEVEL; + res = binding[i].mFunction(keyState); break; case MOUSE_STATE_UP: - res = binding[i].mFunction(KEYSTATE_UP); + keyState.mUp = true; + keyState.mState = KEYSTATE_UP; + res = binding[i].mFunction(keyState); break; default: break; @@ -1819,7 +2104,7 @@ bool LLViewerInput::scanMouse( return false; } -// todo: this recods key, scanMouse() triggers functions with EKeystate +// todo: this recods key, scanMouse() triggers functions with LLKeyPressState bool LLViewerInput::scanMouse(EMouseClickType click, EMouseState state) const { bool res = false; @@ -1834,6 +2119,8 @@ bool LLViewerInput::scanMouse(EMouseClickType click, EMouseState state) const // and sends AGENT_CONTROL_ML_LBUTTON_DOWN, but it only applies if ML controls // are leftButtonGrabbed(), send a normal click otherwise. + LLKeyPressState keyState = LLKeyPressState(false,false,false,1.0); + if (!res && mLMouseDefaultHandling[mode] && (mode != MODE_FIRST_PERSON || !gAgent.leftButtonGrabbed()) @@ -1843,18 +2130,27 @@ bool LLViewerInput::scanMouse(EMouseClickType click, EMouseState state) const switch (state) { case MOUSE_STATE_DOWN: - agent_control_lbutton_handle(KEYSTATE_DOWN); + keyState.mDown = true; + keyState.mState = KEYSTATE_DOWN; + agent_control_lbutton_handle(keyState); res = true; break; case MOUSE_STATE_CLICK: // might not work best with some functions, // but some function need specific states too specifically - agent_control_lbutton_handle(KEYSTATE_DOWN); - agent_control_lbutton_handle(KEYSTATE_UP); + keyState.mDown = true; + keyState.mState = KEYSTATE_DOWN; + agent_control_lbutton_handle(keyState); + keyState.mDown = false; + keyState.mUp = true; + keyState.mState = KEYSTATE_UP; + agent_control_lbutton_handle(keyState); res = true; break; case MOUSE_STATE_UP: - agent_control_lbutton_handle(KEYSTATE_UP); + keyState.mUp = true; + keyState.mState = KEYSTATE_UP; + agent_control_lbutton_handle(keyState); res = true; break; default: diff --git a/indra/newview/llviewerinput.h b/indra/newview/llviewerinput.h index 80ba99e201..c6b4c6e7cc 100644 --- a/indra/newview/llviewerinput.h +++ b/indra/newview/llviewerinput.h @@ -28,6 +28,7 @@ #define LL_LLVIEWERINPUT_H #include "llkeyboard.h" // For EKeystate +#include "llgamecontrol.h" // For LLGameControl::ActionType const S32 MAX_KEY_BINDINGS = 128; // was 60 const S32 keybindings_xml_version = 1; @@ -65,6 +66,15 @@ public: std::string mFunctionName; }; +class LLControllerBinding +{ + public: + LLGameControl::ActionType mActionType; + U8 mAction; + LLKeyFunc mFunction; + std::string mFunctionName; +}; + typedef enum e_keyboard_mode { @@ -84,6 +94,7 @@ public: mask, command; Optional mouse; // Note, not mandatory for the sake of backward campatibility with keys.xml + Optional controller; KeyBinding(); }; @@ -108,6 +119,8 @@ public: LLViewerInput(); virtual ~LLViewerInput(); + void storeDeltaTime(); + bool handleKey(KEY key, MASK mask, bool repeated); bool handleKeyUp(KEY key, MASK mask); @@ -132,6 +145,7 @@ public: // handleMouse() records state, scanMouse() goes through states, scanMouse(click) processes individual saved states after UI is done with them bool handleMouse(LLWindow *window_impl, LLCoordGL pos, MASK mask, EMouseClickType clicktype, bool down); void scanMouse(); + void scanController(); bool isMouseBindUsed(const EMouseClickType mouse, const MASK mask, const S32 mode) const; bool isLMouseHandlingDefault(const S32 mode) const { return mLMouseDefaultHandling[mode]; } @@ -165,9 +179,12 @@ private: EMouseState state, bool ignore_additional_masks) const; + bool scanControllerInput(const LLGameControl::ActionType actionType, const U8 action, const LLKeyPressState& state) const; + S32 loadBindingMode(const LLViewerInput::KeyMode& keymode, S32 mode); bool bindKey(const S32 mode, const KEY key, const MASK mask, const std::string& function_name); bool bindMouse(const S32 mode, const EMouseClickType mouse, const MASK mask, const std::string& function_name); + bool bindController(const S32 mode, const LLGameControl::ActionType actionType, U8 action, const std::string& function_name); void resetBindings(); // Hold all the ugly stuff torn out to make LLKeyboard non-viewer-specific here @@ -175,13 +192,15 @@ private: // TODO: at some point it is better to remake this, especially keyaboard part // would be much better to send to functions actual state of the button than // to send what we think function wants based on collection of bools (mKeyRepeated, mKeyLevel, mKeyDown) - std::vector mKeyBindings[MODE_COUNT]; - std::vector mMouseBindings[MODE_COUNT]; - bool mLMouseDefaultHandling[MODE_COUNT]; // Due to having special priority + std::vector mKeyBindings[MODE_COUNT]; + std::vector mMouseBindings[MODE_COUNT]; + std::vector mControllerBindings[MODE_COUNT]; + bool mLMouseDefaultHandling[MODE_COUNT]; // Due to having special priority // keybindings that do not consume event and are handled earlier, before floaters - std::vector mGlobalKeyBindings[MODE_COUNT]; - std::vector mGlobalMouseBindings[MODE_COUNT]; + std::vector mGlobalKeyBindings[MODE_COUNT]; + std::vector mGlobalMouseBindings[MODE_COUNT]; + std::vector mGlobalControllerBindings[MODE_COUNT]; typedef std::map key_remap_t; key_remap_t mRemapKeys[MODE_COUNT]; diff --git a/indra/newview/llviewerjoystick.cpp b/indra/newview/llviewerjoystick.cpp index b08cfae09d..9a047aa513 100644 --- a/indra/newview/llviewerjoystick.cpp +++ b/indra/newview/llviewerjoystick.cpp @@ -1370,7 +1370,7 @@ void LLViewerJoystick::scanJoystick() { if (mDriverState != JDS_INITIALIZED || !gSavedSettings.getBOOL("JoystickEnabled") - || (!mDeviceIs3DConnexion && LLGameControl::isEnabled())) + || !mDeviceIs3DConnexion) { return; } diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index be75d0b8b8..410b723aae 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -81,7 +81,6 @@ #include "llfloatertools.h" #include "llfloaterworldmap.h" #include "llfloaterbuildoptions.h" -#include "llgamecontrol.h" #include "llgroupmgr.h" #include "lllandmarkactions.h" #include "lltooltip.h" @@ -1024,12 +1023,6 @@ class LLAdvancedToggleExperiment : public view_listener_t bool handleEvent(const LLSD& userdata) { std::string feature = userdata.asString(); - if (feature == "GameControl") - { - LLGameControl::setEnabled(! LLGameControl::isEnabled()); - LLFloaterPreference::refreshInstance(); - return true; - } return false; } }; @@ -1040,10 +1033,6 @@ class LLAdvancedCheckExperiment : public view_listener_t { bool value = false; std::string feature = userdata.asString(); - if (feature == "GameControl") - { - value = LLGameControl::isEnabled(); - } return value; } }; @@ -6898,8 +6887,8 @@ class LLAvatarResetSkeleton : public view_listener_t } else { - avatar->resetSkeleton(false); - } + avatar->resetSkeleton(false); + } } return true; } @@ -6964,9 +6953,9 @@ class LLAvatarResetSelfSkeletonAndAnimations : public view_listener_t effectp->setSourceObject(gAgentAvatarp); effectp->setTargetObject((LLViewerObject*)avatar); effectp->setResetAnimations(true); - } - else - { + } + else + { avatar->resetSkeleton(true); } } diff --git a/indra/newview/llviewerwindow.cpp b/indra/newview/llviewerwindow.cpp index dfd03ece35..f32c0f3940 100644 --- a/indra/newview/llviewerwindow.cpp +++ b/indra/newview/llviewerwindow.cpp @@ -114,6 +114,7 @@ #include "llfloaterworldmap.h" #include "llfocusmgr.h" #include "llfontfreetype.h" +#include "llgamecontrol.h" #include "llgesturemgr.h" #include "llglheaders.h" #include "llhudmanager.h" @@ -763,6 +764,50 @@ public: av_iter++; } } + static LLCachedControl debug_show_game_controller_state(gSavedSettings, "DebugShowControllerState", false); + if (debug_show_game_controller_state()) + { + static const char* axis_names[LLGameControl::NUM_MOVE_DIRS] = { + "LEFT","RIGHT","FORWARD","BACKWARD", "TURN LEFT", "TURN RIGHT", + "LOOK UP","LOOK DOWN", "UP", "DOWN", "ROLL LEFT", "ROLL RIGHT" + }; + static const char* button_names[LLGameControl::NUM_BUTTONS] = { + "A", "B", "X", "Y", "BACK", "SELECT", "START", "LSTICK", "RSTICK", + "LSHOULDER", "RSHOULDER", "UP", "DOWN", "LEFT", "RIGHT", + "MISC1", "PADDLE1", "PADDLE2", "PADDLE3", "PADDLE4", "TOUCHPAD", + "B21", "B22", "B23", "B24", "B25", "B26", "B27", "B28", "B29", "B30", "B31", + }; + const LLGameControl::State& gc_state = LLGameControl::getState(); + for (size_t i = 0; i < gc_state.mAxes.size(); i+=2) + { + addText(xpos, ypos, llformat( + " %-10s %6d %-10s %6d", + (i < LLGameControl::NUM_MOVE_DIRS) ? axis_names[i] : "AXIS", + (S32)gc_state.mAxes[i], + ((i+1) < LLGameControl::NUM_MOVE_DIRS) ? axis_names[i+1] : "AXIS", + i < gc_state.mAxes.size() ? (S32)gc_state.mAxes[i+1] : 0 + )); + ypos += y_inc; + } + addText(xpos, ypos, "Game Controller Final State Axes"); + ypos += y_inc; + static const char* pressed = "░░░░░░░░░░"; + static const char* released = " "; + for (U8 i = 0; i < LLGameControl::NUM_BUTTONS; i += 4) + { + addText(xpos, ypos, llformat( + " %-10s %-10s %-10s %-10s", + gc_state.mButtons & (1 << (i+0)) ? pressed : released, + gc_state.mButtons & (1 << (i+1)) ? pressed : released, + gc_state.mButtons & (1 << (i+2)) ? pressed : released, + gc_state.mButtons & (1 << (i+3)) ? pressed : released + )); + addText(xpos, ypos, llformat(" %-10s %-10s %-10s %-10s", button_names[i],button_names[i+1],button_names[i+2],button_names[i+3])); + ypos += y_inc; + } + addText(xpos, ypos, "Game Controller Final State Buttons"); + ypos += y_inc; + } static LLCachedControl debug_show_render_matrices(gSavedSettings, "DebugShowRenderMatrices", false); if (debug_show_render_matrices()) { diff --git a/indra/newview/skins/default/xui/en/control_table_contents_game_control.xml b/indra/newview/skins/default/xui/en/control_table_contents_game_control.xml index a3a2d66e5e..21b8f87633 100644 --- a/indra/newview/skins/default/xui/en/control_table_contents_game_control.xml +++ b/indra/newview/skins/default/xui/en/control_table_contents_game_control.xml @@ -213,6 +213,16 @@ name="lst_action" value="BUTTON_19" /> + + + diff --git a/indra/newview/skins/default/xui/en/control_table_contents_movement.xml b/indra/newview/skins/default/xui/en/control_table_contents_movement.xml index c9a1c4654e..124e0dd95c 100644 --- a/indra/newview/skins/default/xui/en/control_table_contents_movement.xml +++ b/indra/newview/skins/default/xui/en/control_table_contents_movement.xml @@ -83,7 +83,7 @@ font="SansSerif" halign="left" name="lst_action" - value="Strafe left" /> + value="Strafe Left" /> + value="Strafe Right" /> diff --git a/indra/newview/skins/default/xui/en/game_control_table_rows.xml b/indra/newview/skins/default/xui/en/game_control_table_rows.xml index 1d44717f97..8d3f2b14a5 100644 --- a/indra/newview/skins/default/xui/en/game_control_table_rows.xml +++ b/indra/newview/skins/default/xui/en/game_control_table_rows.xml @@ -41,7 +41,17 @@ font="SansSerif" halign="left" name="action" - value="Jump Up" /> + value="Jump/Up" /> + + + - + - - @@ -950,7 +950,7 @@ function="World.EnvSettings" parameter="sunrise" /> @@ -1027,7 +1027,7 @@ function="World.EnvSettings" parameter="my_environs" /> - + @@ -1043,7 +1043,7 @@ function="World.EnvSettings" parameter="pause_clouds" /> @@ -1056,7 +1056,7 @@ - - +--> + - + - + - + @@ -1635,7 +1635,7 @@ function="World.EnvPreset" function="ToggleControl" parameter="RenderDisablePostProcessing" /> - + - + - + + parameter="https://support.secondlife.com/"/> - + @@ -2586,22 +2586,6 @@ function="World.EnvPreset" name="Develop" tear_off="true" visible="false"> - - - - - - - + + + + + @@ -3609,7 +3603,7 @@ function="World.EnvPreset" - + - + - - - - diff --git a/indra/newview/skins/default/xui/en/panel_preferences_game_control.xml b/indra/newview/skins/default/xui/en/panel_preferences_game_control.xml index fb6cc8dc14..c4aae72d71 100644 --- a/indra/newview/skins/default/xui/en/panel_preferences_game_control.xml +++ b/indra/newview/skins/default/xui/en/panel_preferences_game_control.xml @@ -11,65 +11,119 @@ width="517"> - - + follows="top|left" + height="16" + top="8" + left="10"/> + bottom="-2"> + - + + + + + +