diff --git a/BUILD_WIN.md b/BUILD_WIN.md
index 45373d3093..e37bf27503 100644
--- a/BUILD_WIN.md
+++ b/BUILD_WIN.md
@@ -1,104 +1,81 @@
-Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Windows specific instructions are found in this file.
+This is a stand-alone guide for creating your first High Fidelity build for Windows 64-bit.
-Interface can be built as 32 or 64 bit.
+###Step 1. Installing Visual Studio 2013
-###Visual Studio 2013
+If you don't already have the Community or Professional edition of Visual Studio 2013, download and install [Visual Studio Community 2013](https://www.visualstudio.com/en-us/news/releasenotes/vs2013-community-vs). You do not need to install any of the optional components when going through the installer.
-You can use the Community or Professional editions of Visual Studio 2013.
+Note: Newer versions of Visual Studio are not yet compatible.
-You can start a Visual Studio 2013 command prompt using the shortcut provided in the Visual Studio Tools folder installed as part of Visual Studio 2013.
+###Step 2. Installing CMake
-Or you can start a regular command prompt and then run:
+Download and install the CMake 3.8.0-rc2 "win64-x64 Installer" from the [CMake Website](https://cmake.org/download/). Make sure "Add CMake to system PATH for all users" is checked when going through the installer.
- "%VS120COMNTOOLS%\vsvars32.bat"
+###Step 3. Installing Qt
-####Windows SDK 8.1
+Download and install the [Qt 5.6.1 Installer](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe). Please note that the download file is large (850MB) and may take some time.
-If using Visual Studio 2013 and building as a Visual Studio 2013 project you need the Windows 8 SDK which you should already have as part of installing Visual Studio 2013. You should be able to see it at `C:\Program Files (x86)\Windows Kits\8.1\Lib\winv6.3\um\x86`.
+Make sure to select all components when going through the installer.
-####nmake
+###Step 4. Setting Qt Environment Variable
-Some of the external projects may require nmake to compile and install. If it is not installed at the location listed below, please ensure that it is in your PATH so CMake can find it when required.
+Go to "Control Panel > System > Advanced System Settings > Environment Variables > New..." (or search “Environment Variables” in Start Search).
+* Set "Variable name": QT_CMAKE_PREFIX_PATH
+* Set "Variable value": `C:\Qt\Qt5.6.1\5.6\msvc2013_64\lib\cmake`
-We expect nmake.exe to be located at the following path.
+###Step 5. Installing OpenSSL
- C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin
+Download and install the "Win64 OpenSSL v1.0.2k" Installer from [this website](https://slproweb.com/products/Win32OpenSSL.html).
-###Qt
-You can use the online installer or the offline installer. If you use the offline installer, be sure to select the "OpenGL" version.
-
-* [Download the online installer](http://www.qt.io/download-open-source/#section-2)
- * When it asks you to select components, ONLY select one of the following, 32- or 64-bit to match your build preference:
- * Qt > Qt 5.6.1 > **msvc2013 32-bit**
- * Qt > Qt 5.6.1 > **msvc2013 64-bit**
-
-* Download the offline installer, 32- or 64-bit to match your build preference:
- * [32-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013-5.6.1-1.exe)
- * [64-bit](https://download.qt.io/official_releases/qt/5.6/5.6.1-1/qt-opensource-windows-x86-msvc2013_64-5.6.1-1.exe)
-
-Once Qt is installed, you need to manually configure the following:
-* Set the QT_CMAKE_PREFIX_PATH environment variable to your `Qt\5.6.1\msvc2013\lib\cmake` or `Qt\5.6.1\msvc2013_64\lib\cmake` directory.
- * You can set an environment variable from Control Panel > System > Advanced System Settings > Environment Variables > New
-
-###External Libraries
-
-All libraries should be 32- or 64-bit to match your build preference.
-
-CMake will need to know where the headers and libraries for required external dependencies are.
-
-We use CMake's `fixup_bundle` to find the DLLs all of our executable targets require, and then copy them beside the executable in a post-build step. If `fixup_bundle` is having problems finding a DLL, you can fix it manually on your end by adding the folder containing that DLL to your path. Let us know which DLL CMake had trouble finding, as it is possible a tweak to our CMake files is required.
-
-The recommended route for CMake to find the external dependencies is to place all of the dependencies in one folder and set one ENV variable - HIFI_LIB_DIR. That ENV variable should point to a directory with the following structure:
-
- root_lib_dir
- -> openssl
- -> bin
- -> include
- -> lib
-
-For many of the external libraries where precompiled binaries are readily available you should be able to simply copy the extracted folder that you get from the download links provided at the top of the guide. Otherwise you may need to build from source and install the built product to this directory. The `root_lib_dir` in the above example can be wherever you choose on your system - as long as the environment variable HIFI_LIB_DIR is set to it. From here on, whenever you see %HIFI_LIB_DIR% you should substitute the directory that you chose.
-
-####OpenSSL
-
-Qt will use OpenSSL if it's available, but it doesn't install it, so you must install it separately.
-
-Your system may already have several versions of the OpenSSL DLL's (ssleay32.dll, libeay32.dll) lying around, but they may be the wrong version. If these DLL's are in the PATH then QT will try to use them, and if they're the wrong version then you will see the following errors in the console:
-
- QSslSocket: cannot resolve TLSv1_1_client_method
- QSslSocket: cannot resolve TLSv1_2_client_method
- QSslSocket: cannot resolve TLSv1_1_server_method
- QSslSocket: cannot resolve TLSv1_2_server_method
- QSslSocket: cannot resolve SSL_select_next_proto
- QSslSocket: cannot resolve SSL_CTX_set_next_proto_select_cb
- QSslSocket: cannot resolve SSL_get0_next_proto_negotiated
-
-To prevent these problems, install OpenSSL yourself. Download one of the following binary packages [from this website](https://slproweb.com/products/Win32OpenSSL.html):
-* Win32 OpenSSL v1.0.1q
-* Win64 OpenSSL v1.0.1q
-
-Install OpenSSL into the Windows system directory, to make sure that Qt uses the version that you've just installed, and not some other version.
-
-###Build High Fidelity using Visual Studio
-Follow the same build steps from the CMake section of [BUILD.md](BUILD.md), but pass a different generator to CMake.
-
-For 32-bit builds:
-
- cmake .. -G "Visual Studio 12"
-
-For 64-bit builds:
+###Step 6. Running CMake to Generate Build Files
+Run Command Prompt from Start and run the following commands:
+ cd "%HIFI_DIR%"
+ mkdir build
+ cd build
cmake .. -G "Visual Studio 12 Win64"
+
+Where %HIFI_DIR% is the directory for the highfidelity repository.
-Open %HIFI_DIR%\build\hifi.sln and compile.
+###Step 7. Making a Build
-###Running Interface
-If you need to debug Interface, you can run interface from within Visual Studio (see the section below). You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Debug\interface.exe
+Open '%HIFI_DIR%\build\hifi.sln' using Visual Studio.
-###Debugging Interface
-* In the Solution Explorer, right click interface and click Set as StartUp Project
-* Set the "Working Directory" for the Interface debugging sessions to the Debug output directory so that your application can load resources. Do this: right click interface and click Properties, choose Debugging from Configuration Properties, set Working Directory to .\Debug
-* Now you can run and debug interface through Visual Studio
+Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance.
-For better performance when running debug builds, set the environment variable ```_NO_DEBUG_HEAP``` to ```1```
+Run Build > Build Solution.
+
+###Step 8. Testing Interface
+
+Create another environment variable (see Step #4)
+* Set "Variable name": _NO_DEBUG_HEAP
+* Set "Variable value": 1
+
+In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run Debug > Start Debugging.
+
+Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow.
+
+Note: You can also run Interface by launching it from command line or File Explorer from %HIFI_DIR%\build\interface\Release\interface.exe
+
+###Troubleshooting
+
+For any problems after Step #6, first try this:
+* Delete your locally cloned copy of the highfidelity repository
+* Restart your computer
+* Redownload the [repository](https://github.com/highfidelity/hifi)
+* Restart directions from Step #6
+
+####CMake gives you the same error message repeatedly after the build fails
+
+Remove `CMakeCache.txt` found in the '%HIFI_DIR%\build' directory
+
+####nmake cannot be found
+
+Make sure nmake.exe is located at the following path:
+ C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin
+
+If not, add the directory where nmake is located to the PATH environment variable.
+
+####Qt is throwing an error
+
+Make sure you have the correct version (5.6.1-1) installed and 'QT_CMAKE_PREFIX_PATH' environment variable is set correctly.
-http://preshing.com/20110717/the-windows-heap-is-slow-when-launched-from-the-debugger/
diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json
index 04a3f560b6..9e3b2f4d13 100644
--- a/interface/resources/controllers/standard.json
+++ b/interface/resources/controllers/standard.json
@@ -2,7 +2,27 @@
"name": "Standard to Action",
"channels": [
{ "from": "Standard.LY", "to": "Actions.TranslateZ" },
- { "from": "Standard.LX", "to": "Actions.TranslateX" },
+
+ { "from": "Standard.LX",
+ "when": [
+ "Application.InHMD", "!Application.AdvancedMovement",
+ "Application.SnapTurn", "!Standard.RX"
+ ],
+ "to": "Actions.StepYaw",
+ "filters":
+ [
+ { "type": "deadZone", "min": 0.15 },
+ "constrainToInteger",
+ { "type": "pulse", "interval": 0.25 },
+ { "type": "scale", "scale": 22.5 }
+ ]
+ },
+ { "from": "Standard.LX", "to": "Actions.TranslateX",
+ "when": [ "Application.AdvancedMovement" ]
+ },
+ { "from": "Standard.LX", "to": "Actions.Yaw",
+ "when": [ "!Application.AdvancedMovement", "!Application.SnapTurn" ]
+ },
{ "from": "Standard.RX",
"when": [ "Application.InHMD", "Application.SnapTurn" ],
@@ -15,29 +35,29 @@
{ "type": "scale", "scale": 22.5 }
]
},
+ { "from": "Standard.RX", "to": "Actions.Yaw",
+ "when": [ "!Application.SnapTurn" ]
+ },
- { "from": "Standard.RX", "to": "Actions.Yaw" },
- { "from": "Standard.RY",
- "when": "Application.Grounded",
- "to": "Actions.Up",
- "filters":
+ { "from": "Standard.RY",
+ "when": "Application.Grounded",
+ "to": "Actions.Up",
+ "filters":
[
{ "type": "deadZone", "min": 0.6 },
"invert"
]
- },
+ },
- { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"},
+ { "from": "Standard.RY", "to": "Actions.Up", "filters": "invert"},
{ "from": "Standard.Back", "to": "Actions.CycleCamera" },
{ "from": "Standard.Start", "to": "Actions.ContextMenu" },
- { "from": "Standard.LT", "to": "Actions.LeftHandClick" },
+ { "from": "Standard.LT", "to": "Actions.LeftHandClick" },
{ "from": "Standard.RT", "to": "Actions.RightHandClick" },
- { "from": "Standard.LeftHand", "to": "Actions.LeftHand" },
+ { "from": "Standard.LeftHand", "to": "Actions.LeftHand" },
{ "from": "Standard.RightHand", "to": "Actions.RightHand" }
]
}
-
-
diff --git a/interface/resources/html/img/devices.png b/interface/resources/html/img/devices.png
deleted file mode 100644
index fc4231e96e..0000000000
Binary files a/interface/resources/html/img/devices.png and /dev/null differ
diff --git a/interface/resources/html/img/models.png b/interface/resources/html/img/models.png
deleted file mode 100644
index b09c36011d..0000000000
Binary files a/interface/resources/html/img/models.png and /dev/null differ
diff --git a/interface/resources/html/img/move.png b/interface/resources/html/img/move.png
deleted file mode 100644
index 4444ebbec5..0000000000
Binary files a/interface/resources/html/img/move.png and /dev/null differ
diff --git a/interface/resources/html/img/run-script.png b/interface/resources/html/img/run-script.png
deleted file mode 100644
index 941b8ee9f1..0000000000
Binary files a/interface/resources/html/img/run-script.png and /dev/null differ
diff --git a/interface/resources/html/img/talk.png b/interface/resources/html/img/talk.png
deleted file mode 100644
index 682e034ab0..0000000000
Binary files a/interface/resources/html/img/talk.png and /dev/null differ
diff --git a/interface/resources/html/img/write-script.png b/interface/resources/html/img/write-script.png
deleted file mode 100644
index dae97e59b1..0000000000
Binary files a/interface/resources/html/img/write-script.png and /dev/null differ
diff --git a/interface/resources/html/interface-welcome.html b/interface/resources/html/interface-welcome.html
deleted file mode 100644
index c803ef864b..0000000000
--- a/interface/resources/html/interface-welcome.html
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-
-
-
- Welcome to Interface
-
-
-
-
-
-
-
-
Move around
-
-
- Move around with WASD & fly
- up or down with E & C.
- Cmnd/Ctrl+G will send you
- home. Hitting Enter will let you
- teleport to a user or location.
-
-
-
-
Listen & talk
-
-
- Use your best headphones
- and microphone for high
- fidelity audio.
-
-
-
-
Connect devices
-
-
- Have an Oculus Rift, a Razer
- Hydra, or a PrimeSense 3D
- camera? We support them all.
-
-
-
-
Run a script
-
-
- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts and search
- for new ones to run.
-
-
-
-
Script something
-
-
- Write a script; we're always
- adding new features.
- Cmnd/Cntrl+J will launch a
- Running Scripts dialog to help
- manage your scripts.
-
-
-
-
Import models
-
-
- Use the edit.js script to
- add FBX models in-world. You
- can use grids and fine tune
- placement-related parameters
- with ease.
-
-
-
-
-
Read the docs
-
- We are writing documentation on
- just about everything. Please,
- devour all we've written and make
- suggestions where necessary.
- Documentation is always at
- docs.highfidelity.com
-
-
-
-
-
-
-
-
-
diff --git a/interface/resources/qml/AvatarInputs.qml b/interface/resources/qml/AvatarInputs.qml
index 384504aaa0..28f3c0c7b9 100644
--- a/interface/resources/qml/AvatarInputs.qml
+++ b/interface/resources/qml/AvatarInputs.qml
@@ -15,12 +15,11 @@ import Qt.labs.settings 1.0
Hifi.AvatarInputs {
id: root
objectName: "AvatarInputs"
- width: mirrorWidth
- height: controls.height + mirror.height
+ width: rootWidth
+ height: controls.height
x: 10; y: 5
- readonly property int mirrorHeight: 215
- readonly property int mirrorWidth: 265
+ readonly property int rootWidth: 265
readonly property int iconSize: 24
readonly property int iconPadding: 5
@@ -39,61 +38,15 @@ Hifi.AvatarInputs {
anchors.fill: parent
}
- Item {
- id: mirror
- width: root.mirrorWidth
- height: root.mirrorVisible ? root.mirrorHeight : 0
- visible: root.mirrorVisible
- anchors.left: parent.left
- clip: true
-
- Image {
- id: closeMirror
- visible: hover.containsMouse
- width: root.iconSize
- height: root.iconSize
- anchors.top: parent.top
- anchors.topMargin: root.iconPadding
- anchors.left: parent.left
- anchors.leftMargin: root.iconPadding
- source: "../images/close.svg"
- MouseArea {
- anchors.fill: parent
- onClicked: {
- root.closeMirror();
- }
- }
- }
-
- Image {
- id: zoomIn
- visible: hover.containsMouse
- width: root.iconSize
- height: root.iconSize
- anchors.bottom: parent.bottom
- anchors.bottomMargin: root.iconPadding
- anchors.left: parent.left
- anchors.leftMargin: root.iconPadding
- source: root.mirrorZoomed ? "../images/minus.svg" : "../images/plus.svg"
- MouseArea {
- anchors.fill: parent
- onClicked: {
- root.toggleZoom();
- }
- }
- }
- }
-
Item {
id: controls
- width: root.mirrorWidth
+ width: root.rootWidth
height: 44
visible: root.showAudioTools
- anchors.top: mirror.bottom
Rectangle {
anchors.fill: parent
- color: root.mirrorVisible ? (root.audioClipping ? "red" : "#696969") : "#00000000"
+ color: "#00000000"
Item {
id: audioMeter
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 03f992bf7a..02fe2a2dd3 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -215,18 +215,10 @@ static const QString FBX_EXTENSION = ".fbx";
static const QString OBJ_EXTENSION = ".obj";
static const QString AVA_JSON_EXTENSION = ".ava.json";
-static const int MIRROR_VIEW_TOP_PADDING = 5;
-static const int MIRROR_VIEW_LEFT_PADDING = 10;
-static const int MIRROR_VIEW_WIDTH = 265;
-static const int MIRROR_VIEW_HEIGHT = 215;
static const float MIRROR_FULLSCREEN_DISTANCE = 0.389f;
-static const float MIRROR_REARVIEW_DISTANCE = 0.722f;
-static const float MIRROR_REARVIEW_BODY_DISTANCE = 2.56f;
-static const float MIRROR_FIELD_OF_VIEW = 30.0f;
static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND;
-static const QString INFO_WELCOME_PATH = "html/interface-welcome.html";
static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html";
static const QString INFO_HELP_PATH = "html/help.html";
@@ -425,6 +417,7 @@ static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson";
static const QString STATE_CAMERA_ENTITY = "CameraEntity";
static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent";
static const QString STATE_SNAP_TURN = "SnapTurn";
+static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement";
static const QString STATE_GROUNDED = "Grounded";
static const QString STATE_NAV_FOCUSED = "NavigationFocused";
@@ -515,7 +508,7 @@ bool setupEssentials(int& argc, char** argv) {
DependencyManager::set();
controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR,
STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT,
- STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } });
+ STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED } });
DependencyManager::set();
DependencyManager::set();
DependencyManager::set();
@@ -567,7 +560,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_entityClipboardRenderer(false, this, this),
_entityClipboard(new EntityTree()),
_lastQueriedTime(usecTimestampNow()),
- _mirrorViewRect(QRect(MIRROR_VIEW_LEFT_PADDING, MIRROR_VIEW_TOP_PADDING, MIRROR_VIEW_WIDTH, MIRROR_VIEW_HEIGHT)),
_previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION),
_fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES),
_hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT),
@@ -1131,6 +1123,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
_applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float {
return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0;
});
+ _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float {
+ return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0;
+ });
+
_applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float {
return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0;
});
@@ -2121,21 +2117,6 @@ void Application::paintGL() {
batch.resetStages();
});
- auto inputs = AvatarInputs::getInstance();
- if (inputs->mirrorVisible()) {
- PerformanceTimer perfTimer("Mirror");
-
- renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE;
- renderArgs._blitFramebuffer = DependencyManager::get()->getSelfieFramebuffer();
-
- _mirrorViewRect.moveTo(inputs->x(), inputs->y());
-
- renderRearViewMirror(&renderArgs, _mirrorViewRect, inputs->mirrorZoomed());
-
- renderArgs._blitFramebuffer.reset();
- renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE;
- }
-
{
PerformanceTimer perfTimer("renderOverlay");
// NOTE: There is no batch associated with this renderArgs
@@ -2383,10 +2364,6 @@ void Application::setSettingConstrainToolbarPosition(bool setting) {
DependencyManager::get()->setConstrainToolbarToCenterX(setting);
}
-void Application::aboutApp() {
- InfoView::show(INFO_WELCOME_PATH);
-}
-
void Application::showHelp() {
static const QString HAND_CONTROLLER_NAME_VIVE = "vive";
static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus";
@@ -2888,51 +2865,49 @@ void Application::keyPressEvent(QKeyEvent* event) {
break;
#endif
- case Qt::Key_H:
- if (isShifted) {
- Menu::getInstance()->triggerOption(MenuOption::MiniMirror);
- } else {
- // whenever switching to/from full screen mirror from the keyboard, remember
- // the state you were in before full screen mirror, and return to that.
- auto previousMode = _myCamera.getMode();
- if (previousMode != CAMERA_MODE_MIRROR) {
- switch (previousMode) {
- case CAMERA_MODE_FIRST_PERSON:
- _returnFromFullScreenMirrorTo = MenuOption::FirstPerson;
- break;
- case CAMERA_MODE_THIRD_PERSON:
- _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson;
- break;
+ case Qt::Key_H: {
+ // whenever switching to/from full screen mirror from the keyboard, remember
+ // the state you were in before full screen mirror, and return to that.
+ auto previousMode = _myCamera.getMode();
+ if (previousMode != CAMERA_MODE_MIRROR) {
+ switch (previousMode) {
+ case CAMERA_MODE_FIRST_PERSON:
+ _returnFromFullScreenMirrorTo = MenuOption::FirstPerson;
+ break;
+ case CAMERA_MODE_THIRD_PERSON:
+ _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson;
+ break;
- // FIXME - it's not clear that these modes make sense to return to...
- case CAMERA_MODE_INDEPENDENT:
- _returnFromFullScreenMirrorTo = MenuOption::IndependentMode;
- break;
- case CAMERA_MODE_ENTITY:
- _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode;
- break;
+ // FIXME - it's not clear that these modes make sense to return to...
+ case CAMERA_MODE_INDEPENDENT:
+ _returnFromFullScreenMirrorTo = MenuOption::IndependentMode;
+ break;
+ case CAMERA_MODE_ENTITY:
+ _returnFromFullScreenMirrorTo = MenuOption::CameraEntityMode;
+ break;
- default:
- _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson;
- break;
- }
+ default:
+ _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson;
+ break;
}
-
- bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror);
- Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked);
- if (isMirrorChecked) {
-
- // if we got here without coming in from a non-Full Screen mirror case, then our
- // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old
- // behavior of returning to ThirdPerson
- if (_returnFromFullScreenMirrorTo.isEmpty()) {
- _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson;
- }
- Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true);
- }
- cameraMenuChanged();
}
+
+ bool isMirrorChecked = Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror);
+ Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, !isMirrorChecked);
+ if (isMirrorChecked) {
+
+ // if we got here without coming in from a non-Full Screen mirror case, then our
+ // _returnFromFullScreenMirrorTo is unknown. In that case we'll go to the old
+ // behavior of returning to ThirdPerson
+ if (_returnFromFullScreenMirrorTo.isEmpty()) {
+ _returnFromFullScreenMirrorTo = MenuOption::ThirdPerson;
+ }
+ Menu::getInstance()->setIsOptionChecked(_returnFromFullScreenMirrorTo, true);
+ }
+ cameraMenuChanged();
break;
+ }
+
case Qt::Key_P: {
if (!(isShifted || isMeta || isOption)) {
bool isFirstPersonChecked = Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson);
@@ -3847,8 +3822,6 @@ void Application::init() {
DependencyManager::get()->init();
_myCamera.setMode(CAMERA_MODE_FIRST_PERSON);
- _mirrorCamera.setMode(CAMERA_MODE_MIRROR);
-
_timerStart.start();
_lastTimeUpdated.start();
@@ -4465,9 +4438,12 @@ void Application::update(float deltaTime) {
getEntities()->getTree()->withWriteLock([&] {
PerformanceTimer perfTimer("handleOutgoingChanges");
- const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges();
- _entitySimulation->handleOutgoingChanges(outgoingChanges);
- avatarManager->handleOutgoingChanges(outgoingChanges);
+ const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates();
+ _entitySimulation->handleDeactivatedMotionStates(deactivations);
+
+ const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates();
+ _entitySimulation->handleChangedMotionStates(outgoingChanges);
+ avatarManager->handleChangedMotionStates(outgoingChanges);
});
if (!_aboutToQuit) {
@@ -5124,58 +5100,6 @@ void Application::displaySide(RenderArgs* renderArgs, Camera& theCamera, bool se
activeRenderingThread = nullptr;
}
-void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& region, bool isZoomed) {
- auto originalViewport = renderArgs->_viewport;
- // Grab current viewport to reset it at the end
-
- float aspect = (float)region.width() / region.height();
- float fov = MIRROR_FIELD_OF_VIEW;
-
- auto myAvatar = getMyAvatar();
-
- // bool eyeRelativeCamera = false;
- if (!isZoomed) {
- _mirrorCamera.setPosition(myAvatar->getChestPosition() +
- myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * myAvatar->getScale());
-
- } else { // HEAD zoom level
- // FIXME note that the positioning of the camera relative to the avatar can suffer limited
- // precision as the user's position moves further away from the origin. Thus at
- // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways
- // wildly as you rotate your avatar because the floating point values are becoming
- // larger, squeezing out the available digits of precision you have available at the
- // human scale for camera positioning.
-
- // Previously there was a hack to correct this using the mechanism of repositioning
- // the avatar at the origin of the world for the purposes of rendering the mirror,
- // but it resulted in failing to render the avatar's head model in the mirror view
- // when in first person mode. Presumably this was because of some missed culling logic
- // that was not accounted for in the hack.
-
- // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further
- // investigated in order to adapt the technique while fixing the head rendering issue,
- // but the complexity of the hack suggests that a better approach
- _mirrorCamera.setPosition(myAvatar->getDefaultEyePosition() +
- myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * myAvatar->getScale());
- }
- _mirrorCamera.setProjection(glm::perspective(glm::radians(fov), aspect, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP));
- _mirrorCamera.setOrientation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI, 0.0f)));
-
-
- // set the bounds of rear mirror view
- // the region is in device independent coordinates; must convert to device
- float ratio = (float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale();
- int width = region.width() * ratio;
- int height = region.height() * ratio;
- gpu::Vec4i viewport = gpu::Vec4i(0, 0, width, height);
- renderArgs->_viewport = viewport;
-
- // render rear mirror view
- displaySide(renderArgs, _mirrorCamera, true);
-
- renderArgs->_viewport = originalViewport;
-}
-
void Application::resetSensors(bool andReload) {
DependencyManager::get()->reset();
DependencyManager::get()->reset();
diff --git a/interface/src/Application.h b/interface/src/Application.h
index 35592a4727..7ae4160f8b 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -278,8 +278,6 @@ public:
virtual void pushPostUpdateLambda(void* key, std::function func) override;
- const QRect& getMirrorViewRect() const { return _mirrorViewRect; }
-
void updateMyAvatarLookAtPosition();
float getAvatarSimrate() const { return _avatarSimCounter.rate(); }
@@ -370,7 +368,6 @@ public slots:
void calibrateEyeTracker5Points();
#endif
- void aboutApp();
static void showHelp();
void cycleCamera();
@@ -559,8 +556,6 @@ private:
int _avatarSimsPerSecondReport {0};
quint64 _lastAvatarSimsPerSecondUpdate {0};
Camera _myCamera; // My view onto the world
- Camera _mirrorCamera; // Camera for mirror view
- QRect _mirrorViewRect;
Setting::Handle _previousScriptLocation;
Setting::Handle _fieldOfView;
diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp
index 8e124d27c7..aeea7e0c60 100644
--- a/interface/src/Menu.cpp
+++ b/interface/src/Menu.cpp
@@ -74,9 +74,6 @@ Menu::Menu() {
// File > Help
addActionToQMenuAndActionHash(fileMenu, MenuOption::Help, 0, qApp, SLOT(showHelp()));
- // File > About
- addActionToQMenuAndActionHash(fileMenu, MenuOption::AboutApp, 0, qApp, SLOT(aboutApp()), QAction::AboutRole);
-
// File > Quit
addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole);
@@ -249,9 +246,6 @@ Menu::Menu() {
viewMenu->addSeparator();
- // View > Mini Mirror
- addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::MiniMirror, 0, false);
-
// View > Center Player In View
addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::CenterPlayerInView,
0, true, qApp, SLOT(rotationModeChanged()),
@@ -417,6 +411,9 @@ Menu::Menu() {
}
// Developer > Assets >>>
+ // Menu item is not currently needed but code should be kept in case it proves useful again at some stage.
+//#define WANT_ASSET_MIGRATION
+#ifdef WANT_ASSET_MIGRATION
MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets");
auto& atpMigrator = ATPAssetMigrator::getInstance();
atpMigrator.setDialogParent(this);
@@ -424,6 +421,7 @@ Menu::Menu() {
addActionToQMenuAndActionHash(assetDeveloperMenu, MenuOption::AssetMigration,
0, &atpMigrator,
SLOT(loadEntityServerFile()));
+#endif
// Developer > Avatar >>>
MenuWrapper* avatarDebugMenu = developerMenu->addMenu("Avatar");
@@ -554,14 +552,14 @@ Menu::Menu() {
"NetworkingPreferencesDialog");
});
addActionToQMenuAndActionHash(networkMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
+ addActionToQMenuAndActionHash(networkMenu, MenuOption::ClearDiskCache, 0,
+ DependencyManager::get().data(), SLOT(clearCache()));
addCheckableActionToQMenuAndActionHash(networkMenu,
MenuOption::DisableActivityLogger,
0,
false,
&UserActivityLogger::getInstance(),
SLOT(disable(bool)));
- addActionToQMenuAndActionHash(networkMenu, MenuOption::DiskCacheEditor, 0,
- dialogsManager.data(), SLOT(toggleDiskCacheEditor()));
addActionToQMenuAndActionHash(networkMenu, MenuOption::ShowDSConnectTable, 0,
dialogsManager.data(), SLOT(showDomainConnectionDialog()));
diff --git a/interface/src/Menu.h b/interface/src/Menu.h
index e0ac340edc..a0beb25762 100644
--- a/interface/src/Menu.h
+++ b/interface/src/Menu.h
@@ -26,7 +26,6 @@ public:
};
namespace MenuOption {
- const QString AboutApp = "About Interface";
const QString AddRemoveFriends = "Add/Remove Friends...";
const QString AddressBar = "Show Address Bar";
const QString Animations = "Animations...";
@@ -56,6 +55,7 @@ namespace MenuOption {
const QString CameraEntityMode = "Entity Mode";
const QString CenterPlayerInView = "Center Player In View";
const QString Chat = "Chat...";
+ const QString ClearDiskCache = "Clear Disk Cache";
const QString Collisions = "Collisions";
const QString Connexion = "Activate 3D Connexion Devices";
const QString Console = "Console...";
@@ -82,7 +82,6 @@ namespace MenuOption {
const QString DisableActivityLogger = "Disable Activity Logger";
const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment";
const QString DisableLightEntities = "Disable Light Entities";
- const QString DiskCacheEditor = "Disk Cache Editor";
const QString DisplayCrashOptions = "Display Crash Options";
const QString DisplayHandTargets = "Show Hand Targets";
const QString DisplayModelBounds = "Display Model Bounds";
@@ -123,7 +122,6 @@ namespace MenuOption {
const QString LogExtraTimings = "Log Extra Timing Details";
const QString LowVelocityFilter = "Low Velocity Filter";
const QString MeshVisible = "Draw Mesh";
- const QString MiniMirror = "Mini Mirror";
const QString MuteAudio = "Mute Microphone";
const QString MuteEnvironment = "Mute Environment";
const QString MuteFaceTracking = "Mute Face Tracking";
diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp
index 94ce444416..6152148887 100644
--- a/interface/src/avatar/AvatarManager.cpp
+++ b/interface/src/avatar/AvatarManager.cpp
@@ -424,7 +424,7 @@ void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) {
}
}
-void AvatarManager::handleOutgoingChanges(const VectorOfMotionStates& motionStates) {
+void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motionStates) {
// TODO: extract the MyAvatar results once we use a MotionState for it.
}
diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h
index e1f5a3b411..b94f9e6a96 100644
--- a/interface/src/avatar/AvatarManager.h
+++ b/interface/src/avatar/AvatarManager.h
@@ -70,7 +70,7 @@ public:
void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates);
void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates);
void getObjectsToChange(VectorOfMotionStates& motionStates);
- void handleOutgoingChanges(const VectorOfMotionStates& motionStates);
+ void handleChangedMotionStates(const VectorOfMotionStates& motionStates);
void handleCollisionEvents(const CollisionEvents& collisionEvents);
Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index 401378fff1..b40ef601ea 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -104,6 +104,7 @@ MyAvatar::MyAvatar(RigPointer rig) :
_eyeContactTarget(LEFT_EYE),
_realWorldFieldOfView("realWorldFieldOfView",
DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES),
+ _useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false),
_hmdSensorMatrix(),
_hmdSensorOrientation(),
_hmdSensorPosition(),
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index 9bcec1134f..5f812f1f99 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -74,6 +74,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
+ Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
public:
enum DriveKeys {
@@ -176,6 +177,10 @@ public:
Q_INVOKABLE void setHMDLeanRecenterEnabled(bool value) { _hmdLeanRecenterEnabled = value; }
Q_INVOKABLE bool getHMDLeanRecenterEnabled() const { return _hmdLeanRecenterEnabled; }
+ bool useAdvancedMovementControls() const { return _useAdvancedMovementControls.get(); }
+ void setUseAdvancedMovementControls(bool useAdvancedMovementControls)
+ { _useAdvancedMovementControls.set(useAdvancedMovementControls); }
+
// get/set avatar data
void saveData();
void loadData();
@@ -436,6 +441,7 @@ private:
glm::vec3 _trackedHeadPosition;
Setting::Handle _realWorldFieldOfView;
+ Setting::Handle _useAdvancedMovementControls;
// private methods
void updateOrientation(float deltaTime);
diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp
index 364dff52a3..b9d7fadc97 100644
--- a/interface/src/ui/ApplicationOverlay.cpp
+++ b/interface/src/ui/ApplicationOverlay.cpp
@@ -13,7 +13,6 @@
#include
#include
-#include
#include
#include
#include
@@ -42,7 +41,6 @@ ApplicationOverlay::ApplicationOverlay()
_domainStatusBorder = geometryCache->allocateID();
_magnifierBorder = geometryCache->allocateID();
_qmlGeometryId = geometryCache->allocateID();
- _rearViewGeometryId = geometryCache->allocateID();
}
ApplicationOverlay::~ApplicationOverlay() {
@@ -51,7 +49,6 @@ ApplicationOverlay::~ApplicationOverlay() {
geometryCache->releaseID(_domainStatusBorder);
geometryCache->releaseID(_magnifierBorder);
geometryCache->releaseID(_qmlGeometryId);
- geometryCache->releaseID(_rearViewGeometryId);
}
}
@@ -86,7 +83,6 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) {
// Now render the overlay components together into a single texture
renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line
renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter
- renderRearView(renderArgs); // renders the mirror view selfie
renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope
renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts
renderStatsAndLogs(renderArgs); // currently renders nothing
@@ -163,45 +159,6 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) {
qApp->getOverlays().renderHUD(renderArgs);
}
-void ApplicationOverlay::renderRearViewToFbo(RenderArgs* renderArgs) {
-}
-
-void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) {
- if (!qApp->isHMDMode() && Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) &&
- !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) {
- gpu::Batch& batch = *renderArgs->_batch;
-
- auto geometryCache = DependencyManager::get();
-
- auto framebuffer = DependencyManager::get();
- auto selfieTexture = framebuffer->getSelfieFramebuffer()->getRenderBuffer(0);
-
- int width = renderArgs->_viewport.z;
- int height = renderArgs->_viewport.w;
- mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP);
- batch.setProjectionTransform(legacyProjection);
- batch.setModelTransform(Transform());
- batch.resetViewTransform();
-
- float screenRatio = ((float)qApp->getDevicePixelRatio());
- float renderRatio = ((float)qApp->getRenderResolutionScale());
-
- auto viewport = qApp->getMirrorViewRect();
- glm::vec2 bottomLeft(viewport.left(), viewport.top() + viewport.height());
- glm::vec2 topRight(viewport.left() + viewport.width(), viewport.top());
- bottomLeft *= screenRatio;
- topRight *= screenRatio;
- glm::vec2 texCoordMinCorner(0.0f, 0.0f);
- glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight()));
-
- batch.setResourceTexture(0, selfieTexture);
- float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat();
- geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha), _rearViewGeometryId);
-
- batch.setResourceTexture(0, renderArgs->_whiteTexture);
- }
-}
-
void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) {
// Display stats and log text onscreen
diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h
index 7ace5ee885..af4d8779d4 100644
--- a/interface/src/ui/ApplicationOverlay.h
+++ b/interface/src/ui/ApplicationOverlay.h
@@ -31,8 +31,6 @@ public:
private:
void renderStatsAndLogs(RenderArgs* renderArgs);
void renderDomainConnectionStatusBorder(RenderArgs* renderArgs);
- void renderRearViewToFbo(RenderArgs* renderArgs);
- void renderRearView(RenderArgs* renderArgs);
void renderQmlUi(RenderArgs* renderArgs);
void renderAudioScope(RenderArgs* renderArgs);
void renderOverlays(RenderArgs* renderArgs);
@@ -51,7 +49,6 @@ private:
gpu::TexturePointer _overlayColorTexture;
gpu::FramebufferPointer _overlayFramebuffer;
int _qmlGeometryId { 0 };
- int _rearViewGeometryId { 0 };
};
#endif // hifi_ApplicationOverlay_h
diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp
index b09289c78a..944be4bf9e 100644
--- a/interface/src/ui/AvatarInputs.cpp
+++ b/interface/src/ui/AvatarInputs.cpp
@@ -20,10 +20,6 @@ HIFI_QML_DEF(AvatarInputs)
static AvatarInputs* INSTANCE{ nullptr };
-static const char SETTINGS_GROUP_NAME[] = "Rear View Tools";
-static const char ZOOM_LEVEL_SETTINGS[] = "ZoomLevel";
-
-static Setting::Handle rearViewZoomLevel(QStringList() << SETTINGS_GROUP_NAME << ZOOM_LEVEL_SETTINGS, 0);
AvatarInputs* AvatarInputs::getInstance() {
if (!INSTANCE) {
@@ -36,8 +32,6 @@ AvatarInputs* AvatarInputs::getInstance() {
AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) {
INSTANCE = this;
- int zoomSetting = rearViewZoomLevel.get();
- _mirrorZoomed = zoomSetting == 0;
}
#define AI_UPDATE(name, src) \
@@ -62,8 +56,6 @@ void AvatarInputs::update() {
if (!Menu::getInstance()) {
return;
}
- AI_UPDATE(mirrorVisible, Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror) && !qApp->isHMDMode()
- && !Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror));
AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking));
AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking));
AI_UPDATE(isHMD, qApp->isHMDMode());
@@ -122,15 +114,3 @@ void AvatarInputs::toggleAudioMute() {
void AvatarInputs::resetSensors() {
qApp->resetSensors();
}
-
-void AvatarInputs::toggleZoom() {
- _mirrorZoomed = !_mirrorZoomed;
- rearViewZoomLevel.set(_mirrorZoomed ? 0 : 1);
- emit mirrorZoomedChanged();
-}
-
-void AvatarInputs::closeMirror() {
- if (Menu::getInstance()->isOptionChecked(MenuOption::MiniMirror)) {
- Menu::getInstance()->triggerOption(MenuOption::MiniMirror);
- }
-}
diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h
index 85570ecd3c..5535469445 100644
--- a/interface/src/ui/AvatarInputs.h
+++ b/interface/src/ui/AvatarInputs.h
@@ -28,8 +28,6 @@ class AvatarInputs : public QQuickItem {
AI_PROPERTY(bool, audioMuted, false)
AI_PROPERTY(bool, audioClipping, false)
AI_PROPERTY(float, audioLevel, 0)
- AI_PROPERTY(bool, mirrorVisible, false)
- AI_PROPERTY(bool, mirrorZoomed, true)
AI_PROPERTY(bool, isHMD, false)
AI_PROPERTY(bool, showAudioTools, true)
@@ -44,8 +42,6 @@ signals:
void audioMutedChanged();
void audioClippingChanged();
void audioLevelChanged();
- void mirrorVisibleChanged();
- void mirrorZoomedChanged();
void isHMDChanged();
void showAudioToolsChanged();
@@ -53,8 +49,6 @@ protected:
Q_INVOKABLE void resetSensors();
Q_INVOKABLE void toggleCameraMute();
Q_INVOKABLE void toggleAudioMute();
- Q_INVOKABLE void toggleZoom();
- Q_INVOKABLE void closeMirror();
private:
float _trailingAudioLoudness{ 0 };
diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp
index 20b9f6b173..70ffc5ece9 100644
--- a/interface/src/ui/DialogsManager.cpp
+++ b/interface/src/ui/DialogsManager.cpp
@@ -20,7 +20,6 @@
#include "AddressBarDialog.h"
#include "ConnectionFailureDialog.h"
-#include "DiskCacheEditor.h"
#include "DomainConnectionDialog.h"
#include "HMDToolsDialog.h"
#include "LodToolsDialog.h"
@@ -66,11 +65,6 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) {
}
}
-void DialogsManager::toggleDiskCacheEditor() {
- maybeCreateDialog(_diskCacheEditor);
- _diskCacheEditor->toggle();
-}
-
void DialogsManager::toggleLoginDialog() {
LoginDialog::toggleAction();
}
diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h
index 4e3b785679..5f50b49d94 100644
--- a/interface/src/ui/DialogsManager.h
+++ b/interface/src/ui/DialogsManager.h
@@ -21,7 +21,7 @@
class AnimationsDialog;
class AttachmentsDialog;
-class DiskCacheEditor;
+class CachesSizeDialog;
class LodToolsDialog;
class OctreeStatsDialog;
class ScriptEditorWindow;
@@ -45,7 +45,6 @@ public slots:
void showAddressBar();
void showFeed();
void setDomainConnectionFailureVisibility(bool visible);
- void toggleDiskCacheEditor();
void toggleLoginDialog();
void showLoginDialog();
void octreeStatsDetails();
@@ -74,7 +73,7 @@ private:
QPointer _animationsDialog;
QPointer _attachmentsDialog;
- QPointer _diskCacheEditor;
+ QPointer _cachesSizeDialog;
QPointer _ircInfoBox;
QPointer _hmdToolsDialog;
QPointer _lodToolsDialog;
diff --git a/interface/src/ui/DiskCacheEditor.cpp b/interface/src/ui/DiskCacheEditor.cpp
deleted file mode 100644
index 1a7be8642b..0000000000
--- a/interface/src/ui/DiskCacheEditor.cpp
+++ /dev/null
@@ -1,146 +0,0 @@
-//
-// DiskCacheEditor.cpp
-//
-//
-// Created by Clement on 3/4/15.
-// Copyright 2015 High Fidelity, Inc.
-//
-// Distributed under the Apache License, Version 2.0.
-// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-//
-
-#include "DiskCacheEditor.h"
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include
-
-#include "OffscreenUi.h"
-
-DiskCacheEditor::DiskCacheEditor(QWidget* parent) : QObject(parent) {
-}
-
-QWindow* DiskCacheEditor::windowHandle() {
- return (_dialog) ? _dialog->windowHandle() : nullptr;
-}
-
-void DiskCacheEditor::toggle() {
- if (!_dialog) {
- makeDialog();
- }
-
- if (!_dialog->isActiveWindow()) {
- _dialog->show();
- _dialog->raise();
- _dialog->activateWindow();
- } else {
- _dialog->close();
- }
-}
-
-void DiskCacheEditor::makeDialog() {
- _dialog = new QDialog(static_cast(parent()));
- Q_CHECK_PTR(_dialog);
- _dialog->setAttribute(Qt::WA_DeleteOnClose);
- _dialog->setWindowTitle("Disk Cache Editor");
-
- QGridLayout* layout = new QGridLayout(_dialog);
- Q_CHECK_PTR(layout);
- _dialog->setLayout(layout);
-
-
- QLabel* path = new QLabel("Path : ", _dialog);
- Q_CHECK_PTR(path);
- path->setAlignment(Qt::AlignRight);
- layout->addWidget(path, 0, 0);
-
- QLabel* size = new QLabel("Current Size : ", _dialog);
- Q_CHECK_PTR(size);
- size->setAlignment(Qt::AlignRight);
- layout->addWidget(size, 1, 0);
-
- QLabel* maxSize = new QLabel("Max Size : ", _dialog);
- Q_CHECK_PTR(maxSize);
- maxSize->setAlignment(Qt::AlignRight);
- layout->addWidget(maxSize, 2, 0);
-
-
- _path = new QLabel(_dialog);
- Q_CHECK_PTR(_path);
- _path->setAlignment(Qt::AlignLeft);
- layout->addWidget(_path, 0, 1, 1, 3);
-
- _size = new QLabel(_dialog);
- Q_CHECK_PTR(_size);
- _size->setAlignment(Qt::AlignLeft);
- layout->addWidget(_size, 1, 1, 1, 3);
-
- _maxSize = new QLabel(_dialog);
- Q_CHECK_PTR(_maxSize);
- _maxSize->setAlignment(Qt::AlignLeft);
- layout->addWidget(_maxSize, 2, 1, 1, 3);
-
- refresh();
-
-
- static const int REFRESH_INTERVAL = 100; // msec
- _refreshTimer = new QTimer(_dialog);
- _refreshTimer->setInterval(REFRESH_INTERVAL); // Qt::CoarseTimer acceptable, no need for real time accuracy
- _refreshTimer->setSingleShot(false);
- QObject::connect(_refreshTimer.data(), &QTimer::timeout, this, &DiskCacheEditor::refresh);
- _refreshTimer->start();
-
- QPushButton* clearCacheButton = new QPushButton(_dialog);
- Q_CHECK_PTR(clearCacheButton);
- clearCacheButton->setText("Clear");
- clearCacheButton->setToolTip("Erases the entire content of the disk cache.");
- connect(clearCacheButton, SIGNAL(clicked()), SLOT(clear()));
- layout->addWidget(clearCacheButton, 3, 3);
-}
-
-void DiskCacheEditor::refresh() {
- DependencyManager::get()->cacheInfoRequest(this, "cacheInfoCallback");
-}
-
-void DiskCacheEditor::cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize) {
- static const auto stringify = [](qint64 number) {
- static const QStringList UNITS = QStringList() << "B" << "KB" << "MB" << "GB";
- static const qint64 CHUNK = 1024;
- QString unit;
- int i = 0;
- for (i = 0; i < 4; ++i) {
- if (number / CHUNK > 0) {
- number /= CHUNK;
- } else {
- break;
- }
- }
- return QString("%0 %1").arg(number).arg(UNITS[i]);
- };
-
- if (_path) {
- _path->setText(cacheDirectory);
- }
- if (_size) {
- _size->setText(stringify(cacheSize));
- }
- if (_maxSize) {
- _maxSize->setText(stringify(maximumCacheSize));
- }
-}
-
-void DiskCacheEditor::clear() {
- auto buttonClicked = OffscreenUi::question(_dialog, "Clearing disk cache",
- "You are about to erase all the content of the disk cache, "
- "are you sure you want to do that?",
- QMessageBox::Ok | QMessageBox::Cancel);
- if (buttonClicked == QMessageBox::Ok) {
- DependencyManager::get()->clearCache();
- }
-}
diff --git a/interface/src/ui/DiskCacheEditor.h b/interface/src/ui/DiskCacheEditor.h
deleted file mode 100644
index 3f8fa1a883..0000000000
--- a/interface/src/ui/DiskCacheEditor.h
+++ /dev/null
@@ -1,49 +0,0 @@
-//
-// DiskCacheEditor.h
-//
-//
-// Created by Clement on 3/4/15.
-// Copyright 2015 High Fidelity, Inc.
-//
-// Distributed under the Apache License, Version 2.0.
-// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-//
-
-#ifndef hifi_DiskCacheEditor_h
-#define hifi_DiskCacheEditor_h
-
-#include
-#include
-
-class QDialog;
-class QLabel;
-class QWindow;
-class QTimer;
-
-class DiskCacheEditor : public QObject {
- Q_OBJECT
-
-public:
- DiskCacheEditor(QWidget* parent = nullptr);
-
- QWindow* windowHandle();
-
-public slots:
- void toggle();
-
-private slots:
- void refresh();
- void cacheInfoCallback(QString cacheDirectory, qint64 cacheSize, qint64 maximumCacheSize);
- void clear();
-
-private:
- void makeDialog();
-
- QPointer _dialog;
- QPointer _path;
- QPointer _size;
- QPointer _maxSize;
- QPointer _refreshTimer;
-};
-
-#endif // hifi_DiskCacheEditor_h
\ No newline at end of file
diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
index c6bd70a0cb..8c4498edc6 100644
--- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp
+++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
@@ -146,6 +146,7 @@ void EntityTreeRenderer::clear() {
void EntityTreeRenderer::reloadEntityScripts() {
_entitiesScriptEngine->unloadAllEntityScripts();
+ _entitiesScriptEngine->resetModuleCache();
foreach(auto entity, _entitiesInScene) {
if (!entity->getScript().isEmpty()) {
_entitiesScriptEngine->loadEntityScript(entity->getEntityItemID(), entity->getScript(), true);
diff --git a/libraries/entities/src/EntitiesScriptEngineProvider.h b/libraries/entities/src/EntitiesScriptEngineProvider.h
index 69bf73e688..d87dd105c2 100644
--- a/libraries/entities/src/EntitiesScriptEngineProvider.h
+++ b/libraries/entities/src/EntitiesScriptEngineProvider.h
@@ -15,11 +15,13 @@
#define hifi_EntitiesScriptEngineProvider_h
#include
+#include
#include "EntityItemID.h"
class EntitiesScriptEngineProvider {
public:
virtual void callEntityScriptMethod(const EntityItemID& entityID, const QString& methodName, const QStringList& params = QStringList()) = 0;
+ virtual QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) = 0;
};
-#endif // hifi_EntitiesScriptEngineProvider_h
\ No newline at end of file
+#endif // hifi_EntitiesScriptEngineProvider_h
diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp
index 3ef1648fae..0bb085459e 100644
--- a/libraries/entities/src/EntityItem.cpp
+++ b/libraries/entities/src/EntityItem.cpp
@@ -655,13 +655,11 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
// pack SimulationOwner and terse update properties near each other
-
// NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data
// even when we would otherwise ignore the rest of the packet.
bool filterRejection = false;
if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) {
-
QByteArray simOwnerData;
int bytes = OctreePacketData::unpackDataFromBytes(dataAt, simOwnerData);
SimulationOwner newSimOwner;
@@ -1879,6 +1877,7 @@ void EntityItem::setSimulationOwner(const SimulationOwner& owner) {
}
void EntityItem::updateSimulationOwner(const SimulationOwner& owner) {
+ // NOTE: this method only used by EntityServer. The Interface uses special code in readEntityDataFromBuffer().
if (wantTerseEditLogging() && _simulationOwner != owner) {
qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << owner;
}
@@ -1894,8 +1893,9 @@ void EntityItem::clearSimulationOwnership() {
}
_simulationOwner.clear();
- // don't bother setting the DIRTY_SIMULATOR_ID flag because clearSimulationOwnership()
- // is only ever called on the entity-server and the flags are only used client-side
+ // don't bother setting the DIRTY_SIMULATOR_ID flag because:
+ // (a) when entity-server calls clearSimulationOwnership() the dirty-flags are meaningless (only used by interface)
+ // (b) the interface only calls clearSimulationOwnership() in a context that already knows best about dirty flags
//_dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID;
}
diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp
index ae34024bbc..91d39e9490 100644
--- a/libraries/entities/src/EntityScriptingInterface.cpp
+++ b/libraries/entities/src/EntityScriptingInterface.cpp
@@ -10,6 +10,9 @@
//
#include "EntityScriptingInterface.h"
+#include
+#include
+
#include "EntityItemID.h"
#include
#include
@@ -680,6 +683,118 @@ bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) {
return client->reloadServerScript(entityID);
}
+bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) {
+ using LocalScriptStatusRequest = QFutureWatcher;
+
+ LocalScriptStatusRequest* request = new LocalScriptStatusRequest;
+ QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable {
+ auto details = request->result().toMap();
+ QScriptValue err, result;
+ if (details.contains("isError")) {
+ if (!details.contains("message")) {
+ details["message"] = details["errorInfo"];
+ }
+ err = _engine->makeError(_engine->toScriptValue(details));
+ } else {
+ details["success"] = true;
+ result = _engine->toScriptValue(details);
+ }
+ callScopedHandlerObject(handler, err, result);
+ request->deleteLater();
+ });
+ auto entityScriptingInterface = DependencyManager::get();
+ entityScriptingInterface->withEntitiesScriptEngine([&](EntitiesScriptEngineProvider* entitiesScriptEngine) {
+ if (entitiesScriptEngine) {
+ request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID));
+ }
+ });
+ if (!request->isStarted()) {
+ request->deleteLater();
+ callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue());
+ return false;
+ }
+ return true;
+}
+
+bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) {
+ auto client = DependencyManager::get();
+ auto request = client->createScriptStatusRequest(entityID);
+ QPointer engine = _engine;
+ QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable {
+ auto engine = _engine;
+ if (!engine) {
+ qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID;
+ return;
+ }
+ QVariantMap details;
+ details["success"] = request->getResponseReceived();
+ details["isRunning"] = request->getIsRunning();
+ details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower();
+ details["errorInfo"] = request->getErrorInfo();
+
+ QScriptValue err, result;
+ if (!details["success"].toBool()) {
+ if (!details.contains("message") && details.contains("errorInfo")) {
+ details["message"] = details["errorInfo"];
+ }
+ if (details["message"].toString().isEmpty()) {
+ details["message"] = "entity server script details not found";
+ }
+ err = engine->makeError(engine->toScriptValue(details));
+ } else {
+ result = engine->toScriptValue(details);
+ }
+ callScopedHandlerObject(handler, err, result);
+ request->deleteLater();
+ });
+ request->start();
+ return true;
+}
+
+bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) {
+ auto name = property.toString();
+ auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName);
+ QPointer engine = dynamic_cast(handler.engine());
+ if (!engine) {
+ qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name;
+ return false;
+ }
+#ifdef DEBUG_ENGINE_STATE
+ connect(engine, &QObject::destroyed, this, [=]() {
+ qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine");
+ });
+#endif
+ if (!handler.property("callback").isFunction()) {
+ qDebug() << "!handler.callback.isFunction" << engine;
+ engine->raiseException(engine->makeError("callback is not a function", "TypeError"));
+ return false;
+ }
+
+ // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide
+ // some initial structure for organizing metadata adapters around.
+
+ // The extra layer of indirection is *essential* because in real world conditions errors are often introduced
+ // by accident and sometimes without exact memory of "what just changed."
+
+ // Here the scripter only needs to know an entityID and a property name -- which means all scripters can
+ // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties
+ // like .script that work in terms of side-effects.
+
+ // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later.
+
+ EntityPropertyMetadataRequest request(engine);
+
+ if (name == "script") {
+ return request.script(entityID, handler);
+ } else if (name == "serverScripts") {
+ return request.serverScripts(entityID, handler);
+ } else {
+ engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable"));
+ engine->maybeEmitUncaughtException(__FUNCTION__);
+ return false;
+ }
+}
+
bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) {
auto client = DependencyManager::get();
auto request = client->createScriptStatusRequest(entityID);
diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h
index b0c137ef77..c21e9807e8 100644
--- a/libraries/entities/src/EntityScriptingInterface.h
+++ b/libraries/entities/src/EntityScriptingInterface.h
@@ -34,9 +34,24 @@
#include "EntitiesScriptEngineProvider.h"
#include "EntityItemProperties.h"
+#include "BaseScriptEngine.h"
+
class EntityTree;
class MeshProxy;
+// helper factory to compose standardized, async metadata queries for "magic" Entity properties
+// like .script and .serverScripts. This is used for automated testing of core scripting features
+// as well as to provide early adopters a self-discoverable, consistent way to diagnose common
+// problems with their own Entity scripts.
+class EntityPropertyMetadataRequest {
+public:
+ EntityPropertyMetadataRequest(BaseScriptEngine* engine) : _engine(engine) {};
+ bool script(EntityItemID entityID, QScriptValue handler);
+ bool serverScripts(EntityItemID entityID, QScriptValue handler);
+private:
+ QPointer _engine;
+};
+
class RayToEntityIntersectionResult {
public:
RayToEntityIntersectionResult();
@@ -68,6 +83,7 @@ class EntityScriptingInterface : public OctreeScriptingInterface, public Depende
Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier)
Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity)
+ friend EntityPropertyMetadataRequest;
public:
EntityScriptingInterface(bool bidOnSimulationOwnership);
@@ -212,6 +228,26 @@ public slots:
Q_INVOKABLE RayToEntityIntersectionResult findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking = false, const QScriptValue& entityIdsToInclude = QScriptValue(), const QScriptValue& entityIdsToDiscard = QScriptValue());
Q_INVOKABLE bool reloadServerScripts(QUuid entityID);
+
+ /**jsdoc
+ * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
+ *
+ * @function Entities.queryPropertyMetadata
+ * @param {EntityID} entityID The ID of the entity.
+ * @param {string} property The name of the property extended metadata is wanted for.
+ * @param {ResultCallback} callback Executes callback(err, result) with the query results.
+ */
+ /**jsdoc
+ * Query additional metadata for "magic" Entity properties like `script` and `serverScripts`.
+ *
+ * @function Entities.queryPropertyMetadata
+ * @param {EntityID} entityID The ID of the entity.
+ * @param {string} property The name of the property extended metadata is wanted for.
+ * @param {Object} thisObject The scoping "this" context that callback will be executed within.
+ * @param {ResultCallback} callbackOrMethodName Executes thisObject[callbackOrMethodName](err, result) with the query results.
+ */
+ Q_INVOKABLE bool queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName = QScriptValue());
+
Q_INVOKABLE bool getServerScriptStatus(QUuid entityID, QScriptValue callback);
Q_INVOKABLE void setLightsArePickable(bool value);
@@ -325,6 +361,11 @@ signals:
void webEventReceived(const EntityItemID& entityItemID, const QVariant& message);
+protected:
+ void withEntitiesScriptEngine(std::function function) {
+ std::lock_guard lock(_entitiesScriptEngineLock);
+ function(_entitiesScriptEngine);
+ };
private:
bool actionWorker(const QUuid& entityID, std::function actor);
bool polyVoxWorker(QUuid entityID, std::function actor);
diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp
index 73cf7a520e..0cb932b375 100644
--- a/libraries/fbx/src/OBJReader.cpp
+++ b/libraries/fbx/src/OBJReader.cpp
@@ -546,6 +546,7 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping,
QString queryPart = _url.query();
bool suppressMaterialsHack = queryPart.contains("hifiusemat"); // If this appears in query string, don't fetch mtl even if used.
OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME];
+ preDefinedMaterial.used = true;
if (suppressMaterialsHack) {
needsMaterialLibrary = preDefinedMaterial.userSpecifiesUV = false; // I said it was a hack...
}
@@ -594,8 +595,8 @@ FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping,
}
foreach (QString materialID, materials.keys()) {
- OBJMaterial& objMaterial = materials[materialID];
- if (!objMaterial.used) {
+ OBJMaterial& objMaterial = materials[materialID];
+ if (!objMaterial.used) {
continue;
}
geometry.materials[materialID] = FBXMaterial(objMaterial.diffuseColor,
diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h
index 200f11548d..b4a48c570e 100644
--- a/libraries/fbx/src/OBJReader.h
+++ b/libraries/fbx/src/OBJReader.h
@@ -58,7 +58,7 @@ public:
QByteArray specularTextureFilename;
bool used { false };
bool userSpecifiesUV { false };
- OBJMaterial() : shininess(96.0f), opacity(1.0f), diffuseColor(1.0f), specularColor(1.0f) {}
+ OBJMaterial() : shininess(0.0f), opacity(1.0f), diffuseColor(0.9f), specularColor(0.9f) {}
};
class OBJReader: public QObject { // QObject so we can make network requests.
diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp
index c175a836cc..d383f4c199 100644
--- a/libraries/physics/src/EntityMotionState.cpp
+++ b/libraries/physics/src/EntityMotionState.cpp
@@ -97,6 +97,21 @@ void EntityMotionState::updateServerPhysicsVariables() {
_serverActionData = _entity->getActionData();
}
+void EntityMotionState::handleDeactivation() {
+ // copy _server data to entity
+ bool success;
+ _entity->setPosition(_serverPosition, success, false);
+ _entity->setOrientation(_serverRotation, success, false);
+ _entity->setVelocity(ENTITY_ITEM_ZERO_VEC3);
+ _entity->setAngularVelocity(ENTITY_ITEM_ZERO_VEC3);
+ // and also to RigidBody
+ btTransform worldTrans;
+ worldTrans.setOrigin(glmToBullet(_serverPosition));
+ worldTrans.setRotation(glmToBullet(_serverRotation));
+ _body->setWorldTransform(worldTrans);
+ // no need to update velocities... should already be zero
+}
+
// virtual
void EntityMotionState::handleEasyChanges(uint32_t& flags) {
assert(entityTreeIsLocked());
@@ -111,6 +126,8 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) {
flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION;
_body->setActivationState(WANTS_DEACTIVATION);
_outgoingPriority = 0;
+ const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet
+ _body->setDeactivationTime(ACTIVATION_EXPIRY);
} else {
// disowned object is still moving --> start timer for ownership bid
// TODO? put a delay in here proportional to distance from object?
@@ -221,12 +238,9 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const {
}
// This callback is invoked by the physics simulation at the end of each simulation step...
-// iff the corresponding RigidBody is DYNAMIC and has moved.
+// iff the corresponding RigidBody is DYNAMIC and ACTIVE.
void EntityMotionState::setWorldTransform(const btTransform& worldTrans) {
- if (!_entity) {
- return;
- }
-
+ assert(_entity);
assert(entityTreeIsLocked());
measureBodyAcceleration();
bool positionSuccess;
diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h
index feac47d8ec..380edf3927 100644
--- a/libraries/physics/src/EntityMotionState.h
+++ b/libraries/physics/src/EntityMotionState.h
@@ -29,6 +29,7 @@ public:
virtual ~EntityMotionState();
void updateServerPhysicsVariables();
+ void handleDeactivation();
virtual void handleEasyChanges(uint32_t& flags) override;
virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override;
diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp
index 903b160a5e..bd76b2d70f 100644
--- a/libraries/physics/src/PhysicalEntitySimulation.cpp
+++ b/libraries/physics/src/PhysicalEntitySimulation.cpp
@@ -259,13 +259,27 @@ void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result)
_pendingChanges.clear();
}
-void PhysicalEntitySimulation::handleOutgoingChanges(const VectorOfMotionStates& motionStates) {
+void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) {
+ for (auto stateItr : motionStates) {
+ ObjectMotionState* state = &(*stateItr);
+ assert(state);
+ if (state->getType() == MOTIONSTATE_TYPE_ENTITY) {
+ EntityMotionState* entityState = static_cast(state);
+ entityState->handleDeactivation();
+ EntityItemPointer entity = entityState->getEntity();
+ _entitiesToSort.insert(entity);
+ }
+ }
+}
+
+void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionStates& motionStates) {
QMutexLocker lock(&_mutex);
// walk the motionStates looking for those that correspond to entities
for (auto stateItr : motionStates) {
ObjectMotionState* state = &(*stateItr);
- if (state && state->getType() == MOTIONSTATE_TYPE_ENTITY) {
+ assert(state);
+ if (state->getType() == MOTIONSTATE_TYPE_ENTITY) {
EntityMotionState* entityState = static_cast(state);
EntityItemPointer entity = entityState->getEntity();
assert(entity.get());
diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h
index af5def9775..5f6185add3 100644
--- a/libraries/physics/src/PhysicalEntitySimulation.h
+++ b/libraries/physics/src/PhysicalEntitySimulation.h
@@ -56,7 +56,8 @@ public:
void setObjectsToChange(const VectorOfMotionStates& objectsToChange);
void getObjectsToChange(VectorOfMotionStates& result);
- void handleOutgoingChanges(const VectorOfMotionStates& motionStates);
+ void handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates);
+ void handleChangedMotionStates(const VectorOfMotionStates& motionStates);
void handleCollisionEvents(const CollisionEvents& collisionEvents);
EntityEditPacketSender* getPacketSender() { return _entityPacketSender; }
@@ -67,7 +68,7 @@ private:
SetOfEntities _entitiesToAddToPhysics;
SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed
- SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we need to send updates to entity-server
+ SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server
SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine
diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp
index 363887de25..a8a8e6acfd 100644
--- a/libraries/physics/src/PhysicsEngine.cpp
+++ b/libraries/physics/src/PhysicsEngine.cpp
@@ -472,7 +472,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() {
return _collisionEvents;
}
-const VectorOfMotionStates& PhysicsEngine::getOutgoingChanges() {
+const VectorOfMotionStates& PhysicsEngine::getChangedMotionStates() {
BT_PROFILE("copyOutgoingChanges");
// Bullet will not deactivate static objects (it doesn't expect them to be active)
// so we must deactivate them ourselves
diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h
index bbafbb06b6..b2ebe58f08 100644
--- a/libraries/physics/src/PhysicsEngine.h
+++ b/libraries/physics/src/PhysicsEngine.h
@@ -65,7 +65,8 @@ public:
bool hasOutgoingChanges() const { return _hasOutgoingChanges; }
/// \return reference to list of changed MotionStates. The list is only valid until beginning of next simulation loop.
- const VectorOfMotionStates& getOutgoingChanges();
+ const VectorOfMotionStates& getChangedMotionStates();
+ const VectorOfMotionStates& getDeactivatedMotionStates() const { return _dynamicsWorld->getDeactivatedMotionStates(); }
/// \return reference to list of Collision events. The list is only valid until beginning of next simulation loop.
const CollisionEvents& getCollisionEvents();
diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp
index 5fe99f137c..24cfbc2609 100644
--- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp
+++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp
@@ -120,30 +120,41 @@ void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) {
void ThreadSafeDynamicsWorld::synchronizeMotionStates() {
BT_PROFILE("synchronizeMotionStates");
_changedMotionStates.clear();
+
+ // NOTE: m_synchronizeAllMotionStates is 'false' by default for optimization.
+ // See PhysicsEngine::init() where we call _dynamicsWorld->setForceUpdateAllAabbs(false)
if (m_synchronizeAllMotionStates) {
//iterate over all collision objects
for (int i=0;igetMotionState()) {
- synchronizeMotionState(body);
- _changedMotionStates.push_back(static_cast(body->getMotionState()));
- }
+ if (body && body->getMotionState()) {
+ synchronizeMotionState(body);
+ _changedMotionStates.push_back(static_cast(body->getMotionState()));
}
}
} else {
//iterate over all active rigid bodies
+ // TODO? if this becomes a performance bottleneck we could derive our own SimulationIslandManager
+ // that remembers a list of objects deactivated last step
+ _activeStates.clear();
+ _deactivatedStates.clear();
for (int i=0;iisActive()) {
- if (body->getMotionState()) {
+ ObjectMotionState* motionState = static_cast(body->getMotionState());
+ if (motionState) {
+ if (body->isActive()) {
synchronizeMotionState(body);
- _changedMotionStates.push_back(static_cast(body->getMotionState()));
+ _changedMotionStates.push_back(motionState);
+ _activeStates.insert(motionState);
+ } else if (_lastActiveStates.find(motionState) != _lastActiveStates.end()) {
+ // this object was active last frame but is no longer
+ _deactivatedStates.push_back(motionState);
}
}
}
}
+ _activeStates.swap(_lastActiveStates);
}
void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) {
diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h
index 68062d8d29..b4fcca8cdb 100644
--- a/libraries/physics/src/ThreadSafeDynamicsWorld.h
+++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h
@@ -49,12 +49,16 @@ public:
float getLocalTimeAccumulation() const { return m_localTime; }
const VectorOfMotionStates& getChangedMotionStates() const { return _changedMotionStates; }
+ const VectorOfMotionStates& getDeactivatedMotionStates() const { return _deactivatedStates; }
private:
// call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState()
void synchronizeMotionState(btRigidBody* body);
VectorOfMotionStates _changedMotionStates;
+ VectorOfMotionStates _deactivatedStates;
+ SetOfMotionStates _activeStates;
+ SetOfMotionStates _lastActiveStates;
};
#endif // hifi_ThreadSafeDynamicsWorld_h
diff --git a/libraries/render-utils/src/FramebufferCache.cpp b/libraries/render-utils/src/FramebufferCache.cpp
index 27429595b4..72b3c2ceb4 100644
--- a/libraries/render-utils/src/FramebufferCache.cpp
+++ b/libraries/render-utils/src/FramebufferCache.cpp
@@ -21,7 +21,6 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) {
//If the size changed, we need to delete our FBOs
if (_frameBufferSize != frameBufferSize) {
_frameBufferSize = frameBufferSize;
- _selfieFramebuffer.reset();
{
std::unique_lock lock(_mutex);
_cachedFramebuffers.clear();
@@ -30,16 +29,8 @@ void FramebufferCache::setFrameBufferSize(QSize frameBufferSize) {
}
void FramebufferCache::createPrimaryFramebuffer() {
- auto colorFormat = gpu::Element::COLOR_SRGBA_32;
- auto width = _frameBufferSize.width();
- auto height = _frameBufferSize.height();
-
auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT);
- _selfieFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("selfie"));
- auto tex = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, width * 0.5, height * 0.5, defaultSampler));
- _selfieFramebuffer->setRenderBuffer(0, tex);
-
auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR);
}
@@ -60,10 +51,3 @@ void FramebufferCache::releaseFramebuffer(const gpu::FramebufferPointer& framebu
_cachedFramebuffers.push_back(framebuffer);
}
}
-
-gpu::FramebufferPointer FramebufferCache::getSelfieFramebuffer() {
- if (!_selfieFramebuffer) {
- createPrimaryFramebuffer();
- }
- return _selfieFramebuffer;
-}
diff --git a/libraries/render-utils/src/FramebufferCache.h b/libraries/render-utils/src/FramebufferCache.h
index f74d224a61..8065357615 100644
--- a/libraries/render-utils/src/FramebufferCache.h
+++ b/libraries/render-utils/src/FramebufferCache.h
@@ -27,9 +27,6 @@ public:
void setFrameBufferSize(QSize frameBufferSize);
const QSize& getFrameBufferSize() const { return _frameBufferSize; }
- /// Returns the framebuffer object used to render selfie maps;
- gpu::FramebufferPointer getSelfieFramebuffer();
-
/// Returns a free framebuffer with a single color attachment for temp or intra-frame operations
gpu::FramebufferPointer getFramebuffer();
@@ -42,8 +39,6 @@ private:
gpu::FramebufferPointer _shadowFramebuffer;
- gpu::FramebufferPointer _selfieFramebuffer;
-
QSize _frameBufferSize{ 100, 100 };
std::mutex _mutex;
diff --git a/libraries/script-engine/src/BaseScriptEngine.h b/libraries/script-engine/src/BaseScriptEngine.h
deleted file mode 100644
index 27a6eff33d..0000000000
--- a/libraries/script-engine/src/BaseScriptEngine.h
+++ /dev/null
@@ -1,67 +0,0 @@
-//
-// BaseScriptEngine.h
-// libraries/script-engine/src
-//
-// Created by Timothy Dedischew on 02/01/17.
-// Copyright 2017 High Fidelity, Inc.
-//
-// Distributed under the Apache License, Version 2.0.
-// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-//
-
-#ifndef hifi_BaseScriptEngine_h
-#define hifi_BaseScriptEngine_h
-
-#include
-#include
-#include
-
-#include "SettingHandle.h"
-
-// common base class for extending QScriptEngine itself
-class BaseScriptEngine : public QScriptEngine {
- Q_OBJECT
-public:
- static const QString SCRIPT_EXCEPTION_FORMAT;
- static const QString SCRIPT_BACKTRACE_SEP;
-
- BaseScriptEngine() {}
-
- Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
-
- Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
- Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
- Q_INVOKABLE QString formatException(const QScriptValue& exception);
- QScriptValue cloneUncaughtException(const QString& detail = QString());
-
-signals:
- void unhandledException(const QScriptValue& exception);
-
-protected:
- void _emitUnhandledException(const QScriptValue& exception);
- QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
-
- static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
- Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
-#ifdef DEBUG_JS
- static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
-#endif
-};
-
-// Lambda helps create callable QScriptValues out of std::functions:
-// (just meant for use from within the script engine itself)
-class Lambda : public QObject {
- Q_OBJECT
-public:
- Lambda(QScriptEngine *engine, std::function operation, QScriptValue data);
- ~Lambda();
- public slots:
- QScriptValue call();
- QString toString() const;
-private:
- QScriptEngine* engine;
- std::function operation;
- QScriptValue data;
-};
-
-#endif // hifi_BaseScriptEngine_h
diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp
index 0b5f09a4df..3e4e070717 100644
--- a/libraries/script-engine/src/ScriptEngine.cpp
+++ b/libraries/script-engine/src/ScriptEngine.cpp
@@ -19,6 +19,9 @@
#include
#include
+#include
+#include
+
#include
#include
@@ -72,13 +75,18 @@
#include "MIDIEvent.h"
+const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
+ "com.highfidelity.experimental.enableExtendedJSExceptions"
+};
+
+static const int MAX_MODULE_ID_LENGTH { 4096 };
+static const int MAX_DEBUG_VALUE_LENGTH { 80 };
+
static const QScriptEngine::QObjectWrapOptions DEFAULT_QOBJECT_WRAP_OPTIONS =
QScriptEngine::ExcludeDeleteLater | QScriptEngine::ExcludeChildObjects;
static const QScriptValue::PropertyFlags READONLY_PROP_FLAGS { QScriptValue::ReadOnly | QScriptValue::Undeletable };
static const QScriptValue::PropertyFlags READONLY_HIDDEN_PROP_FLAGS { READONLY_PROP_FLAGS | QScriptValue::SkipInEnumeration };
-
-
static const bool HIFI_AUTOREFRESH_FILE_SCRIPTS { true };
Q_DECLARE_METATYPE(QScriptEngine::FunctionSignature)
@@ -86,7 +94,7 @@ int functionSignatureMetaID = qRegisterMetaTypeargumentCount(); i++) {
if (i > 0) {
@@ -143,7 +151,7 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
}
QString ScriptEngine::logException(const QScriptValue& exception) {
- auto message = formatException(exception);
+ auto message = formatException(exception, _enableExtendedJSExceptions.get());
scriptErrorMessage(message);
return message;
}
@@ -335,7 +343,7 @@ void ScriptEngine::runInThread() {
// The thread interface cannot live on itself, and we want to move this into the thread, so
// the thread cannot have this as a parent.
QThread* workerThread = new QThread();
- workerThread->setObjectName(QString("Script Thread:") + getFilename());
+ workerThread->setObjectName(QString("js:") + getFilename().replace("about:",""));
moveToThread(workerThread);
// NOTE: If you connect any essential signals for proper shutdown or cleanup of
@@ -534,6 +542,40 @@ static QScriptValue createScriptableResourcePrototype(QScriptEngine* engine) {
return prototype;
}
+void ScriptEngine::resetModuleCache(bool deleteScriptCache) {
+ if (QThread::currentThread() != thread()) {
+ executeOnScriptThread([=]() { resetModuleCache(deleteScriptCache); });
+ return;
+ }
+ auto jsRequire = globalObject().property("Script").property("require");
+ auto cache = jsRequire.property("cache");
+ auto cacheMeta = jsRequire.data();
+
+ if (deleteScriptCache) {
+ QScriptValueIterator it(cache);
+ while (it.hasNext()) {
+ it.next();
+ if (it.flags() & QScriptValue::SkipInEnumeration) {
+ continue;
+ }
+ qCDebug(scriptengine) << "resetModuleCache(true) -- staging " << it.name() << " for cache reset at next require";
+ cacheMeta.setProperty(it.name(), true);
+ }
+ }
+ cache = newObject();
+ if (!cacheMeta.isObject()) {
+ cacheMeta = newObject();
+ cacheMeta.setProperty("id", "Script.require.cacheMeta");
+ cacheMeta.setProperty("type", "cacheMeta");
+ jsRequire.setData(cacheMeta);
+ }
+ cache.setProperty("__created__", (double)QDateTime::currentMSecsSinceEpoch(), QScriptValue::SkipInEnumeration);
+#if DEBUG_JS_MODULES
+ cache.setProperty("__meta__", cacheMeta, READONLY_HIDDEN_PROP_FLAGS);
+#endif
+ jsRequire.setProperty("cache", cache, READONLY_PROP_FLAGS);
+}
+
void ScriptEngine::init() {
if (_isInitialized) {
return; // only initialize once
@@ -587,6 +629,15 @@ void ScriptEngine::init() {
registerGlobalObject("Script", this);
+ {
+ // set up Script.require.resolve and Script.require.cache
+ auto Script = globalObject().property("Script");
+ auto require = Script.property("require");
+ auto resolve = Script.property("_requireResolve");
+ require.setProperty("resolve", resolve, READONLY_PROP_FLAGS);
+ resetModuleCache();
+ }
+
registerGlobalObject("Audio", &AudioScriptingInterface::getInstance());
registerGlobalObject("Entities", entityScriptingInterface.data());
registerGlobalObject("Quat", &_quatLibrary);
@@ -859,6 +910,11 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
handlersForEvent << handlerData; // Note that the same handler can be added many times. See removeEntityEventHandler().
}
+// this is not redundant -- the version in BaseScriptEngine is specifically not Q_INVOKABLE
+QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
+ return BaseScriptEngine::evaluateInClosure(closure, program);
+}
+
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
if (DependencyManager::get()->isStopped()) {
return QScriptValue(); // bail early
@@ -881,29 +937,26 @@ QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fi
// Check syntax
auto syntaxError = lintScript(sourceCode, fileName);
if (syntaxError.isError()) {
- if (isEvaluating()) {
- currentContext()->throwValue(syntaxError);
- } else {
+ if (!isEvaluating()) {
syntaxError.setProperty("detail", "evaluate");
- emit unhandledException(syntaxError);
}
+ raiseException(syntaxError);
+ maybeEmitUncaughtException("lint");
return syntaxError;
}
QScriptProgram program { sourceCode, fileName, lineNumber };
if (program.isNull()) {
// can this happen?
auto err = makeError("could not create QScriptProgram for " + fileName);
- emit unhandledException(err);
+ raiseException(err);
+ maybeEmitUncaughtException("compile");
return err;
}
QScriptValue result;
{
result = BaseScriptEngine::evaluate(program);
- if (!isEvaluating() && hasUncaughtException()) {
- emit unhandledException(cloneUncaughtException(__FUNCTION__));
- clearExceptions();
- }
+ maybeEmitUncaughtException("evaluate");
}
return result;
}
@@ -926,10 +979,7 @@ void ScriptEngine::run() {
{
evaluate(_scriptContents, _fileNameString);
- if (!isEvaluating() && hasUncaughtException()) {
- emit unhandledException(cloneUncaughtException(__FUNCTION__));
- clearExceptions();
- }
+ maybeEmitUncaughtException(__FUNCTION__);
}
#ifdef _WIN32
// VS13 does not sleep_until unless it uses the system_clock, see:
@@ -1300,11 +1350,361 @@ void ScriptEngine::print(const QString& message) {
emit printedMessage(message);
}
+// Script.require.resolve -- like resolvePath, but performs more validation and throws exceptions on invalid module identifiers (for consistency with Node.js)
+QString ScriptEngine::_requireResolve(const QString& moduleId, const QString& relativeTo) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return QString();
+ }
+ QUrl defaultScriptsLoc = defaultScriptsLocation();
+ QUrl url(moduleId);
+
+ auto displayId = moduleId;
+ if (displayId.length() > MAX_DEBUG_VALUE_LENGTH) {
+ displayId = displayId.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
+ }
+ auto message = QString("Cannot find module '%1' (%2)").arg(displayId);
+
+ auto throwResolveError = [&](const QScriptValue& error) -> QString {
+ raiseException(error);
+ maybeEmitUncaughtException("require.resolve");
+ return QString();
+ };
+
+ // de-fuzz the input a little by restricting to rational sizes
+ auto idLength = url.toString().length();
+ if (idLength < 1 || idLength > MAX_MODULE_ID_LENGTH) {
+ auto details = QString("rejecting invalid module id size (%1 chars [1,%2])")
+ .arg(idLength).arg(MAX_MODULE_ID_LENGTH);
+ return throwResolveError(makeError(message.arg(details), "RangeError"));
+ }
+
+ // this regex matches: absolute, dotted or path-like URLs
+ // (ie: the kind of stuff ScriptEngine::resolvePath already handles)
+ QRegularExpression qualified ("^\\w+:|^/|^[.]{1,2}(/|$)");
+
+ // this is for module.require (which is a bound version of require that's always relative to the module path)
+ if (!relativeTo.isEmpty()) {
+ url = QUrl(relativeTo).resolved(moduleId);
+ url = resolvePath(url.toString());
+ } else if (qualified.match(moduleId).hasMatch()) {
+ url = resolvePath(moduleId);
+ } else {
+ // check if the moduleId refers to a "system" module
+ QString systemPath = defaultScriptsLoc.path();
+ QString systemModulePath = QString("%1/modules/%2.js").arg(systemPath).arg(moduleId);
+ url = defaultScriptsLoc;
+ url.setPath(systemModulePath);
+ if (!QFileInfo(url.toLocalFile()).isFile()) {
+ if (!moduleId.contains("./")) {
+ // the user might be trying to refer to a relative file without anchoring it
+ // let's do them a favor and test for that case -- offering specific advice if detected
+ auto unanchoredUrl = resolvePath("./" + moduleId);
+ if (QFileInfo(unanchoredUrl.toLocalFile()).isFile()) {
+ auto msg = QString("relative module ids must be anchored; use './%1' instead")
+ .arg(moduleId);
+ return throwResolveError(makeError(message.arg(msg)));
+ }
+ }
+ return throwResolveError(makeError(message.arg("system module not found")));
+ }
+ }
+
+ if (url.isRelative()) {
+ return throwResolveError(makeError(message.arg("could not resolve module id")));
+ }
+
+ // if it looks like a local file, verify that it's an allowed path and really a file
+ if (url.isLocalFile()) {
+ QFileInfo file(url.toLocalFile());
+ QUrl canonical = url;
+ if (file.exists()) {
+ canonical.setPath(file.canonicalFilePath());
+ }
+
+ bool disallowOutsideFiles = !defaultScriptsLocation().isParentOf(canonical) && !currentSandboxURL.isLocalFile();
+ if (disallowOutsideFiles && !PathUtils::isDescendantOf(canonical, currentSandboxURL)) {
+ return throwResolveError(makeError(message.arg(
+ QString("path '%1' outside of origin script '%2' '%3'")
+ .arg(PathUtils::stripFilename(url))
+ .arg(PathUtils::stripFilename(currentSandboxURL))
+ .arg(canonical.toString())
+ )));
+ }
+ if (!file.exists()) {
+ return throwResolveError(makeError(message.arg("path does not exist: " + url.toLocalFile())));
+ }
+ if (!file.isFile()) {
+ return throwResolveError(makeError(message.arg("path is not a file: " + url.toLocalFile())));
+ }
+ }
+
+ maybeEmitUncaughtException(__FUNCTION__);
+ return url.toString();
+}
+
+// retrieves the current parent module from the JS scope chain
+QScriptValue ScriptEngine::currentModule() {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return unboundNullValue();
+ }
+ auto jsRequire = globalObject().property("Script").property("require");
+ auto cache = jsRequire.property("cache");
+ auto candidate = QScriptValue();
+ for (auto c = currentContext(); c && !candidate.isObject(); c = c->parentContext()) {
+ QScriptContextInfo contextInfo { c };
+ candidate = cache.property(contextInfo.fileName());
+ }
+ if (!candidate.isObject()) {
+ return QScriptValue();
+ }
+ return candidate;
+}
+
+// replaces or adds "module" to "parent.children[]" array
+// (for consistency with Node.js and userscript cache invalidation without "cache busters")
+bool ScriptEngine::registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent) {
+ auto children = parent.property("children");
+ if (children.isArray()) {
+ auto key = module.property("id");
+ auto length = children.property("length").toInt32();
+ for (int i = 0; i < length; i++) {
+ if (children.property(i).property("id").strictlyEquals(key)) {
+ qCDebug(scriptengine_module) << key.toString() << " updating parent.children[" << i << "] = module";
+ children.setProperty(i, module);
+ return true;
+ }
+ }
+ qCDebug(scriptengine_module) << key.toString() << " appending parent.children[" << length << "] = module";
+ children.setProperty(length, module);
+ return true;
+ } else if (parent.isValid()) {
+ qCDebug(scriptengine_module) << "registerModuleWithParent -- unrecognized parent" << parent.toVariant().toString();
+ }
+ return false;
+}
+
+// creates a new JS "module" Object with default metadata properties
+QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptValue& parent) {
+ auto closure = newObject();
+ auto exports = newObject();
+ auto module = newObject();
+ qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString();
+
+ closure.setProperty("module", module, READONLY_PROP_FLAGS);
+
+ // note: this becomes the "exports" free variable, so should not be set read only
+ closure.setProperty("exports", exports);
+
+ // make the closure available to module instantiation
+ module.setProperty("__closure__", closure, READONLY_HIDDEN_PROP_FLAGS);
+
+ // for consistency with Node.js Module
+ module.setProperty("id", modulePath, READONLY_PROP_FLAGS);
+ module.setProperty("filename", modulePath, READONLY_PROP_FLAGS);
+ module.setProperty("exports", exports); // not readonly
+ module.setProperty("loaded", false, READONLY_PROP_FLAGS);
+ module.setProperty("parent", parent, READONLY_PROP_FLAGS);
+ module.setProperty("children", newArray(), READONLY_PROP_FLAGS);
+
+ // module.require is a bound version of require that always resolves relative to that module's path
+ auto boundRequire = QScriptEngine::evaluate("(function(id) { return Script.require(Script.require.resolve(id, this.filename)); })", "(boundRequire)");
+ module.setProperty("require", boundRequire, READONLY_PROP_FLAGS);
+
+ return module;
+}
+
+// synchronously fetch a module's source code using BatchLoader
+QVariantMap ScriptEngine::fetchModuleSource(const QString& modulePath, const bool forceDownload) {
+ using UrlMap = QMap;
+ auto scriptCache = DependencyManager::get();
+ QVariantMap req;
+ qCDebug(scriptengine_module) << "require.fetchModuleSource: " << QUrl(modulePath).fileName() << QThread::currentThread();
+
+ auto onload = [=, &req](const UrlMap& data, const UrlMap& _status) {
+ auto url = modulePath;
+ auto status = _status[url];
+ auto contents = data[url];
+ qCDebug(scriptengine_module) << "require.fetchModuleSource.onload: " << QUrl(url).fileName() << status << QThread::currentThread();
+ if (isStopping()) {
+ req["status"] = "Stopped";
+ req["success"] = false;
+ } else {
+ req["url"] = url;
+ req["status"] = status;
+ req["success"] = ScriptCache::isSuccessStatus(status);
+ req["contents"] = contents;
+ }
+ };
+
+ if (forceDownload) {
+ qCDebug(scriptengine_module) << "require.requestScript -- clearing cache for" << modulePath;
+ scriptCache->deleteScript(modulePath);
+ }
+ BatchLoader* loader = new BatchLoader(QList({ modulePath }));
+ connect(loader, &BatchLoader::finished, this, onload);
+ connect(this, &QObject::destroyed, loader, &QObject::deleteLater);
+ // fail faster? (since require() blocks the engine thread while resolving dependencies)
+ const int MAX_RETRIES = 1;
+
+ loader->start(MAX_RETRIES);
+
+ if (!loader->isFinished()) {
+ QTimer monitor;
+ QEventLoop loop;
+ QObject::connect(loader, &BatchLoader::finished, this, [this, &monitor, &loop]{
+ monitor.stop();
+ loop.quit();
+ });
+
+ // this helps detect the case where stop() is invoked during the download
+ // but not seen in time to abort processing in onload()...
+ connect(&monitor, &QTimer::timeout, this, [this, &loop, &loader]{
+ if (isStopping()) {
+ loop.exit(-1);
+ }
+ });
+ monitor.start(500);
+ loop.exec();
+ }
+ loader->deleteLater();
+ return req;
+}
+
+// evaluate a pending module object using the fetched source code
+QScriptValue ScriptEngine::instantiateModule(const QScriptValue& module, const QString& sourceCode) {
+ QScriptValue result;
+ auto modulePath = module.property("filename").toString();
+ auto closure = module.property("__closure__");
+
+ qCDebug(scriptengine_module) << QString("require.instantiateModule: %1 / %2 bytes")
+ .arg(QUrl(modulePath).fileName()).arg(sourceCode.length());
+
+ if (module.property("content-type").toString() == "application/json") {
+ qCDebug(scriptengine_module) << "... parsing as JSON";
+ closure.setProperty("__json", sourceCode);
+ result = evaluateInClosure(closure, { "module.exports = JSON.parse(__json)", modulePath });
+ } else {
+ // scoped vars for consistency with Node.js
+ closure.setProperty("require", module.property("require"));
+ closure.setProperty("__filename", modulePath, READONLY_HIDDEN_PROP_FLAGS);
+ closure.setProperty("__dirname", QString(modulePath).replace(QRegExp("/[^/]*$"), ""), READONLY_HIDDEN_PROP_FLAGS);
+ result = evaluateInClosure(closure, { sourceCode, modulePath });
+ }
+ maybeEmitUncaughtException(__FUNCTION__);
+ return result;
+}
+
+// CommonJS/Node.js like require/module support
+QScriptValue ScriptEngine::require(const QString& moduleId) {
+ qCDebug(scriptengine_module) << "ScriptEngine::require(" << moduleId.left(MAX_DEBUG_VALUE_LENGTH) << ")";
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return unboundNullValue();
+ }
+
+ auto jsRequire = globalObject().property("Script").property("require");
+ auto cacheMeta = jsRequire.data();
+ auto cache = jsRequire.property("cache");
+ auto parent = currentModule();
+
+ auto throwModuleError = [&](const QString& modulePath, const QScriptValue& error) {
+ cache.setProperty(modulePath, nullValue());
+ if (!error.isNull()) {
+#ifdef DEBUG_JS_MODULES
+ qCWarning(scriptengine_module) << "throwing module error:" << error.toString() << modulePath << error.property("stack").toString();
+#endif
+ raiseException(error);
+ }
+ maybeEmitUncaughtException("module");
+ return unboundNullValue();
+ };
+
+ // start by resolving the moduleId into a fully-qualified path/URL
+ QString modulePath = _requireResolve(moduleId);
+ if (modulePath.isNull() || hasUncaughtException()) {
+ // the resolver already threw an exception -- bail early
+ maybeEmitUncaughtException(__FUNCTION__);
+ return unboundNullValue();
+ }
+
+ // check the resolved path against the cache
+ auto module = cache.property(modulePath);
+
+ // modules get cached in `Script.require.cache` and (similar to Node.js) users can access it
+ // to inspect particular entries and invalidate them by deleting the key:
+ // `delete Script.require.cache[Script.require.resolve(moduleId)];`
+
+ // cacheMeta is just used right now to tell deleted keys apart from undefined ones
+ bool invalidateCache = module.isUndefined() && cacheMeta.property(moduleId).isValid();
+
+ // reset the cacheMeta record so invalidation won't apply next time, even if the module fails to load
+ cacheMeta.setProperty(modulePath, QScriptValue());
+
+ auto exports = module.property("exports");
+ if (!invalidateCache && exports.isObject()) {
+ // we have found a cached module -- just need to possibly register it with current parent
+ qCDebug(scriptengine_module) << QString("require - using cached module '%1' for '%2' (loaded: %3)")
+ .arg(modulePath).arg(moduleId).arg(module.property("loaded").toString());
+ registerModuleWithParent(module, parent);
+ maybeEmitUncaughtException("cached module");
+ return exports;
+ }
+
+ // bootstrap / register new empty module
+ module = newModule(modulePath, parent);
+ registerModuleWithParent(module, parent);
+
+ // add it to the cache (this is done early so any cyclic dependencies pick up)
+ cache.setProperty(modulePath, module);
+
+ // download the module source
+ auto req = fetchModuleSource(modulePath, invalidateCache);
+
+ if (!req.contains("success") || !req["success"].toBool()) {
+ auto error = QString("error retrieving script (%1)").arg(req["status"].toString());
+ return throwModuleError(modulePath, error);
+ }
+
+#if DEBUG_JS_MODULES
+ qCDebug(scriptengine_module) << "require.loaded: " <<
+ QUrl(req["url"].toString()).fileName() << req["status"].toString();
+#endif
+
+ auto sourceCode = req["contents"].toString();
+
+ if (QUrl(modulePath).fileName().endsWith(".json", Qt::CaseInsensitive)) {
+ module.setProperty("content-type", "application/json");
+ } else {
+ module.setProperty("content-type", "application/javascript");
+ }
+
+ // evaluate the module
+ auto result = instantiateModule(module, sourceCode);
+
+ if (result.isError() && !result.strictlyEquals(module.property("exports"))) {
+ qCWarning(scriptengine_module) << "-- result.isError --" << result.toString();
+ return throwModuleError(modulePath, result);
+ }
+
+ // mark as fully-loaded
+ module.setProperty("loaded", true, READONLY_PROP_FLAGS);
+
+ // set up a new reference point for detecting cache key deletion
+ cacheMeta.setProperty(modulePath, module);
+
+ qCDebug(scriptengine_module) << "//ScriptEngine::require(" << moduleId << ")";
+
+ maybeEmitUncaughtException(__FUNCTION__);
+ return module.property("exports");
+}
+
// If a callback is specified, the included files will be loaded asynchronously and the callback will be called
// when all of the files have finished loading.
// If no callback is specified, the included files will be loaded synchronously and will block execution until
// all of the files have finished loading.
void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callback) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return;
+ }
if (DependencyManager::get()->isStopped()) {
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
+ includeFiles.join(",") + "parent script:" + getFilename());
@@ -1367,7 +1767,7 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
doWithEnvironment(capturedEntityIdentifier, capturedSandboxURL, operation);
if (hasUncaughtException()) {
- emit unhandledException(cloneUncaughtException(__FUNCTION__));
+ emit unhandledException(cloneUncaughtException("evaluateInclude"));
clearExceptions();
}
} else {
@@ -1414,6 +1814,9 @@ void ScriptEngine::include(const QString& includeFile, QScriptValue callback) {
// as a stand-alone script. To accomplish this, the ScriptEngine class just emits a signal which
// the Application or other context will connect to in order to know to actually load the script
void ScriptEngine::load(const QString& loadFile) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return;
+ }
if (DependencyManager::get()->isStopped()) {
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
+ loadFile + "parent script:" + getFilename());
@@ -1483,6 +1886,52 @@ void ScriptEngine::updateEntityScriptStatus(const EntityItemID& entityID, const
emit entityScriptDetailsUpdated();
}
+QVariant ScriptEngine::cloneEntityScriptDetails(const EntityItemID& entityID) {
+ static const QVariant NULL_VARIANT { qVariantFromValue((QObject*)nullptr) };
+ QVariantMap map;
+ if (entityID.isNull()) {
+ // TODO: find better way to report JS Error across thread/process boundaries
+ map["isError"] = true;
+ map["errorInfo"] = "Error: getEntityScriptDetails -- invalid entityID";
+ } else {
+#ifdef DEBUG_ENTITY_STATES
+ qDebug() << "cloneEntityScriptDetails" << entityID << QThread::currentThread();
+#endif
+ EntityScriptDetails scriptDetails;
+ if (getEntityScriptDetails(entityID, scriptDetails)) {
+#ifdef DEBUG_ENTITY_STATES
+ qDebug() << "gotEntityScriptDetails" << scriptDetails.status << QThread::currentThread();
+#endif
+ map["isRunning"] = isEntityScriptRunning(entityID);
+ map["status"] = EntityScriptStatus_::valueToKey(scriptDetails.status).toLower();
+ map["errorInfo"] = scriptDetails.errorInfo;
+ map["entityID"] = entityID.toString();
+#ifdef DEBUG_ENTITY_STATES
+ {
+ auto debug = QVariantMap();
+ debug["script"] = scriptDetails.scriptText;
+ debug["scriptObject"] = scriptDetails.scriptObject.toVariant();
+ debug["lastModified"] = (qlonglong)scriptDetails.lastModified;
+ debug["sandboxURL"] = scriptDetails.definingSandboxURL;
+ map["debug"] = debug;
+ }
+#endif
+ } else {
+#ifdef DEBUG_ENTITY_STATES
+ qDebug() << "!gotEntityScriptDetails" << QThread::currentThread();
+#endif
+ map["isError"] = true;
+ map["errorInfo"] = "Entity script details unavailable";
+ map["entityID"] = entityID.toString();
+ }
+ }
+ return map;
+}
+
+QFuture ScriptEngine::getLocalEntityScriptDetails(const EntityItemID& entityID) {
+ return QtConcurrent::run(this, &ScriptEngine::cloneEntityScriptDetails, entityID);
+}
+
bool ScriptEngine::getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const {
auto it = _entityScripts.constFind(entityID);
if (it == _entityScripts.constEnd()) {
@@ -1621,10 +2070,10 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString&
auto scriptCache = DependencyManager::get();
// note: see EntityTreeRenderer.cpp for shared pointer lifecycle management
- QWeakPointer weakRef(sharedFromThis());
+ QWeakPointer weakRef(sharedFromThis());
scriptCache->getScriptContents(entityScript,
[this, weakRef, entityScript, entityID](const QString& url, const QString& contents, bool isURL, bool success, const QString& status) {
- QSharedPointer strongRef(weakRef);
+ QSharedPointer strongRef(weakRef);
if (!strongRef) {
qCWarning(scriptengine) << "loadEntityScript.contentAvailable -- ScriptEngine was deleted during getScriptContents!!";
return;
@@ -1743,13 +2192,12 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
timeout.setSingleShot(true);
timeout.start(SANDBOX_TIMEOUT);
connect(&timeout, &QTimer::timeout, [&sandbox, SANDBOX_TIMEOUT, scriptOrURL]{
- auto context = sandbox.currentContext();
- if (context) {
qCDebug(scriptengine) << "ScriptEngine::entityScriptContentAvailable timeout(" << scriptOrURL << ")";
// Guard against infinite loops and non-performant code
- context->throwError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT));
- }
+ sandbox.raiseException(
+ sandbox.makeError(QString("Timed out (entity constructors are limited to %1ms)").arg(SANDBOX_TIMEOUT))
+ );
});
testConstructor = sandbox.evaluate(program);
@@ -1765,7 +2213,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (exception.isError()) {
// create a local copy using makeError to decouple from the sandbox engine
exception = makeError(exception);
- setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
+ setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
@@ -1777,9 +2225,8 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
testConstructorType = "empty";
}
QString testConstructorValue = testConstructor.toString();
- const int maxTestConstructorValueSize = 80;
- if (testConstructorValue.size() > maxTestConstructorValueSize) {
- testConstructorValue = testConstructorValue.mid(0, maxTestConstructorValueSize) + "...";
+ if (testConstructorValue.size() > MAX_DEBUG_VALUE_LENGTH) {
+ testConstructorValue = testConstructorValue.mid(0, MAX_DEBUG_VALUE_LENGTH) + "...";
}
auto message = QString("failed to load entity script -- expected a function, got %1, %2")
.arg(testConstructorType).arg(testConstructorValue);
@@ -1817,7 +2264,7 @@ void ScriptEngine::entityScriptContentAvailable(const EntityItemID& entityID, co
if (entityScriptObject.isError()) {
auto exception = entityScriptObject;
- setError(formatException(exception), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
+ setError(formatException(exception, _enableExtendedJSExceptions.get()), EntityScriptStatus::ERROR_RUNNING_SCRIPT);
emit unhandledException(exception);
return;
}
@@ -1861,10 +2308,12 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
const EntityScriptDetails &oldDetails = _entityScripts[entityID];
if (isEntityScriptRunning(entityID)) {
callEntityScriptMethod(entityID, "unload");
- } else {
+ }
+#ifdef DEBUG_ENTITY_STATES
+ else {
qCDebug(scriptengine) << "unload called while !running" << entityID << oldDetails.status;
}
-
+#endif
if (shouldRemoveFromMap) {
// this was a deleted entity, we've been asked to remove it from the map
_entityScripts.remove(entityID);
@@ -1956,10 +2405,7 @@ void ScriptEngine::doWithEnvironment(const EntityItemID& entityID, const QUrl& s
#else
operation();
#endif
- if (!isEvaluating() && hasUncaughtException()) {
- emit unhandledException(cloneUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__));
- clearExceptions();
- }
+ maybeEmitUncaughtException(!entityID.isNull() ? entityID.toString() : __FUNCTION__);
currentEntityIdentifier = oldIdentifier;
currentSandboxURL = oldSandboxURL;
}
diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h
index 681499779c..95f377d92b 100644
--- a/libraries/script-engine/src/ScriptEngine.h
+++ b/libraries/script-engine/src/ScriptEngine.h
@@ -41,6 +41,7 @@
#include "ScriptCache.h"
#include "ScriptUUID.h"
#include "Vec3.h"
+#include "SettingHandle.h"
class QScriptEngineDebugger;
@@ -78,7 +79,7 @@ public:
QUrl definingSandboxURL { QUrl("about:EntityScript") };
};
-class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider, public QEnableSharedFromThis {
+class ScriptEngine : public BaseScriptEngine, public EntitiesScriptEngineProvider {
Q_OBJECT
Q_PROPERTY(QString context READ getContext)
public:
@@ -137,6 +138,8 @@ public:
/// evaluate some code in the context of the ScriptEngine and return the result
Q_INVOKABLE QScriptValue evaluate(const QString& program, const QString& fileName, int lineNumber = 1); // this is also used by the script tool widget
+ Q_INVOKABLE QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
+
/// if the script engine is not already running, this will download the URL and start the process of seting it up
/// to run... NOTE - this is used by Application currently to load the url. We don't really want it to be exposed
/// to scripts. we may not need this to be invokable
@@ -157,6 +160,16 @@ public:
Q_INVOKABLE void include(const QStringList& includeFiles, QScriptValue callback = QScriptValue());
Q_INVOKABLE void include(const QString& includeFile, QScriptValue callback = QScriptValue());
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // MODULE related methods
+ Q_INVOKABLE QScriptValue require(const QString& moduleId);
+ Q_INVOKABLE void resetModuleCache(bool deleteScriptCache = false);
+ QScriptValue currentModule();
+ bool registerModuleWithParent(const QScriptValue& module, const QScriptValue& parent);
+ QScriptValue newModule(const QString& modulePath, const QScriptValue& parent = QScriptValue());
+ QVariantMap fetchModuleSource(const QString& modulePath, const bool forceDownload = false);
+ QScriptValue instantiateModule(const QScriptValue& module, const QString& sourceCode);
+
Q_INVOKABLE QObject* setInterval(const QScriptValue& function, int intervalMS);
Q_INVOKABLE QObject* setTimeout(const QScriptValue& function, int timeoutMS);
Q_INVOKABLE void clearInterval(QObject* timer) { stopTimer(reinterpret_cast(timer)); }
@@ -170,6 +183,8 @@ public:
Q_INVOKABLE bool isEntityScriptRunning(const EntityItemID& entityID) {
return _entityScripts.contains(entityID) && _entityScripts[entityID].status == EntityScriptStatus::RUNNING;
}
+ QVariant cloneEntityScriptDetails(const EntityItemID& entityID);
+ QFuture getLocalEntityScriptDetails(const EntityItemID& entityID) override;
Q_INVOKABLE void loadEntityScript(const EntityItemID& entityID, const QString& entityScript, bool forceRedownload);
Q_INVOKABLE void unloadEntityScript(const EntityItemID& entityID, bool shouldRemoveFromMap = false); // will call unload method
Q_INVOKABLE void unloadAllEntityScripts();
@@ -237,6 +252,9 @@ signals:
protected:
void init();
Q_INVOKABLE void executeOnScriptThread(std::function function, const Qt::ConnectionType& type = Qt::QueuedConnection );
+ // note: this is not meant to be called directly, but just to have QMetaObject take care of wiring it up in general;
+ // then inside of init() we just have to do "Script.require.resolve = Script._requireResolve;"
+ Q_INVOKABLE QString _requireResolve(const QString& moduleId, const QString& relativeTo = QString());
QString logException(const QScriptValue& exception);
void timerFired();
@@ -290,11 +308,16 @@ protected:
AssetScriptingInterface _assetScriptingInterface{ this };
- std::function _emitScriptUpdates{ [](){ return true; } };
+ std::function _emitScriptUpdates{ []() { return true; } };
std::recursive_mutex _lock;
std::chrono::microseconds _totalTimerExecution { 0 };
+
+ static const QString _SETTINGS_ENABLE_EXTENDED_MODULE_COMPAT;
+ static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
+
+ Setting::Handle _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
};
#endif // hifi_ScriptEngine_h
diff --git a/libraries/script-engine/src/ScriptEngineLogging.cpp b/libraries/script-engine/src/ScriptEngineLogging.cpp
index 2e5d293728..392bc05129 100644
--- a/libraries/script-engine/src/ScriptEngineLogging.cpp
+++ b/libraries/script-engine/src/ScriptEngineLogging.cpp
@@ -12,3 +12,4 @@
#include "ScriptEngineLogging.h"
Q_LOGGING_CATEGORY(scriptengine, "hifi.scriptengine")
+Q_LOGGING_CATEGORY(scriptengine_module, "hifi.scriptengine.module")
diff --git a/libraries/script-engine/src/ScriptEngineLogging.h b/libraries/script-engine/src/ScriptEngineLogging.h
index 0e614dd5bf..62e46632a6 100644
--- a/libraries/script-engine/src/ScriptEngineLogging.h
+++ b/libraries/script-engine/src/ScriptEngineLogging.h
@@ -15,6 +15,7 @@
#include
Q_DECLARE_LOGGING_CATEGORY(scriptengine)
+Q_DECLARE_LOGGING_CATEGORY(scriptengine_module)
#endif // hifi_ScriptEngineLogging_h
diff --git a/libraries/script-engine/src/BaseScriptEngine.cpp b/libraries/shared/src/BaseScriptEngine.cpp
similarity index 68%
rename from libraries/script-engine/src/BaseScriptEngine.cpp
rename to libraries/shared/src/BaseScriptEngine.cpp
index 16308c0650..c92d629b75 100644
--- a/libraries/script-engine/src/BaseScriptEngine.cpp
+++ b/libraries/shared/src/BaseScriptEngine.cpp
@@ -10,6 +10,7 @@
//
#include "BaseScriptEngine.h"
+#include "SharedLogging.h"
#include
#include
@@ -18,18 +19,27 @@
#include
#include
-#include "ScriptEngineLogging.h"
#include "Profile.h"
-const QString BaseScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS {
- "com.highfidelity.experimental.enableExtendedJSExceptions"
-};
-
const QString BaseScriptEngine::SCRIPT_EXCEPTION_FORMAT { "[%0] %1 in %2:%3" };
const QString BaseScriptEngine::SCRIPT_BACKTRACE_SEP { "\n " };
+bool BaseScriptEngine::IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method) {
+ if (QThread::currentThread() == thread) {
+ return true;
+ }
+ qCCritical(shared) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3")
+ .arg(method).arg(thread ? thread->objectName() : "(!thread)").arg(QThread::currentThread()->objectName());
+ qCDebug(shared) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)";
+ Q_ASSERT(false);
+ return false;
+}
+
// engine-aware JS Error copier and factory
QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QString& type) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return unboundNullValue();
+ }
auto other = _other;
if (other.isString()) {
other = newObject();
@@ -41,7 +51,7 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
}
if (!proto.isFunction()) {
#ifdef DEBUG_JS_EXCEPTIONS
- qCDebug(scriptengine) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
+ qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";
#endif
proto = globalObject().property("Error");
}
@@ -64,6 +74,9 @@ QScriptValue BaseScriptEngine::makeError(const QScriptValue& _other, const QStri
// check syntax and when there are issues returns an actual "SyntaxError" with the details
QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return unboundNullValue();
+ }
const auto syntaxCheck = checkSyntax(sourceCode);
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
auto err = globalObject().property("SyntaxError")
@@ -82,13 +95,16 @@ QScriptValue BaseScriptEngine::lintScript(const QString& sourceCode, const QStri
}
return err;
}
- return undefinedValue();
+ return QScriptValue();
}
// this pulls from the best available information to create a detailed snapshot of the current exception
QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return unboundNullValue();
+ }
if (!hasUncaughtException()) {
- return QScriptValue();
+ return unboundNullValue();
}
auto exception = uncaughtException();
// ensure the error object is engine-local
@@ -144,7 +160,10 @@ QScriptValue BaseScriptEngine::cloneUncaughtException(const QString& extraDetail
return err;
}
-QString BaseScriptEngine::formatException(const QScriptValue& exception) {
+QString BaseScriptEngine::formatException(const QScriptValue& exception, bool includeExtendedDetails) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return QString();
+ }
QString note { "UncaughtException" };
QString result;
@@ -156,8 +175,8 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
const auto lineNumber = exception.property("lineNumber").toString();
const auto stacktrace = exception.property("stack").toString();
- if (_enableExtendedJSExceptions.get()) {
- // This setting toggles display of the hints now being added during the loading process.
+ if (includeExtendedDetails) {
+ // Display additional exception / troubleshooting hints that can be added via the custom Error .detail property
// Example difference:
// [UncaughtExceptions] Error: Can't find variable: foobar in atp:/myentity.js\n...
// [UncaughtException (construct {1eb5d3fa-23b1-411c-af83-163af7220e3f})] Error: Can't find variable: foobar in atp:/myentity.js\n...
@@ -173,14 +192,39 @@ QString BaseScriptEngine::formatException(const QScriptValue& exception) {
return result;
}
+bool BaseScriptEngine::raiseException(const QScriptValue& exception) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return false;
+ }
+ if (currentContext()) {
+ // we have an active context / JS stack frame so throw the exception per usual
+ currentContext()->throwValue(makeError(exception));
+ return true;
+ } else {
+ // we are within a pure C++ stack frame (ie: being called directly by other C++ code)
+ // in this case no context information is available so just emit the exception for reporting
+ emit unhandledException(makeError(exception));
+ }
+ return false;
+}
+
+bool BaseScriptEngine::maybeEmitUncaughtException(const QString& debugHint) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return false;
+ }
+ if (!isEvaluating() && hasUncaughtException()) {
+ emit unhandledException(cloneUncaughtException(debugHint));
+ clearExceptions();
+ return true;
+ }
+ return false;
+}
+
QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, const QScriptProgram& program) {
PROFILE_RANGE(script, "evaluateInClosure");
- if (QThread::currentThread() != thread()) {
- qCCritical(scriptengine) << "*** CRITICAL *** ScriptEngine::evaluateInClosure() is meant to be called from engine thread only.";
- // note: a recursive mutex might be needed around below code if this method ever becomes Q_INVOKABLE
- return QScriptValue();
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return unboundNullValue();
}
-
const auto fileName = program.fileName();
const auto shortName = QUrl(fileName).fileName();
@@ -189,7 +233,7 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto global = closure.property("global");
if (global.isObject()) {
#ifdef DEBUG_JS
- qCDebug(scriptengine) << " setting global = closure.global" << shortName;
+ qCDebug(shared) << " setting global = closure.global" << shortName;
#endif
oldGlobal = globalObject();
setGlobalObject(global);
@@ -200,34 +244,34 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
auto thiz = closure.property("this");
if (thiz.isObject()) {
#ifdef DEBUG_JS
- qCDebug(scriptengine) << " setting this = closure.this" << shortName;
+ qCDebug(shared) << " setting this = closure.this" << shortName;
#endif
context->setThisObject(thiz);
}
context->pushScope(closure);
#ifdef DEBUG_JS
- qCDebug(scriptengine) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
+ qCDebug(shared) << QString("[%1] evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
{
result = BaseScriptEngine::evaluate(program);
if (hasUncaughtException()) {
auto err = cloneUncaughtException(__FUNCTION__);
#ifdef DEBUG_JS_EXCEPTIONS
- qCWarning(scriptengine) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
+ qCWarning(shared) << __FUNCTION__ << "---------- hasCaught:" << err.toString() << result.toString();
err.setProperty("_result", result);
#endif
result = err;
}
}
#ifdef DEBUG_JS
- qCDebug(scriptengine) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
+ qCDebug(shared) << QString("[%1] //evaluateInClosure %2").arg(isEvaluating()).arg(shortName);
#endif
popContext();
if (oldGlobal.isValid()) {
#ifdef DEBUG_JS
- qCDebug(scriptengine) << " restoring global" << shortName;
+ qCDebug(shared) << " restoring global" << shortName;
#endif
setGlobalObject(oldGlobal);
}
@@ -236,7 +280,6 @@ QScriptValue BaseScriptEngine::evaluateInClosure(const QScriptValue& closure, co
}
// Lambda
-
QScriptValue BaseScriptEngine::newLambdaFunction(std::function operation, const QScriptValue& data, const QScriptEngine::ValueOwnership& ownership) {
auto lambda = new Lambda(this, operation, data);
auto object = newQObject(lambda, ownership);
@@ -262,26 +305,57 @@ Lambda::Lambda(QScriptEngine *engine, std::functionthread(), __FUNCTION__)) {
+ return BaseScriptEngine::unboundNullValue();
+ }
return operation(engine->currentContext(), engine);
}
+QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName) {
+ auto engine = scopeOrCallback.engine();
+ if (!engine) {
+ return scopeOrCallback;
+ }
+ auto scope = QScriptValue();
+ auto callback = scopeOrCallback;
+ if (scopeOrCallback.isObject()) {
+ if (methodOrName.isString()) {
+ scope = scopeOrCallback;
+ callback = scope.property(methodOrName.toString());
+ } else if (methodOrName.isFunction()) {
+ scope = scopeOrCallback;
+ callback = methodOrName;
+ }
+ }
+ auto handler = engine->newObject();
+ handler.setProperty("scope", scope);
+ handler.setProperty("callback", callback);
+ return handler;
+}
+
+QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result) {
+ return handler.property("callback").call(handler.property("scope"), QScriptValueList({ err, result }));
+}
+
#ifdef DEBUG_JS
void BaseScriptEngine::_debugDump(const QString& header, const QScriptValue& object, const QString& footer) {
+ if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
+ return;
+ }
if (!header.isEmpty()) {
- qCDebug(scriptengine) << header;
+ qCDebug(shared) << header;
}
if (!object.isObject()) {
- qCDebug(scriptengine) << "(!isObject)" << object.toVariant().toString() << object.toString();
+ qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString();
return;
}
QScriptValueIterator it(object);
while (it.hasNext()) {
it.next();
- qCDebug(scriptengine) << it.name() << ":" << it.value().toString();
+ qCDebug(shared) << it.name() << ":" << it.value().toString();
}
if (!footer.isEmpty()) {
- qCDebug(scriptengine) << footer;
+ qCDebug(shared) << footer;
}
}
#endif
-
diff --git a/libraries/shared/src/BaseScriptEngine.h b/libraries/shared/src/BaseScriptEngine.h
new file mode 100644
index 0000000000..138e46fafa
--- /dev/null
+++ b/libraries/shared/src/BaseScriptEngine.h
@@ -0,0 +1,90 @@
+//
+// BaseScriptEngine.h
+// libraries/script-engine/src
+//
+// Created by Timothy Dedischew on 02/01/17.
+// Copyright 2017 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_BaseScriptEngine_h
+#define hifi_BaseScriptEngine_h
+
+#include
+#include
+#include
+
+// common base class for extending QScriptEngine itself
+class BaseScriptEngine : public QScriptEngine, public QEnableSharedFromThis {
+ Q_OBJECT
+public:
+ static const QString SCRIPT_EXCEPTION_FORMAT;
+ static const QString SCRIPT_BACKTRACE_SEP;
+
+ // threadsafe "unbound" version of QScriptEngine::nullValue()
+ static const QScriptValue unboundNullValue() { return QScriptValue(0, QScriptValue::NullValue); }
+
+ BaseScriptEngine() {}
+
+ Q_INVOKABLE QScriptValue lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber = 1);
+ Q_INVOKABLE QScriptValue makeError(const QScriptValue& other = QScriptValue(), const QString& type = "Error");
+ Q_INVOKABLE QString formatException(const QScriptValue& exception, bool includeExtendedDetails);
+
+ QScriptValue cloneUncaughtException(const QString& detail = QString());
+ QScriptValue evaluateInClosure(const QScriptValue& locals, const QScriptProgram& program);
+
+ // if there is a pending exception and we are at the top level (non-recursive) stack frame, this emits and resets it
+ bool maybeEmitUncaughtException(const QString& debugHint = QString());
+
+ // if the currentContext() is valid then throw the passed exception; otherwise, immediately emit it.
+ // note: this is used in cases where C++ code might call into JS API methods directly
+ bool raiseException(const QScriptValue& exception);
+
+ // helper to detect and log warnings when other code invokes QScriptEngine/BaseScriptEngine in thread-unsafe ways
+ static bool IS_THREADSAFE_INVOCATION(const QThread *thread, const QString& method);
+signals:
+ void unhandledException(const QScriptValue& exception);
+
+protected:
+ // like `newFunction`, but allows mapping inline C++ lambdas with captures as callable QScriptValues
+ // even though the context/engine parameters are redundant in most cases, the function signature matches `newFunction`
+ // anyway so that newLambdaFunction can be used to rapidly prototype / test utility APIs and then if becoming
+ // permanent more easily promoted into regular static newFunction scenarios.
+ QScriptValue newLambdaFunction(std::function operation, const QScriptValue& data = QScriptValue(), const QScriptEngine::ValueOwnership& ownership = QScriptEngine::AutoOwnership);
+
+#ifdef DEBUG_JS
+ static void _debugDump(const QString& header, const QScriptValue& object, const QString& footer = QString());
+#endif
+};
+
+// Standardized CPS callback helpers (see: http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/)
+// These two helpers allow async JS APIs that use a callback parameter to be more friendly to scripters by accepting thisObject
+// context and adopting a consistent and intuitable callback signature:
+// function callback(err, result) { if (err) { ... } else { /* do stuff with result */ } }
+//
+// To use, first pass the user-specified callback args in the same order used with optionally-scoped Qt signal connections:
+// auto handler = makeScopedHandlerObject(scopeOrCallback, optionalMethodOrName);
+// And then invoke the scoped handler later per CPS conventions:
+// auto result = callScopedHandlerObject(handler, err, result);
+QScriptValue makeScopedHandlerObject(QScriptValue scopeOrCallback, QScriptValue methodOrName);
+QScriptValue callScopedHandlerObject(QScriptValue handler, QScriptValue err, QScriptValue result);
+
+// Lambda helps create callable QScriptValues out of std::functions:
+// (just meant for use from within the script engine itself)
+class Lambda : public QObject {
+ Q_OBJECT
+public:
+ Lambda(QScriptEngine *engine, std::function operation, QScriptValue data);
+ ~Lambda();
+ public slots:
+ QScriptValue call();
+ QString toString() const;
+private:
+ QScriptEngine* engine;
+ std::function operation;
+ QScriptValue data;
+};
+
+#endif // hifi_BaseScriptEngine_h
diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp
index f68fff0204..a793942056 100644
--- a/libraries/ui/src/ui/Menu.cpp
+++ b/libraries/ui/src/ui/Menu.cpp
@@ -470,8 +470,8 @@ void Menu::removeSeparator(const QString& menuName, const QString& separatorName
if (menu) {
int textAt = findPositionOfMenuItem(menu, separatorName);
QList menuActions = menu->actions();
- QAction* separatorText = menuActions[textAt];
if (textAt > 0 && textAt < menuActions.size()) {
+ QAction* separatorText = menuActions[textAt];
QAction* separatorLine = menuActions[textAt - 1];
if (separatorLine) {
if (separatorLine->isSeparator()) {
diff --git a/scripts/developer/libraries/jasmine/hifi-boot.js b/scripts/developer/libraries/jasmine/hifi-boot.js
index f490a3618f..772dd8c17e 100644
--- a/scripts/developer/libraries/jasmine/hifi-boot.js
+++ b/scripts/developer/libraries/jasmine/hifi-boot.js
@@ -6,7 +6,7 @@
var lastSpecStartTime;
function ConsoleReporter(options) {
var startTime = new Date().getTime();
- var errorCount = 0;
+ var errorCount = 0, pending = [];
this.jasmineStarted = function (obj) {
print('Jasmine started with ' + obj.totalSpecsDefined + ' tests.');
};
@@ -15,11 +15,14 @@
var endTime = new Date().getTime();
print('');
if (errorCount === 0) {
- print ('All tests passed!');
+ print ('All enabled tests passed!');
} else {
print('Tests completed with ' +
errorCount + ' ' + ERROR + '.');
}
+ if (pending.length)
+ print ('disabled: '+
+ pending.join(' ')+'');
print('Tests completed in ' + (endTime - startTime) + 'ms.');
};
this.suiteStarted = function(obj) {
@@ -32,6 +35,10 @@
lastSpecStartTime = new Date().getTime();
};
this.specDone = function(obj) {
+ if (obj.status === 'pending') {
+ pending.push(obj.fullName);
+ return print('...(pending ' + obj.fullName +')');
+ }
var specEndTime = new Date().getTime();
var symbol = obj.status === PASSED ?
'' + CHECKMARK + '' :
@@ -55,7 +62,7 @@
clearTimeout = Script.clearTimeout;
clearInterval = Script.clearInterval;
- var jasmine = jasmineRequire.core(jasmineRequire);
+ var jasmine = this.jasmine = jasmineRequire.core(jasmineRequire);
var env = jasmine.getEnv();
diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js
new file mode 100644
index 0000000000..265cfaa2df
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/a.js
@@ -0,0 +1,10 @@
+/* eslint-env node */
+var a = exports;
+a.done = false;
+var b = require('./b.js');
+a.done = true;
+a.name = 'a';
+a['a.done?'] = a.done;
+a['b.done?'] = b.done;
+
+print('from a.js a.done =', a.done, '/ b.done =', b.done);
diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js
new file mode 100644
index 0000000000..c46c872828
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/b.js
@@ -0,0 +1,10 @@
+/* eslint-env node */
+var b = exports;
+b.done = false;
+var a = require('./a.js');
+b.done = true;
+b.name = 'b';
+b['a.done?'] = a.done;
+b['b.done?'] = b.done;
+
+print('from b.js a.done =', a.done, '/ b.done =', b.done);
diff --git a/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js
new file mode 100644
index 0000000000..0ec39cd656
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/cycles/main.js
@@ -0,0 +1,17 @@
+/* eslint-env node */
+/* global print */
+/* eslint-disable comma-dangle */
+
+print('main.js');
+var a = require('./a.js'),
+ b = require('./b.js');
+
+print('from main.js a.done =', a.done, 'and b.done =', b.done);
+
+module.exports = {
+ name: 'main',
+ a: a,
+ b: b,
+ 'a.done?': a.done,
+ 'b.done?': b.done,
+};
diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js
new file mode 100644
index 0000000000..bbe694b578
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorAPIException.js
@@ -0,0 +1,13 @@
+/* eslint-disable comma-dangle */
+// test module method exception being thrown within main constructor
+(function() {
+ var apiMethod = Script.require('../exceptions/exceptionInFunction.js');
+ print(Script.resolvePath(''), "apiMethod", apiMethod);
+ // this next line throws from within apiMethod
+ print(apiMethod());
+ return {
+ preload: function(uuid) {
+ print("entityConstructorAPIException::preload -- never seen --", uuid, Script.resolvePath(''));
+ },
+ };
+});
diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js
new file mode 100644
index 0000000000..a4e8c17ab6
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorModule.js
@@ -0,0 +1,23 @@
+/* global module */
+/* eslint-disable comma-dangle */
+// test dual-purpose module and standalone Entity script
+function MyEntity(filename) {
+ return {
+ preload: function(uuid) {
+ print("entityConstructorModule.js::preload");
+ if (typeof module === 'object') {
+ print("module.filename", module.filename);
+ print("module.parent.filename", module.parent && module.parent.filename);
+ }
+ },
+ clickDownOnEntity: function(uuid, evt) {
+ print("entityConstructorModule.js::clickDownOnEntity");
+ },
+ };
+}
+
+try {
+ module.exports = MyEntity;
+} catch (e) {} // eslint-disable-line no-empty
+print('entityConstructorModule::MyEntity', typeof MyEntity);
+(MyEntity);
diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js
new file mode 100644
index 0000000000..a90d979877
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested.js
@@ -0,0 +1,14 @@
+/* global module */
+// test Entity constructor based on inherited constructor from a module
+function constructor() {
+ print("entityConstructorNested::constructor");
+ var MyEntity = Script.require('./entityConstructorModule.js');
+ return new MyEntity("-- created from entityConstructorNested --");
+}
+
+try {
+ module.exports = constructor;
+} catch (e) {
+ constructor;
+}
+
diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js
new file mode 100644
index 0000000000..29e0ed65b1
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorNested2.js
@@ -0,0 +1,25 @@
+/* global module */
+// test Entity constructor based on nested, inherited module constructors
+function constructor() {
+ print("entityConstructorNested2::constructor");
+
+ // inherit from entityConstructorNested
+ var MyEntity = Script.require('./entityConstructorNested.js');
+ function SubEntity() {}
+ SubEntity.prototype = new MyEntity('-- created from entityConstructorNested2 --');
+
+ // create new instance
+ var entity = new SubEntity();
+ // "override" clickDownOnEntity for just this new instance
+ entity.clickDownOnEntity = function(uuid, evt) {
+ print("entityConstructorNested2::clickDownOnEntity");
+ SubEntity.prototype.clickDownOnEntity.apply(this, arguments);
+ };
+ return entity;
+}
+
+try {
+ module.exports = constructor;
+} catch (e) {
+ constructor;
+}
diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js
new file mode 100644
index 0000000000..5872bce529
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityConstructorRequireException.js
@@ -0,0 +1,10 @@
+/* eslint-disable comma-dangle */
+// test module-related exception from within "require" evaluation itself
+(function() {
+ var mod = Script.require('../exceptions/exception.js');
+ return {
+ preload: function(uuid) {
+ print("entityConstructorRequireException::preload (never happens)", uuid, Script.resolvePath(''), mod);
+ },
+ };
+});
diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js
new file mode 100644
index 0000000000..eaee178b0a
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadAPIError.js
@@ -0,0 +1,13 @@
+/* eslint-disable comma-dangle */
+// test module method exception being thrown within preload
+(function() {
+ var apiMethod = Script.require('../exceptions/exceptionInFunction.js');
+ print(Script.resolvePath(''), "apiMethod", apiMethod);
+ return {
+ preload: function(uuid) {
+ // this next line throws from within apiMethod
+ print(apiMethod());
+ print("entityPreloadAPIException::preload -- never seen --", uuid, Script.resolvePath(''));
+ },
+ };
+});
diff --git a/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js
new file mode 100644
index 0000000000..50dab9fa7c
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/entity/entityPreloadRequire.js
@@ -0,0 +1,11 @@
+/* eslint-disable comma-dangle */
+// test requiring a module from within preload
+(function constructor() {
+ return {
+ preload: function(uuid) {
+ print("entityPreloadRequire::preload");
+ var example = Script.require('../example.json');
+ print("entityPreloadRequire::example::name", example.name);
+ },
+ };
+});
diff --git a/scripts/developer/tests/unit_tests/moduleTests/example.json b/scripts/developer/tests/unit_tests/moduleTests/example.json
new file mode 100644
index 0000000000..42d7fe07da
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/example.json
@@ -0,0 +1,9 @@
+{
+ "name": "Example JSON Module",
+ "last-modified": 1485789862,
+ "config": {
+ "title": "My Title",
+ "width": 800,
+ "height": 600
+ }
+}
diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js
new file mode 100644
index 0000000000..8d25d6b7a4
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exception.js
@@ -0,0 +1,4 @@
+/* eslint-env node */
+module.exports = "n/a";
+throw new Error('exception on line 2');
+
diff --git a/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js
new file mode 100644
index 0000000000..69415a0741
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleTests/exceptions/exceptionInFunction.js
@@ -0,0 +1,38 @@
+/* eslint-env node */
+// dummy lines to make sure exception line number is well below parent test script
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+
+
+function myfunc() {
+ throw new Error('exception on line 32 in myfunc');
+}
+module.exports = myfunc;
+if (Script[module.filename] === 'throw') {
+ myfunc();
+}
diff --git a/scripts/developer/tests/unit_tests/moduleUnitTests.js b/scripts/developer/tests/unit_tests/moduleUnitTests.js
new file mode 100644
index 0000000000..6810dd8b6d
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/moduleUnitTests.js
@@ -0,0 +1,378 @@
+/* eslint-env jasmine, node */
+/* global print:true, Script:true, global:true, require:true */
+/* eslint-disable comma-dangle */
+var isNode = instrumentTestrunner(),
+ runInterfaceTests = !isNode,
+ runNetworkTests = true;
+
+// describe wrappers (note: `xdescribe` indicates a disabled or "pending" jasmine test)
+var INTERFACE = { describe: runInterfaceTests ? describe : xdescribe },
+ NETWORK = { describe: runNetworkTests ? describe : xdescribe };
+
+describe('require', function() {
+ describe('resolve', function() {
+ it('should resolve relative filenames', function() {
+ var expected = Script.resolvePath('./moduleTests/example.json');
+ expect(require.resolve('./moduleTests/example.json')).toEqual(expected);
+ });
+ describe('exceptions', function() {
+ it('should reject blank "" module identifiers', function() {
+ expect(function() {
+ require.resolve('');
+ }).toThrowError(/Cannot find/);
+ });
+ it('should reject excessive identifier sizes', function() {
+ expect(function() {
+ require.resolve(new Array(8193).toString());
+ }).toThrowError(/Cannot find/);
+ });
+ it('should reject implicitly-relative filenames', function() {
+ expect(function() {
+ var mod = require.resolve('example.js');
+ mod.exists;
+ }).toThrowError(/Cannot find/);
+ });
+ it('should reject unanchored, existing filenames with advice', function() {
+ expect(function() {
+ var mod = require.resolve('moduleTests/example.json');
+ mod.exists;
+ }).toThrowError(/use '.\/moduleTests\/example\.json'/);
+ });
+ it('should reject unanchored, non-existing filenames', function() {
+ expect(function() {
+ var mod = require.resolve('asdfssdf/example.json');
+ mod.exists;
+ }).toThrowError(/Cannot find.*system module not found/);
+ });
+ it('should reject non-existent filenames', function() {
+ expect(function() {
+ require.resolve('./404error.js');
+ }).toThrowError(/Cannot find/);
+ });
+ it('should reject identifiers resolving to a directory', function() {
+ expect(function() {
+ var mod = require.resolve('.');
+ mod.exists;
+ // console.warn('resolved(.)', mod);
+ }).toThrowError(/Cannot find/);
+ expect(function() {
+ var mod = require.resolve('..');
+ mod.exists;
+ // console.warn('resolved(..)', mod);
+ }).toThrowError(/Cannot find/);
+ expect(function() {
+ var mod = require.resolve('../');
+ mod.exists;
+ // console.warn('resolved(../)', mod);
+ }).toThrowError(/Cannot find/);
+ });
+ (isNode ? xit : it)('should reject non-system, extensionless identifiers', function() {
+ expect(function() {
+ require.resolve('./example');
+ }).toThrowError(/Cannot find/);
+ });
+ });
+ });
+
+ describe('JSON', function() {
+ it('should import .json modules', function() {
+ var example = require('./moduleTests/example.json');
+ expect(example.name).toEqual('Example JSON Module');
+ });
+ // noet: support for loading JSON via content type workarounds reverted
+ // (leaving these tests intact in case ever revisited later)
+ // INTERFACE.describe('interface', function() {
+ // NETWORK.describe('network', function() {
+ // xit('should import #content-type=application/json modules', function() {
+ // var results = require('https://jsonip.com#content-type=application/json');
+ // expect(results.ip).toMatch(/^[.0-9]+$/);
+ // });
+ // xit('should import content-type: application/json modules', function() {
+ // var scope = { 'content-type': 'application/json' };
+ // var results = require.call(scope, 'https://jsonip.com');
+ // expect(results.ip).toMatch(/^[.0-9]+$/);
+ // });
+ // });
+ // });
+
+ });
+
+ INTERFACE.describe('system', function() {
+ it('require("vec3")', function() {
+ expect(require('vec3')).toEqual(jasmine.any(Function));
+ });
+ it('require("vec3").method', function() {
+ expect(require('vec3')().isValid).toEqual(jasmine.any(Function));
+ });
+ it('require("vec3") as constructor', function() {
+ var vec3 = require('vec3');
+ var v = vec3(1.1, 2.2, 3.3);
+ expect(v).toEqual(jasmine.any(Object));
+ expect(v.isValid).toEqual(jasmine.any(Function));
+ expect(v.isValid()).toBe(true);
+ expect(v.toString()).toEqual('[Vec3 (1.100,2.200,3.300)]');
+ });
+ });
+
+ describe('cache', function() {
+ it('should cache modules by resolved module id', function() {
+ var value = new Date;
+ var example = require('./moduleTests/example.json');
+ // earmark the module object with a unique value
+ example['.test'] = value;
+ var example2 = require('../../tests/unit_tests/moduleTests/example.json');
+ expect(example2).toBe(example);
+ // verify earmark is still the same after a second require()
+ expect(example2['.test']).toBe(example['.test']);
+ });
+ it('should reload cached modules set to null', function() {
+ var value = new Date;
+ var example = require('./moduleTests/example.json');
+ example['.test'] = value;
+ require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')] = null;
+ var example2 = require('../../tests/unit_tests/moduleTests/example.json');
+ // verify the earmark is *not* the same as before
+ expect(example2).not.toBe(example);
+ expect(example2['.test']).not.toBe(example['.test']);
+ });
+ it('should reload when module property is deleted', function() {
+ var value = new Date;
+ var example = require('./moduleTests/example.json');
+ example['.test'] = value;
+ delete require.cache[require.resolve('../../tests/unit_tests/moduleTests/example.json')];
+ var example2 = require('../../tests/unit_tests/moduleTests/example.json');
+ // verify the earmark is *not* the same as before
+ expect(example2).not.toBe(example);
+ expect(example2['.test']).not.toBe(example['.test']);
+ });
+ });
+
+ describe('cyclic dependencies', function() {
+ describe('should allow lazy-ref cyclic module resolution', function() {
+ var main;
+ beforeEach(function() {
+ // eslint-disable-next-line
+ try { this._print = print; } catch (e) {}
+ // during these tests print() is no-op'd so that it doesn't disrupt the reporter output
+ print = function() {};
+ Script.resetModuleCache();
+ });
+ afterEach(function() {
+ print = this._print;
+ });
+ it('main is requirable', function() {
+ main = require('./moduleTests/cycles/main.js');
+ expect(main).toEqual(jasmine.any(Object));
+ });
+ it('transient a and b done values', function() {
+ expect(main.a['b.done?']).toBe(true);
+ expect(main.b['a.done?']).toBe(false);
+ });
+ it('ultimate a.done?', function() {
+ expect(main['a.done?']).toBe(true);
+ });
+ it('ultimate b.done?', function() {
+ expect(main['b.done?']).toBe(true);
+ });
+ });
+ });
+
+ describe('JS', function() {
+ it('should throw catchable local file errors', function() {
+ expect(function() {
+ require('file:///dev/null/non-existent-file.js');
+ }).toThrowError(/path not found|Cannot find.*non-existent-file/);
+ });
+ it('should throw catchable invalid id errors', function() {
+ expect(function() {
+ require(new Array(4096 * 2).toString());
+ }).toThrowError(/invalid.*size|Cannot find.*,{30}/);
+ });
+ it('should throw catchable unresolved id errors', function() {
+ expect(function() {
+ require('foobar:/baz.js');
+ }).toThrowError(/could not resolve|Cannot find.*foobar:/);
+ });
+
+ NETWORK.describe('network', function() {
+ // note: depending on retries these tests can take up to 60 seconds each to timeout
+ var timeout = 75 * 1000;
+ it('should throw catchable host errors', function() {
+ expect(function() {
+ var mod = require('http://non.existent.highfidelity.io/moduleUnitTest.js');
+ print("mod", Object.keys(mod));
+ }).toThrowError(/error retrieving script .ServerUnavailable.|Cannot find.*non.existent/);
+ }, timeout);
+ it('should throw catchable network timeouts', function() {
+ expect(function() {
+ require('http://ping.highfidelity.io:1024');
+ }).toThrowError(/error retrieving script .Timeout.|Cannot find.*ping.highfidelity/);
+ }, timeout);
+ });
+ });
+
+ INTERFACE.describe('entity', function() {
+ var sampleScripts = [
+ 'entityConstructorAPIException.js',
+ 'entityConstructorModule.js',
+ 'entityConstructorNested2.js',
+ 'entityConstructorNested.js',
+ 'entityConstructorRequireException.js',
+ 'entityPreloadAPIError.js',
+ 'entityPreloadRequire.js',
+ ].filter(Boolean).map(function(id) {
+ return Script.require.resolve('./moduleTests/entity/'+id);
+ });
+
+ var uuids = [];
+ function cleanup() {
+ uuids.splice(0,uuids.length).forEach(function(uuid) {
+ Entities.deleteEntity(uuid);
+ });
+ }
+ afterAll(cleanup);
+ // extra sanity check to avoid lingering entities
+ Script.scriptEnding.connect(cleanup);
+
+ for (var i=0; i < sampleScripts.length; i++) {
+ maketest(i);
+ }
+
+ function maketest(i) {
+ var script = sampleScripts[ i % sampleScripts.length ];
+ var shortname = '['+i+'] ' + script.split('/').pop();
+ var position = MyAvatar.position;
+ position.y -= i/2;
+ // define a unique jasmine test for the current entity script
+ it(shortname, function(done) {
+ var uuid = Entities.addEntity({
+ text: shortname,
+ description: Script.resolvePath('').split('/').pop(),
+ type: 'Text',
+ position: position,
+ rotation: MyAvatar.orientation,
+ script: script,
+ scriptTimestamp: +new Date,
+ lifetime: 20,
+ lineHeight: 1/8,
+ dimensions: { x: 2, y: 0.5, z: 0.01 },
+ backgroundColor: { red: 0, green: 0, blue: 0 },
+ color: { red: 0xff, green: 0xff, blue: 0xff },
+ }, !Entities.serversExist() || !Entities.canRezTmp());
+ uuids.push(uuid);
+ function stopChecking() {
+ if (ii) {
+ Script.clearInterval(ii);
+ ii = 0;
+ }
+ }
+ var ii = Script.setInterval(function() {
+ Entities.queryPropertyMetadata(uuid, "script", function(err, result) {
+ if (err) {
+ stopChecking();
+ throw new Error(err);
+ }
+ if (result.success) {
+ stopChecking();
+ if (/Exception/.test(script)) {
+ expect(result.status).toMatch(/^error_(loading|running)_script$/);
+ } else {
+ expect(result.status).toEqual("running");
+ }
+ Entities.deleteEntity(uuid);
+ done();
+ } else {
+ print('!result.success', JSON.stringify(result));
+ }
+ });
+ }, 100);
+ Script.setTimeout(stopChecking, 4900);
+ }, 5000 /* jasmine async timeout */);
+ }
+ });
+});
+
+// support for isomorphic Node.js / Interface unit testing
+// note: run `npm install` from unit_tests/ and then `node moduleUnitTests.js`
+function run() {}
+function instrumentTestrunner() {
+ var isNode = typeof process === 'object' && process.title === 'node';
+ if (typeof describe === 'function') {
+ // already running within a test runner; assume jasmine is ready-to-go
+ return isNode;
+ }
+ if (isNode) {
+ /* eslint-disable no-console */
+ // Node.js test mode
+ // to keep things consistent Node.js uses the local jasmine.js library (instead of an npm version)
+ var jasmineRequire = require('../../libraries/jasmine/jasmine.js');
+ var jasmine = jasmineRequire.core(jasmineRequire);
+ var env = jasmine.getEnv();
+ var jasmineInterface = jasmineRequire.interface(jasmine, env);
+ for (var p in jasmineInterface) {
+ global[p] = jasmineInterface[p];
+ }
+ env.addReporter(new (require('jasmine-console-reporter')));
+ // testing mocks
+ Script = {
+ resetModuleCache: function() {
+ module.require.cache = {};
+ },
+ setTimeout: setTimeout,
+ clearTimeout: clearTimeout,
+ resolvePath: function(id) {
+ // this attempts to accurately emulate how Script.resolvePath works
+ var trace = {}; Error.captureStackTrace(trace);
+ var base = trace.stack.split('\n')[2].replace(/^.*[(]|[)].*$/g,'').replace(/:[0-9]+:[0-9]+.*$/,'');
+ if (!id) {
+ return base;
+ }
+ var rel = base.replace(/[^\/]+$/, id);
+ console.info('rel', rel);
+ return require.resolve(rel);
+ },
+ require: function(mod) {
+ return require(Script.require.resolve(mod));
+ },
+ };
+ Script.require.cache = require.cache;
+ Script.require.resolve = function(mod) {
+ if (mod === '.' || /^\.\.($|\/)/.test(mod)) {
+ throw new Error("Cannot find module '"+mod+"' (is dir)");
+ }
+ var path = require.resolve(mod);
+ // console.info('node-require-reoslved', mod, path);
+ try {
+ if (require('fs').lstatSync(path).isDirectory()) {
+ throw new Error("Cannot find module '"+path+"' (is directory)");
+ }
+ // console.info('!path', path);
+ } catch (e) {
+ console.error(e);
+ }
+ return path;
+ };
+ print = console.info.bind(console, '[print]');
+ /* eslint-enable no-console */
+ } else {
+ // Interface test mode
+ global = this;
+ Script.require('../../../system/libraries/utils.js');
+ this.jasmineRequire = Script.require('../../libraries/jasmine/jasmine.js');
+ Script.require('../../libraries/jasmine/hifi-boot.js');
+ require = Script.require;
+ // polyfill console
+ /* global console:true */
+ console = {
+ log: print,
+ info: print.bind(this, '[info]'),
+ warn: print.bind(this, '[warn]'),
+ error: print.bind(this, '[error]'),
+ debug: print.bind(this, '[debug]'),
+ };
+ }
+ // eslint-disable-next-line
+ run = function() { global.jasmine.getEnv().execute(); };
+ return isNode;
+}
+run();
diff --git a/scripts/developer/tests/unit_tests/package.json b/scripts/developer/tests/unit_tests/package.json
new file mode 100644
index 0000000000..91d719b687
--- /dev/null
+++ b/scripts/developer/tests/unit_tests/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "unit_tests",
+ "devDependencies": {
+ "jasmine-console-reporter": "^1.2.7"
+ }
+}
diff --git a/scripts/developer/tests/unit_tests/scriptUnitTests.js b/scripts/developer/tests/unit_tests/scriptUnitTests.js
index 63b451e97f..fa8cb44608 100644
--- a/scripts/developer/tests/unit_tests/scriptUnitTests.js
+++ b/scripts/developer/tests/unit_tests/scriptUnitTests.js
@@ -15,10 +15,20 @@ describe('Script', function () {
// characterization tests
// initially these are just to capture how the app works currently
var testCases = {
+ // special relative resolves
'': filename,
'.': dirname,
'..': parentdir,
+
+ // local file "magic" tilde path expansion
+ '/~/defaultScripts.js': ScriptDiscoveryService.defaultScriptsPath + '/defaultScripts.js',
+
+ // these schemes appear to always get resolved to empty URLs
+ 'qrc://test': '',
'about:Entities 1': '',
+ 'ftp://host:port/path': '',
+ 'data:text/html;text,foo': '',
+
'Entities 1': dirname + 'Entities 1',
'./file.js': dirname + 'file.js',
'c:/temp/': 'file:///c:/temp/',
@@ -31,6 +41,12 @@ describe('Script', function () {
'/~/libraries/utils.js': 'file:///~/libraries/utils.js',
'/temp/file.js': 'file:///temp/file.js',
'/~/': 'file:///~/',
+
+ // these schemes appear to always get resolved to the same URL again
+ 'http://highfidelity.com': 'http://highfidelity.com',
+ 'atp:/highfidelity': 'atp:/highfidelity',
+ 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f':
+ 'atp:c2d7e3a48cadf9ba75e4f8d9f4d80e75276774880405a093fdee36543aa04f',
};
describe('resolvePath', function () {
Object.keys(testCases).forEach(function(input) {
@@ -42,7 +58,7 @@ describe('Script', function () {
describe('include', function () {
var old_cache_buster;
- var cache_buster = '#' + +new Date;
+ var cache_buster = '#' + new Date().getTime().toString(36);
beforeAll(function() {
old_cache_buster = Settings.getValue('cache_buster');
Settings.setValue('cache_buster', cache_buster);
diff --git a/scripts/modules/vec3.js b/scripts/modules/vec3.js
new file mode 100644
index 0000000000..f164f01374
--- /dev/null
+++ b/scripts/modules/vec3.js
@@ -0,0 +1,69 @@
+// Example of using a "system module" to decouple Vec3's implementation details.
+//
+// Users would bring Vec3 support in as a module:
+// var vec3 = Script.require('vec3');
+//
+
+// (this example is compatible with using as a Script.include and as a Script.require module)
+try {
+ // Script.require
+ module.exports = vec3;
+} catch(e) {
+ // Script.include
+ Script.registerValue("vec3", vec3);
+}
+
+vec3.fromObject = function(v) {
+ //return new vec3(v.x, v.y, v.z);
+ //... this is even faster and achieves the same effect
+ v.__proto__ = vec3.prototype;
+ return v;
+};
+
+vec3.prototype = {
+ multiply: function(v2) {
+ // later on could support overrides like so:
+ // if (v2 instanceof quat) { [...] }
+ // which of the below is faster (C++ or JS)?
+ // (dunno -- but could systematically find out and go with that version)
+
+ // pure JS option
+ // return new vec3(this.x * v2.x, this.y * v2.y, this.z * v2.z);
+
+ // hybrid C++ option
+ return vec3.fromObject(Vec3.multiply(this, v2));
+ },
+ // detects any NaN and Infinity values
+ isValid: function() {
+ return isFinite(this.x) && isFinite(this.y) && isFinite(this.z);
+ },
+ // format Vec3's, eg:
+ // var v = vec3();
+ // print(v); // outputs [Vec3 (0.000, 0.000, 0.000)]
+ toString: function() {
+ if (this === vec3.prototype) {
+ return "{Vec3 prototype}";
+ }
+ function fixed(n) { return n.toFixed(3); }
+ return "[Vec3 (" + [this.x, this.y, this.z].map(fixed) + ")]";
+ },
+};
+
+vec3.DEBUG = true;
+
+function vec3(x, y, z) {
+ if (!(this instanceof vec3)) {
+ // if vec3 is called as a function then re-invoke as a constructor
+ // (so that `value instanceof vec3` holds true for created values)
+ return new vec3(x, y, z);
+ }
+
+ // unfold default arguments (vec3(), vec3(.5), vec3(0,1), etc.)
+ this.x = x !== undefined ? x : 0;
+ this.y = y !== undefined ? y : this.x;
+ this.z = z !== undefined ? z : this.y;
+
+ if (vec3.DEBUG && !this.isValid())
+ throw new Error('vec3() -- invalid initial values ['+[].slice.call(arguments)+']');
+};
+
diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js
index 46464dc2e1..e6c9b0aee0 100644
--- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js
+++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js
@@ -17,15 +17,14 @@ var mappingName, basicMapping, isChecked;
var TURN_RATE = 1000;
var MENU_ITEM_NAME = "Advanced Movement For Hand Controllers";
-var SETTINGS_KEY = 'advancedMovementForHandControllersIsChecked';
var isDisabled = false;
-var previousSetting = Settings.getValue(SETTINGS_KEY);
-if (previousSetting === '' || previousSetting === false || previousSetting === 'false') {
+var previousSetting = MyAvatar.useAdvancedMovementControls;
+if (previousSetting === false) {
previousSetting = false;
isChecked = false;
}
-if (previousSetting === true || previousSetting === 'true') {
+if (previousSetting === true) {
previousSetting = true;
isChecked = true;
}
@@ -37,7 +36,6 @@ function addAdvancedMovementItemToSettingsMenu() {
isCheckable: true,
isChecked: previousSetting
});
-
}
function rotate180() {
@@ -72,7 +70,6 @@ function registerBasicMapping() {
}
return;
});
- basicMapping.from(Controller.Standard.LX).to(Controller.Standard.RX);
basicMapping.from(Controller.Standard.RY).to(function(value) {
if (isDisabled) {
return;
@@ -112,10 +109,10 @@ function menuItemEvent(menuItem) {
if (menuItem == MENU_ITEM_NAME) {
isChecked = Menu.isOptionChecked(MENU_ITEM_NAME);
if (isChecked === true) {
- Settings.setValue(SETTINGS_KEY, true);
+ MyAvatar.useAdvancedMovementControls = true;
disableMappings();
} else if (isChecked === false) {
- Settings.setValue(SETTINGS_KEY, false);
+ MyAvatar.useAdvancedMovementControls = false;
enableMappings();
}
}