/**
 * @file llviewerjoystick.cpp
 * @brief Joystick / NDOF device functionality.
 *
 * $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$
 */

#include "llviewerprecompiledheaders.h"

#include "llviewerjoystick.h"

#include "llviewercontrol.h"
#include "llviewerwindow.h"
#include "llviewercamera.h"
#include "llappviewer.h"
#include "llkeyboard.h"
#include "lltoolmgr.h"
#include "llselectmgr.h"
#include "llviewermenu.h"
#include "llviewerwindow.h"
#include "llwindow.h"
#include "llagent.h"
#include "llagentcamera.h"
#include "llfocusmgr.h"

#if LL_WINDOWS && !LL_MESA_HEADLESS
// Require DirectInput version 8
#define DIRECTINPUT_VERSION 0x0800

#include <dinput.h>
#endif


// ----------------------------------------------------------------------------
// Constants

#define  X_I    1
#define  Y_I    2
#define  Z_I    0
#define RX_I    4
#define RY_I    5
#define RZ_I    3

F32  LLViewerJoystick::sLastDelta[] = {0,0,0,0,0,0,0};
F32  LLViewerJoystick::sDelta[] = {0,0,0,0,0,0,0};

// These constants specify the maximum absolute value coming in from the device.
// HACK ALERT! the value of MAX_JOYSTICK_INPUT_VALUE is not arbitrary as it
// should be.  It has to be equal to 3000 because the SpaceNavigator on Windows
// refuses to respond to the DirectInput SetProperty call; it always returns
// values in the [-3000, 3000] range.
#define MAX_SPACENAVIGATOR_INPUT  3000.0f
#define MAX_JOYSTICK_INPUT_VALUE  MAX_SPACENAVIGATOR_INPUT


#if LIB_NDOF
std::ostream& operator<<(std::ostream& out, NDOF_Device* ptr)
{
    if (! ptr)
    {
        return out << "nullptr";
    }
    out << "NDOF_Device{ ";
    out << "axes [";
    const char* delim = "";
    for (short axis = 0; axis < ptr->axes_count; ++axis)
    {
        out << delim << ptr->axes[axis];
        delim = ", ";
    }
    out << "]";
    out << ", buttons [";
    delim = "";
    for (short button = 0; button < ptr->btn_count; ++button)
    {
        out << delim << ptr->buttons[button];
        delim = ", ";
    }
    out << "]";
    out << ", range " << ptr->axes_min << ':' << ptr->axes_max;
    // If we don't coerce these to unsigned, they're streamed as characters,
    // e.g. ctrl-A or nul.
    out << ", absolute " << unsigned(ptr->absolute);
    out << ", valid " << unsigned(ptr->valid);
    out << ", manufacturer '" << ptr->manufacturer << "'";
    out << ", product '" << ptr->product << "'";
    out << ", private " << ptr->private_data;
    out << " }";
    return out;
}
#endif // LIB_NDOF


#if LL_WINDOWS && !LL_MESA_HEADLESS
// this should reflect ndof and set axises, see ndofdev_win.cpp from ndof package
BOOL CALLBACK EnumObjectsCallback(const DIDEVICEOBJECTINSTANCE* inst, VOID* user_data)
{
    if (inst->dwType & DIDFT_AXIS)
    {
        LPDIRECTINPUTDEVICE8 device = *((LPDIRECTINPUTDEVICE8 *)user_data);
        DIPROPRANGE diprg;
        diprg.diph.dwSize = sizeof(DIPROPRANGE);
        diprg.diph.dwHeaderSize = sizeof(DIPROPHEADER);
        diprg.diph.dwHow = DIPH_BYID;
        diprg.diph.dwObj = inst->dwType; // specify the enumerated axis

        // Set the range for the axis
        diprg.lMin = (long)-MAX_JOYSTICK_INPUT_VALUE;
        diprg.lMax = (long)+MAX_JOYSTICK_INPUT_VALUE;
        HRESULT hr = device->SetProperty(DIPROP_RANGE, &diprg.diph);

        if (FAILED(hr))
        {
            return DIENUM_STOP;
        }
    }

    return DIENUM_CONTINUE;
}

BOOL CALLBACK di8_devices_callback(LPCDIDEVICEINSTANCE device_instance_ptr, LPVOID pvRef)
{
    // Note: If a single device can function as more than one DirectInput
    // device type, it is enumerated as each device type that it supports.
    // Capable of detecting devices like Oculus Rift
    if (device_instance_ptr)
    {
        std::string product_name = utf16str_to_utf8str(llutf16string(device_instance_ptr->tszProductName));

        LLSD guid = LLViewerJoystick::getInstance()->getDeviceUUID();

        bool init_device = false;
        if (guid.isBinary())
        {
            std::vector<U8> bin_bucket = guid.asBinary();
            init_device = memcmp(&bin_bucket[0], &device_instance_ptr->guidInstance, sizeof(GUID)) == 0;
        }
        else
        {
            // It might be better to init space navigator here, but if system doesn't has one,
            // ndof will pick a random device, it is simpler to pick first device now to have an id
            init_device = true;
        }

        if (init_device)
        {
            LL_DEBUGS("Joystick") << "Found and attempting to use device: " << product_name << LL_ENDL;
            LPDIRECTINPUT8       di8_interface = *((LPDIRECTINPUT8 *)gViewerWindow->getWindow()->getDirectInput8());
            LPDIRECTINPUTDEVICE8 device = NULL;

            HRESULT status = di8_interface->CreateDevice(
                device_instance_ptr->guidInstance, // REFGUID rguid,
                &device,                           // LPDIRECTINPUTDEVICE * lplpDirectInputDevice,
                NULL                               // LPUNKNOWN pUnkOuter
                );

            if (status == DI_OK)
            {
                // prerequisite for aquire()
                LL_DEBUGS("Joystick") << "Device created" << LL_ENDL;
                status = device->SetDataFormat(&c_dfDIJoystick); // c_dfDIJoystick2
            }

            if (status == DI_OK)
            {
                // set properties
                LL_DEBUGS("Joystick") << "Format set" << LL_ENDL;
                status = device->EnumObjects(EnumObjectsCallback, &device, DIDFT_ALL);
            }

            if (status == DI_OK)
            {
                LL_DEBUGS("Joystick") << "Properties updated" << LL_ENDL;

                S32 size = sizeof(GUID);
                LLSD::Binary data; //just an std::vector
                data.resize(size);
                memcpy(&data[0], &device_instance_ptr->guidInstance /*POD _GUID*/, size);
                LLViewerJoystick::getInstance()->initDevice(&device, product_name, LLSD(data));
                return DIENUM_STOP;
            }
        }
        else
        {
            LL_DEBUGS("Joystick") << "Found device: " << product_name << LL_ENDL;
        }
    }
    return DIENUM_CONTINUE;
}

// Windows guids
// This is GUID2 so teoretically it can be memcpy copied into LLUUID
void guid_from_string(GUID &guid, const std::string &input)
{
    CLSIDFromString(utf8str_to_utf16str(input).c_str(), &guid);
}

std::string string_from_guid(const GUID &guid)
{
    OLECHAR* guidString; //wchat
    StringFromCLSID(guid, &guidString);

    // use guidString...

    std::string res = utf16str_to_utf8str(llutf16string(guidString));
    // ensure memory is freed
    ::CoTaskMemFree(guidString);

    return res;
}
#elif LL_DARWIN

bool macos_devices_callback(std::string &product_name, LLSD &data, void* userdata)
{
    std::string product = data["product"].asString();

    return LLViewerJoystick::getInstance()->initDevice(nullptr, product, data);
}

#endif


// -----------------------------------------------------------------------------
void LLViewerJoystick::updateEnabled(bool autoenable)
{
    if (mDriverState == JDS_UNINITIALIZED)
    {
        gSavedSettings.setBOOL("JoystickEnabled", false);
    }
    else
    {
        // autoenable if user specifically chose this device
        if (autoenable && (isLikeSpaceNavigator() || isDeviceUUIDSet()))
        {
            gSavedSettings.setBOOL("JoystickEnabled", true );
        }
    }
    if (!gSavedSettings.getBOOL("JoystickEnabled"))
    {
        mOverrideCamera = false;
    }
}

void LLViewerJoystick::setOverrideCamera(bool val)
{
    if (!gSavedSettings.getBOOL("JoystickEnabled"))
    {
        mOverrideCamera = false;
    }
    else
    {
        mOverrideCamera = val;
    }

    if (mOverrideCamera)
    {
        gAgentCamera.changeCameraToDefault();
    }
}

// -----------------------------------------------------------------------------
#if LIB_NDOF
NDOF_HotPlugResult LLViewerJoystick::HotPlugAddCallback(NDOF_Device *dev)
{
    NDOF_HotPlugResult res = NDOF_DISCARD_HOTPLUGGED;
    LLViewerJoystick* joystick(LLViewerJoystick::getInstance());
    if (joystick->mDriverState == JDS_UNINITIALIZED)
    {
        LL_INFOS("Joystick") << "HotPlugAddCallback: will use device:" << LL_ENDL;
        ndof_dump(stderr, dev);
        joystick->mNdofDev = dev;
        joystick->mDriverState = JDS_INITIALIZED;
        res = NDOF_KEEP_HOTPLUGGED;
    }
    joystick->updateEnabled(true);
    return res;
}
#endif

// -----------------------------------------------------------------------------
#if LIB_NDOF
void LLViewerJoystick::HotPlugRemovalCallback(NDOF_Device *dev)
{
    LLViewerJoystick* joystick(LLViewerJoystick::getInstance());
    if (joystick->mNdofDev == dev)
    {
        LL_INFOS("Joystick") << "HotPlugRemovalCallback: joystick->mNdofDev="
                << joystick->mNdofDev << "; removed device:" << LL_ENDL;
        ndof_dump(stderr, dev);
        joystick->mDriverState = JDS_UNINITIALIZED;
    }
    joystick->updateEnabled(true);
}
#endif

// -----------------------------------------------------------------------------
LLViewerJoystick::LLViewerJoystick()
:   mDriverState(JDS_UNINITIALIZED),
    mNdofDev(NULL),
    mResetFlag(false),
    mCameraUpdated(true),
    mOverrideCamera(false),
    mJoystickRun(0)
{
    for (int i = 0; i < 6; i++)
    {
        mAxes[i] = sDelta[i] = sLastDelta[i] = 0.0f;
    }

    memset(mBtn, 0, sizeof(mBtn));

    // factor in bandwidth? bandwidth = gViewerStats->mKBitStat
    mPerfScale = 4000.f / (F32)gSysCPU.getMHz(); // hmm.  why?

    mLastDeviceUUID = LLSD::Integer(1);
}

// -----------------------------------------------------------------------------
LLViewerJoystick::~LLViewerJoystick()
{
    if (mDriverState == JDS_INITIALIZED)
    {
        terminate();
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::init(bool autoenable)
{
#if LIB_NDOF
    static bool libinit = false;
    mDriverState = JDS_INITIALIZING;

    loadDeviceIdFromSettings();

    if (!libinit)
    {
        // Note: The HotPlug callbacks are not actually getting called on Windows
        if (ndof_libinit(HotPlugAddCallback,
                         HotPlugRemovalCallback,
                         gViewerWindow->getWindow()->getDirectInput8()))
        {
            mDriverState = JDS_UNINITIALIZED;
        }
        else
        {
            // NB: ndof_libinit succeeds when there's no device
            libinit = true;

            // allocate memory once for an eventual device
            mNdofDev = ndof_create();
        }
    }

    if (libinit)
    {
        if (mNdofDev)
        {
            U32 device_type = 0;
            void* win_callback = nullptr;
            std::function<bool(std::string&, LLSD&, void*)> osx_callback;
            // di8_devices_callback callback is immediate and happens in scope of getInputDevices()
#if LL_WINDOWS && !LL_MESA_HEADLESS
            // space navigator is marked as DI8DEVCLASS_GAMECTRL in ndof lib
            device_type = DI8DEVCLASS_GAMECTRL;
            win_callback = &di8_devices_callback;
#elif LL_DARWIN
            osx_callback = macos_devices_callback;

            if (mLastDeviceUUID.isMap())
            {
                std::string manufacturer = mLastDeviceUUID["manufacturer"].asString();
                std::string product = mLastDeviceUUID["product"].asString();

                strncpy(mNdofDev->manufacturer, manufacturer.c_str(), sizeof(mNdofDev->manufacturer));
                strncpy(mNdofDev->product, product.c_str(), sizeof(mNdofDev->product));

                if (ndof_init_first(mNdofDev, nullptr))
                {
                    mDriverState = JDS_INITIALIZING;
                    // Saved device no longer exist
                    // No device found
                    LL_WARNS() << "ndof_init_first FAILED" << LL_ENDL;
                }
                else
                {
                    mDriverState = JDS_INITIALIZED;
                }
            }
#endif
            if (mDriverState != JDS_INITIALIZED)
            {
                if (!gViewerWindow->getWindow()->getInputDevices(device_type, osx_callback, win_callback, NULL))
                {
                    LL_INFOS("Joystick") << "Failed to gather input devices. Falling back to ndof's init" << LL_ENDL;
                    // Failed to gather devices, init first suitable one
                mLastDeviceUUID = LLSD();
                void *preffered_device = NULL;
                initDevice(preffered_device);
            }
            }

            if (mDriverState == JDS_INITIALIZING)
            {
                LL_INFOS("Joystick") << "Found no matching joystick devices." << LL_ENDL;
                mDriverState = JDS_UNINITIALIZED;
            }
        }
        else
        {
            mDriverState = JDS_UNINITIALIZED;
        }
    }

    // Autoenable the joystick for recognized devices if nothing was connected previously
    if (!autoenable)
    {
        autoenable = gSavedSettings.getString("JoystickInitialized").empty();
    }
    updateEnabled(autoenable);

    if (mDriverState == JDS_INITIALIZED)
    {
        // A Joystick device is plugged in
        if (isLikeSpaceNavigator())
        {
            // It's a space navigator, we have defaults for it.
            if (gSavedSettings.getString("JoystickInitialized") != "SpaceNavigator")
            {
                // Only set the defaults if we haven't already (in case they were overridden)
                setSNDefaults();
                gSavedSettings.setString("JoystickInitialized", "SpaceNavigator");
            }
        }
        else
        {
            // It's not a Space Navigator
            gSavedSettings.setString("JoystickInitialized", "UnknownDevice");
        }
    }
    else
    {
        // No device connected, don't change any settings
    }

    LL_INFOS("Joystick") << "ndof: mDriverState=" << mDriverState << "; mNdofDev="
            << mNdofDev << "; libinit=" << libinit << LL_ENDL;
#endif
}

void LLViewerJoystick::initDevice(LLSD &guid)
{
#if LIB_NDOF
    mLastDeviceUUID = guid;
    U32 device_type = 0;
    void* win_callback = nullptr;
    std::function<bool(std::string&, LLSD&, void*)> osx_callback;
    mDriverState = JDS_INITIALIZING;

#if LL_WINDOWS && !LL_MESA_HEADLESS
    // space navigator is marked as DI8DEVCLASS_GAMECTRL in ndof lib
    device_type = DI8DEVCLASS_GAMECTRL;
    win_callback = &di8_devices_callback;
#elif LL_DARWIN
    osx_callback = macos_devices_callback;
    if (mLastDeviceUUID.isMap())
    {
        std::string manufacturer = mLastDeviceUUID["manufacturer"].asString();
        std::string product = mLastDeviceUUID["product"].asString();

        strncpy(mNdofDev->manufacturer, manufacturer.c_str(), sizeof(mNdofDev->manufacturer));
        strncpy(mNdofDev->product, product.c_str(), sizeof(mNdofDev->product));

        if (ndof_init_first(mNdofDev, nullptr))
        {
            mDriverState = JDS_INITIALIZING;
            // Saved device no longer exist
            // Np other device present
            LL_WARNS() << "ndof_init_first FAILED" << LL_ENDL;
        }
        else
        {
            mDriverState = JDS_INITIALIZED;
        }
    }
#endif

    if (mDriverState != JDS_INITIALIZED)
    {
        if (!gViewerWindow->getWindow()->getInputDevices(device_type, osx_callback, win_callback, NULL))
        {
            LL_INFOS("Joystick") << "Failed to gather input devices. Falling back to ndof's init" << LL_ENDL;
            // Failed to gather devices from window, init first suitable one
        void *preffered_device = NULL;
        mLastDeviceUUID = LLSD();
        initDevice(preffered_device);
    }
    }

    if (mDriverState == JDS_INITIALIZING)
    {
        LL_INFOS("Joystick") << "Found no matching joystick devices." << LL_ENDL;
        mDriverState = JDS_UNINITIALIZED;
    }
#endif
}

bool LLViewerJoystick::initDevice(void * preffered_device /*LPDIRECTINPUTDEVICE8*/, const std::string &name, const LLSD &guid)
{
#if LIB_NDOF
    mLastDeviceUUID = guid;

#if LL_DARWIN
    if (guid.isMap())
    {
        std::string manufacturer = mLastDeviceUUID["manufacturer"].asString();
        std::string product = mLastDeviceUUID["product"].asString();

        strncpy(mNdofDev->manufacturer, manufacturer.c_str(), sizeof(mNdofDev->manufacturer));
        strncpy(mNdofDev->product, product.c_str(), sizeof(mNdofDev->product));
    }
    else
    {
        mNdofDev->product[0] = '\0';
        mNdofDev->manufacturer[0] = '\0';
    }
#else
    strncpy(mNdofDev->product, name.c_str(), sizeof(mNdofDev->product));
    mNdofDev->manufacturer[0] = '\0';
#endif

    return initDevice(preffered_device);
#else
    return false;
#endif
}

bool LLViewerJoystick::initDevice(void * preffered_device /* LPDIRECTINPUTDEVICE8* */)
{
#if LIB_NDOF
    // Different joysticks will return different ranges of raw values.
    // Since we want to handle every device in the same uniform way,
    // we initialize the mNdofDev struct and we set the range
    // of values we would like to receive.
    //
    // HACK: On Windows, libndofdev passes our range to DI with a
    // SetProperty call. This works but with one notable exception, the
    // SpaceNavigator, who doesn't seem to care about the SetProperty
    // call. In theory, we should handle this case inside libndofdev.
    // However, the range we're setting here is arbitrary anyway,
    // so let's just use the SpaceNavigator range for our purposes.
    mNdofDev->axes_min = (long)-MAX_JOYSTICK_INPUT_VALUE;
    mNdofDev->axes_max = (long)+MAX_JOYSTICK_INPUT_VALUE;

    // libndofdev could be used to return deltas.  Here we choose to
    // just have the absolute values instead.
    mNdofDev->absolute = 1;
    // init & use the first suitable NDOF device found on the USB chain
    // On windows preffered_device needs to be a pointer to LPDIRECTINPUTDEVICE8
    if (ndof_init_first(mNdofDev, preffered_device))
    {
        mDriverState = JDS_UNINITIALIZED;
        LL_WARNS() << "ndof_init_first FAILED" << LL_ENDL;
    }
    else
    {
        mDriverState = JDS_INITIALIZED;
        return true;
    }
#endif
    return false;
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::terminate()
{
#if LIB_NDOF
    if (mNdofDev != NULL)
    {
        ndof_libcleanup(); // frees alocated memory in mNdofDev
        mDriverState = JDS_UNINITIALIZED;
        mNdofDev = NULL;
        LL_INFOS("Joystick") << "Terminated connection with NDOF device." << LL_ENDL;
    }
#endif
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::updateStatus()
{
#if LIB_NDOF

    ndof_update(mNdofDev);

    for (int i=0; i<6; i++)
    {
        mAxes[i] = (F32) mNdofDev->axes[i] / mNdofDev->axes_max;
    }

    for (int i=0; i<16; i++)
    {
        mBtn[i] = mNdofDev->buttons[i];
    }

#endif
}

// -----------------------------------------------------------------------------
F32 LLViewerJoystick::getJoystickAxis(U32 axis) const
{
    if (axis < 6)
    {
        return mAxes[axis];
    }
    return 0.f;
}

// -----------------------------------------------------------------------------
U32 LLViewerJoystick::getJoystickButton(U32 button) const
{
    if (button < 16)
    {
        return mBtn[button];
    }
    return 0;
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::handleRun(F32 inc)
{
    // Decide whether to walk or run by applying a threshold, with slight
    // hysteresis to avoid oscillating between the two with input spikes.
    // Analog speed control would be better, but not likely any time soon.
    if (inc > gSavedSettings.getF32("JoystickRunThreshold"))
    {
        if (1 == mJoystickRun)
        {
            ++mJoystickRun;
            gAgent.setRunning();
            gAgent.sendWalkRun(gAgent.getRunning());
        }
        else if (0 == mJoystickRun)
        {
            // hysteresis - respond NEXT frame
            ++mJoystickRun;
        }
    }
    else
    {
        if (mJoystickRun > 0)
        {
            --mJoystickRun;
            if (0 == mJoystickRun)
            {
                gAgent.clearRunning();
                gAgent.sendWalkRun(gAgent.getRunning());
            }
        }
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::agentJump()
{
    gAgent.moveUp(1);
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::agentSlide(F32 inc)
{
    if (inc < 0.f)
    {
        gAgent.moveLeft(1);
    }
    else if (inc > 0.f)
    {
        gAgent.moveLeft(-1);
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::agentPush(F32 inc)
{
    if (inc < 0.f)                            // forward
    {
        gAgent.moveAt(1, false);
    }
    else if (inc > 0.f)                       // backward
    {
        gAgent.moveAt(-1, false);
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::agentFly(F32 inc)
{
    if (inc < 0.f)
    {
        if (! (gAgent.getFlying() ||
               !gAgent.canFly() ||
               gAgent.upGrabbed() ||
               !gSavedSettings.getBOOL("AutomaticFly")) )
        {
            gAgent.setFlying(true);
        }
        gAgent.moveUp(1);
    }
    else if (inc > 0.f)
    {
        // crouch
        gAgent.moveUp(-1);
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::agentPitch(F32 pitch_inc)
{
    if (pitch_inc < 0)
    {
        gAgent.setControlFlags(AGENT_CONTROL_PITCH_POS);
    }
    else if (pitch_inc > 0)
    {
        gAgent.setControlFlags(AGENT_CONTROL_PITCH_NEG);
    }

    gAgent.pitch(-pitch_inc);
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::agentYaw(F32 yaw_inc)
{
    // Cannot steer some vehicles in mouselook if the script grabs the controls
    if (gAgentCamera.cameraMouselook() && !gSavedSettings.getBOOL("JoystickMouselookYaw"))
    {
        gAgent.rotate(-yaw_inc, gAgent.getReferenceUpVector());
    }
    else
    {
        if (yaw_inc < 0)
        {
            gAgent.setControlFlags(AGENT_CONTROL_YAW_POS);
        }
        else if (yaw_inc > 0)
        {
            gAgent.setControlFlags(AGENT_CONTROL_YAW_NEG);
        }

        gAgent.yaw(-yaw_inc);
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::resetDeltas(S32 axis[])
{
    for (U32 i = 0; i < 6; i++)
    {
        sLastDelta[i] = -mAxes[axis[i]];
        sDelta[i] = 0.f;
    }

    sLastDelta[6] = sDelta[6] = 0.f;
    mResetFlag = false;
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::moveObjects(bool reset)
{
    static bool toggle_send_to_sim = false;

    if (!gFocusMgr.getAppHasFocus() || mDriverState != JDS_INITIALIZED
        || !gSavedSettings.getBOOL("JoystickEnabled") || !gSavedSettings.getBOOL("JoystickBuildEnabled"))
    {
        return;
    }

    S32 axis[] =
    {
        gSavedSettings.getS32("JoystickAxis0"),
        gSavedSettings.getS32("JoystickAxis1"),
        gSavedSettings.getS32("JoystickAxis2"),
        gSavedSettings.getS32("JoystickAxis3"),
        gSavedSettings.getS32("JoystickAxis4"),
        gSavedSettings.getS32("JoystickAxis5"),
    };

    if (reset || mResetFlag)
    {
        resetDeltas(axis);
        return;
    }

    F32 axis_scale[] =
    {
        gSavedSettings.getF32("BuildAxisScale0"),
        gSavedSettings.getF32("BuildAxisScale1"),
        gSavedSettings.getF32("BuildAxisScale2"),
        gSavedSettings.getF32("BuildAxisScale3"),
        gSavedSettings.getF32("BuildAxisScale4"),
        gSavedSettings.getF32("BuildAxisScale5"),
    };

    F32 dead_zone[] =
    {
        gSavedSettings.getF32("BuildAxisDeadZone0"),
        gSavedSettings.getF32("BuildAxisDeadZone1"),
        gSavedSettings.getF32("BuildAxisDeadZone2"),
        gSavedSettings.getF32("BuildAxisDeadZone3"),
        gSavedSettings.getF32("BuildAxisDeadZone4"),
        gSavedSettings.getF32("BuildAxisDeadZone5"),
    };

    F32 cur_delta[6];
    F32 time = gFrameIntervalSeconds.value();

    // avoid making ridicously big movements if there's a big drop in fps
    if (time > .2f)
    {
        time = .2f;
    }

    // max feather is 32
    F32 feather = gSavedSettings.getF32("BuildFeathering");
    bool is_zero = true, absolute = gSavedSettings.getBOOL("Cursor3D");

    for (U32 i = 0; i < 6; i++)
    {
        cur_delta[i] = -mAxes[axis[i]];
        F32 tmp = cur_delta[i];
        if (absolute)
        {
            cur_delta[i] = cur_delta[i] - sLastDelta[i];
        }
        sLastDelta[i] = tmp;
        is_zero = is_zero && (cur_delta[i] == 0.f);

        if (cur_delta[i] > 0)
        {
            cur_delta[i] = llmax(cur_delta[i]-dead_zone[i], 0.f);
        }
        else
        {
            cur_delta[i] = llmin(cur_delta[i]+dead_zone[i], 0.f);
        }
        cur_delta[i] *= axis_scale[i];

        if (!absolute)
        {
            cur_delta[i] *= time;
        }

        sDelta[i] = sDelta[i] + (cur_delta[i]-sDelta[i])*time*feather;
    }

    U32 upd_type = UPD_NONE;
    LLVector3 v;

    if (!is_zero)
    {
        // Clear AFK state if moved beyond the deadzone
        if (gAwayTimer.getElapsedTimeF32() > LLAgent::MIN_AFK_TIME)
        {
            gAgent.clearAFK();
        }

        if (sDelta[0] || sDelta[1] || sDelta[2])
        {
            upd_type |= UPD_POSITION;
            v.setVec(sDelta[0], sDelta[1], sDelta[2]);
        }

        if (sDelta[3] || sDelta[4] || sDelta[5])
        {
            upd_type |= UPD_ROTATION;
        }

        // the selection update could fail, so we won't send
        if (LLSelectMgr::getInstance()->selectionMove(v, sDelta[3],sDelta[4],sDelta[5], upd_type))
        {
            toggle_send_to_sim = true;
        }
    }
    else if (toggle_send_to_sim)
    {
        LLSelectMgr::getInstance()->sendSelectionMove();
        toggle_send_to_sim = false;
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::moveAvatar(bool reset)
{
    if (!gFocusMgr.getAppHasFocus() || mDriverState != JDS_INITIALIZED
        || !gSavedSettings.getBOOL("JoystickEnabled") || !gSavedSettings.getBOOL("JoystickAvatarEnabled"))
    {
        return;
    }

    S32 axis[] =
    {
        // [1 0 2 4  3  5]
        // [Z X Y RZ RX RY]
        gSavedSettings.getS32("JoystickAxis0"),
        gSavedSettings.getS32("JoystickAxis1"),
        gSavedSettings.getS32("JoystickAxis2"),
        gSavedSettings.getS32("JoystickAxis3"),
        gSavedSettings.getS32("JoystickAxis4"),
        gSavedSettings.getS32("JoystickAxis5")
    };

    if (reset || mResetFlag)
    {
        resetDeltas(axis);
        if (reset)
        {
            // Note: moving the agent triggers agent camera mode;
            //  don't do this every time we set mResetFlag (e.g. because we gained focus)
            gAgent.moveAt(0, true);
        }
        return;
    }

    bool is_zero = true;
    static bool button_held = false;

    if (mBtn[1] == 1)
    {
        // If AutomaticFly is enabled, then button1 merely causes a
        // jump (as the up/down axis already controls flying) if on the
        // ground, or cease flight if already flying.
        // If AutomaticFly is disabled, then button1 toggles flying.
        if (gSavedSettings.getBOOL("AutomaticFly"))
        {
            if (!gAgent.getFlying())
            {
                gAgent.moveUp(1);
            }
            else if (!button_held)
            {
                button_held = true;
                gAgent.setFlying(false);
            }
        }
        else if (!button_held)
        {
            button_held = true;
            gAgent.setFlying(!gAgent.getFlying());
        }

        is_zero = false;
    }
    else
    {
        button_held = false;
    }

    F32 axis_scale[] =
    {
        gSavedSettings.getF32("AvatarAxisScale0"),
        gSavedSettings.getF32("AvatarAxisScale1"),
        gSavedSettings.getF32("AvatarAxisScale2"),
        gSavedSettings.getF32("AvatarAxisScale3"),
        gSavedSettings.getF32("AvatarAxisScale4"),
        gSavedSettings.getF32("AvatarAxisScale5")
    };

    F32 dead_zone[] =
    {
        gSavedSettings.getF32("AvatarAxisDeadZone0"),
        gSavedSettings.getF32("AvatarAxisDeadZone1"),
        gSavedSettings.getF32("AvatarAxisDeadZone2"),
        gSavedSettings.getF32("AvatarAxisDeadZone3"),
        gSavedSettings.getF32("AvatarAxisDeadZone4"),
        gSavedSettings.getF32("AvatarAxisDeadZone5")
    };

    // time interval in seconds between this frame and the previous
    F32 time = gFrameIntervalSeconds.value();

    // avoid making ridicously big movements if there's a big drop in fps
    if (time > .2f)
    {
        time = .2f;
    }

    // note: max feather is 32.0
    F32 feather = gSavedSettings.getF32("AvatarFeathering");

    F32 cur_delta[6];
    F32 val, dom_mov = 0.f;
    U32 dom_axis = Z_I;
#if LIB_NDOF
    bool absolute = (gSavedSettings.getBOOL("Cursor3D") && mNdofDev->absolute);
#else
    bool absolute = false;
#endif
    // remove dead zones and determine biggest movement on the joystick
    for (U32 i = 0; i < 6; i++)
    {
        cur_delta[i] = -mAxes[axis[i]];
        if (absolute)
        {
            F32 tmp = cur_delta[i];
            cur_delta[i] = cur_delta[i] - sLastDelta[i];
            sLastDelta[i] = tmp;
        }

        if (cur_delta[i] > 0)
        {
            cur_delta[i] = llmax(cur_delta[i]-dead_zone[i], 0.f);
        }
        else
        {
            cur_delta[i] = llmin(cur_delta[i]+dead_zone[i], 0.f);
        }

        // we don't care about Roll (RZ) and Z is calculated after the loop
        if (i != Z_I && i != RZ_I)
        {
            // find out the axis with the biggest joystick motion
            val = fabs(cur_delta[i]);
            if (val > dom_mov)
            {
                dom_axis = i;
                dom_mov = val;
            }
        }

        is_zero = is_zero && (cur_delta[i] == 0.f);
    }

    if (!is_zero)
    {
        // Clear AFK state if moved beyond the deadzone
        if (gAwayTimer.getElapsedTimeF32() > LLAgent::MIN_AFK_TIME)
        {
            gAgent.clearAFK();
        }

        setCameraNeedsUpdate(true);
    }

    // forward|backward movements overrule the real dominant movement if
    // they're bigger than its 20%. This is what you want 'cos moving forward
    // is what you do most. We also added a special (even more lenient) case
    // for RX|RY to allow walking while pitching and turning
    if (fabs(cur_delta[Z_I]) > .2f * dom_mov
        || ((dom_axis == RX_I || dom_axis == RY_I)
        && fabs(cur_delta[Z_I]) > .05f * dom_mov))
    {
        dom_axis = Z_I;
    }

    sDelta[X_I] = -cur_delta[X_I] * axis_scale[X_I];
    sDelta[Y_I] = -cur_delta[Y_I] * axis_scale[Y_I];
    sDelta[Z_I] = -cur_delta[Z_I] * axis_scale[Z_I];
    cur_delta[RX_I] *= -axis_scale[RX_I] * mPerfScale;
    cur_delta[RY_I] *= -axis_scale[RY_I] * mPerfScale;

    if (!absolute)
    {
        cur_delta[RX_I] *= time;
        cur_delta[RY_I] *= time;
    }
    sDelta[RX_I] += (cur_delta[RX_I] - sDelta[RX_I]) * time * feather;
    sDelta[RY_I] += (cur_delta[RY_I] - sDelta[RY_I]) * time * feather;

    handleRun((F32) sqrt(sDelta[Z_I]*sDelta[Z_I] + sDelta[X_I]*sDelta[X_I]));

    // Allow forward/backward movement some priority
    if (dom_axis == Z_I)
    {
        agentPush(sDelta[Z_I]);         // forward/back

        if (fabs(sDelta[X_I])  > .1f)
        {
            agentSlide(sDelta[X_I]);    // move sideways
        }

        if (fabs(sDelta[Y_I])  > .1f)
        {
            agentFly(sDelta[Y_I]);      // up/down & crouch
        }

        // too many rotations during walking can be confusing, so apply
        // the deadzones one more time (quick & dirty), at 50%|30% power
        F32 eff_rx = .3f * dead_zone[RX_I];
        F32 eff_ry = .3f * dead_zone[RY_I];

        if (sDelta[RX_I] > 0)
        {
            eff_rx = llmax(sDelta[RX_I] - eff_rx, 0.f);
        }
        else
        {
            eff_rx = llmin(sDelta[RX_I] + eff_rx, 0.f);
        }

        if (sDelta[RY_I] > 0)
        {
            eff_ry = llmax(sDelta[RY_I] - eff_ry, 0.f);
        }
        else
        {
            eff_ry = llmin(sDelta[RY_I] + eff_ry, 0.f);
        }


        if (fabs(eff_rx) > 0.f || fabs(eff_ry) > 0.f)
        {
            if (gAgent.getFlying())
            {
                agentPitch(eff_rx);
                agentYaw(eff_ry);
            }
            else
            {
                agentPitch(eff_rx);
                agentYaw(2.f * eff_ry);
            }
        }
    }
    else
    {
        agentSlide(sDelta[X_I]);        // move sideways
        agentFly(sDelta[Y_I]);          // up/down & crouch
        agentPush(sDelta[Z_I]);         // forward/back
        agentPitch(sDelta[RX_I]);       // pitch
        agentYaw(sDelta[RY_I]);         // turn
    }
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::moveFlycam(bool reset)
{
    static LLQuaternion         sFlycamRotation;
    static LLVector3            sFlycamPosition;
    static F32                  sFlycamZoom;

    if (!gFocusMgr.getAppHasFocus() || mDriverState != JDS_INITIALIZED
        || !gSavedSettings.getBOOL("JoystickEnabled") || !gSavedSettings.getBOOL("JoystickFlycamEnabled"))
    {
        return;
    }

    S32 axis[] =
    {
        gSavedSettings.getS32("JoystickAxis0"),
        gSavedSettings.getS32("JoystickAxis1"),
        gSavedSettings.getS32("JoystickAxis2"),
        gSavedSettings.getS32("JoystickAxis3"),
        gSavedSettings.getS32("JoystickAxis4"),
        gSavedSettings.getS32("JoystickAxis5"),
        gSavedSettings.getS32("JoystickAxis6")
    };

    bool in_build_mode = LLToolMgr::getInstance()->inBuildMode();
    if (reset || mResetFlag)
    {
        sFlycamPosition = LLViewerCamera::getInstance()->getOrigin();
        sFlycamRotation = LLViewerCamera::getInstance()->getQuaternion();
        sFlycamZoom = LLViewerCamera::getInstance()->getView();

        resetDeltas(axis);

        return;
    }

    F32 axis_scale[] =
    {
        gSavedSettings.getF32("FlycamAxisScale0"),
        gSavedSettings.getF32("FlycamAxisScale1"),
        gSavedSettings.getF32("FlycamAxisScale2"),
        gSavedSettings.getF32("FlycamAxisScale3"),
        gSavedSettings.getF32("FlycamAxisScale4"),
        gSavedSettings.getF32("FlycamAxisScale5"),
        gSavedSettings.getF32("FlycamAxisScale6")
    };

    F32 dead_zone[] =
    {
        gSavedSettings.getF32("FlycamAxisDeadZone0"),
        gSavedSettings.getF32("FlycamAxisDeadZone1"),
        gSavedSettings.getF32("FlycamAxisDeadZone2"),
        gSavedSettings.getF32("FlycamAxisDeadZone3"),
        gSavedSettings.getF32("FlycamAxisDeadZone4"),
        gSavedSettings.getF32("FlycamAxisDeadZone5"),
        gSavedSettings.getF32("FlycamAxisDeadZone6")
    };

    F32 time = gFrameIntervalSeconds.value();

    // avoid making ridiculously big movements if there's a big drop in fps
    if (time > .2f)
    {
        time = .2f;
    }

    F32 cur_delta[7];
    F32 feather = gSavedSettings.getF32("FlycamFeathering");
    bool absolute = gSavedSettings.getBOOL("Cursor3D");
    bool is_zero = true;

    for (U32 i = 0; i < 7; i++)
    {
        cur_delta[i] = -getJoystickAxis(axis[i]);


        F32 tmp = cur_delta[i];
        if (absolute)
        {
            cur_delta[i] = cur_delta[i] - sLastDelta[i];
        }
        sLastDelta[i] = tmp;

        if (cur_delta[i] > 0)
        {
            cur_delta[i] = llmax(cur_delta[i]-dead_zone[i], 0.f);
        }
        else
        {
            cur_delta[i] = llmin(cur_delta[i]+dead_zone[i], 0.f);
        }

        // We may want to scale camera movements up or down in build mode.
        // NOTE: this needs to remain after the deadzone calculation, otherwise
        // we have issues with flycam "jumping" when the build dialog is opened/closed  -Nyx
        if (in_build_mode)
        {
            if (i == X_I || i == Y_I || i == Z_I)
            {
                static LLCachedControl<F32> build_mode_scale(gSavedSettings,"FlycamBuildModeScale", 1.0);
                cur_delta[i] *= build_mode_scale;
            }
        }

        cur_delta[i] *= axis_scale[i];

        if (!absolute)
        {
            cur_delta[i] *= time;
        }

        sDelta[i] = sDelta[i] + (cur_delta[i]-sDelta[i])*time*feather;

        is_zero = is_zero && (cur_delta[i] == 0.f);

    }

    // Clear AFK state if moved beyond the deadzone
    if (!is_zero && gAwayTimer.getElapsedTimeF32() > LLAgent::MIN_AFK_TIME)
    {
        gAgent.clearAFK();
    }

    sFlycamPosition += LLVector3(sDelta) * sFlycamRotation;

    LLMatrix3 rot_mat(sDelta[3], sDelta[4], sDelta[5]);
    sFlycamRotation = LLQuaternion(rot_mat)*sFlycamRotation;

    if (gSavedSettings.getBOOL("AutoLeveling"))
    {
        LLMatrix3 level(sFlycamRotation);

        LLVector3 x = LLVector3(level.mMatrix[0]);
        LLVector3 y = LLVector3(level.mMatrix[1]);
        LLVector3 z = LLVector3(level.mMatrix[2]);

        y.mV[2] = 0.f;
        y.normVec();

        level.setRows(x,y,z);
        level.orthogonalize();

        LLQuaternion quat(level);
        sFlycamRotation = nlerp(llmin(feather*time,1.f), sFlycamRotation, quat);
    }

    if (gSavedSettings.getBOOL("ZoomDirect"))
    {
        sFlycamZoom = sLastDelta[6]*axis_scale[6]+dead_zone[6];
    }
    else
    {
        sFlycamZoom += sDelta[6];
    }

    LLMatrix3 mat(sFlycamRotation);

    LLViewerCamera::getInstance()->setView(sFlycamZoom);
    LLViewerCamera::getInstance()->setOrigin(sFlycamPosition);
    LLViewerCamera::getInstance()->mXAxis = LLVector3(mat.mMatrix[0]);
    LLViewerCamera::getInstance()->mYAxis = LLVector3(mat.mMatrix[1]);
    LLViewerCamera::getInstance()->mZAxis = LLVector3(mat.mMatrix[2]);
}

// -----------------------------------------------------------------------------
bool LLViewerJoystick::toggleFlycam()
{
    if (!gSavedSettings.getBOOL("JoystickEnabled") || !gSavedSettings.getBOOL("JoystickFlycamEnabled"))
    {
        mOverrideCamera = false;
        return false;
    }

    if (!mOverrideCamera)
    {
        gAgentCamera.changeCameraToDefault();
    }

    if (gAwayTimer.getElapsedTimeF32() > LLAgent::MIN_AFK_TIME)
    {
        gAgent.clearAFK();
    }

    mOverrideCamera = !mOverrideCamera;
    if (mOverrideCamera)
    {
        moveFlycam(true);

    }
    else
    {
        // Exiting from the flycam mode: since we are going to keep the flycam POV for
        // the main camera until the avatar moves, we need to track this situation.
        setCameraNeedsUpdate(false);
        setNeedsReset(true);
    }
    return true;
}

void LLViewerJoystick::scanJoystick()
{
    if (mDriverState != JDS_INITIALIZED || !gSavedSettings.getBOOL("JoystickEnabled"))
    {
        return;
    }

#if LL_WINDOWS
    // On windows, the flycam is updated syncronously with a timer, so there is
    // no need to update the status of the joystick here.
    if (!mOverrideCamera)
#endif
    updateStatus();

    // App focus check Needs to happen AFTER updateStatus in case the joystick
    // is not centred when the app loses focus.
    if (!gFocusMgr.getAppHasFocus())
    {
        return;
    }

    static long toggle_flycam = 0;

    if (mBtn[0] == 1)
    {
        if (mBtn[0] != toggle_flycam)
        {
            toggle_flycam = toggleFlycam() ? 1 : 0;
        }
    }
    else
    {
        toggle_flycam = 0;
    }

    if (!mOverrideCamera && !(LLToolMgr::getInstance()->inBuildMode() && gSavedSettings.getBOOL("JoystickBuildEnabled")))
    {
        moveAvatar();
    }
}

// -----------------------------------------------------------------------------
bool LLViewerJoystick::isDeviceUUIDSet()
{
#if LL_WINDOWS && !LL_MESA_HEADLESS
    // for ease of comparison and to dial less with platform specific variables, we store id as LLSD binary
    return mLastDeviceUUID.isBinary();
#elif LL_DARWIN
    return mLastDeviceUUID.isMap();
#else
    return false;
#endif
}

LLSD LLViewerJoystick::getDeviceUUID()
{
    return mLastDeviceUUID;
}

std::string LLViewerJoystick::getDeviceUUIDString()
{
#if LL_WINDOWS && !LL_MESA_HEADLESS
    // Might be simpler to just convert _GUID into string everywhere, store and compare as string
    if (mLastDeviceUUID.isBinary())
    {
        S32 size = sizeof(GUID);
        LLSD::Binary data = mLastDeviceUUID.asBinary();
        GUID guid;
        memcpy(&guid, &data[0], size);
        return string_from_guid(guid);
    }
    else
    {
        return std::string();
    }
#elif LL_DARWIN
    if (mLastDeviceUUID.isMap())
    {
        std::string manufacturer = mLastDeviceUUID["manufacturer"].asString();
        std::string product = mLastDeviceUUID["product"].asString();
        return manufacturer + ":" + product;
    }
    else
    {
        return std::string();
    }
#else
    return std::string();
#endif
}

void LLViewerJoystick::saveDeviceIdToSettings()
{
#if LL_WINDOWS && !LL_MESA_HEADLESS
    // can't save as binary directly,
    // someone editing the xml will corrupt it
    // so convert to string first
    std::string device_string = getDeviceUUIDString();
    gSavedSettings.setLLSD("JoystickDeviceUUID", LLSD(device_string));
#else
    LLSD device_id = getDeviceUUID();
    gSavedSettings.setLLSD("JoystickDeviceUUID", device_id);
#endif
}

void LLViewerJoystick::loadDeviceIdFromSettings()
{
    LLSD dev_id = gSavedSettings.getLLSD("JoystickDeviceUUID");
#if LL_WINDOWS && !LL_MESA_HEADLESS
    // We can't save binary data to gSavedSettings, somebody editing the file will corrupt it,
    // so _GUID data gets converted to string (we probably can convert it to LLUUID with memcpy)
    // and here we need to convert it back to binary from string
    std::string device_string;
    if (dev_id.isString())
    {
        device_string = dev_id.asString();
    }
    if (device_string.empty())
    {
        mLastDeviceUUID = LLSD();
    }
    else
    {
        LL_DEBUGS("Joystick") << "Looking for device by id: " << device_string << LL_ENDL;
        GUID guid;
        guid_from_string(guid, device_string);
        S32 size = sizeof(GUID);
        LLSD::Binary data; //just an std::vector
        data.resize(size);
        memcpy(&data[0], &guid /*POD _GUID*/, size);
        // We store this data in LLSD since it can handle both GUID2 and long
        mLastDeviceUUID = LLSD(data);
    }
#elif LL_DARWIN
    if (!dev_id.isMap())
    {
        mLastDeviceUUID = LLSD();
    }
    else
    {
        std::string manufacturer = mLastDeviceUUID["manufacturer"].asString();
        std::string product = mLastDeviceUUID["product"].asString();
        LL_DEBUGS("Joystick") << "Looking for device by manufacturer: " << manufacturer << " and product: " << product <<  LL_ENDL;
        // We store this data in LLSD since it can handle both GUID2 and long
        mLastDeviceUUID = dev_id;
    }
#else
    mLastDeviceUUID = LLSD();
    //mLastDeviceUUID = gSavedSettings.getLLSD("JoystickDeviceUUID");
#endif
}

// -----------------------------------------------------------------------------
std::string LLViewerJoystick::getDescription()
{
    std::string res;
#if LIB_NDOF
    if (mDriverState == JDS_INITIALIZED && mNdofDev)
    {
        res = ll_safe_string(mNdofDev->product);
    }
#endif
    return res;
}

bool LLViewerJoystick::isLikeSpaceNavigator() const
{
#if LIB_NDOF
    return (isJoystickInitialized()
            && (strncmp(mNdofDev->product, "SpaceNavigator", 14) == 0
                || strncmp(mNdofDev->product, "SpaceExplorer", 13) == 0
                || strncmp(mNdofDev->product, "SpaceTraveler", 13) == 0
                || strncmp(mNdofDev->product, "SpacePilot", 10) == 0));
#else
    return false;
#endif
}

// -----------------------------------------------------------------------------
void LLViewerJoystick::setSNDefaults()
{
#if LL_DARWIN || LL_LINUX
    const float platformScale = 20.f;
    const float platformScaleAvXZ = 1.f;
    // The SpaceNavigator doesn't act as a 3D cursor on macOS / Linux.
    const bool is_3d_cursor = false;
#else
    const float platformScale = 1.f;
    const float platformScaleAvXZ = 2.f;
    const bool is_3d_cursor = true;
#endif

    //gViewerWindow->alertXml("CacheWillClear");
    LL_INFOS("Joystick") << "restoring SpaceNavigator defaults..." << LL_ENDL;

    gSavedSettings.setS32("JoystickAxis0", 1); // z (at)
    gSavedSettings.setS32("JoystickAxis1", 0); // x (slide)
    gSavedSettings.setS32("JoystickAxis2", 2); // y (up)
    gSavedSettings.setS32("JoystickAxis3", 4); // pitch
    gSavedSettings.setS32("JoystickAxis4", 3); // roll
    gSavedSettings.setS32("JoystickAxis5", 5); // yaw
    gSavedSettings.setS32("JoystickAxis6", -1);

    gSavedSettings.setBOOL("Cursor3D", is_3d_cursor);
    gSavedSettings.setBOOL("AutoLeveling", true);
    gSavedSettings.setBOOL("ZoomDirect", false);

    gSavedSettings.setF32("AvatarAxisScale0", 1.f * platformScaleAvXZ);
    gSavedSettings.setF32("AvatarAxisScale1", 1.f * platformScaleAvXZ);
    gSavedSettings.setF32("AvatarAxisScale2", 1.f);
    gSavedSettings.setF32("AvatarAxisScale4", .1f * platformScale);
    gSavedSettings.setF32("AvatarAxisScale5", .1f * platformScale);
    gSavedSettings.setF32("AvatarAxisScale3", 0.f * platformScale);
    gSavedSettings.setF32("BuildAxisScale1", .3f * platformScale);
    gSavedSettings.setF32("BuildAxisScale2", .3f * platformScale);
    gSavedSettings.setF32("BuildAxisScale0", .3f * platformScale);
    gSavedSettings.setF32("BuildAxisScale4", .3f * platformScale);
    gSavedSettings.setF32("BuildAxisScale5", .3f * platformScale);
    gSavedSettings.setF32("BuildAxisScale3", .3f * platformScale);
    gSavedSettings.setF32("FlycamAxisScale1", 2.f * platformScale);
    gSavedSettings.setF32("FlycamAxisScale2", 2.f * platformScale);
    gSavedSettings.setF32("FlycamAxisScale0", 2.1f * platformScale);
    gSavedSettings.setF32("FlycamAxisScale4", .1f * platformScale);
    gSavedSettings.setF32("FlycamAxisScale5", .15f * platformScale);
    gSavedSettings.setF32("FlycamAxisScale3", 0.f * platformScale);
    gSavedSettings.setF32("FlycamAxisScale6", 0.f * platformScale);

    gSavedSettings.setF32("AvatarAxisDeadZone0", .1f);
    gSavedSettings.setF32("AvatarAxisDeadZone1", .1f);
    gSavedSettings.setF32("AvatarAxisDeadZone2", .1f);
    gSavedSettings.setF32("AvatarAxisDeadZone3", 1.f);
    gSavedSettings.setF32("AvatarAxisDeadZone4", .02f);
    gSavedSettings.setF32("AvatarAxisDeadZone5", .01f);
    gSavedSettings.setF32("BuildAxisDeadZone0", .01f);
    gSavedSettings.setF32("BuildAxisDeadZone1", .01f);
    gSavedSettings.setF32("BuildAxisDeadZone2", .01f);
    gSavedSettings.setF32("BuildAxisDeadZone3", .01f);
    gSavedSettings.setF32("BuildAxisDeadZone4", .01f);
    gSavedSettings.setF32("BuildAxisDeadZone5", .01f);
    gSavedSettings.setF32("FlycamAxisDeadZone0", .01f);
    gSavedSettings.setF32("FlycamAxisDeadZone1", .01f);
    gSavedSettings.setF32("FlycamAxisDeadZone2", .01f);
    gSavedSettings.setF32("FlycamAxisDeadZone3", .01f);
    gSavedSettings.setF32("FlycamAxisDeadZone4", .01f);
    gSavedSettings.setF32("FlycamAxisDeadZone5", .01f);
    gSavedSettings.setF32("FlycamAxisDeadZone6", 1.f);

    gSavedSettings.setF32("AvatarFeathering", 6.f);
    gSavedSettings.setF32("BuildFeathering", 12.f);
    gSavedSettings.setF32("FlycamFeathering", 5.f);
}