diff --git a/.gitignore b/.gitignore
index f5605d7090..3f58e46b69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,11 +14,13 @@ Makefile
 
 # Android Studio
 *.iml
+*.class
 local.properties
 android/gradle*
 android/.gradle
 android/**/src/main/jniLibs
 android/**/libs
+android/**/bin
 android/**/src/main/res/values/libs.xml
 android/**/src/main/assets
 android/**/gradle*
@@ -102,3 +104,4 @@ tools/unity-avatar-exporter/Logs
 tools/unity-avatar-exporter/Packages
 tools/unity-avatar-exporter/ProjectSettings
 tools/unity-avatar-exporter/Temp
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4e5dbe935a..d0a2e57dd5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,7 +12,7 @@ target_python()
 
 if (HIFI_ANDROID )
     execute_process(
-        COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --android --build-root ${CMAKE_BINARY_DIR}
+        COMMAND ${HIFI_PYTHON_EXEC} ${CMAKE_CURRENT_SOURCE_DIR}/prebuild.py --android ${HIFI_ANDROID_APP} --build-root ${CMAKE_BINARY_DIR}
         WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
     )
 else()
@@ -174,7 +174,7 @@ set_packaging_parameters()
 
 # FIXME hack to work on the proper Android toolchain
 if (ANDROID)
-  add_subdirectory(android/app)
+  add_subdirectory(android/apps/${HIFI_ANDROID_APP})
   return()
 endif()
 
diff --git a/android/app/CMakeLists.txt b/android/apps/interface/CMakeLists.txt
similarity index 61%
rename from android/app/CMakeLists.txt
rename to android/apps/interface/CMakeLists.txt
index 19dce330c1..500d555915 100644
--- a/android/app/CMakeLists.txt
+++ b/android/apps/interface/CMakeLists.txt
@@ -4,10 +4,10 @@ link_hifi_libraries(shared task networking gl gpu qml image fbx hfm render-utils
 target_opengl()
 target_bullet()
 
-set(INTERFACE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../interface")
+set(INTERFACE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../interface")
 add_subdirectory("${INTERFACE_DIR}" "libraries/interface")
 include_directories("${INTERFACE_DIR}/src")
-set(HIFI_CODEC_PLUGIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../plugins/hifiCodec")
+set(HIFI_CODEC_PLUGIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../plugins/hifiCodec")
 add_subdirectory("${HIFI_CODEC_PLUGIN_DIR}" "libraries/hifiCodecPlugin")
 
 target_link_libraries(native-lib android log m interface)
@@ -15,16 +15,3 @@ target_link_libraries(native-lib android log m interface)
 set(GVR_ROOT "${HIFI_ANDROID_PRECOMPILED}/gvr/gvr-android-sdk-1.101.0/")
 target_include_directories(native-lib PRIVATE  "${GVR_ROOT}/libraries/headers" "libraries/ui/src")
 target_link_libraries(native-lib "${GVR_ROOT}/libraries/libgvr.so" ui)
-
-# finished libraries
-# core -> qt
-# networking -> openssl, tbb
-# fbx -> draco
-# physics -> bullet
-# entities-renderer -> polyvox
-
-# unfinished libraries
-# image -> nvtt (doesn't look good, but can be made optional)
-# script-engine -> quazip (probably not required for the android client)
-
-
diff --git a/android/app/build.gradle b/android/apps/interface/build.gradle
similarity index 83%
rename from android/app/build.gradle
rename to android/apps/interface/build.gradle
index e3c6989baf..4163df03b7 100644
--- a/android/app/build.gradle
+++ b/android/apps/interface/build.gradle
@@ -1,5 +1,37 @@
 import org.apache.tools.ant.taskdefs.condition.Os
 
+buildscript {
+    repositories {
+        jcenter()
+        google()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.2.1'
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+        google()
+    }
+}
+
+task renameHifiACTaskDebug() {
+    doLast {
+        def sourceFile = new File("${appDir}/build/intermediates/cmake/debug/obj/arm64-v8a/","libhifiCodec.so")
+        def destinationFile = new File("${appDir}/src/main/jniLibs/arm64-v8a", "libplugins_libhifiCodec.so")
+        copy { from sourceFile;  into destinationFile.parent;  rename(sourceFile.name, destinationFile.name) }
+    }
+}
+task renameHifiACTaskRelease(type: Copy) {
+    doLast {
+        def sourceFile = new File("${appDir}/build/intermediates/cmake/release/obj/arm64-v8a/","libhifiCodec.so")
+        def destinationFile = new File("${appDir}/src/main/jniLibs/arm64-v8a", "libplugins_libhifiCodec.so")
+        copy { from sourceFile;  into destinationFile.parent;  rename(sourceFile.name, destinationFile.name) }
+    }
+}
+
 apply plugin: 'com.android.application'
 
 android {
@@ -19,17 +51,17 @@ android {
         externalNativeBuild {
             cmake {
                 arguments '-DHIFI_ANDROID=1',
+                    '-DHIFI_ANDROID_APP=interface',
                     '-DANDROID_PLATFORM=android-24',
                     '-DANDROID_TOOLCHAIN=clang',
                     '-DANDROID_STL=c++_shared',
-                    '-DQT_CMAKE_PREFIX_PATH=' + HIFI_ANDROID_PRECOMPILED + '/qt/lib/cmake',
-                    '-DHIFI_ANDROID_PRECOMPILED=' + HIFI_ANDROID_PRECOMPILED,
                     '-DRELEASE_NUMBER=' + RELEASE_NUMBER,
                     '-DRELEASE_TYPE=' + RELEASE_TYPE,
                     '-DSTABLE_BUILD=' + STABLE_BUILD,
                     '-DDISABLE_QML=OFF',
                     '-DDISABLE_KTX_CACHE=OFF',
                     '-DUSE_BREAKPAD=' + (System.getenv("CMAKE_BACKTRACE_URL") && System.getenv("CMAKE_BACKTRACE_TOKEN") ? 'ON' : 'OFF');
+                targets = ['native-lib']
             }
         }
         signingConfigs {
@@ -72,7 +104,7 @@ android {
 
     externalNativeBuild {
         cmake {
-            path '../../CMakeLists.txt'
+            path '../../../CMakeLists.txt'
         }
     }
 
@@ -82,6 +114,7 @@ android {
         variant.externalNativeBuildTasks.each { task ->
             variant.mergeResources.dependsOn(task)
             if (Os.isFamily(Os.FAMILY_UNIX)) {
+                // FIXME
                 def uploadDumpSymsTask = rootProject.getTasksByName("uploadBreakpadDumpSyms${variant.name.capitalize()}", false).first()
                 def runDumpSymsTask = rootProject.getTasksByName("runBreakpadDumpSyms${variant.name.capitalize()}", false).first()
                 def renameHifiACTask = rootProject.getTasksByName("renameHifiACTask${variant.name.capitalize()}", false).first()
@@ -97,7 +130,7 @@ android {
 
             // Copy the compiled resources generated by the external native build
             copy {
-                from new File(projectDir, "../../interface/compiledResources")
+                from new File(projectDir, "../../../interface/compiledResources")
                 into outputDir
                 duplicatesStrategy DuplicatesStrategy.INCLUDE
                 eachFile { details ->
@@ -108,7 +141,7 @@ android {
 
             // Copy the scripts directory
             copy {
-                from new File(projectDir, "../../scripts")
+                from new File(projectDir, "../../../scripts")
                 into new File(outputDir, "scripts")
                 duplicatesStrategy DuplicatesStrategy.INCLUDE
                 eachFile { details->
@@ -123,12 +156,6 @@ android {
                 assetList.each { file -> out.println(file) }
             }
         }
-
-        variant.outputs.all {
-            if (RELEASE_NUMBER != '0') {
-                outputFileName = "app_" + RELEASE_NUMBER + "_" + RELEASE_TYPE + ".apk"
-            }
-        }
     }
 }
 
@@ -157,5 +184,6 @@ dependencies {
 
     api 'com.sothree.slidinguppanel:library:3.4.0'
 
-    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation fileTree(include: ['*.jar'], dir: '../../libraries/qt/libs')
+    implementation project(':qt')
 }
diff --git a/android/app/proguard-rules.pro b/android/apps/interface/proguard-rules.pro
similarity index 100%
rename from android/app/proguard-rules.pro
rename to android/apps/interface/proguard-rules.pro
diff --git a/android/app/src/main/AndroidManifest.xml b/android/apps/interface/src/main/AndroidManifest.xml
similarity index 100%
rename from android/app/src/main/AndroidManifest.xml
rename to android/apps/interface/src/main/AndroidManifest.xml
diff --git a/android/app/src/main/assets/privacy_policy.html b/android/apps/interface/src/main/assets/privacy_policy.html
similarity index 100%
rename from android/app/src/main/assets/privacy_policy.html
rename to android/apps/interface/src/main/assets/privacy_policy.html
diff --git a/android/app/src/main/cpp/native.cpp b/android/apps/interface/src/main/cpp/native.cpp
similarity index 99%
rename from android/app/src/main/cpp/native.cpp
rename to android/apps/interface/src/main/cpp/native.cpp
index f9c7751a3e..2bb851bb85 100644
--- a/android/app/src/main/cpp/native.cpp
+++ b/android/apps/interface/src/main/cpp/native.cpp
@@ -149,7 +149,7 @@ void unpackAndroidAssets() {
 
 extern "C" {
 
-JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCreate(JNIEnv* env, jobject obj, jobject instance, jobject asset_mgr) {
+JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCreate(JNIEnv* env, jobject instance, jobject asset_mgr) {
     g_assetManager = AAssetManager_fromJava(env, asset_mgr);
     qRegisterMetaType<QAndroidJniObject>("QAndroidJniObject");
     __interfaceActivity = QAndroidJniObject(instance);
diff --git a/android/app/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java
rename to android/apps/interface/src/main/java/io/highfidelity/gvrinterface/InterfaceActivity.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/BreakpadUploaderService.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/HifiUtils.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java
similarity index 98%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java
index 50aea59663..b7d2157737 100644
--- a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java
+++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java
@@ -61,7 +61,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
     private HeadsetStateReceiver headsetStateReceiver;
 
     //public static native void handleHifiURL(String hifiURLString);
-    private native long nativeOnCreate(InterfaceActivity instance, AssetManager assetManager);
+    private native void nativeOnCreate(AssetManager assetManager);
     private native void nativeOnDestroy();
     private native void nativeGotoUrl(String url);
     private native void nativeGoToUser(String username);
@@ -114,7 +114,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
         assetManager = getResources().getAssets();
 
         //nativeGvrApi =
-            nativeOnCreate(this, assetManager /*, gvrApi.getNativeGvrContext()*/);
+            nativeOnCreate(assetManager /*, gvrApi.getNativeGvrContext()*/);
 
         final View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
 
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/LoginMenuActivity.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/MainActivity.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/SplashActivity.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/WebViewActivity.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/FriendsFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/OnBackPressedListener.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/PolicyFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/StartMenuFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/fragment/WebViewFragment.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/Callback.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/DomainProvider.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/EndpointUsersProvider.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/provider/UsersProvider.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/task/DownloadProfileImageTask.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java
diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java
similarity index 100%
rename from android/app/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java
rename to android/apps/interface/src/main/java/io/highfidelity/hifiinterface/view/UserListAdapter.java
diff --git a/android/app/src/main/res/drawable/default_profile_avatar.xml b/android/apps/interface/src/main/res/drawable/default_profile_avatar.xml
similarity index 100%
rename from android/app/src/main/res/drawable/default_profile_avatar.xml
rename to android/apps/interface/src/main/res/drawable/default_profile_avatar.xml
diff --git a/android/app/src/main/res/drawable/domain_placeholder.png b/android/apps/interface/src/main/res/drawable/domain_placeholder.png
similarity index 100%
rename from android/app/src/main/res/drawable/domain_placeholder.png
rename to android/apps/interface/src/main/res/drawable/domain_placeholder.png
diff --git a/android/app/src/main/res/drawable/encourage_login_background.jpg b/android/apps/interface/src/main/res/drawable/encourage_login_background.jpg
similarity index 100%
rename from android/app/src/main/res/drawable/encourage_login_background.jpg
rename to android/apps/interface/src/main/res/drawable/encourage_login_background.jpg
diff --git a/android/app/src/main/res/drawable/hifi_header.xml b/android/apps/interface/src/main/res/drawable/hifi_header.xml
similarity index 100%
rename from android/app/src/main/res/drawable/hifi_header.xml
rename to android/apps/interface/src/main/res/drawable/hifi_header.xml
diff --git a/android/app/src/main/res/drawable/hifi_logo_header.xml b/android/apps/interface/src/main/res/drawable/hifi_logo_header.xml
similarity index 100%
rename from android/app/src/main/res/drawable/hifi_logo_header.xml
rename to android/apps/interface/src/main/res/drawable/hifi_logo_header.xml
diff --git a/android/app/src/main/res/drawable/hifi_logo_splash.xml b/android/apps/interface/src/main/res/drawable/hifi_logo_splash.xml
similarity index 100%
rename from android/app/src/main/res/drawable/hifi_logo_splash.xml
rename to android/apps/interface/src/main/res/drawable/hifi_logo_splash.xml
diff --git a/android/app/src/main/res/drawable/ic_bookmark.xml b/android/apps/interface/src/main/res/drawable/ic_bookmark.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_bookmark.xml
rename to android/apps/interface/src/main/res/drawable/ic_bookmark.xml
diff --git a/android/app/src/main/res/drawable/ic_clear.xml b/android/apps/interface/src/main/res/drawable/ic_clear.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_clear.xml
rename to android/apps/interface/src/main/res/drawable/ic_clear.xml
diff --git a/android/app/src/main/res/drawable/ic_close.xml b/android/apps/interface/src/main/res/drawable/ic_close.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_close.xml
rename to android/apps/interface/src/main/res/drawable/ic_close.xml
diff --git a/android/app/src/main/res/drawable/ic_close_black_24dp.xml b/android/apps/interface/src/main/res/drawable/ic_close_black_24dp.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_close_black_24dp.xml
rename to android/apps/interface/src/main/res/drawable/ic_close_black_24dp.xml
diff --git a/android/app/src/main/res/drawable/ic_delete_black_24dp.xml b/android/apps/interface/src/main/res/drawable/ic_delete_black_24dp.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_delete_black_24dp.xml
rename to android/apps/interface/src/main/res/drawable/ic_delete_black_24dp.xml
diff --git a/android/app/src/main/res/drawable/ic_expand.xml b/android/apps/interface/src/main/res/drawable/ic_expand.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_expand.xml
rename to android/apps/interface/src/main/res/drawable/ic_expand.xml
diff --git a/android/app/src/main/res/drawable/ic_eye_noshow.xml b/android/apps/interface/src/main/res/drawable/ic_eye_noshow.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_eye_noshow.xml
rename to android/apps/interface/src/main/res/drawable/ic_eye_noshow.xml
diff --git a/android/app/src/main/res/drawable/ic_eye_show.xml b/android/apps/interface/src/main/res/drawable/ic_eye_show.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_eye_show.xml
rename to android/apps/interface/src/main/res/drawable/ic_eye_show.xml
diff --git a/android/app/src/main/res/drawable/ic_launcher.xml b/android/apps/interface/src/main/res/drawable/ic_launcher.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_launcher.xml
rename to android/apps/interface/src/main/res/drawable/ic_launcher.xml
diff --git a/android/app/src/main/res/drawable/ic_menu.xml b/android/apps/interface/src/main/res/drawable/ic_menu.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_menu.xml
rename to android/apps/interface/src/main/res/drawable/ic_menu.xml
diff --git a/android/app/src/main/res/drawable/ic_person.xml b/android/apps/interface/src/main/res/drawable/ic_person.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_person.xml
rename to android/apps/interface/src/main/res/drawable/ic_person.xml
diff --git a/android/app/src/main/res/drawable/ic_right_arrow.xml b/android/apps/interface/src/main/res/drawable/ic_right_arrow.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_right_arrow.xml
rename to android/apps/interface/src/main/res/drawable/ic_right_arrow.xml
diff --git a/android/app/src/main/res/drawable/ic_search.xml b/android/apps/interface/src/main/res/drawable/ic_search.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_search.xml
rename to android/apps/interface/src/main/res/drawable/ic_search.xml
diff --git a/android/app/src/main/res/drawable/ic_share.xml b/android/apps/interface/src/main/res/drawable/ic_share.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_share.xml
rename to android/apps/interface/src/main/res/drawable/ic_share.xml
diff --git a/android/app/src/main/res/drawable/ic_star.xml b/android/apps/interface/src/main/res/drawable/ic_star.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_star.xml
rename to android/apps/interface/src/main/res/drawable/ic_star.xml
diff --git a/android/app/src/main/res/drawable/ic_steam.xml b/android/apps/interface/src/main/res/drawable/ic_steam.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_steam.xml
rename to android/apps/interface/src/main/res/drawable/ic_steam.xml
diff --git a/android/app/src/main/res/drawable/ic_teleporticon.xml b/android/apps/interface/src/main/res/drawable/ic_teleporticon.xml
similarity index 100%
rename from android/app/src/main/res/drawable/ic_teleporticon.xml
rename to android/apps/interface/src/main/res/drawable/ic_teleporticon.xml
diff --git a/android/app/src/main/res/drawable/launch_screen.xml b/android/apps/interface/src/main/res/drawable/launch_screen.xml
similarity index 100%
rename from android/app/src/main/res/drawable/launch_screen.xml
rename to android/apps/interface/src/main/res/drawable/launch_screen.xml
diff --git a/android/app/src/main/res/drawable/rounded_button_color1.xml b/android/apps/interface/src/main/res/drawable/rounded_button_color1.xml
similarity index 100%
rename from android/app/src/main/res/drawable/rounded_button_color1.xml
rename to android/apps/interface/src/main/res/drawable/rounded_button_color1.xml
diff --git a/android/app/src/main/res/drawable/rounded_button_color3.xml b/android/apps/interface/src/main/res/drawable/rounded_button_color3.xml
similarity index 100%
rename from android/app/src/main/res/drawable/rounded_button_color3.xml
rename to android/apps/interface/src/main/res/drawable/rounded_button_color3.xml
diff --git a/android/app/src/main/res/drawable/rounded_button_color4.xml b/android/apps/interface/src/main/res/drawable/rounded_button_color4.xml
similarity index 100%
rename from android/app/src/main/res/drawable/rounded_button_color4.xml
rename to android/apps/interface/src/main/res/drawable/rounded_button_color4.xml
diff --git a/android/app/src/main/res/drawable/rounded_secondary_button.xml b/android/apps/interface/src/main/res/drawable/rounded_secondary_button.xml
similarity index 100%
rename from android/app/src/main/res/drawable/rounded_secondary_button.xml
rename to android/apps/interface/src/main/res/drawable/rounded_secondary_button.xml
diff --git a/android/app/src/main/res/drawable/search_bg.xml b/android/apps/interface/src/main/res/drawable/search_bg.xml
similarity index 100%
rename from android/app/src/main/res/drawable/search_bg.xml
rename to android/apps/interface/src/main/res/drawable/search_bg.xml
diff --git a/android/app/src/main/res/drawable/selector_show_password.xml b/android/apps/interface/src/main/res/drawable/selector_show_password.xml
similarity index 100%
rename from android/app/src/main/res/drawable/selector_show_password.xml
rename to android/apps/interface/src/main/res/drawable/selector_show_password.xml
diff --git a/android/app/src/main/res/font/raleway.ttf b/android/apps/interface/src/main/res/font/raleway.ttf
similarity index 100%
rename from android/app/src/main/res/font/raleway.ttf
rename to android/apps/interface/src/main/res/font/raleway.ttf
diff --git a/android/app/src/main/res/font/raleway_bold.xml b/android/apps/interface/src/main/res/font/raleway_bold.xml
similarity index 100%
rename from android/app/src/main/res/font/raleway_bold.xml
rename to android/apps/interface/src/main/res/font/raleway_bold.xml
diff --git a/android/app/src/main/res/font/raleway_italic.xml b/android/apps/interface/src/main/res/font/raleway_italic.xml
similarity index 100%
rename from android/app/src/main/res/font/raleway_italic.xml
rename to android/apps/interface/src/main/res/font/raleway_italic.xml
diff --git a/android/app/src/main/res/font/raleway_light_italic.xml b/android/apps/interface/src/main/res/font/raleway_light_italic.xml
similarity index 100%
rename from android/app/src/main/res/font/raleway_light_italic.xml
rename to android/apps/interface/src/main/res/font/raleway_light_italic.xml
diff --git a/android/app/src/main/res/font/raleway_medium.xml b/android/apps/interface/src/main/res/font/raleway_medium.xml
similarity index 100%
rename from android/app/src/main/res/font/raleway_medium.xml
rename to android/apps/interface/src/main/res/font/raleway_medium.xml
diff --git a/android/app/src/main/res/font/raleway_semibold.xml b/android/apps/interface/src/main/res/font/raleway_semibold.xml
similarity index 100%
rename from android/app/src/main/res/font/raleway_semibold.xml
rename to android/apps/interface/src/main/res/font/raleway_semibold.xml
diff --git a/android/app/src/main/res/layout/activity_encourage_login.xml b/android/apps/interface/src/main/res/layout/activity_encourage_login.xml
similarity index 100%
rename from android/app/src/main/res/layout/activity_encourage_login.xml
rename to android/apps/interface/src/main/res/layout/activity_encourage_login.xml
diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/apps/interface/src/main/res/layout/activity_main.xml
similarity index 100%
rename from android/app/src/main/res/layout/activity_main.xml
rename to android/apps/interface/src/main/res/layout/activity_main.xml
diff --git a/android/app/src/main/res/layout/activity_splash.xml b/android/apps/interface/src/main/res/layout/activity_splash.xml
similarity index 100%
rename from android/app/src/main/res/layout/activity_splash.xml
rename to android/apps/interface/src/main/res/layout/activity_splash.xml
diff --git a/android/app/src/main/res/layout/activity_web_view.xml b/android/apps/interface/src/main/res/layout/activity_web_view.xml
similarity index 100%
rename from android/app/src/main/res/layout/activity_web_view.xml
rename to android/apps/interface/src/main/res/layout/activity_web_view.xml
diff --git a/android/app/src/main/res/layout/domain_view.xml b/android/apps/interface/src/main/res/layout/domain_view.xml
similarity index 100%
rename from android/app/src/main/res/layout/domain_view.xml
rename to android/apps/interface/src/main/res/layout/domain_view.xml
diff --git a/android/app/src/main/res/layout/fragment_friends.xml b/android/apps/interface/src/main/res/layout/fragment_friends.xml
similarity index 100%
rename from android/app/src/main/res/layout/fragment_friends.xml
rename to android/apps/interface/src/main/res/layout/fragment_friends.xml
diff --git a/android/app/src/main/res/layout/fragment_home.xml b/android/apps/interface/src/main/res/layout/fragment_home.xml
similarity index 100%
rename from android/app/src/main/res/layout/fragment_home.xml
rename to android/apps/interface/src/main/res/layout/fragment_home.xml
diff --git a/android/app/src/main/res/layout/fragment_login.xml b/android/apps/interface/src/main/res/layout/fragment_login.xml
similarity index 100%
rename from android/app/src/main/res/layout/fragment_login.xml
rename to android/apps/interface/src/main/res/layout/fragment_login.xml
diff --git a/android/app/src/main/res/layout/fragment_login_menu.xml b/android/apps/interface/src/main/res/layout/fragment_login_menu.xml
similarity index 100%
rename from android/app/src/main/res/layout/fragment_login_menu.xml
rename to android/apps/interface/src/main/res/layout/fragment_login_menu.xml
diff --git a/android/app/src/main/res/layout/fragment_policy.xml b/android/apps/interface/src/main/res/layout/fragment_policy.xml
similarity index 100%
rename from android/app/src/main/res/layout/fragment_policy.xml
rename to android/apps/interface/src/main/res/layout/fragment_policy.xml
diff --git a/android/app/src/main/res/layout/fragment_signup.xml b/android/apps/interface/src/main/res/layout/fragment_signup.xml
similarity index 100%
rename from android/app/src/main/res/layout/fragment_signup.xml
rename to android/apps/interface/src/main/res/layout/fragment_signup.xml
diff --git a/android/app/src/main/res/layout/fragment_web_view.xml b/android/apps/interface/src/main/res/layout/fragment_web_view.xml
similarity index 100%
rename from android/app/src/main/res/layout/fragment_web_view.xml
rename to android/apps/interface/src/main/res/layout/fragment_web_view.xml
diff --git a/android/app/src/main/res/layout/navigation_header.xml b/android/apps/interface/src/main/res/layout/navigation_header.xml
similarity index 100%
rename from android/app/src/main/res/layout/navigation_header.xml
rename to android/apps/interface/src/main/res/layout/navigation_header.xml
diff --git a/android/app/src/main/res/layout/user_item.xml b/android/apps/interface/src/main/res/layout/user_item.xml
similarity index 100%
rename from android/app/src/main/res/layout/user_item.xml
rename to android/apps/interface/src/main/res/layout/user_item.xml
diff --git a/android/app/src/main/res/layout/web_drawer.xml b/android/apps/interface/src/main/res/layout/web_drawer.xml
similarity index 100%
rename from android/app/src/main/res/layout/web_drawer.xml
rename to android/apps/interface/src/main/res/layout/web_drawer.xml
diff --git a/android/app/src/main/res/menu/menu_navigation.xml b/android/apps/interface/src/main/res/menu/menu_navigation.xml
similarity index 100%
rename from android/app/src/main/res/menu/menu_navigation.xml
rename to android/apps/interface/src/main/res/menu/menu_navigation.xml
diff --git a/android/app/src/main/res/menu/web_view_menu.xml b/android/apps/interface/src/main/res/menu/web_view_menu.xml
similarity index 100%
rename from android/app/src/main/res/menu/web_view_menu.xml
rename to android/apps/interface/src/main/res/menu/web_view_menu.xml
diff --git a/android/app/src/main/res/values-w385dp/dimens.xml b/android/apps/interface/src/main/res/values-w385dp/dimens.xml
similarity index 100%
rename from android/app/src/main/res/values-w385dp/dimens.xml
rename to android/apps/interface/src/main/res/values-w385dp/dimens.xml
diff --git a/android/app/src/main/res/values/colors.xml b/android/apps/interface/src/main/res/values/colors.xml
similarity index 100%
rename from android/app/src/main/res/values/colors.xml
rename to android/apps/interface/src/main/res/values/colors.xml
diff --git a/android/app/src/main/res/values/dimens.xml b/android/apps/interface/src/main/res/values/dimens.xml
similarity index 100%
rename from android/app/src/main/res/values/dimens.xml
rename to android/apps/interface/src/main/res/values/dimens.xml
diff --git a/android/app/src/main/res/values/font_certs.xml b/android/apps/interface/src/main/res/values/font_certs.xml
similarity index 100%
rename from android/app/src/main/res/values/font_certs.xml
rename to android/apps/interface/src/main/res/values/font_certs.xml
diff --git a/android/app/src/main/res/values/preloaded_fonts.xml b/android/apps/interface/src/main/res/values/preloaded_fonts.xml
similarity index 100%
rename from android/app/src/main/res/values/preloaded_fonts.xml
rename to android/apps/interface/src/main/res/values/preloaded_fonts.xml
diff --git a/android/app/src/main/res/values/strings.xml b/android/apps/interface/src/main/res/values/strings.xml
similarity index 100%
rename from android/app/src/main/res/values/strings.xml
rename to android/apps/interface/src/main/res/values/strings.xml
diff --git a/android/app/src/main/res/values/styles.xml b/android/apps/interface/src/main/res/values/styles.xml
similarity index 100%
rename from android/app/src/main/res/values/styles.xml
rename to android/apps/interface/src/main/res/values/styles.xml
diff --git a/android/app/src/main/res/xml/settings.xml b/android/apps/interface/src/main/res/xml/settings.xml
similarity index 100%
rename from android/app/src/main/res/xml/settings.xml
rename to android/apps/interface/src/main/res/xml/settings.xml
diff --git a/android/build.gradle b/android/build.gradle
index 8d03b9f6b3..ed2ca1c47e 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -10,8 +10,8 @@ import java.util.regex.Pattern
 
 buildscript {
     repositories {
-        jcenter()
         google()
+        jcenter()
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:3.2.1'
@@ -26,8 +26,8 @@ plugins {
 
 allprojects {
     repositories {
-        jcenter()
         google()
+        jcenter()
         mavenCentral()
     }
 }
@@ -42,378 +42,13 @@ ext {
     RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV'
     STABLE_BUILD = project.hasProperty('STABLE_BUILD') ? project.getProperty('STABLE_BUILD') : '0'
     EXEC_SUFFIX = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : ''
-    QT5_DEPS = [
-        'Qt5Concurrent',
-        'Qt5Core',
-        'Qt5Gui',
-        'Qt5Multimedia',
-        'Qt5Network',
-        'Qt5OpenGL',
-        'Qt5Qml',
-        'Qt5Quick',
-        'Qt5QuickControls2',
-        'Qt5QuickTemplates2',
-        'Qt5Script',
-        'Qt5ScriptTools',
-        'Qt5Svg',
-        'Qt5WebChannel',
-        'Qt5WebSockets',
-        'Qt5Widgets',
-        'Qt5XmlPatterns',
-        // Android specific
-        'Qt5AndroidExtras',
-        'Qt5WebView',
-    ]
 }
 
-def baseFolder = new File(HIFI_ANDROID_PRECOMPILED)
-def appDir = new File(projectDir, 'app')
+def appDir = new File(projectDir, 'apps/interface')
 def jniFolder = new File(appDir, 'src/main/jniLibs/arm64-v8a')
 def baseUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/android/'
 def breakpadDumpSymsDir = new File("${appDir}/build/tmp/breakpadDumpSyms")
 
-def qtFile='qt-5.11.1_linux_armv8-libcpp_openssl_patched.tgz'
-def qtChecksum='aa449d4bfa963f3bc9a9dfe558ba29df'
-def qtVersionId='3S97HBM5G5Xw9EfE52sikmgdN3t6C2MN'
-if (Os.isFamily(Os.FAMILY_MAC)) {
-    qtFile = 'qt-5.11.1_osx_armv8-libcpp_openssl_patched.tgz'
-    qtChecksum='c83cc477c08a892e00c71764dca051a0'
-    qtVersionId='OxBD7iKINv1HbyOXmAmDrBb8AF3N.Kup'
-} else if (Os.isFamily(Os.FAMILY_WINDOWS)) {
-    qtFile = 'qt-5.11.1_win_armv8-libcpp_openssl_patched.tgz'
-    qtChecksum='0582191cc55431aa4f660848a542883e'
-    qtVersionId='JfWM0P_Mz5Qp0LwpzhrsRwN3fqlLSFeT'
-}
-
-def packages = [
-    qt: [
-        file: qtFile,
-        versionId: qtVersionId,
-        checksum: qtChecksum,
-    ],
-    bullet: [
-        file: 'bullet-2.88_armv8-libcpp.tgz',
-        versionId: 'S8YaoED0Cl8sSb8fSV7Q2G1lQJSNDxqg',
-        checksum: '81642779ccb110f8c7338e8739ac38a0',
-    ],
-    draco: [
-        file: 'draco_armv8-libcpp.tgz',
-        versionId: '3.B.uBj31kWlgND3_R2xwQzT_TP6Dz_8',
-        checksum: '617a80d213a5ec69fbfa21a1f2f738cd',
-    ],
-    glad: [
-        file: 'glad_armv8-libcpp.zip',
-        versionId: 'r5Zran.JSCtvrrB6Q4KaqfIoALPw3lYY',
-        checksum: 'a8ee8584cf1ccd34766c7ddd9d5e5449',
-    ],
-    gvr: [
-        file: 'gvrsdk_v1.101.0.tgz',
-        versionId: 'nqBV_j81Uc31rC7bKIrlya_Hah4v3y5r',
-        checksum: '57fd02baa069176ba18597a29b6b4fc7',
-    ],
-    nvtt: [
-        file: 'nvtt_armv8-libcpp.zip',
-        versionId: 'lmkBVR5t4UF1UUwMwEirnk9H_8Nt90IO',
-        checksum: 'eb46d0b683e66987190ed124aabf8910',
-        sharedLibFolder: 'lib',
-        includeLibs: ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so'],
-    ],
-    openssl: [
-        file: 'openssl-1.1.0g_armv8.tgz',
-        versionId: 'AiiPjmgUZTgNj7YV1EEx2lL47aDvvvAW',
-        checksum: 'cabb681fbccd79594f65fcc266e02f32',
-    ],
-    polyvox: [
-        file: 'polyvox_armv8-libcpp.tgz',
-        versionId: 'A2kbKiNhpIenGq23bKRRzg7IMAI5BI92',
-        checksum: 'dba88b3a098747af4bb169e9eb9af57e',
-        sharedLibFolder: 'lib',
-        includeLibs: ['Release/libPolyVoxCore.so', 'libPolyVoxUtil.so'],
-    ],
-    tbb: [
-        file: 'tbb-2018_U1_armv8_libcpp.tgz',
-        versionId: 'mrRbWnv4O4evcM1quRH43RJqimlRtaKB',
-        checksum: '20768f298f53b195e71b414b0ae240c4',
-        sharedLibFolder: 'lib/release',
-        includeLibs: ['libtbb.so', 'libtbbmalloc.so'],
-    ],
-    hifiAC: [
-            baseUrl: 'http://s3.amazonaws.com/hifi-public/dependencies/',
-            file: 'codecSDK-android_armv8-2.0.zip',
-            checksum: '1cbef929675818fc64c4101b72f84a6a'
-    ],
-    etc2comp: [
-        file: 'etc2comp-patched-armv8-libcpp.tgz',
-        versionId: 'bHhGECRAQR1vkpshBcK6ByNc1BQIM8gU',
-        checksum: '14b02795d774457a33bbc60e00a786bc'
-    ],
-    breakpad: [
-        file: 'breakpad.tgz',
-        versionId: '8VrYXz7oyc.QBxNia0BVJOUBvrFO61jI',
-        checksum: 'ddcb23df336b08017042ba4786db1d9e',
-        sharedLibFolder: 'lib',
-        includeLibs: ['libbreakpad_client.a']
-    ]
-]
-
-def options = [
-    files: new TreeSet<File>(),
-    features: new HashSet<String>(),
-    permissions: new HashSet<String>()
-]
-
-def qmlRoot = new File(HIFI_ANDROID_PRECOMPILED, 'qt')
-
-def captureOutput = { String command, List<String> commandArgs ->
-    def result
-    new ByteArrayOutputStream().withStream { os ->
-        def execResult = exec {
-            executable = command
-            args = commandArgs
-            standardOutput = os
-            errorOutput = new ByteArrayOutputStream()
-        }
-        result = os.toString()
-    }
-    return result;
-}
-
-def relativize = { File root, File absolute ->
-    def relativeURI = root.toURI().relativize(absolute.toURI())
-    return new File(relativeURI.toString())
-}
-
-def scanQmlImports = { File qmlRootPath ->
-    def qmlImportCommandFile = new File(qmlRoot, 'bin/qmlimportscanner' + EXEC_SUFFIX)
-    if (!qmlImportCommandFile.exists()) {
-        throw new GradleException('Unable to find required qmlimportscanner executable at ' + qmlImportCommandFile.parent.toString())
-    }
-
-    def command = qmlImportCommandFile.absolutePath
-    def args = [
-        '-rootPath', qmlRootPath.absolutePath,
-        '-importPath', "${qmlRoot.absolutePath}/qml"
-    ]
-
-    def commandResult = captureOutput(command, args)
-    new JsonSlurper().parseText(commandResult).each {
-        if (!it.containsKey('path')) {
-            println "Warning: QML import could not be resolved in any of the import paths: ${it.name}"
-            return
-        }
-        def file = new File(it.path)
-        // Ignore non-existent files
-        if (!file.exists()) {
-            return
-        }
-        // Ignore files in the import path
-        if (file.canonicalPath.startsWith(qmlRootPath.canonicalPath)) {
-            return
-        }
-        if (file.isFile()) {
-            options.files.add(file)
-        } else {
-            file.eachFileRecurse(FileType.FILES, {
-                options.files.add(it)
-            })
-        }
-    }
-}
-
-def parseQtDependencies = { List qtLibs ->
-    qtLibs.each({
-        def libFile = new File(qmlRoot, "lib/lib${it}.so")
-        options.files.add(libFile)
-
-        def androidDeps = new File(qmlRoot, "lib/${it}-android-dependencies.xml")
-        if (!libFile.exists()) return
-        if (!androidDeps.exists()) return
-
-        new XmlSlurper().parse(androidDeps).dependencies.lib.depends.'*'.each{ node ->
-            switch (node.name()) {
-                case 'lib':
-                case 'bundled':
-                    def relativeFilename = node.@file.toString()
-
-                    // Special case, since this is handled by qmlimportscanner instead
-                    if (relativeFilename.startsWith('qml'))
-                        return
-
-                    def file = new File(qmlRoot, relativeFilename)
-
-                    if (!file.exists())
-                        return
-
-                    if (file.isFile()) {
-                        options.files.add(file)
-                    } else {
-                        file.eachFileRecurse(FileType.FILES, { options.files.add(it) })
-                    }
-                    break
-
-
-                case 'jar':
-                    if (node.@bundling == "1") {
-                        def jar = new File(qmlRoot, node.@file.toString())
-                        if (!jar.exists()) {
-                            throw new GradleException('Unable to find required JAR ' + jar.path)
-                        }
-                        options.files.add(jar)
-                    }
-                    break
-
-                case 'permission':
-                    options.permissions.add(node.@name)
-                    break
-
-                case 'feature':
-                    options.features.add(node.@name)
-                    break
-
-                default:
-                    throw new GradleException('Unhandled Android Dependency node ' + node.name())
-            }
-        }
-    })
-}
-
-def generateLibsXml = {
-    def libDestinationDirectory = jniFolder
-    def jarDestinationDirectory = new File(appDir, 'libs')
-    def assetDestinationDirectory = new File(appDir, 'src/main/assets/--Added-by-androiddeployqt--');
-    def libsXmlFile = new File(appDir, 'src/main/res/values/libs.xml')
-    def libPrefix = 'lib' + File.separator
-    def jarPrefix = 'jar' + File.separator
-
-    def xmlParser = new XmlParser()
-    def libsXmlRoot = xmlParser.parseText('<?xml version="1.0" encoding="UTF-8"?><resources/>')
-    def qtLibsNode = xmlParser.createNode(libsXmlRoot, 'array', [name: 'qt_libs'])
-    def bundledLibsNode = xmlParser.createNode(libsXmlRoot, 'array', [name: 'bundled_in_lib'])
-    def bundledAssetsNode = xmlParser.createNode(libsXmlRoot, 'array', [name: 'bundled_in_assets'])
-
-    options.files.each {
-        def sourceFile = it
-        if (!sourceFile.exists()) {
-            throw new GradleException("Unable to find dependency file " + sourceFile.toString())
-        }
-
-        def relativePath = relativize( qmlRoot, sourceFile ).toString()
-        def destinationFile
-        if (relativePath.endsWith('.so')) {
-            def garbledFileName
-            if (relativePath.startsWith(libPrefix)) {
-                garbledFileName = relativePath.substring(libPrefix.size())
-                Pattern p = ~/lib(Qt5.*).so/
-                Matcher m = p.matcher(garbledFileName)
-                assert m.matches()
-                def libName = m.group(1)
-                xmlParser.createNode(qtLibsNode, 'item', [:]).setValue(libName)
-            } else {
-                garbledFileName = 'lib' + relativePath.replace(File.separator, '_'[0])
-                xmlParser.createNode(bundledLibsNode, 'item', [:]).setValue("${garbledFileName}:${relativePath}".replace(File.separator, '/'))
-            }
-            destinationFile = new File(libDestinationDirectory, garbledFileName)
-        } else if (relativePath.startsWith('jar')) {
-            destinationFile = new File(jarDestinationDirectory, relativePath.substring(jarPrefix.size()))
-        } else {
-            xmlParser.createNode(bundledAssetsNode, 'item', [:]).setValue("--Added-by-androiddeployqt--/${relativePath}:${relativePath}".replace(File.separator, '/'))
-            destinationFile = new File(assetDestinationDirectory, relativePath)
-        }
-
-        copy { from sourceFile;  into destinationFile.parent;  rename(sourceFile.name, destinationFile.name) }
-        assert destinationFile.exists() && destinationFile.isFile()
-    }
-    def xml = XmlUtil.serialize(libsXmlRoot)
-    new FileWriter(libsXmlFile).withPrintWriter { writer ->
-        writer.write(xml)
-    }
-}
-
-task downloadDependencies {
-    doLast {
-        packages.each { entry ->
-            def filename = entry.value['file'];
-            def dependencyBaseUrl = entry.value['baseUrl']
-            def url = (dependencyBaseUrl?.trim() ? dependencyBaseUrl : baseUrl) + filename;
-            if (entry.value.containsKey('versionId')) {
-                url = url + '?versionId=' + entry.value['versionId']
-            }
-            download {
-                src url
-                dest new File(baseFolder, filename)
-                onlyIfNewer true
-            }
-        }
-    }
-}
-
-task verifyQt(type: Verify) { def p = packages['qt'];  src new File(baseFolder, p['file']);  checksum p['checksum']; }
-task verifyBullet(type: Verify) { def p = packages['bullet']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-task verifyDraco(type: Verify) { def p = packages['draco']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-task verifyGvr(type: Verify) { def p = packages['gvr']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-task verifyOpenSSL(type: Verify) { def p = packages['openssl']; src new File(baseFolder, p['file']);  checksum p['checksum'] }
-task verifyPolyvox(type: Verify) { def p = packages['polyvox']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-task verifyTBB(type: Verify) { def p = packages['tbb']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-task verifyHifiAC(type: Verify) { def p = packages['hifiAC']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-task verifyEtc2Comp(type: Verify) { def p = packages['etc2comp']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-task verifyBreakpad(type: Verify) { def p = packages['breakpad']; src new File(baseFolder, p['file']); checksum p['checksum'] }
-
-task verifyDependencyDownloads(dependsOn: downloadDependencies) { }
-verifyDependencyDownloads.dependsOn verifyQt
-verifyDependencyDownloads.dependsOn verifyBullet
-verifyDependencyDownloads.dependsOn verifyDraco
-verifyDependencyDownloads.dependsOn verifyGvr
-verifyDependencyDownloads.dependsOn verifyOpenSSL
-verifyDependencyDownloads.dependsOn verifyPolyvox
-verifyDependencyDownloads.dependsOn verifyTBB
-verifyDependencyDownloads.dependsOn verifyHifiAC
-verifyDependencyDownloads.dependsOn verifyEtc2Comp
-verifyDependencyDownloads.dependsOn verifyBreakpad
-
-task extractDependencies(dependsOn: verifyDependencyDownloads) {
-    doLast {
-        packages.each { entry ->
-            def folder = entry.key
-            def filename = entry.value['file']
-            def localFile = new File(HIFI_ANDROID_PRECOMPILED, filename)
-            def localFolder = new File(HIFI_ANDROID_PRECOMPILED, folder)
-            def fileTree;
-            if (filename.endsWith('zip')) {
-                fileTree = zipTree(localFile)
-            } else {
-                fileTree = tarTree(resources.gzip(localFile))
-            }
-            copy {
-                from fileTree
-                into localFolder
-            }
-        }
-    }
-}
-
-// Copies the non Qt dependencies.  Qt dependencies (primary libraries and plugins) are handled by the qtBundle task
-task copyDependencies() {
-    doLast {
-        packages.each { entry ->
-            def packageName = entry.key
-            def currentPackage = entry.value;
-            if (currentPackage.containsKey('sharedLibFolder')) {
-                def localFolder = new File(baseFolder, packageName + '/' + currentPackage['sharedLibFolder'])
-                def tree = fileTree(localFolder);
-                if (currentPackage.containsKey('includeLibs')) {
-                    currentPackage['includeLibs'].each { includeSpec -> tree.include includeSpec }
-                }
-                tree.visit { element ->
-                    if (!element.file.isDirectory()) {
-                        println "Copying " + element.file + " to " + jniFolder
-                        copy {  from element.file; into jniFolder }
-                    }
-                }
-            }
-        }
-    }
-}
-
 task extractGvrBinaries() {
     doLast {
         def gvrLibFolder = new File(HIFI_ANDROID_PRECOMPILED, 'gvr/gvr-android-sdk-1.101.0/libraries');
@@ -500,13 +135,11 @@ task qtBundle {
    }
 }
 
-task setupDependencies(dependsOn: [copyDependencies, extractGvrBinaries, qtBundle]) { }
+task setupDependencies() {
+    // migrated to python
+}
 
 task cleanDependencies(type: Delete) {
-    delete HIFI_ANDROID_PRECOMPILED
-    delete 'app/src/main/jniLibs/arm64-v8a'
-    delete 'app/src/main/assets/--Added-by-androiddeployqt--'
-    delete 'app/src/main/res/values/libs.xml'
 }
 
 def runBreakpadDumpSyms = { buildType ->
diff --git a/android/build_android.sh b/android/build_android.sh
index 189e6099a8..9c68b8969b 100755
--- a/android/build_android.sh
+++ b/android/build_android.sh
@@ -1,4 +1,11 @@
 #!/usr/bin/env bash
 set -xeuo pipefail
-./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
-./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
\ No newline at end of file
+./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET}
+
+# This is the actual output from gradle, which no longer attempts to muck with the naming of the APK
+OUTPUT_APK=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_DIR}/${ANDROID_APP}-${ANDROID_BUILD_DIR}.apk
+# This is the APK name requested by Jenkins
+TARGET_APK=./${ANDROID_APK_NAME}
+# Make sure this matches up with the new ARTIFACT_EXPRESSION for jenkins builds, which should be "android/*.apk"
+cp ${OUTPUT_APK} ${TARGET_APK}
+
diff --git a/android/containerized_build.sh b/android/containerized_build.sh
index e5ec895146..42118a8e38 100755
--- a/android/containerized_build.sh
+++ b/android/containerized_build.sh
@@ -5,12 +5,21 @@ DOCKER_IMAGE_NAME="hifi_androidbuild"
 
 docker build --build-arg BUILD_UID=`id -u` -t "${DOCKER_IMAGE_NAME}" -f docker/Dockerfile docker
 
+# The Jenkins PR builds use VERSION_CODE, but the release builds use VERSION
+# So make sure we use VERSION_CODE consistently
+if [-z "$VERSION_CODE"]; then
+   export VERSION_CODE=$VERSION
+fi
+
 docker run \
    --rm \
-    --security-opt seccomp:unconfined \
+   --security-opt seccomp:unconfined \
    -v "${WORKSPACE}":/home/jenkins/hifi \
+   -v /home/jenkins/.gradle:/home/jenkins/.gradle \
    -e RELEASE_NUMBER \
    -e RELEASE_TYPE \
+   -e ANDROID_APP \
+   -e ANDROID_APK_NAME \
    -e ANDROID_BUILD_TARGET \
    -e ANDROID_BUILD_DIR \
    -e CMAKE_BACKTRACE_URL \
diff --git a/android/docker/Dockerfile b/android/docker/Dockerfile
index 2a6943cbc2..c37f73cb2a 100644
--- a/android/docker/Dockerfile
+++ b/android/docker/Dockerfile
@@ -26,10 +26,9 @@ RUN mkdir -p "$ANDROID_HOME" "$ANDROID_SDK_HOME" && \
     curl -s -S -o sdk.zip -L "${SDK_URL}" && \
     unzip sdk.zip && \
     rm sdk.zip && \
-    yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses
+    yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses && yes | $ANDROID_HOME/tools/bin/sdkmanager --update
 
 # Install Android Build Tool and Libraries
-RUN $ANDROID_HOME/tools/bin/sdkmanager --update
 RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \
     "platforms;android-${ANDROID_VERSION}" \
     "platform-tools"
@@ -52,11 +51,14 @@ ENV PATH ${PATH}:${ANDROID_NDK_HOME}
 RUN apt-get -y install \
     g++ \
     gcc \
+    sudo \
+    emacs-nox \
     -
 
 # --- Gradle
 ARG BUILD_UID=1001
 RUN useradd -ms /bin/bash -u $BUILD_UID jenkins
+RUN echo "jenkins ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
 USER jenkins
 WORKDIR /home/jenkins
 
@@ -71,22 +73,21 @@ RUN mkdir "$HIFI_BASE" && \
 
 RUN git clone https://github.com/jherico/hifi.git && \
     cd ~/hifi && \
-    git checkout feature/build/gradle-wrapper 
-
+    git checkout feature/quest_move_interface
 
 WORKDIR /home/jenkins/hifi
 
-RUN touch .test4 && \ 
-    git fetch && git reset origin/feature/build/gradle-wrapper --hard
+RUN touch .test6 && \ 
+    git fetch && git reset origin/feature/quest_move_interface --hard
 
 RUN mkdir build
 
 # Pre-cache the vcpkg managed dependencies
 WORKDIR /home/jenkins/hifi/build
-RUN python3 ../prebuild.py --build-root `pwd` --android
+RUN python3 ../prebuild.py --build-root `pwd` --android interface
 
 # Pre-cache the gradle dependencies
 WORKDIR /home/jenkins/hifi/android
 RUN ./gradlew -m tasks -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED
-RUN ./gradlew extractDependencies -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED 
+#RUN ./gradlew extractDependencies -PHIFI_ANDROID_PRECOMPILED=$HIFI_ANDROID_PRECOMPILED 
 
diff --git a/android/docker/update.txt b/android/docker/update.txt
new file mode 100644
index 0000000000..a12c215a06
--- /dev/null
+++ b/android/docker/update.txt
@@ -0,0 +1,13 @@
+git fetch
+git checkout feature/quest_move_interface
+export VERSION_CODE=1
+export RELEASE_NUMBER=1
+export RELEASE_TYPE=DEV
+export ANDROID_APP=interface
+touch ~/.gradle/gradle.properties
+echo HIFI_ANDROID_KEYSTORE=/home/jenkins/keystore.jks > ~/.gradle/gradle.properties
+echo HIFI_ANDROID_KEYSTORE_PASSWORD=password >> ~/.gradle/gradle.properties
+echo HIFI_ANDROID_KEY_ALIAS=key0 >> ~/.gradle/gradle.properties
+echo HIFI_ANDROID_KEY_PASSWORD=password >> ~/.gradle/gradle.properties
+./build_android.sh
+cp ./apps/${ANDROID_APP}/build/outputs/apk/release/${ANDROID_APP}-release.apk ${ANDROID_APP}.apk
\ No newline at end of file
diff --git a/android/libraries/qt/build.gradle b/android/libraries/qt/build.gradle
new file mode 100644
index 0000000000..e6141f4cdf
--- /dev/null
+++ b/android/libraries/qt/build.gradle
@@ -0,0 +1,22 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        minSdkVersion 24
+        targetSdkVersion 28
+        versionCode 1
+        versionName "1.0"
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    api 'com.google.guava:guava:23.0'
+}
diff --git a/android/libraries/qt/src/main/AndroidManifest.xml b/android/libraries/qt/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..c6638c09e8
--- /dev/null
+++ b/android/libraries/qt/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.highfidelity.shared.qt" />
diff --git a/android/libraries/qt/src/main/java/io/highfidelity/utils/HifiUtils.java b/android/libraries/qt/src/main/java/io/highfidelity/utils/HifiUtils.java
new file mode 100644
index 0000000000..e8e9f04d9f
--- /dev/null
+++ b/android/libraries/qt/src/main/java/io/highfidelity/utils/HifiUtils.java
@@ -0,0 +1,69 @@
+
+package io.highfidelity.utils;
+
+import android.content.res.AssetManager;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.util.LinkedList;
+
+public class HifiUtils {
+
+    private static LinkedList<String> readAssetLines(AssetManager assetManager, String asset) throws IOException {
+        LinkedList<String> assets = new LinkedList<>();
+        InputStream is = assetManager.open(asset);
+        BufferedReader in = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+        String line;
+        while ((line=in.readLine()) != null) {
+            assets.add(line);
+        }
+        in.close();
+        return assets;
+    }
+
+    private static void copyAsset(AssetManager assetManager, String asset, String destFileName) throws IOException {
+        try (InputStream is = assetManager.open(asset)) {
+            try (OutputStream os = Files.asByteSink(new File(destFileName)).openStream()) {
+                ByteStreams.copy(is, os);
+            }
+        }
+    }
+
+    public static void upackAssets(AssetManager assetManager, String destDir) {
+        try {
+            if (!destDir.endsWith("/"))
+                destDir = destDir + "/";
+            LinkedList<String> assets = readAssetLines(assetManager, "cache_assets.txt");
+            String dateStamp = assets.poll();
+            String dateStampFilename = destDir + dateStamp;
+            File dateStampFile = new File(dateStampFilename);
+            if (dateStampFile.exists()) {
+                return;
+            }
+            for (String fileToCopy : assets) {
+                String destFileName = destDir  + fileToCopy;
+                {
+                    File destFile = new File(destFileName);
+                    File destFolder = destFile.getParentFile();
+                    if (!destFolder.exists()) {
+                        destFolder.mkdirs();
+                    }
+                    if (destFile.exists()) {
+                        destFile.delete();
+                    }
+                }
+                copyAsset(assetManager, fileToCopy, destFileName);
+            }
+            Files.write("touch".getBytes(), dateStampFile);
+        } catch (IOException e){
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java
similarity index 100%
rename from android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java
rename to android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java
diff --git a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java
similarity index 100%
rename from android/app/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java
rename to android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivityLoader.java
diff --git a/android/app/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java
similarity index 100%
rename from android/app/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java
rename to android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtApplication.java
diff --git a/android/settings.gradle b/android/settings.gradle
index e7b4def49c..40b5eb44bf 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -1 +1,5 @@
-include ':app'
+include ':qt'
+project(':qt').projectDir = new File(settingsDir, 'libraries/qt')
+
+include ':interface'
+project(':interface').projectDir = new File(settingsDir, 'apps/interface')
diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp
index cc2973f61d..5c644cb132 100644
--- a/assignment-client/src/Agent.cpp
+++ b/assignment-client/src/Agent.cpp
@@ -196,7 +196,8 @@ void Agent::run() {
     connect(nodeList.data(), &LimitedNodeList::nodeKilled, this,  &Agent::nodeKilled);
 
     nodeList->addSetOfNodeTypesToNodeInterestSet({
-        NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer, NodeType::AssetServer
+        NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer,
+        NodeType::MessagesMixer, NodeType::AssetServer, NodeType::EntityScriptServer
     });
 }
 
@@ -505,16 +506,6 @@ void Agent::executeScript() {
 
         DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
 
-        // Agents should run at 45hz
-        static const int AVATAR_DATA_HZ = 45;
-        static const int AVATAR_DATA_IN_MSECS = MSECS_PER_SECOND / AVATAR_DATA_HZ;
-        QTimer* avatarDataTimer = new QTimer(this);
-        connect(avatarDataTimer, &QTimer::timeout, this, &Agent::processAgentAvatar);
-        avatarDataTimer->setSingleShot(false);
-        avatarDataTimer->setInterval(AVATAR_DATA_IN_MSECS);
-        avatarDataTimer->setTimerType(Qt::PreciseTimer);
-        avatarDataTimer->start();
-
         _scriptEngine->run();
 
         Frame::clearFrameHandler(AUDIO_FRAME_TYPE);
@@ -528,8 +519,6 @@ void Agent::executeScript() {
             recordingInterface->stopRecording();
         }
 
-        avatarDataTimer->stop();
-
         setIsAvatar(false); // will stop timers for sending identity packets
     }
 
@@ -584,20 +573,16 @@ void Agent::setIsAvatar(bool isAvatar) {
 
     auto scriptableAvatar = DependencyManager::get<ScriptableAvatar>();
     if (_isAvatar) {
-        if (!_avatarIdentityTimer) {
+        if (!_avatarQueryTimer) {
             // set up the avatar timers
-            _avatarIdentityTimer = new QTimer(this);
             _avatarQueryTimer = new QTimer(this);
 
             // connect our slot
-            connect(_avatarIdentityTimer, &QTimer::timeout, this, &Agent::sendAvatarIdentityPacket);
             connect(_avatarQueryTimer, &QTimer::timeout, this, &Agent::queryAvatars);
 
-            static const int AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS = 1000;
             static const int AVATAR_VIEW_PACKET_SEND_INTERVAL_MSECS = 1000;
 
-            // start the timers
-            _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS);  // FIXME - we shouldn't really need to constantly send identity packets
+            // start the timer
             _avatarQueryTimer->start(AVATAR_VIEW_PACKET_SEND_INTERVAL_MSECS);
 
             connect(_scriptEngine.data(), &ScriptEngine::update,
@@ -609,11 +594,7 @@ void Agent::setIsAvatar(bool isAvatar) {
 
         _entityEditSender.setMyAvatar(scriptableAvatar.data());
     } else {
-        if (_avatarIdentityTimer) {
-            _avatarIdentityTimer->stop();
-            delete _avatarIdentityTimer;
-            _avatarIdentityTimer = nullptr;
-
+        if (_avatarQueryTimer) {
             _avatarQueryTimer->stop();
             delete _avatarQueryTimer;
             _avatarQueryTimer = nullptr;
@@ -646,14 +627,6 @@ void Agent::setIsAvatar(bool isAvatar) {
     }
 }
 
-void Agent::sendAvatarIdentityPacket() {
-    if (_isAvatar) {
-        auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
-        scriptedAvatar->markIdentityDataChanged();
-        scriptedAvatar->sendIdentityPacket();
-    }
-}
-
 void Agent::queryAvatars() {
     auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
 
@@ -681,44 +654,6 @@ void Agent::queryAvatars() {
                                                          { NodeType::AvatarMixer });
 }
 
-void Agent::processAgentAvatar() {
-    if (!_scriptEngine->isFinished() && _isAvatar) {
-        auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
-
-        AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData;
-        QByteArray avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail);
-
-        int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber);
-
-        if (avatarByteArray.size() > maximumByteArraySize) {
-            qWarning() << " scriptedAvatar->toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data";
-            avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail, true);
-
-            if (avatarByteArray.size() > maximumByteArraySize) {
-                qWarning() << " scriptedAvatar->toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData";
-                avatarByteArray = scriptedAvatar->toByteArrayStateful(AvatarData::MinimumData, true);
-
-                if (avatarByteArray.size() > maximumByteArraySize) {
-                    qWarning() << " scriptedAvatar->toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!";
-                    return;
-                }
-            }
-        }
-
-        scriptedAvatar->doneEncoding(true);
-
-        static AvatarDataSequenceNumber sequenceNumber = 0;
-        auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size() + sizeof(sequenceNumber));
-        avatarPacket->writePrimitive(sequenceNumber++);
-
-        avatarPacket->write(avatarByteArray);
-
-        auto nodeList = DependencyManager::get<NodeList>();
-
-        nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
-    }
-}
-
 void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) {
     _flushEncoder = false;
     static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0);
diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h
index 244f72e624..b8e7652cca 100644
--- a/assignment-client/src/Agent.h
+++ b/assignment-client/src/Agent.h
@@ -81,7 +81,6 @@ private slots:
     void nodeActivated(SharedNodePointer activatedNode);
     void nodeKilled(SharedNodePointer killedNode);
 
-    void processAgentAvatar();
     void processAgentAvatarAudio();
 
 private:
@@ -99,7 +98,6 @@ private:
 
     void setAvatarSound(SharedSoundPointer avatarSound) { _avatarSound = avatarSound; }
 
-    void sendAvatarIdentityPacket();
     void queryAvatars();
 
     QString _scriptContents;
@@ -110,7 +108,6 @@ private:
     bool _shouldMuteRecordingAudio { false };
     int _numAvatarSoundSentBytes = 0;
     bool _isAvatar = false;
-    QTimer* _avatarIdentityTimer = nullptr;
     QTimer* _avatarQueryTimer = nullptr;
     QHash<QUuid, quint16> _outgoingScriptAudioSequenceNumbers;
 
diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp
index 004e4ad2ea..bdec17bd8d 100644
--- a/assignment-client/src/audio/AudioMixer.cpp
+++ b/assignment-client/src/audio/AudioMixer.cpp
@@ -68,6 +68,13 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
     // hash the available codecs (on the mixer)
     _availableCodecs.clear(); // Make sure struct is clean
     auto pluginManager = DependencyManager::set<PluginManager>();
+    // Only load codec plugins; for now assume codec plugins have 'codec' in their name.
+    auto codecPluginFilter = [](const QJsonObject& metaData) {
+        QJsonValue nameValue = metaData["MetaData"]["name"];
+        return nameValue.toString().contains("codec", Qt::CaseInsensitive);
+    };
+    pluginManager->setPluginFilter(codecPluginFilter);
+
     auto codecPlugins = pluginManager->getCodecPlugins();
     for_each(codecPlugins.cbegin(), codecPlugins.cend(),
         [&](const CodecPluginPointer& codec) {
diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index 6b90a8fbbd..500772c1b5 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -54,7 +54,6 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
     packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
     packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
     packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
-    packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket");
     packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
     packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
 
@@ -582,36 +581,6 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
     _handleAvatarIdentityPacketElapsedTime += (end - start);
 }
 
-void AvatarMixer::handleAvatarIdentityRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
-    if (message->getSize() < NUM_BYTES_RFC4122_UUID) {
-        qCDebug(avatars) << "Malformed AvatarIdentityRequest received from" << message->getSenderSockAddr().toString();
-        return;
-    }
-
-    QUuid avatarID(QUuid::fromRfc4122(message->getMessage()) );
-    if (!avatarID.isNull()) {
-        auto nodeList = DependencyManager::get<NodeList>();
-        auto requestedNode = nodeList->nodeWithUUID(avatarID);
-
-        if (requestedNode) {
-            AvatarMixerClientData* avatarClientData = static_cast<AvatarMixerClientData*>(requestedNode->getLinkedData());
-            if (avatarClientData) {
-                const AvatarData& avatarData = avatarClientData->getAvatar();
-                QByteArray serializedAvatar = avatarData.identityByteArray();
-                auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
-                identityPackets->write(serializedAvatar);
-                nodeList->sendPacketList(std::move(identityPackets), *senderNode);
-                ++_sumIdentityPackets;
-            }
-
-            AvatarMixerClientData* senderData = static_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
-            if (senderData) {
-                senderData->resetSentTraitData(requestedNode->getLocalID());
-            }
-        }
-    }
-}
-
 void AvatarMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
     auto start = usecTimestampNow();
     handleAvatarKilled(node);
diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h
index 8ae7fc9931..764656a2d5 100644
--- a/assignment-client/src/avatars/AvatarMixer.h
+++ b/assignment-client/src/avatars/AvatarMixer.h
@@ -54,7 +54,6 @@ private slots:
     void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
     void handleReplicatedPacket(QSharedPointer<ReceivedMessage> message);
     void handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message);
-    void handleAvatarIdentityRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
     void domainSettingsRequestComplete();
     void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
     void start();
diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp
index c61e41fbbe..044ab86942 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.cpp
+++ b/assignment-client/src/avatars/ScriptableAvatar.cpp
@@ -91,6 +91,39 @@ void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
     updateJointMappings();
 }
 
+int ScriptableAvatar::sendAvatarDataPacket(bool sendAll) {
+    using namespace std::chrono;
+    auto now = Clock::now();
+
+    int MAX_DATA_RATE_MBPS = 3;
+    int maxDataRateBytesPerSeconds = MAX_DATA_RATE_MBPS * BYTES_PER_KILOBYTE * KILO_PER_MEGA / BITS_IN_BYTE;
+    int maxDataRateBytesPerMilliseconds = maxDataRateBytesPerSeconds / MSECS_PER_SECOND;
+
+    auto bytesSent = 0;
+
+    if (now > _nextTraitsSendWindow) {
+        if (getIdentityDataChanged()) {
+            bytesSent += sendIdentityPacket();
+        }
+
+        bytesSent += _clientTraitsHandler->sendChangedTraitsToMixer();
+
+        // Compute the next send window based on how much data we sent and what
+        // data rate we're trying to max at.
+        milliseconds timeUntilNextSend { bytesSent / maxDataRateBytesPerMilliseconds };
+        _nextTraitsSendWindow += timeUntilNextSend;
+
+        // Don't let the next send window lag behind if we're not sending a lot of data.
+        if (_nextTraitsSendWindow < now) {
+            _nextTraitsSendWindow = now;
+        }
+    }
+
+    bytesSent += AvatarData::sendAvatarDataPacket(sendAll);
+
+    return bytesSent;
+}
+
 static AnimPose composeAnimPose(const HFMJoint& joint, const glm::quat rotation, const glm::vec3 translation) {
     glm::mat4 translationMat = glm::translate(translation);
     glm::mat4 rotationMat = glm::mat4_cast(joint.preRotation * rotation * joint.postRotation);
@@ -161,7 +194,13 @@ void ScriptableAvatar::update(float deltatime) {
         }
     }
 
-    _clientTraitsHandler->sendChangedTraitsToMixer();
+    quint64 now = usecTimestampNow();
+    quint64 dt = now - _lastSendAvatarDataTime;
+
+    if (dt > MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS) {
+        sendAvatarDataPacket();
+        _lastSendAvatarDataTime = now;
+    }
 }
 
 void ScriptableAvatar::updateJointMappings() {
diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h
index df949f8bff..e93be897d5 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.h
+++ b/assignment-client/src/avatars/ScriptableAvatar.h
@@ -123,6 +123,10 @@
 
 class ScriptableAvatar : public AvatarData, public Dependency {
     Q_OBJECT
+
+    using Clock = std::chrono::system_clock;
+    using TimePoint = Clock::time_point;
+
 public:
 
     ScriptableAvatar();
@@ -177,6 +181,8 @@ public:
 
     virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
 
+    int sendAvatarDataPacket(bool sendAll = false) override;
+
     virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override;
 
     void setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement);
@@ -228,6 +234,10 @@ private:
 
     /// Loads the joint indices, names from the FST file (if any)
     void updateJointMappings();
+
+    quint64 _lastSendAvatarDataTime { 0 };
+
+    TimePoint _nextTraitsSendWindow;
 };
 
 #endif // hifi_ScriptableAvatar_h
diff --git a/hifi_android.py b/hifi_android.py
index e3944cda9a..13c9cdccf2 100644
--- a/hifi_android.py
+++ b/hifi_android.py
@@ -6,6 +6,7 @@ import re
 import shutil
 import xml.etree.ElementTree as ET
 import functools
+import zipfile
 
 print = functools.partial(print, flush=True)
 
@@ -163,6 +164,31 @@ def copyAndroidLibs(packagePath, appPath):
                         print("Copying {}".format(lib))
                         shutil.copy(sourceFile, destFile)
 
+    gvrLibFolder = os.path.join(packagePath, 'gvr/gvr-android-sdk-1.101.0/libraries')
+    audioSoOut = os.path.join(gvrLibFolder, 'libgvr_audio.so')
+    if not os.path.isfile(audioSoOut):
+        audioAar = os.path.join(gvrLibFolder, 'sdk-audio-1.101.0.aar')
+        with zipfile.ZipFile(audioAar) as z:
+            with z.open('jni/arm64-v8a/libgvr_audio.so') as f:
+                with open(audioSoOut, 'wb') as of:
+                    shutil.copyfileobj(f, of)
+
+    audioSoOut2 = os.path.join(jniPath, 'libgvr_audio.so')
+    if not os.path.isfile(audioSoOut2):
+        shutil.copy(audioSoOut, audioSoOut2)
+
+    baseSoOut = os.path.join(gvrLibFolder, 'libgvr.so')
+    if not os.path.isfile(baseSoOut):
+        baseAar = os.path.join(gvrLibFolder, 'sdk-base-1.101.0.aar')
+        with zipfile.ZipFile(baseAar) as z:
+            with z.open('jni/arm64-v8a/libgvr.so') as f:
+                with open(baseSoOut, 'wb') as of:
+                    shutil.copyfileobj(f, of)
+
+    baseSoOut2 = os.path.join(jniPath, 'libgvr.so')
+    if not os.path.isfile(baseSoOut2):
+        shutil.copy(baseSoOut, baseSoOut2)
+
 class QtPackager:
     def __init__(self, appPath, qtRootPath):
         self.appPath = appPath
@@ -170,6 +196,7 @@ class QtPackager:
         self.jniPath = os.path.join(self.appPath, 'src/main/jniLibs/arm64-v8a')
         self.assetPath = os.path.join(self.appPath, 'src/main/assets')
         self.qtAssetPath = os.path.join(self.assetPath, '--Added-by-androiddeployqt--')
+        self.qtAssetCacheList = os.path.join(self.qtAssetPath, 'qt_cache_pregenerated_file_list')
         # Jars go into the qt library
         self.jarPath = os.path.realpath(os.path.join(self.appPath, '../../libraries/qt/libs'))
         self.xmlFile = os.path.join(self.appPath, 'src/main/res/values/libs.xml')
@@ -195,7 +222,7 @@ class QtPackager:
                     if (relativeFilename.startswith('qml')):
                         continue
                     filename = os.path.join(self.qtRootPath, relativeFilename)
-                    self.files.extend(hifi_utils.recursiveFileList(filename))
+                    self.files.extend(hifi_utils.recursiveFileList(filename, excludeNamePattern=r"^\."))
                 elif item.tag == 'jar' and 'bundling' in item.attrib and item.attrib['bundling'] == "1":
                     self.files.append(os.path.join(self.qtRootPath, item.attrib['file']))
                 elif item.tag == 'permission':
@@ -220,7 +247,6 @@ class QtPackager:
         qmlImportResults = json.loads(commandResult)
         for item in qmlImportResults:
             if 'path' not in item:
-                print("Warning: QML import could not be resolved in any of the import paths: {}".format(item['name']))
                 continue
             path = os.path.realpath(item['path'])
             if not os.path.exists(path):
@@ -231,7 +257,7 @@ class QtPackager:
             basePath = os.path.normcase(basePath)
             if basePath.startswith(qmlRootPath):
                 continue
-            self.files.extend(hifi_utils.recursiveFileList(path))
+            self.files.extend(hifi_utils.recursiveFileList(path, excludeNamePattern=r"^\."))
 
     def processFiles(self):
         self.files = list(set(self.files))
@@ -244,7 +270,7 @@ class QtPackager:
         for sourceFile in self.files:
             if not os.path.isfile(sourceFile):
                 raise RuntimeError("Unable to find dependency file " + sourceFile)
-            relativePath = os.path.relpath(sourceFile, self.qtRootPath)
+            relativePath = os.path.relpath(sourceFile, self.qtRootPath).replace('\\', '/')
             destinationFile = None
             if relativePath.endswith('.so'):
                 garbledFileName = None
@@ -257,7 +283,7 @@ class QtPackager:
                     libName = m.group(1)
                     ET.SubElement(qtLibsNode, 'item').text = libName
                 else:
-                    garbledFileName = 'lib' + relativePath.replace('\\', '_'[0])
+                    garbledFileName = 'lib' + relativePath.replace('/', '_'[0])
                     value = "{}:{}".format(garbledFileName, relativePath).replace('\\', '/')
                     ET.SubElement(bundledLibsNode, 'item').text = value
                 destinationFile = os.path.join(self.jniPath, garbledFileName)
@@ -277,10 +303,44 @@ class QtPackager:
         tree = ET.ElementTree(libsXmlRoot)
         tree.write(self.xmlFile, 'UTF-8', True)
 
+    def generateAssetsFileList(self):
+        print("Implement asset file list")
+        # outputFilename = os.path.join(self.qtAssetPath, "qt_cache_pregenerated_file_list")
+        # fileList = hifi_utils.recursiveFileList(self.qtAssetPath)
+        # fileMap = {}
+        # for fileName in fileList:
+        #     relativeFileName = os.path.relpath(fileName, self.assetPath)
+        #     dirName, localFileName = os.path.split(relativeFileName)
+        #     if not dirName in fileMap:
+        #         fileMap[dirName] = []
+        #     fileMap[dirName].append(localFileName)
+
+        # for dirName in fileMap:
+        #     for localFileName in fileMap[dirName]:
+        #         ????
+
+        #
+        # Gradle version
+        #
+        # DataOutputStream fos = new DataOutputStream(new FileOutputStream(outputFile));
+        # for (Map.Entry<String, List<String>> e: directoryContents.entrySet()) {
+        #     def entryList = e.getValue()
+        #     fos.writeInt(e.key.length()*2); // 2 bytes per char
+        #     fos.writeChars(e.key);
+        #     fos.writeInt(entryList.size());
+        #     for (String entry: entryList) {
+        #         fos.writeInt(entry.length()*2);
+        #         fos.writeChars(entry);
+        #     }
+        # }
+
     def bundle(self):
-        if not os.path.isfile(self.xmlFile) or True:
+        if not os.path.isfile(self.xmlFile):
+            print("Bundling Qt info into {}".format(self.xmlFile))
             self.copyQtDeps()
             self.scanQmlImports()
             self.processFiles()
+        # if not os.path.isfile(self.qtAssetCacheList):
+        #     self.generateAssetsFileList()
 
 
diff --git a/hifi_utils.py b/hifi_utils.py
index f53258d4f6..24e43dc83c 100644
--- a/hifi_utils.py
+++ b/hifi_utils.py
@@ -6,6 +6,7 @@ import ssl
 import subprocess
 import sys
 import tarfile
+import re
 import urllib
 import urllib.request
 import zipfile
@@ -23,13 +24,15 @@ def scriptRelative(*paths):
     return result
 
 
-def recursiveFileList(startPath):
+def recursiveFileList(startPath, excludeNamePattern=None ):
     result = []
     if os.path.isfile(startPath):
         result.append(startPath)
     elif os.path.isdir(startPath):
         for dirName, subdirList, fileList in os.walk(startPath):
             for fname in fileList:
+                if excludeNamePattern and re.match(excludeNamePattern, fname):
+                    continue
                 result.append(os.path.realpath(os.path.join(startPath, dirName, fname)))
     result.sort()
     return result
@@ -97,16 +100,12 @@ def downloadFile(url, hash=None, hasher=hashlib.sha512(), retries=3):
         else:
             tempFileName, headers = urllib.request.urlretrieve(url)
 
-        # for some reason the hash we get back from the downloaded file is sometimes wrong if we check it right away
-        # but if we examine the file later, it is correct.  
-        time.sleep(3)
         downloadHash = hashFile(tempFileName, hasher)
         # Verify the hash
         if hash is not None and hash != downloadHash:
             print("Try {}: Downloaded file {} hash {} does not match expected hash {} for url {}".format(i + 1, tempFileName, downloadHash, hash, url))
             os.remove(tempFileName)
             continue
-
         return tempFileName
 
     raise RuntimeError("Downloaded file hash {} does not match expected hash {} for\n{}".format(downloadHash, hash, url))
diff --git a/hifi_vcpkg.py b/hifi_vcpkg.py
index 5492109864..e062b40d86 100644
--- a/hifi_vcpkg.py
+++ b/hifi_vcpkg.py
@@ -85,7 +85,7 @@ endif()
 
         if self.args.android:
             self.triplet = 'arm64-android'
-            self.androidPackagePath = os.path.join(self.path, 'android')
+            self.androidPackagePath = os.getenv('HIFI_ANDROID_PRECOMPILED', os.path.join(self.path, 'android'))
         else:
             self.triplet = self.hostTriplet
 
@@ -189,6 +189,18 @@ endif()
             #hifi_utils.downloadAndExtract(url, dest, hash)
             hifi_utils.downloadAndExtract(url, dest)
 
+        print("Installing additional android archives")
+        androidPackages = hifi_android.getPlatformPackages()
+        for packageName in androidPackages:
+            package = androidPackages[packageName]
+            dest = os.path.join(self.androidPackagePath, packageName)
+            if os.path.isdir(dest):
+                continue
+            url = hifi_android.getPackageUrl(package)
+            zipFile = package['file'].endswith('.zip')
+            print("Android archive {}".format(package['file']))
+            hifi_utils.downloadAndExtract(url, dest, isZip=zipFile, hash=package['checksum'], hasher=hashlib.md5())
+
     def writeTag(self):
         print("Writing tag {} to {}".format(self.tagContents, self.tagFile))
         with open(self.tagFile, 'w') as f:
@@ -203,6 +215,12 @@ endif()
         cmakeTemplate = VcpkgRepo.CMAKE_TEMPLATE
         if not self.args.android:
             cmakeTemplate += VcpkgRepo.CMAKE_TEMPLATE_NON_ANDROID
+        else:
+            precompiled = os.path.realpath(self.androidPackagePath)
+            qtCmakePrefix = os.path.realpath(os.path.join(precompiled, 'qt/lib/cmake'))
+            cmakeTemplate += 'set(HIFI_ANDROID_PRECOMPILED "{}")\n'.format(precompiled)
+            cmakeTemplate += 'set(QT_CMAKE_PREFIX_PATH "{}")\n'.format(qtCmakePrefix)
+
         cmakeConfig = cmakeTemplate.format(cmakeScript, cmakeScript, installPath, toolsPath).replace('\\', '/')
         with open(self.configFilePath, 'w') as f:
             f.write(cmakeConfig)
diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml
index 341f7ba1c9..2d5c68c0e8 100644
--- a/interface/resources/qml/LoginDialog.qml
+++ b/interface/resources/qml/LoginDialog.qml
@@ -54,6 +54,7 @@ FocusScope {
     Image {
         z: -10
         id: loginDialogBackground
+        fillMode: Image.PreserveAspectCrop
         source: "LoginDialog/images/background.png"
         anchors.fill: parent
     }
diff --git a/interface/resources/qml/OverlayLoginDialog.qml b/interface/resources/qml/OverlayLoginDialog.qml
index 8a08fea3de..0ad2c57e5f 100644
--- a/interface/resources/qml/OverlayLoginDialog.qml
+++ b/interface/resources/qml/OverlayLoginDialog.qml
@@ -55,6 +55,7 @@ FocusScope {
     Image {
         z: -10
         id: loginDialogBackground
+        fillMode: Image.PreserveAspectCrop
         source: "LoginDialog/images/background.png"
         anchors.fill: parent
     }
diff --git a/interface/resources/qml/dialogs/TabletLoginDialog.qml b/interface/resources/qml/dialogs/TabletLoginDialog.qml
index 01a597fb9e..8d6444bc0e 100644
--- a/interface/resources/qml/dialogs/TabletLoginDialog.qml
+++ b/interface/resources/qml/dialogs/TabletLoginDialog.qml
@@ -97,6 +97,7 @@ FocusScope {
     Image {
         z: -10
         id: loginDialogBackground
+        fillMode: Image.PreserveAspectCrop
         source: "../LoginDialog/images/background_tablet.png"
         anchors.fill: parent
     }
diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml
index 247a42428a..1abd4f45ff 100644
--- a/interface/resources/qml/hifi/AssetServer.qml
+++ b/interface/resources/qml/hifi/AssetServer.qml
@@ -148,7 +148,7 @@ Windows.ScrollingWindow {
     }
 
     function canAddToWorld(path) {
-        var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i];
+        var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i, /\.gltf\b/i];
 
         if (selectedItemCount > 1) {
             return false;
diff --git a/interface/resources/qml/hifi/avatarapp/Settings.qml b/interface/resources/qml/hifi/avatarapp/Settings.qml
index 668a950d0d..b6d0167ba5 100644
--- a/interface/resources/qml/hifi/avatarapp/Settings.qml
+++ b/interface/resources/qml/hifi/avatarapp/Settings.qml
@@ -35,8 +35,8 @@ Rectangle {
 
     property real scaleValue: scaleSlider.value / 10
     property alias dominantHandIsLeft: leftHandRadioButton.checked
-    property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledCheckBox.checked
-    property alias environmentCollisionsOn: environmentCollisionsEnabledCheckBox.checked
+    property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledRadiobutton.checked
+    property alias environmentCollisionsOn: environmentCollisionsEnabledRadiobutton.checked
     property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text
     property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText
     property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text
@@ -56,10 +56,14 @@ Rectangle {
             rightHandRadioButton.checked = true;
         }
         if (settings.otherAvatarsCollisionsEnabled) {
-            otherAvatarsCollisionsEnabledCheckBox.checked = true;
+            otherAvatarsCollisionsEnabledRadiobutton.checked = true;
+        } else {
+            otherAvatarsCollisionsDisabledRadiobutton.checked = true;
         }
         if (settings.collisionsEnabled) {
-            environmentCollisionsEnabledCheckBox.checked = true;
+            environmentCollisionsEnabledRadiobutton.checked = true;
+        } else {
+            environmentCollisionsDisabledRadiobutton.checked = true;
         }
 
         avatarAnimationJSON = settings.animGraphUrl;
@@ -229,7 +233,7 @@ Rectangle {
 
                 Layout.row: 0
                 Layout.column: 1
-                Layout.leftMargin: -40
+                Layout.leftMargin: -20
 
                 ButtonGroup.group: leftRight
                 checked: true
@@ -245,8 +249,8 @@ Rectangle {
                 id: rightHandRadioButton
 
                 Layout.row: 0
-                Layout.column: 2
-                Layout.rightMargin: 20
+                Layout.column: 3
+                Layout.rightMargin: -15
 
                 ButtonGroup.group: leftRight
 
@@ -266,16 +270,43 @@ Rectangle {
                 size: 17;
                 Layout.row: 1
                 Layout.column: 0
-                text: "Avatar collides with other avatars"
+                text: "Avatar to avatar collision"
+            }
+            
+            ButtonGroup {
+                id: otherAvatarsOnOff
+            }
+            
+            HifiControlsUit.RadioButton {
+                id: otherAvatarsCollisionsEnabledRadiobutton
+
+                Layout.row: 1
+                Layout.column: 1
+                Layout.leftMargin: -20
+
+                ButtonGroup.group: otherAvatarsOnOff
+
+                colorScheme: hifi.colorSchemes.light
+                fontSize: 17
+                letterSpacing: 1.4
+                text: "On"
+                boxSize: 20
             }
 
-            HifiControlsUit.CheckBox {
-                id: otherAvatarsCollisionsEnabledCheckBox;
-                boxSize: 20;
+            HifiControlsUit.RadioButton {
+                id: otherAvatarsCollisionsDisabledRadiobutton
+
                 Layout.row: 1
-                Layout.column: 2
-                Layout.leftMargin: 60
+                Layout.column: 3
+                Layout.rightMargin: -15
+
+                ButtonGroup.group: otherAvatarsOnOff
+
                 colorScheme: hifi.colorSchemes.light
+                fontSize: 17
+                letterSpacing: 1.4
+                text: "Off"
+                boxSize: 20
             }
 
             // TextStyle9
@@ -283,16 +314,43 @@ Rectangle {
                 size: 17;
                 Layout.row: 2
                 Layout.column: 0
-                text: "Avatar collides with environment"
+                text: "Avatar to environment collision"
             }
 
-            HifiControlsUit.CheckBox {
-                id: environmentCollisionsEnabledCheckBox;
-                boxSize: 20;
+            ButtonGroup {
+                id: worldOnOff
+            }
+            
+            HifiControlsUit.RadioButton {
+                id: environmentCollisionsEnabledRadiobutton
+
                 Layout.row: 2
-                Layout.column: 2
-                Layout.leftMargin: 60
+                Layout.column: 1
+                Layout.leftMargin: -20
+
+                ButtonGroup.group: worldOnOff
+
                 colorScheme: hifi.colorSchemes.light
+                fontSize: 17
+                letterSpacing: 1.4
+                text: "On"
+                boxSize: 20
+            }
+
+            HifiControlsUit.RadioButton {
+                id: environmentCollisionsDisabledRadiobutton
+
+                Layout.row: 2
+                Layout.column: 3
+                Layout.rightMargin: -15
+
+                ButtonGroup.group: worldOnOff
+
+                colorScheme: hifi.colorSchemes.light
+                fontSize: 17
+                letterSpacing: 1.4
+                text: "Off"
+                boxSize: 20
             }
         }
 
diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml
index b5374b2fe0..62ec264fc9 100644
--- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml
+++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml
@@ -148,7 +148,7 @@ Rectangle {
     }
 
     function canAddToWorld(path) {
-        var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i];
+        var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i, /\.jpg\b/i, /\.png\b/i, /\.gltf\b/i];
 
         if (selectedItemCount > 1) {
             return false;
diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml
index a01d978b2f..b19dcbb919 100644
--- a/interface/resources/qml/hifi/tablet/TabletRoot.qml
+++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml
@@ -134,8 +134,7 @@ Item {
         if (isWebPage) {
             var webUrl = tabletApps.get(currentApp).appWebUrl;
             var scriptUrl = tabletApps.get(currentApp).scriptUrl;
-            loadSource("hifi/tablet/TabletWebView.qml");
-            loadWebUrl(webUrl, scriptUrl);
+            loadWebBase(webUrl, scriptUrl);
         } else {
         	loader.load(tabletApps.get(currentApp).appUrl);
         }
@@ -150,16 +149,6 @@ Item {
         tabletRoot.openBrowser = newWindow;
     }
 
-    function loadWebUrl(url, injectedJavaScriptUrl) {
-        tabletApps.clear();
-        loader.item.url = url;
-        loader.item.scriptURL = injectedJavaScriptUrl;
-        tabletApps.append({"appUrl": "TabletWebView.qml", "isWebUrl": true, "scriptUrl": injectedJavaScriptUrl, "appWebUrl": url});
-        if (loader.item.hasOwnProperty("closeButtonVisible")) {
-            loader.item.closeButtonVisible = false;
-        }
-    }
-
     // used to send a message from qml to interface script.
     signal sendToScript(var message);
 
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 964ac60a90..2db440291d 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -919,6 +919,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
     DependencyManager::set<Wallet>();
     DependencyManager::set<WalletScriptingInterface>();
     DependencyManager::set<TTSScriptingInterface>();
+    DependencyManager::set<QmlCommerce>();
 
     DependencyManager::set<FadeEffect>();
     DependencyManager::set<ResourceRequestObserver>();
@@ -1200,6 +1201,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
     connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle()));
     connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
     connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() {
+        auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
+        if (tabletScriptingInterface) {
+            tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr);
+        }
         getOverlays().deleteOverlay(getTabletScreenID());
         getOverlays().deleteOverlay(getTabletHomeButtonID());
         getOverlays().deleteOverlay(getTabletFrameID());
@@ -2201,7 +2206,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
             || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose));
         lastLeftHandPose = leftHandPose;
         lastRightHandPose = rightHandPose;
-        properties["avatar_identity_requests_sent"] = DependencyManager::get<AvatarManager>()->getIdentityRequestsSent();
 
         UserActivityLogger::getInstance().logAction("stats", properties);
     });
@@ -2599,6 +2603,7 @@ void Application::cleanupBeforeQuit() {
     DependencyManager::destroy<ContextOverlayInterface>(); // Must be destroyed before TabletScriptingInterface
 
     // stop QML
+    DependencyManager::destroy<QmlCommerce>();
     DependencyManager::destroy<TabletScriptingInterface>();
     DependencyManager::destroy<ToolbarScriptingInterface>();
     DependencyManager::destroy<OffscreenUi>();
@@ -2888,7 +2893,7 @@ void Application::initializeUi() {
     Tooltip::registerType();
     UpdateDialog::registerType();
     QmlContextCallback commerceCallback = [](QQmlContext* context) {
-        context->setContextProperty("Commerce", new QmlCommerce());
+        context->setContextProperty("Commerce", DependencyManager::get<QmlCommerce>().data());
     };
     OffscreenQmlSurface::addWhitelistContextHandler({
         QUrl{ "hifi/commerce/checkout/Checkout.qml" },
@@ -2913,6 +2918,7 @@ void Application::initializeUi() {
         QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" },
         QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" },
         QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" },
+        QUrl{ "hifi/tablet/TabletMenu.qml" },
     }, commerceCallback);
     QmlContextCallback ttsCallback = [](QQmlContext* context) {
         context->setContextProperty("TextToSpeech", DependencyManager::get<TTSScriptingInterface>().data());
@@ -6279,7 +6285,7 @@ void Application::update(float deltaTime) {
         // TODO: Fix this by modeling the way the secondary camera works on how the main camera works
         // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary
         // camera should be.
-    //    updateSecondaryCameraViewFrustum();
+        updateSecondaryCameraViewFrustum();
     }
 
     quint64 now = usecTimestampNow();
@@ -8020,8 +8026,7 @@ void Application::openUrl(const QUrl& url) const {
         if (url.scheme() == URL_SCHEME_HIFI) {
             DependencyManager::get<AddressManager>()->handleLookupString(url.toString());
         } else if (url.scheme() == URL_SCHEME_HIFIAPP) {
-            QmlCommerce commerce;
-            commerce.openSystemApp(url.path());
+            DependencyManager::get<QmlCommerce>()->openSystemApp(url.path());
         } else {
             // address manager did not handle - ask QDesktopServices to handle
             QDesktopServices::openUrl(url);
diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp
old mode 100644
new mode 100755
index 53c16c8a61..5e4f02742e
--- a/interface/src/avatar/AvatarManager.cpp
+++ b/interface/src/avatar/AvatarManager.cpp
@@ -48,8 +48,6 @@
 // 50 times per second - target is 45hz, but this helps account for any small deviations
 // in the update loop - this also results in ~30hz when in desktop mode which is essentially
 // what we want
-const int CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 50;
-static const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
 
 // We add _myAvatar into the hash with all the other AvatarData, and we use the default NULL QUid as the key.
 const QUuid MY_AVATAR_KEY;  // NULL key
@@ -67,6 +65,11 @@ AvatarManager::AvatarManager(QObject* parent) :
     connect(nodeList.data(), &NodeList::ignoredNode, this, [this](const QUuid& nodeID, bool enabled) {
         if (enabled) {
             removeAvatar(nodeID, KillAvatarReason::AvatarIgnored);
+        } else {
+            auto avatar = std::static_pointer_cast<Avatar>(getAvatarBySessionID(nodeID));
+            if (avatar) {
+                avatar->createOrb();
+            }
         }
     });
 
@@ -267,7 +270,6 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
         if (avatar->getSkeletonModel()->isLoaded()) {
             // remove the orb if it is there
             avatar->removeOrb();
-            avatar->updateCollisionGroup(_myAvatar->getOtherAvatarsCollisionsEnabled());
             if (avatar->needsPhysicsUpdate()) {
                 _avatarsToChangeInPhysics.insert(avatar);
             }
@@ -347,25 +349,6 @@ void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scen
     }
 }
 
-void AvatarManager::sendIdentityRequest(const QUuid& avatarID) const {
-    auto nodeList = DependencyManager::get<NodeList>();
-    QWeakPointer<NodeList> nodeListWeak = nodeList;
-    nodeList->eachMatchingNode(
-        [](const SharedNodePointer& node)->bool {
-            return node->getType() == NodeType::AvatarMixer && node->getActiveSocket();
-        },
-        [this, avatarID, nodeListWeak](const SharedNodePointer& node) {
-            auto nodeList = nodeListWeak.lock();
-            if (nodeList) {
-                auto packet = NLPacket::create(PacketType::AvatarIdentityRequest, NUM_BYTES_RFC4122_UUID, true);
-                packet->write(avatarID.toRfc4122());
-                nodeList->sendPacket(std::move(packet), *node);
-                ++_identityRequestsSent;
-            }
-        }
-    );
-}
-
 void AvatarManager::simulateAvatarFades(float deltaTime) {
     if (_avatarsToFadeOut.empty()) {
         return;
@@ -391,8 +374,14 @@ void AvatarManager::simulateAvatarFades(float deltaTime) {
     scene->enqueueTransaction(transaction);
 }
 
-AvatarSharedPointer AvatarManager::newSharedAvatar() {
-    return AvatarSharedPointer(new OtherAvatar(qApp->thread()), [](OtherAvatar* ptr) { ptr->deleteLater(); });
+AvatarSharedPointer AvatarManager::newSharedAvatar(const QUuid& sessionUUID) {
+    auto otherAvatar = new OtherAvatar(qApp->thread());
+    otherAvatar->setSessionUUID(sessionUUID);
+    auto nodeList = DependencyManager::get<NodeList>();
+    if (!nodeList || !nodeList->isIgnoringNode(sessionUUID)) {
+        otherAvatar->createOrb();
+    }
+    return AvatarSharedPointer(otherAvatar, [](OtherAvatar* ptr) { ptr->deleteLater(); });
 }
 
 void AvatarManager::queuePhysicsChange(const OtherAvatarPointer& avatar) {
diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h
index 359af8e361..7bd4e8236a 100644
--- a/interface/src/avatar/AvatarManager.h
+++ b/interface/src/avatar/AvatarManager.h
@@ -92,7 +92,6 @@ public:
 
     void updateMyAvatar(float deltaTime);
     void updateOtherAvatars(float deltaTime);
-    void sendIdentityRequest(const QUuid& avatarID) const;
 
     void setMyAvatarDataPacketsPaused(bool puase);
 
@@ -191,7 +190,6 @@ public:
     Q_INVOKABLE QVariantMap getPalData(const QStringList& specificAvatarIdentifiers = QStringList());
 
     float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); }
-    int getIdentityRequestsSent() const { return _identityRequestsSent; }
 
     void queuePhysicsChange(const OtherAvatarPointer& avatar);
     void buildPhysicsTransaction(PhysicsEngine::Transaction& transaction);
@@ -216,7 +214,7 @@ private:
 
     void simulateAvatarFades(float deltaTime);
 
-    AvatarSharedPointer newSharedAvatar() override;
+    AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID) override;
 
     // called only from the AvatarHashMap thread - cannot be called while this thread holds the
     // hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree
@@ -241,7 +239,6 @@ private:
     float _avatarSimulationTime { 0.0f };
     bool _shouldRender { true };
     bool _myAvatarDataPacketsPaused { false };
-    mutable int _identityRequestsSent { 0 };
 
     mutable std::mutex _spaceLock;
     workload::SpacePointer _space;
diff --git a/interface/src/avatar/AvatarMotionState.cpp b/interface/src/avatar/AvatarMotionState.cpp
old mode 100644
new mode 100755
index 3fa59ea967..77fc81fa04
--- a/interface/src/avatar/AvatarMotionState.cpp
+++ b/interface/src/avatar/AvatarMotionState.cpp
@@ -15,7 +15,6 @@
 #include <PhysicsEngine.h>
 #include <PhysicsHelpers.h>
 
-
 AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
     assert(_avatar);
     _type = MOTIONSTATE_TYPE_AVATAR;
@@ -172,7 +171,10 @@ QUuid AvatarMotionState::getSimulatorID() const {
 // virtual
 void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const {
     group = _collisionGroup;
-    mask = _collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ? 0 : Physics::getDefaultCollisionMask(group);
+    mask = Physics::getDefaultCollisionMask(group);
+    if (!_avatar->getCollideWithOtherAvatars()) {
+        mask &= ~(BULLET_COLLISION_GROUP_MY_AVATAR | BULLET_COLLISION_GROUP_OTHER_AVATAR);
+    }
 }
 
 // virtual
diff --git a/interface/src/avatar/GrabManager.cpp b/interface/src/avatar/GrabManager.cpp
index c41435d67e..db1337b64d 100644
--- a/interface/src/avatar/GrabManager.cpp
+++ b/interface/src/avatar/GrabManager.cpp
@@ -18,6 +18,8 @@ void GrabManager::simulateGrabs() {
     // Update grabbed objects
     auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
     auto entityTree = entityTreeRenderer->getTree();
+    auto sessionID = DependencyManager::get<NodeList>()->getSessionUUID();
+    EntityEditPacketSender* packetSender = entityTreeRenderer ? entityTreeRenderer->getPacketSender() : nullptr;
     entityTree->withReadLock([&] {
         PROFILE_RANGE(simulation, "Grabs");
 
@@ -33,6 +35,8 @@ void GrabManager::simulateGrabs() {
                 glm::vec3 finalPosition = acc.finalizePosition();
                 glm::quat finalOrientation = acc.finalizeOrientation();
                 grabbedThing->setTransform(createMatFromQuatAndPos(finalOrientation, finalPosition));
+                bool iShouldTellServer = grabbedThing->getEditSenderID() == sessionID;
+                entityTree->updateEntityQueryAACube(grabbedThing, packetSender, false, iShouldTellServer);
             }
         }
     });
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index cc7742c517..fec988b29f 100755
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -205,12 +205,12 @@ MyAvatar::MyAvatar(QThread* thread) :
             if (recordingInterface->getPlayFromCurrentLocation()) {
                 setRecordingBasis();
             }
-            _previousCollisionGroup = _characterController.computeCollisionGroup();
+            _previousCollisionMask = _characterController.computeCollisionMask();
             _characterController.setCollisionless(true);
         } else {
             clearRecordingBasis();
             useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
-            if (_previousCollisionGroup != BULLET_COLLISION_GROUP_COLLISIONLESS) {
+            if (_previousCollisionMask != BULLET_COLLISION_MASK_COLLISIONLESS) {
                 _characterController.setCollisionless(false);
             }
         }
@@ -668,12 +668,6 @@ void MyAvatar::update(float deltaTime) {
         Q_ARG(glm::vec3, (getWorldPosition() - halfBoundingBoxDimensions)),
         Q_ARG(glm::vec3, (halfBoundingBoxDimensions*2.0f)));
 
-    if (getIdentityDataChanged()) {
-        sendIdentityPacket();
-    }
-
-    _clientTraitsHandler->sendChangedTraitsToMixer();
-
     simulate(deltaTime, true);
 
     currentEnergy += energyChargeRate;
@@ -773,7 +767,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
         auto headBoneSet = _skeletonModel->getCauterizeBoneSet();
         forEachChild([&](SpatiallyNestablePointer object) {
             bool isChildOfHead = headBoneSet.find(object->getParentJointIndex()) != headBoneSet.end();
-            if (isChildOfHead) {
+            if (isChildOfHead && !object->hasGrabs()) {
                 // Cauterize or display children of head per head drawing state.
                 updateChildCauterization(object, !_prevShouldDrawHead);
                 object->forEachDescendant([&](SpatiallyNestablePointer descendant) {
@@ -823,7 +817,9 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
     // and all of its joints, now update our attachements.
     Avatar::simulateAttachments(deltaTime);
     relayJointDataToChildren();
-    updateGrabs();
+    if (updateGrabs()) {
+        _cauterizationNeedsUpdate = true;
+    }
 
     if (!_skeletonModel->hasSkeleton()) {
         // All the simulation that can be done has been done
@@ -879,9 +875,13 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
                 collisionlessAllowed = zone->getGhostingAllowed();
             }
             EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
-            bool force = false;
-            bool iShouldTellServer = true;
             forEachDescendant([&](SpatiallyNestablePointer object) {
+                // we need to update attached queryAACubes in our own local tree so point-select always works
+                // however we don't want to flood the update pipeline with AvatarEntity updates, so we assume
+                // others have all info required to properly update queryAACube of AvatarEntities on their end
+                EntityItemPointer entity = std::dynamic_pointer_cast<EntityItem>(object);
+                bool iShouldTellServer = !(entity && entity->isAvatarEntity());
+                const bool force = false;
                 entityTree->updateEntityQueryAACube(object, packetSender, force, iShouldTellServer);
             });
         });
@@ -2534,7 +2534,7 @@ void MyAvatar::updateMotors() {
     float verticalMotorTimescale;
 
     if (_characterController.getState() == CharacterController::State::Hover ||
-            _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
+            _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) {
         horizontalMotorTimescale = FLYING_MOTOR_TIMESCALE;
         verticalMotorTimescale = FLYING_MOTOR_TIMESCALE;
     } else {
@@ -2544,7 +2544,7 @@ void MyAvatar::updateMotors() {
 
     if (_motionBehaviors & AVATAR_MOTION_ACTION_MOTOR_ENABLED) {
         if (_characterController.getState() == CharacterController::State::Hover ||
-                _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
+                _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) {
             motorRotation = getMyHead()->getHeadOrientation();
         } else {
             // non-hovering = walking: follow camera twist about vertical but not lift
@@ -2599,7 +2599,7 @@ void MyAvatar::prepareForPhysicsSimulation() {
         qDebug() << "Warning: getParentVelocity failed" << getID();
         parentVelocity = glm::vec3();
     }
-    _characterController.handleChangedCollisionGroup();
+    _characterController.handleChangedCollisionMask();
     _characterController.setParentVelocity(parentVelocity);
     _characterController.setScaleFactor(getSensorToWorldScale());
 
@@ -3103,6 +3103,39 @@ void MyAvatar::preDisplaySide(const RenderArgs* renderArgs) {
     _prevShouldDrawHead = shouldDrawHead;
 }
 
+int MyAvatar::sendAvatarDataPacket(bool sendAll) {
+    using namespace std::chrono;
+    auto now = Clock::now();
+
+    int MAX_DATA_RATE_MBPS = 3;
+    int maxDataRateBytesPerSeconds = MAX_DATA_RATE_MBPS * BYTES_PER_KILOBYTE * KILO_PER_MEGA / BITS_IN_BYTE;
+    int maxDataRateBytesPerMilliseconds = maxDataRateBytesPerSeconds / MSECS_PER_SECOND;
+
+    auto bytesSent = 0;
+
+    if (now > _nextTraitsSendWindow) {
+        if (getIdentityDataChanged()) {
+            bytesSent += sendIdentityPacket();
+        }
+
+        bytesSent += _clientTraitsHandler->sendChangedTraitsToMixer();
+
+        // Compute the next send window based on how much data we sent and what
+        // data rate we're trying to max at.
+        milliseconds timeUntilNextSend { bytesSent / maxDataRateBytesPerMilliseconds };
+        _nextTraitsSendWindow += timeUntilNextSend;
+
+        // Don't let the next send window lag behind if we're not sending a lot of data.
+        if (_nextTraitsSendWindow < now) {
+            _nextTraitsSendWindow = now;
+        }
+    }
+
+    bytesSent += Avatar::sendAvatarDataPacket(sendAll);
+
+    return bytesSent;
+}
+
 const float RENDER_HEAD_CUTOFF_DISTANCE = 0.47f;
 
 bool MyAvatar::cameraInsideHead(const glm::vec3& cameraPosition) const {
@@ -3246,7 +3279,7 @@ void MyAvatar::updateOrientation(float deltaTime) {
         head->setBaseRoll(ROLL(euler));
     } else {
         head->setBaseYaw(0.0f);
-        head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime
+        head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime 
             + getDriveKey(DELTA_PITCH) * _pitchSpeed / PITCH_SPEED_DEFAULT);
         head->setBaseRoll(0.0f);
     }
@@ -3292,7 +3325,7 @@ void MyAvatar::updateActionMotor(float deltaTime) {
 
     glm::vec3 direction = forward + right;
     if (state == CharacterController::State::Hover ||
-            _characterController.computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS) {
+            _characterController.computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS) {
         glm::vec3 up = (getDriveKey(TRANSLATE_Y)) * IDENTITY_UP;
         direction += up;
     }
@@ -3848,7 +3881,7 @@ void MyAvatar::setCollisionsEnabled(bool enabled) {
 bool MyAvatar::getCollisionsEnabled() {
     // may return 'false' even though the collisionless option was requested
     // because the zone may disallow collisionless avatars
-    return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
+    return _characterController.computeCollisionMask() != BULLET_COLLISION_MASK_COLLISIONLESS;
 }
 
 void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) {
@@ -3857,7 +3890,11 @@ void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) {
         QMetaObject::invokeMethod(this, "setOtherAvatarsCollisionsEnabled", Q_ARG(bool, enabled));
         return;
     }
+    bool change = _collideWithOtherAvatars != enabled;
     _collideWithOtherAvatars = enabled;
+    if (change) {
+        setCollisionWithOtherAvatarsFlags();
+    }
     emit otherAvatarsCollisionsEnabledChanged(enabled);
 }
 
@@ -3865,6 +3902,11 @@ bool MyAvatar::getOtherAvatarsCollisionsEnabled() {
     return _collideWithOtherAvatars;
 }
 
+void MyAvatar::setCollisionWithOtherAvatarsFlags() {
+    _characterController.setCollideWithOtherAvatars(_collideWithOtherAvatars);
+    _characterController.setPendingFlagsUpdateCollisionMask();
+}
+
 void MyAvatar::updateCollisionCapsuleCache() {
     glm::vec3 start, end;
     float radius;
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
old mode 100644
new mode 100755
index 58880acb08..af08955ca0
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -253,6 +253,9 @@ class MyAvatar : public Avatar {
     const QString DOMINANT_LEFT_HAND = "left";
     const QString DOMINANT_RIGHT_HAND = "right";
 
+    using Clock = std::chrono::system_clock;
+    using TimePoint = Clock::time_point;
+
 public:
     enum DriveKeys {
         TRANSLATE_X = 0,
@@ -294,6 +297,8 @@ public:
 
     void reset(bool andRecenter = false, bool andReload = true, bool andHead = true);
 
+    void setCollisionWithOtherAvatarsFlags() override;
+
     /**jsdoc
      * @function MyAvatar.resetSensorsAndBody
      */
@@ -1213,6 +1218,7 @@ public:
     void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override;
     void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override;
     void avatarEntityDataToJson(QJsonObject& root) const override;
+    int sendAvatarDataPacket(bool sendAll = false) override;
 
 public slots:
 
@@ -1728,7 +1734,7 @@ private:
     SharedSoundPointer _collisionSound;
 
     MyCharacterController _characterController;
-    int32_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR };
+    int32_t _previousCollisionMask { BULLET_COLLISION_MASK_MY_AVATAR };
 
     AvatarWeakPointer _lookAtTargetAvatar;
     glm::vec3 _targetAvatarPosition;
@@ -1937,6 +1943,8 @@ private:
     bool _skeletonModelLoaded { false };
     bool _reloadAvatarEntityDataFromSettings { true };
 
+    TimePoint _nextTraitsSendWindow;
+
     Setting::Handle<QString> _dominantHandSetting;
     Setting::Handle<float> _headPitchSetting;
     Setting::Handle<float> _scaleSetting;
diff --git a/interface/src/avatar/MyCharacterController.cpp b/interface/src/avatar/MyCharacterController.cpp
index 798dbc91ed..821b01c2c6 100755
--- a/interface/src/avatar/MyCharacterController.cpp
+++ b/interface/src/avatar/MyCharacterController.cpp
@@ -202,6 +202,29 @@ bool MyCharacterController::testRayShotgun(const glm::vec3& position, const glm:
     return result.hitFraction < 1.0f;
 }
 
+int32_t MyCharacterController::computeCollisionMask() const {
+    int32_t collisionMask = BULLET_COLLISION_MASK_MY_AVATAR; 
+    if (_collisionless && _collisionlessAllowed) {
+        collisionMask = BULLET_COLLISION_MASK_COLLISIONLESS;
+    } else if (!_collideWithOtherAvatars) {
+        collisionMask &= ~BULLET_COLLISION_GROUP_OTHER_AVATAR;
+    }
+    return collisionMask;
+}
+
+void MyCharacterController::handleChangedCollisionMask() {
+    if (_pendingFlags & PENDING_FLAG_UPDATE_COLLISION_MASK) {
+        // ATM the easiest way to update collision groups/masks is to remove/re-add the RigidBody
+        if (_dynamicsWorld) {
+            _dynamicsWorld->removeRigidBody(_rigidBody);
+            int32_t collisionMask = computeCollisionMask();
+            _dynamicsWorld->addRigidBody(_rigidBody, BULLET_COLLISION_GROUP_MY_AVATAR, collisionMask);
+        }
+        _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_MASK;
+        updateCurrentGravity();
+    }
+}
+
 btConvexHullShape* MyCharacterController::computeShape() const {
     // HACK: the avatar collides using convex hull with a collision margin equal to
     // the old capsule radius.  Two points define a capsule and additional points are
diff --git a/interface/src/avatar/MyCharacterController.h b/interface/src/avatar/MyCharacterController.h
old mode 100644
new mode 100755
index fd9caface2..76fe588e71
--- a/interface/src/avatar/MyCharacterController.h
+++ b/interface/src/avatar/MyCharacterController.h
@@ -42,6 +42,12 @@ public:
 
     void setDensity(btScalar density) { _density = density; }
 
+    int32_t computeCollisionMask() const override;
+    void handleChangedCollisionMask() override;
+
+    bool _collideWithOtherAvatars{ true };
+    void setCollideWithOtherAvatars(bool collideWithOtherAvatars) { _collideWithOtherAvatars = collideWithOtherAvatars; }
+
 protected:
     void initRayShotgun(const btCollisionWorld* world);
     void updateMassProperties() override;
diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp
old mode 100644
new mode 100755
index 356b365f93..26d69841d0
--- a/interface/src/avatar/MySkeletonModel.cpp
+++ b/interface/src/avatar/MySkeletonModel.cpp
@@ -187,7 +187,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
         }
     }
 
-    bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS);
+    bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS);
     if (isFlying != _prevIsFlying) {
         const float FLY_TO_IDLE_HIPS_TRANSITION_TIME = 0.5f;
         _flyIdleTimer = FLY_TO_IDLE_HIPS_TRANSITION_TIME;
@@ -198,7 +198,7 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
 
     // if hips are not under direct control, estimate the hips position.
     if (avatarHeadPose.isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] & (uint8_t)Rig::ControllerFlags::Enabled)) {
-        bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS);
+        bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionMask() == BULLET_COLLISION_MASK_COLLISIONLESS);
 
         // timescale in seconds
         const float TRANS_HORIZ_TIMESCALE = 0.15f;
diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp
old mode 100644
new mode 100755
index 0dfc349e18..754d914135
--- a/interface/src/avatar/OtherAvatar.cpp
+++ b/interface/src/avatar/OtherAvatar.cpp
@@ -46,9 +46,6 @@ OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) {
     connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished);
     connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady);
     connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset);
-
-    // add the purple orb
-    createOrb();
 }
 
 OtherAvatar::~OtherAvatar() {
@@ -138,17 +135,9 @@ void OtherAvatar::rebuildCollisionShape() {
     }
 }
 
-void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) {
+void OtherAvatar::setCollisionWithOtherAvatarsFlags() {
     if (_motionState) {
-        bool collides = _motionState->getCollisionGroup() == BULLET_COLLISION_GROUP_OTHER_AVATAR && myAvatarCollide;
-        if (_collideWithOtherAvatars != collides) {
-            if (!myAvatarCollide) {
-                _collideWithOtherAvatars = false;
-            }
-            auto newCollisionGroup = _collideWithOtherAvatars ? BULLET_COLLISION_GROUP_OTHER_AVATAR : BULLET_COLLISION_GROUP_COLLISIONLESS;
-            _motionState->setCollisionGroup(newCollisionGroup);
-            _motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP);
-        }
+        _motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP);
     }
 }
 
diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h
old mode 100644
new mode 100755
index a1dc5724a9..969f551783
--- a/interface/src/avatar/OtherAvatar.h
+++ b/interface/src/avatar/OtherAvatar.h
@@ -46,7 +46,9 @@ public:
     bool shouldBeInPhysicsSimulation() const;
     bool needsPhysicsUpdate() const;
 
-    void updateCollisionGroup(bool myAvatarCollide);
+    bool getCollideWithOtherAvatars() const { return _collideWithOtherAvatars; } 
+
+    void setCollisionWithOtherAvatarsFlags() override;
 
     void simulate(float deltaTime, bool inView) override;
 
diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp
index 00acd40e70..5236c5a7fb 100644
--- a/interface/src/commerce/QmlCommerce.cpp
+++ b/interface/src/commerce/QmlCommerce.cpp
@@ -22,7 +22,9 @@
 #include <ui/TabletScriptingInterface.h>
 #include "scripting/HMDScriptingInterface.h"
 
-QmlCommerce::QmlCommerce() {
+QmlCommerce::QmlCommerce() :
+    _appsPath(PathUtils::getAppDataPath() + "Apps/")
+{
     auto ledger = DependencyManager::get<Ledger>();
     auto wallet = DependencyManager::get<Wallet>();
     connect(ledger.data(), &Ledger::buyResult, this, &QmlCommerce::buyResult);
@@ -44,22 +46,18 @@ QmlCommerce::QmlCommerce() {
 
     auto accountManager = DependencyManager::get<AccountManager>();
     connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { setPassphrase(""); });
-
-    _appsPath = PathUtils::getAppDataPath() + "Apps/";
 }
 
 
-
-
 void QmlCommerce::openSystemApp(const QString& appName) {
-    static QMap<QString, QString> systemApps {
+    static const QMap<QString, QString> systemApps {
         {"GOTO",        "hifi/tablet/TabletAddressDialog.qml"},
         {"PEOPLE",      "hifi/Pal.qml"},
         {"WALLET",      "hifi/commerce/wallet/Wallet.qml"},
         {"MARKET",      "/marketplace.html"}
     };
 
-    static QMap<QString, QString> systemInject{
+    static const QMap<QString, QString> systemInject{
         {"MARKET",      "/scripts/system/html/js/marketplacesInject.js"}
     };
 
diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h
index ad21899ebf..3217b8a1f9 100644
--- a/interface/src/commerce/QmlCommerce.h
+++ b/interface/src/commerce/QmlCommerce.h
@@ -19,7 +19,9 @@
 
 #include <QPixmap>
 
-class QmlCommerce : public QObject {
+#include <DependencyManager.h>
+
+class QmlCommerce : public QObject, public Dependency {
     Q_OBJECT
 
 public:
@@ -98,7 +100,7 @@ protected:
     Q_INVOKABLE void updateItem(const QString& certificateId);
 
 private:
-    QString _appsPath;
+    const QString _appsPath;
 };
 
 #endif // hifi_QmlCommerce_h
diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp
index 14b8182abf..14e5cdc7f5 100644
--- a/interface/src/ui/overlays/ModelOverlay.cpp
+++ b/interface/src/ui/overlays/ModelOverlay.cpp
@@ -114,14 +114,9 @@ void ModelOverlay::update(float deltatime) {
         _model->setVisibleInScene(getVisible(), scene);
         metaDirty = true;
     }
-    if (_drawInFrontDirty) {
-        _drawInFrontDirty = false;
-        _model->setLayeredInFront(getDrawInFront(), scene);
-        metaDirty = true;
-    }
-    if (_drawInHUDDirty) {
-        _drawInHUDDirty = false;
-        _model->setLayeredInHUD(getDrawHUDLayer(), scene);
+    if (_renderLayerDirty) {
+        _renderLayerDirty = false;
+        _model->setHifiRenderLayer(_drawHUDLayer ? render::hifi::LAYER_3D_HUD : (_drawInFront ? render::hifi::LAYER_3D_FRONT : render::hifi::LAYER_3D), scene);
         metaDirty = true;
     }
     if (_groupCulledDirty) {
@@ -175,14 +170,14 @@ void ModelOverlay::setVisible(bool visible) {
 void ModelOverlay::setDrawInFront(bool drawInFront) {
     if (drawInFront != getDrawInFront()) {
         Base3DOverlay::setDrawInFront(drawInFront);
-        _drawInFrontDirty = true;
+        _renderLayerDirty = true;
     }
 }
 
 void ModelOverlay::setDrawHUDLayer(bool drawHUDLayer) {
     if (drawHUDLayer != getDrawHUDLayer()) {
         Base3DOverlay::setDrawHUDLayer(drawHUDLayer);
-        _drawInHUDDirty = true;
+        _renderLayerDirty = true;
     }
 }
 
diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h
index bd922e258a..17a2327d02 100644
--- a/interface/src/ui/overlays/ModelOverlay.h
+++ b/interface/src/ui/overlays/ModelOverlay.h
@@ -126,8 +126,7 @@ private:
     QVector<int> _jointMapping; // domain is index into model-joints, range is index into animation-joints
 
     bool _visibleDirty { true };
-    bool _drawInFrontDirty { false };
-    bool _drawInHUDDirty { false };
+    bool _renderLayerDirty { false };
     bool _isGroupCulled { false };
     bool _groupCulledDirty { false };
 
diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp
index ec6b62e237..4fe3708ba9 100644
--- a/interface/src/ui/overlays/Web3DOverlay.cpp
+++ b/interface/src/ui/overlays/Web3DOverlay.cpp
@@ -134,13 +134,6 @@ void Web3DOverlay::destroyWebSurface() {
 
     QQuickItem* rootItem = _webSurface->getRootItem();
 
-    if (rootItem && rootItem->objectName() == "tabletRoot") {
-        auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
-        if (tabletScriptingInterface) {
-            tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", nullptr);
-        }
-    }
-
     // Fix for crash in QtWebEngineCore when rapidly switching domains
     // Call stop on the QWebEngineView before destroying OffscreenQMLSurface.
     if (rootItem) {
diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp
index 16c2c1cc7e..cc48308f17 100644
--- a/libraries/animation/src/AnimSkeleton.cpp
+++ b/libraries/animation/src/AnimSkeleton.cpp
@@ -237,8 +237,17 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector<HFMJoint>& joints,
     _relativeDefaultPoses = _absoluteDefaultPoses;
     convertAbsolutePosesToRelative(_relativeDefaultPoses);
 
+    // build _jointIndicesByName hash
     for (int i = 0; i < _jointsSize; i++) {
-        _jointIndicesByName[_joints[i].name] = i;
+        auto iter = _jointIndicesByName.find(_joints[i].name);
+        if (iter != _jointIndicesByName.end()) {
+            // prefer joints over meshes if there is a name collision.
+            if (_joints[i].isSkeletonJoint && !_joints[iter.value()].isSkeletonJoint) {
+                iter.value() = i;
+            }
+        } else {
+            _jointIndicesByName.insert(_joints[i].name, i);
+        }
     }
 
     // build mirror map.
diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp
index 6e27bee06f..bc4dca54f2 100644
--- a/libraries/animation/src/Rig.cpp
+++ b/libraries/animation/src/Rig.cpp
@@ -1984,11 +1984,10 @@ void Rig::copyJointsIntoJointData(QVector<JointData>& jointDataVec) const {
             data.rotation = !_sendNetworkNode ? _internalPoseSet._absolutePoses[i].rot() : _networkPoseSet._absolutePoses[i].rot();
             data.rotationIsDefaultPose = isEqual(data.rotation, defaultAbsRot);
 
-            // translations are in relative frame but scaled so that they are in meters,
-            // instead of model units.
+            // translations are in relative frame.
             glm::vec3 defaultRelTrans = _animSkeleton->getRelativeDefaultPose(i).trans();
             glm::vec3 currentRelTrans = _sendNetworkNode ? _networkPoseSet._relativePoses[i].trans() : _internalPoseSet._relativePoses[i].trans();
-            data.translation = geometryToRigScale * currentRelTrans;
+            data.translation = currentRelTrans;
             data.translationIsDefaultPose = isEqual(currentRelTrans, defaultRelTrans);
         } else {
             data.translationIsDefaultPose = true;
@@ -2015,7 +2014,6 @@ void Rig::copyJointsFromJointData(const QVector<JointData>& jointDataVec) {
     std::vector<glm::quat> rotations;
     rotations.reserve(numJoints);
     const glm::quat rigToGeometryRot(glmExtractRotation(_rigToGeometryTransform));
-    const glm::vec3 rigToGeometryScale(extractScale(_rigToGeometryTransform));
 
     for (int i = 0; i < numJoints; i++) {
         const JointData& data = jointDataVec.at(i);
@@ -2041,8 +2039,8 @@ void Rig::copyJointsFromJointData(const QVector<JointData>& jointDataVec) {
         if (data.translationIsDefaultPose) {
             _internalPoseSet._relativePoses[i].trans() = relativeDefaultPoses[i].trans();
         } else {
-            // JointData translations are in scaled relative-frame so we scale back to regular relative-frame
-            _internalPoseSet._relativePoses[i].trans() = rigToGeometryScale * data.translation;
+            // JointData translations are in relative-frame
+            _internalPoseSet._relativePoses[i].trans() = data.translation;
         }
     }
 }
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
index a6185d7e79..17d10cdf49 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
@@ -324,8 +324,8 @@ void Avatar::removeAvatarEntitiesFromTree() {
     }
 }
 
-void Avatar::updateGrabs() {
-
+bool Avatar::updateGrabs() {
+    bool grabAddedOrRemoved = false;
     // update the Grabs according to any changes in _avatarGrabData
     _avatarGrabsLock.withWriteLock([&] {
         if (_avatarGrabDataChanged) {
@@ -385,6 +385,7 @@ void Avatar::updateGrabs() {
                         entityTree->updateEntityQueryAACube(target, packetSender, force, iShouldTellServer);
                     });
                 }
+                grabAddedOrRemoved = true;
             }
             _avatarGrabs.remove(grabID);
             _changedAvatarGrabs.remove(grabID);
@@ -402,9 +403,11 @@ void Avatar::updateGrabs() {
                 target->addGrab(grab);
                 // only clear this entry from the _changedAvatarGrabs if we found the entity.
                 changeItr.remove();
+                grabAddedOrRemoved = true;
             }
         }
     });
+    return grabAddedOrRemoved;
 }
 
 void Avatar::accumulateGrabPositions(std::map<QUuid, GrabLocationAccumulator>& grabAccumulators) {
@@ -1375,7 +1378,7 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
     AvatarData::setSkeletonModelURL(skeletonModelURL);
     if (QThread::currentThread() == thread()) {
 
-        if (!isMyAvatar()) {
+        if (!isMyAvatar() && !DependencyManager::get<NodeList>()->isIgnoringNode(getSessionUUID())) {
             createOrb();
         }
 
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
index 4ff3e9cc13..d5431ad2d2 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
@@ -538,7 +538,7 @@ protected:
 
     // protected methods...
     bool isLookingAtMe(AvatarSharedPointer avatar) const;
-    void updateGrabs();
+    bool updateGrabs();
     void relayJointDataToChildren();
 
     void fade(render::Transaction& transaction, render::Transition::Type type);
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
old mode 100644
new mode 100755
index 24e30d20c1..17dad715f9
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -54,7 +54,8 @@ using namespace std;
 
 const QString AvatarData::FRAME_NAME = "com.highfidelity.recording.AvatarData";
 
-static const int TRANSLATION_COMPRESSION_RADIX = 12;
+static const int TRANSLATION_COMPRESSION_RADIX = 14;
+static const int FAUX_JOINT_COMPRESSION_RADIX = 12;
 static const int SENSOR_TO_WORLD_SCALE_RADIX = 10;
 static const float AUDIO_LOUDNESS_SCALE = 1024.0f;
 static const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water
@@ -73,6 +74,7 @@ size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints)
     totalSize += validityBitsSize; // Orientations mask
     totalSize += numJoints * sizeof(SixByteQuat); // Orientations
     totalSize += validityBitsSize; // Translations mask
+    totalSize += sizeof(float); // maxTranslationDimension
     totalSize += numJoints * sizeof(SixByteTrans); // Translations
 
     size_t NUM_FAUX_JOINT = 2;
@@ -85,6 +87,23 @@ size_t AvatarDataPacket::maxJointDataSize(size_t numJoints, bool hasGrabJoints)
     return totalSize;
 }
 
+size_t AvatarDataPacket::minJointDataSize(size_t numJoints) {
+    const size_t validityBitsSize = calcBitVectorSize((int)numJoints);
+
+    size_t totalSize = sizeof(uint8_t); // numJoints
+
+    totalSize += validityBitsSize; // Orientations mask
+    // assume no valid rotations
+    totalSize += validityBitsSize; // Translations mask
+    totalSize += sizeof(float); // maxTranslationDimension
+    // assume no valid translations
+
+    size_t NUM_FAUX_JOINT = 2;
+    totalSize += NUM_FAUX_JOINT * (sizeof(SixByteQuat) + sizeof(SixByteTrans)); // faux joints
+
+    return totalSize;
+}
+
 size_t AvatarDataPacket::maxJointDefaultPoseFlagsSize(size_t numJoints) {
     const size_t bitVectorSize = calcBitVectorSize((int)numJoints);
     size_t totalSize = sizeof(uint8_t); // numJoints
@@ -611,13 +630,24 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
     assert(numJoints <= 255);
     const int jointBitVectorSize = calcBitVectorSize(numJoints);
 
-    // Start joints if room for at least the faux joints.
-    IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, 1 + 2 * jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE) {
+    // include jointData if there is room for the most minimal section. i.e. no translations or rotations.
+    IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) {
         // Allow for faux joints + translation bit-vector:
         const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat)
             + jointBitVectorSize + AvatarDataPacket::FAUX_JOINTS_SIZE;
         auto startSection = destinationBuffer;
 
+        // compute maxTranslationDimension before we send any joint data.
+        float maxTranslationDimension = 0.001f;
+        for (int i = sendStatus.translationsSent; i < numJoints; ++i) {
+            const JointData& data = jointData[i];
+            if (!data.translationIsDefaultPose) {
+                maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension);
+                maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension);
+                maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension);
+            }
+        }
+
         // joint rotation data
         *destinationBuffer++ = (uint8_t)numJoints;
 
@@ -684,9 +714,11 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
         memset(destinationBuffer, 0, jointBitVectorSize);
         destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes
 
+        // write maxTranslationDimension
+        AVATAR_MEMCPY(maxTranslationDimension);
+
         float minTranslation = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinTranslationDistance(viewerPosition) : AVATAR_MIN_TRANSLATION;
 
-        float maxTranslationDimension = 0.0;
         i = sendStatus.translationsSent;
         for (; i < numJoints; ++i) {
             const JointData& data = joints[i];
@@ -700,12 +732,8 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
 #ifdef WANT_DEBUG
                         translationSentCount++;
 #endif
-                        maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension);
-                        maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension);
-                        maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension);
-
-                        destinationBuffer +=
-                            packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX);
+                        destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation / maxTranslationDimension,
+                                                                               TRANSLATION_COMPRESSION_RADIX);
 
                         if (sentJoints) {
                             sentJoints[i].translation = data.translation;
@@ -727,12 +755,12 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
         Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix());
         destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerLeftHandTransform.getRotation());
         destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerLeftHandTransform.getTranslation(),
-            TRANSLATION_COMPRESSION_RADIX);
+            FAUX_JOINT_COMPRESSION_RADIX);
 
         Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix());
         destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, controllerRightHandTransform.getRotation());
         destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, controllerRightHandTransform.getTranslation(),
-            TRANSLATION_COMPRESSION_RADIX);
+            FAUX_JOINT_COMPRESSION_RADIX);
 
         IF_AVATAR_SPACE(PACKET_HAS_GRAB_JOINTS, sizeof (AvatarDataPacket::FarGrabJoints)) {
             // the far-grab joints may range further than 3 meters, so we can't use packFloatVec3ToSignedTwoByteFixed etc
@@ -785,7 +813,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
             outboundDataRateOut->jointDataRate.increment(numBytes);
         }
     }
-    
+
     IF_AVATAR_SPACE(PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS, 1 + 2 * jointBitVectorSize) {
         auto startSection = destinationBuffer;
 
@@ -871,7 +899,7 @@ const unsigned char* unpackFauxJoint(const unsigned char* sourceBuffer, ThreadSa
     glm::vec3 position;
     Transform transform;
     sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, orientation);
-    sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, TRANSLATION_COMPRESSION_RADIX);
+    sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, position, FAUX_JOINT_COMPRESSION_RADIX);
     transform.setTranslation(position);
     transform.setRotation(orientation);
     matrixCache.set(transform.getMatrix());
@@ -1144,6 +1172,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
 
         sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
 
+        if (collideWithOtherAvatarsChanged) {
+            setCollisionWithOtherAvatarsFlags();
+        }
         if (somethingChanged) {
             _additionalFlagsChanged = now;
         }
@@ -1280,6 +1311,12 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
             }
         } // 1 + bytesOfValidity bytes
 
+        // read maxTranslationDimension
+        float maxTranslationDimension;
+        PACKET_READ_CHECK(JointMaxTranslationDimension, sizeof(float));
+        memcpy(&maxTranslationDimension, sourceBuffer, sizeof(float));
+        sourceBuffer += sizeof(float);
+
         // each joint translation component is stored in 6 bytes.
         const int COMPRESSED_TRANSLATION_SIZE = 6;
         PACKET_READ_CHECK(JointTranslation, numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE);
@@ -1288,6 +1325,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
             JointData& data = _jointData[i];
             if (validTranslations[i]) {
                 sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, data.translation, TRANSLATION_COMPRESSION_RADIX);
+                data.translation *= maxTranslationDimension;
                 _hasNewJointData = true;
                 data.translationIsDefaultPose = false;
             }
@@ -2157,7 +2195,7 @@ void AvatarData::detachAll(const QString& modelURL, const QString& jointName) {
     setAttachmentData(attachmentData);
 }
 
-void AvatarData::sendAvatarDataPacket(bool sendAll) {
+int AvatarData::sendAvatarDataPacket(bool sendAll) {
     auto nodeList = DependencyManager::get<NodeList>();
 
     // about 2% of the time, we send a full update (meaning, we transmit all the joint data), even if nothing has changed.
@@ -2170,16 +2208,14 @@ void AvatarData::sendAvatarDataPacket(bool sendAll) {
     int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber);
 
     if (avatarByteArray.size() > maximumByteArraySize) {
-        qCWarning(avatars) << "toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data";
         avatarByteArray = toByteArrayStateful(dataDetail, true);
 
         if (avatarByteArray.size() > maximumByteArraySize) {
-            qCWarning(avatars) << "toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData";
             avatarByteArray = toByteArrayStateful(MinimumData, true);
 
             if (avatarByteArray.size() > maximumByteArraySize) {
                 qCWarning(avatars) << "toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!";
-                return;
+                return 0;
             }
         }
     }
@@ -2191,18 +2227,20 @@ void AvatarData::sendAvatarDataPacket(bool sendAll) {
     auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size() + sizeof(sequenceNumber));
     avatarPacket->writePrimitive(sequenceNumber++);
     avatarPacket->write(avatarByteArray);
+    auto packetSize = avatarPacket->getWireSize();
 
     nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer);
+
+    return packetSize;
 }
 
-void AvatarData::sendIdentityPacket() {
+int AvatarData::sendIdentityPacket() {
     auto nodeList = DependencyManager::get<NodeList>();
 
     if (_identityDataChanged) {
         // if the identity data has changed, push the sequence number forwards
         ++_identitySequenceNumber;
     }
-
     QByteArray identityData = identityByteArray();
 
     auto packetList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
@@ -2216,6 +2254,7 @@ void AvatarData::sendIdentityPacket() {
     });
 
     _identityDataChanged = false;
+    return identityData.size();
 }
 
 static const QString JSON_ATTACHMENT_URL = QStringLiteral("modelUrl");
@@ -2392,7 +2431,8 @@ static const QString JSON_AVATAR_VERSION = QStringLiteral("version");
 enum class JsonAvatarFrameVersion : int {
     JointRotationsInRelativeFrame = 0,
     JointRotationsInAbsoluteFrame,
-    JointDefaultPoseBits
+    JointDefaultPoseBits,
+    JointUnscaledTranslations,
 };
 
 QJsonValue toJsonValue(const JointData& joint) {
@@ -2409,7 +2449,16 @@ JointData jointDataFromJsonValue(int version, const QJsonValue& json) {
     if (json.isArray()) {
         QJsonArray array = json.toArray();
         result.rotation = quatFromJsonValue(array[0]);
+
         result.translation = vec3FromJsonValue(array[1]);
+
+        // In old recordings, translations are scaled by _geometryOffset.  Undo that scaling.
+        if (version < (int)JsonAvatarFrameVersion::JointUnscaledTranslations) {
+            // because we don't have access to the actual _geometryOffset used. we have to guess.
+            // most avatar FBX files were authored in centimeters.
+            const float METERS_TO_CENTIMETERS = 100.0f;
+            result.translation *= METERS_TO_CENTIMETERS;
+        }
         if (version >= (int)JsonAvatarFrameVersion::JointDefaultPoseBits) {
             result.rotationIsDefaultPose = array[2].toBool();
             result.translationIsDefaultPose = array[3].toBool();
@@ -2428,7 +2477,7 @@ void AvatarData::avatarEntityDataToJson(QJsonObject& root) const {
 QJsonObject AvatarData::toJson() const {
     QJsonObject root;
 
-    root[JSON_AVATAR_VERSION] = (int)JsonAvatarFrameVersion::JointDefaultPoseBits;
+    root[JSON_AVATAR_VERSION] = (int)JsonAvatarFrameVersion::JointUnscaledTranslations;
 
     if (!getSkeletonModelURL().isEmpty()) {
         root[JSON_AVATAR_BODY_MODEL] = getSkeletonModelURL().toString();
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
old mode 100644
new mode 100755
index bd17233dbf..9128c2dbf9
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -277,8 +277,8 @@ namespace AvatarDataPacket {
         uint8_t rotationValidityBits[ceil(numJoints / 8)];     // one bit per joint, if true then a compressed rotation follows.
         SixByteQuat rotation[numValidRotations];               // encodeded and compressed by packOrientationQuatToSixBytes()
         uint8_t translationValidityBits[ceil(numJoints / 8)];  // one bit per joint, if true then a compressed translation follows.
-        SixByteTrans translation[numValidTranslations];        // encodeded and compressed by packFloatVec3ToSignedTwoByteFixed()
-
+        float maxTranslationDimension;                         // used to normalize fixed point translation values.
+        SixByteTrans translation[numValidTranslations];        // normalized and compressed by packFloatVec3ToSignedTwoByteFixed()
         SixByteQuat leftHandControllerRotation;
         SixByteTrans leftHandControllerTranslation;
         SixByteQuat rightHandControllerRotation;
@@ -286,6 +286,7 @@ namespace AvatarDataPacket {
     };
     */
     size_t maxJointDataSize(size_t numJoints, bool hasGrabJoints);
+    size_t minJointDataSize(size_t numJoints);
 
     /*
     struct JointDefaultPoseFlags {
@@ -495,6 +496,8 @@ public:
     /// \return number of bytes parsed
     virtual int parseDataFromBuffer(const QByteArray& buffer);
 
+    virtual void setCollisionWithOtherAvatarsFlags() {};
+
     // Body Rotation (degrees)
     float getBodyYaw() const;
     void setBodyYaw(float bodyYaw);
@@ -1271,12 +1274,12 @@ public slots:
      * @function MyAvatar.sendAvatarDataPacket
      * @param {boolean} [sendAll=false]
      */
-    void sendAvatarDataPacket(bool sendAll = false);
+    virtual int sendAvatarDataPacket(bool sendAll = false);
 
     /**jsdoc
      * @function MyAvatar.sendIdentityPacket
      */
-    void sendIdentityPacket();
+    int sendIdentityPacket();
 
     /**jsdoc
      * @function MyAvatar.setSessionUUID
diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp
index 6a67ef6638..5f30d98ed6 100644
--- a/libraries/avatars/src/AvatarHashMap.cpp
+++ b/libraries/avatars/src/AvatarHashMap.cpp
@@ -195,21 +195,22 @@ int AvatarHashMap::numberOfAvatarsInRange(const glm::vec3& position, float range
     return count;
 }
 
-AvatarSharedPointer AvatarHashMap::newSharedAvatar() {
-    return std::make_shared<AvatarData>();
+AvatarSharedPointer AvatarHashMap::newSharedAvatar(const QUuid& sessionUUID) {
+    auto avatarData = std::make_shared<AvatarData>();
+    avatarData->setSessionUUID(sessionUUID);
+    return avatarData;
 }
 
 AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) {
     qCDebug(avatars) << "Adding avatar with sessionUUID " << sessionUUID << "to AvatarHashMap.";
 
-    auto avatar = newSharedAvatar();
+    auto avatar = newSharedAvatar(sessionUUID);
     avatar->setSessionUUID(sessionUUID);
     avatar->setOwningAvatarMixer(mixerWeakPointer);
 
     // addAvatar is only called from newOrExistingAvatar, which already locks _hashLock
     _avatarHash.insert(sessionUUID, avatar);
     emit avatarAddedEvent(sessionUUID);
-
     return avatar;
 }
 
diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h
index 3bb38dd081..8395651d6b 100644
--- a/libraries/avatars/src/AvatarHashMap.h
+++ b/libraries/avatars/src/AvatarHashMap.h
@@ -32,6 +32,9 @@
 #include "AvatarData.h"
 #include "AssociatedTraitValues.h"
 
+const int CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 50;
+const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
+
 /**jsdoc
  * <strong>Note:</strong> An <code>AvatarList</code> API is also provided for Interface and client entity scripts: it is a 
  * synonym for the {@link AvatarManager} API.
@@ -179,7 +182,7 @@ protected:
     AvatarHashMap();
 
     virtual AvatarSharedPointer parseAvatarData(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
-    virtual AvatarSharedPointer newSharedAvatar();
+    virtual AvatarSharedPointer newSharedAvatar(const QUuid& sessionUUID);
     virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer);
     AvatarSharedPointer newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer,
         bool& isNew);
diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp
index 3e24c1f9ad..bcbe5308c7 100644
--- a/libraries/avatars/src/ClientTraitsHandler.cpp
+++ b/libraries/avatars/src/ClientTraitsHandler.cpp
@@ -65,8 +65,9 @@ void ClientTraitsHandler::resetForNewMixer() {
     _owningAvatar->prepareResetTraitInstances();
 }
 
-void ClientTraitsHandler::sendChangedTraitsToMixer() {
+int ClientTraitsHandler::sendChangedTraitsToMixer() {
     std::unique_lock<Mutex> lock(_traitLock);
+    int bytesWritten = 0;
 
     if (hasChangedTraits() || _shouldPerformInitialSend) {
         // we have at least one changed trait to send
@@ -75,7 +76,7 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() {
         auto avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
         if (!avatarMixer || !avatarMixer->getActiveSocket()) {
             // we don't have an avatar mixer with an active socket, we can't send changed traits at this time
-            return;
+            return 0;
         }
 
         // we have a mixer to send to, setup our set traits packet
@@ -106,7 +107,7 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() {
 
             if (initialSend || *simpleIt == Updated) {
                 if (traitType == AvatarTraits::SkeletonModelURL) {
-                    _owningAvatar->packTrait(traitType, *traitsPacketList);
+                    bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList);
 
                     // keep track of our skeleton version in case we get an override back
                     _currentSkeletonVersion = _currentTraitVersion;
@@ -123,10 +124,10 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() {
                     || instanceIDValuePair.value == Updated) {
                     // this is a changed trait we need to send or we haven't send out trait information yet
                     // ask the owning avatar to pack it
-                    _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList);
+                    bytesWritten += _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList);
                 } else if (!initialSend && instanceIDValuePair.value == Deleted) {
                     // pack delete for this trait instance
-                    AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id,
+                    bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id,
                                                            *traitsPacketList);
                 }
             }
@@ -136,6 +137,8 @@ void ClientTraitsHandler::sendChangedTraitsToMixer() {
 
         nodeList->sendPacketList(std::move(traitsPacketList), *avatarMixer);
     }
+
+    return bytesWritten;
 }
 
 void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
diff --git a/libraries/avatars/src/ClientTraitsHandler.h b/libraries/avatars/src/ClientTraitsHandler.h
index 3900268101..35499fd2cf 100644
--- a/libraries/avatars/src/ClientTraitsHandler.h
+++ b/libraries/avatars/src/ClientTraitsHandler.h
@@ -24,7 +24,7 @@ class ClientTraitsHandler : public QObject {
 public:
     ClientTraitsHandler(AvatarData* owningAvatar);
 
-    void sendChangedTraitsToMixer();
+    int sendChangedTraitsToMixer();
 
     bool hasChangedTraits() const { return _hasChangedTraits; }
 
diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp
index 5fb5a15d2c..53e62ee35f 100644
--- a/libraries/entities-renderer/src/RenderableEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp
@@ -159,20 +159,40 @@ Item::Bound EntityRenderer::getBound() {
     return _bound;
 }
 
+ShapeKey EntityRenderer::getShapeKey() {
+    if (_primitiveMode == PrimitiveMode::LINES) {
+        return ShapeKey::Builder().withOwnPipeline().withWireframe();
+    }
+    return ShapeKey::Builder().withOwnPipeline();
+}
+
 render::hifi::Tag EntityRenderer::getTagMask() const {
     return _isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW;
 }
 
+render::hifi::Layer EntityRenderer::getHifiRenderLayer() const {
+    switch (_renderLayer) {
+        case RenderLayer::WORLD:
+            return render::hifi::LAYER_3D;
+        case RenderLayer::FRONT:
+            return render::hifi::LAYER_3D_FRONT;
+        case RenderLayer::HUD:
+            return render::hifi::LAYER_3D_HUD;
+        default:
+            return render::hifi::LAYER_3D;
+    }
+}
+
 ItemKey EntityRenderer::getKey() {
     if (isTransparent()) {
-        return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask());
+        return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
     }
 
     // This allows shapes to cast shadows
     if (_canCastShadow) {
-        return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withShadowCaster();
+        return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withShadowCaster().withLayer(getHifiRenderLayer());
     } else {
-        return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask());
+        return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
     }
 }
 
@@ -411,6 +431,8 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa
         _moving = entity->isMovingRelativeToParent();
         _visible = entity->getVisible();
         setIsVisibleInSecondaryCamera(entity->isVisibleInSecondaryCamera());
+        setRenderLayer(entity->getRenderLayer());
+        setPrimitiveMode(entity->getPrimitiveMode());
         _canCastShadow = entity->getCanCastShadow();
         _cauterized = entity->getCauterized();
         _needsRenderUpdate = false;
diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h
index 9c4d10190c..fde63f78fa 100644
--- a/libraries/entities-renderer/src/RenderableEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableEntityItem.h
@@ -72,11 +72,12 @@ protected:
 
     // Implementing the PayloadProxyInterface methods
     virtual ItemKey getKey() override;
-    virtual ShapeKey getShapeKey() override { return ShapeKey::Builder::ownPipeline(); }
+    virtual ShapeKey getShapeKey() override;
     virtual Item::Bound getBound() override;
     virtual void render(RenderArgs* args) override final;
     virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override;
     virtual render::hifi::Tag getTagMask() const;
+    virtual render::hifi::Layer getHifiRenderLayer() const;
 
     // Returns true if the item in question needs to have updateInScene called because of internal rendering state changes
     virtual bool needsRenderUpdate() const;
@@ -103,6 +104,8 @@ protected:
     inline bool isValidRenderItem() const { return _renderItemID != Item::INVALID_ITEM_ID; }
 
     virtual void setIsVisibleInSecondaryCamera(bool value) { _isVisibleInSecondaryCamera = value; }
+    virtual void setRenderLayer(RenderLayer value) { _renderLayer = value; }
+    virtual void setPrimitiveMode(PrimitiveMode value) { _primitiveMode = value; }
     
     template <typename F, typename T>
     T withReadLockResult(const std::function<T()>& f) {
@@ -136,6 +139,8 @@ protected:
     bool _visible { false };
     bool _isVisibleInSecondaryCamera { false };
     bool _canCastShadow { false };
+    RenderLayer _renderLayer { RenderLayer::WORLD };
+    PrimitiveMode _primitiveMode { PrimitiveMode::SOLID };
     bool _cauterized { false };
     bool _moving { false };
     bool _needsRenderUpdate { false };
diff --git a/libraries/entities-renderer/src/RenderableGridEntityItem.cpp b/libraries/entities-renderer/src/RenderableGridEntityItem.cpp
index 22cf72cec6..4576358699 100644
--- a/libraries/entities-renderer/src/RenderableGridEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableGridEntityItem.cpp
@@ -97,6 +97,10 @@ ShapeKey GridEntityRenderer::getShapeKey() {
         builder.withTranslucent();
     }
 
+    if (_primitiveMode == PrimitiveMode::LINES) {
+        builder.withWireframe();
+    }
+
     return builder.build();
 }
 
diff --git a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp
index 7c5b7fc0da..c1d6d3211d 100644
--- a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp
@@ -123,6 +123,10 @@ ShapeKey ImageEntityRenderer::getShapeKey() {
         if (_emissive) {
             builder.withUnlit();
         }
+
+        if (_primitiveMode == PrimitiveMode::LINES) {
+            builder.withWireframe();
+        }
     });
 
     return builder.build();
diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
index 6451e873c9..483f9ffe1c 100644
--- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
@@ -55,7 +55,7 @@ void MaterialEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer&
 
 ItemKey MaterialEntityRenderer::getKey() {
     ItemKey::Builder builder;
-    builder.withTypeShape().withTagBits(getTagMask());
+    builder.withTypeShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
 
     if (!_visible) {
         builder.withInvisible();
@@ -98,6 +98,10 @@ ShapeKey MaterialEntityRenderer::getShapeKey() {
         builder.withUnlit();
     }
 
+    if (_primitiveMode == PrimitiveMode::LINES) {
+        builder.withWireframe();
+    }
+
     return builder.build();
 }
 
diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
index aa449b8919..7e01af04dd 100644
--- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
@@ -1074,10 +1074,16 @@ ModelEntityRenderer::ModelEntityRenderer(const EntityItemPointer& entity) : Pare
 }
 
 void ModelEntityRenderer::setKey(bool didVisualGeometryRequestSucceed) {
+    auto builder = ItemKey::Builder().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
+
+    if (_model && _model->isGroupCulled()) {
+        builder.withMetaCullGroup();
+    }
+
     if (didVisualGeometryRequestSucceed) {
-        _itemKey = ItemKey::Builder().withTypeMeta().withTagBits(getTagMask());
+        _itemKey = builder.build();
     } else {
-        _itemKey = ItemKey::Builder().withTypeMeta().withTypeShape().withTagBits(getTagMask());
+        _itemKey = builder.withTypeShape().build();
     }
 }
 
@@ -1295,6 +1301,10 @@ bool ModelEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPoin
             model->getRegistrationPoint() != entity->getRegistrationPoint()) {
             return true;
         }
+
+        if (model->isGroupCulled() != entity->getGroupCulled()) {
+            return true;
+        }
     }
 
     return false;
@@ -1351,6 +1361,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
         connect(model.get(), &Model::requestRenderUpdate, this, &ModelEntityRenderer::requestRenderUpdate);
         connect(model.get(), &Model::setURLFinished, this, [&](bool didVisualGeometryRequestSucceed) {
             setKey(didVisualGeometryRequestSucceed);
+            _model->setTagMask(getTagMask());
+            _model->setHifiRenderLayer(getHifiRenderLayer());
             emit requestRenderUpdate();
             if(didVisualGeometryRequestSucceed) {
                 emit DependencyManager::get<scriptable::ModelProviderFactory>()->
@@ -1437,6 +1449,14 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
         model->setCanCastShadow(_canCastShadow, scene);
     }
 
+    {
+        bool groupCulled = entity->getGroupCulled();
+        if (model->isGroupCulled() != groupCulled) {
+            model->setGroupCulled(groupCulled);
+            setKey(_didLastVisualGeometryRequestSucceed);
+        }
+    }
+
     {
         DETAILED_PROFILE_RANGE(simulation_physics, "Fixup");
         if (model->needsFixupInScene()) {
@@ -1494,6 +1514,24 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce
 void ModelEntityRenderer::setIsVisibleInSecondaryCamera(bool value) {
     Parent::setIsVisibleInSecondaryCamera(value);
     setKey(_didLastVisualGeometryRequestSucceed);
+    if (_model) {
+        _model->setTagMask(getTagMask());
+    }
+}
+
+void ModelEntityRenderer::setRenderLayer(RenderLayer value) {
+    Parent::setRenderLayer(value);
+    setKey(_didLastVisualGeometryRequestSucceed);
+    if (_model) {
+        _model->setHifiRenderLayer(getHifiRenderLayer());
+    }
+}
+
+void ModelEntityRenderer::setPrimitiveMode(PrimitiveMode value) {
+    Parent::setPrimitiveMode(value);
+    if (_model) {
+        _model->setPrimitiveMode(_primitiveMode);
+    }
 }
 
 // NOTE: this only renders the "meta" portion of the Model, namely it renders debugging items
diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h
index 725c1d96c3..16c3664f28 100644
--- a/libraries/entities-renderer/src/RenderableModelEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h
@@ -169,6 +169,8 @@ protected:
     render::hifi::Tag getTagMask() const override;
 
     void setIsVisibleInSecondaryCamera(bool value) override;
+    void setRenderLayer(RenderLayer value) override;
+    void setPrimitiveMode(PrimitiveMode value) override;
 
 private:
     void animate(const TypedEntityPointer& entity);
diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
index 38027a80ed..351d72baf5 100644
--- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
@@ -159,14 +159,18 @@ void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEn
 
 ItemKey ParticleEffectEntityRenderer::getKey() {
     if (_visible) {
-        return ItemKey::Builder::transparentShape().withTagBits(getTagMask());
+        return ItemKey::Builder::transparentShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
     } else {
-        return ItemKey::Builder().withInvisible().withTagBits(getTagMask()).build();
+        return ItemKey::Builder().withInvisible().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()).build();
     }
 }
 
 ShapeKey ParticleEffectEntityRenderer::getShapeKey() {
-    return ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).withTranslucent().build();
+    auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).withTranslucent();
+    if (_primitiveMode == PrimitiveMode::LINES) {
+        builder.withWireframe();
+    }
+    return builder.build();
 }
 
 Item::Bound ParticleEffectEntityRenderer::getBound() {
diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp
index d4a10e551d..68371e4e13 100644
--- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp
@@ -55,11 +55,15 @@ void PolyLineEntityRenderer::buildPipeline() {
 }
 
 ItemKey PolyLineEntityRenderer::getKey() {
-    return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask());
+    return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
 }
 
 ShapeKey PolyLineEntityRenderer::getShapeKey() {
-    return ShapeKey::Builder().withOwnPipeline().withTranslucent().withoutCullFace();
+    auto builder = ShapeKey::Builder().withOwnPipeline().withTranslucent().withoutCullFace();
+    if (_primitiveMode == PrimitiveMode::LINES) {
+        builder.withWireframe();
+    }
+    return builder.build();
 }
 
 bool PolyLineEntityRenderer::needsRenderUpdate() const {
diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
index 514ed3cec1..183d2881f3 100644
--- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
@@ -1613,7 +1613,11 @@ PolyVoxEntityRenderer::PolyVoxEntityRenderer(const EntityItemPointer& entity) :
 }
 
 ShapeKey PolyVoxEntityRenderer::getShapeKey() {
-    return ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).build();
+    auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER);
+    if (_primitiveMode == PrimitiveMode::LINES) {
+        builder.withWireframe();
+    }
+    return builder.build();
 }
 
 bool PolyVoxEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const {
diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
index 366a3fdc70..7aea87535e 100644
--- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
+++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
@@ -173,7 +173,7 @@ public:
     }
     
 protected:
-    virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(getTagMask()); }
+    virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); }
     virtual ShapeKey getShapeKey() override;
     virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
     virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
index 1569c75eec..c47904213b 100644
--- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
@@ -186,6 +186,10 @@ ShapeKey ShapeEntityRenderer::getShapeKey() {
             builder.withUnlit();
         }
 
+        if (_primitiveMode == PrimitiveMode::LINES) {
+            builder.withWireframe();
+        }
+
         return builder.build();
     } else {
         ShapeKey::Builder builder;
@@ -198,6 +202,10 @@ ShapeKey ShapeEntityRenderer::getShapeKey() {
         if (isTransparent()) {
             builder.withTranslucent();
         }
+
+        if (_primitiveMode == PrimitiveMode::LINES) {
+            builder.withWireframe();
+        }
         return builder.build();
     }
 }
@@ -241,8 +249,13 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
     } else if (!useMaterialPipeline()) {
         // FIXME, support instanced multi-shape rendering using multidraw indirect
         outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f;
-        auto pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline();
-        if (render::ShapeKey(args->_globalShapeKey).isWireframe()) {
+        render::ShapePipelinePointer pipeline;
+        if (_renderLayer == RenderLayer::WORLD) {
+            pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline();
+        } else {
+            pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline();
+        }
+        if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) {
             geometryCache->renderWireShapeInstance(args, batch, geometryShape, outColor, pipeline);
         } else {
             geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline);
diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp
index 4ddb398fbf..d7da8e7e1a 100644
--- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp
@@ -49,6 +49,9 @@ ShapeKey TextEntityRenderer::getShapeKey() {
     if (isTransparent()) {
         builder.withTranslucent();
     }
+    if (_primitiveMode == PrimitiveMode::LINES) {
+        builder.withWireframe();
+    }
     return builder.build();
 }
 
diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp
index 498f0ff066..024c891da4 100644
--- a/libraries/entities/src/EntityItem.cpp
+++ b/libraries/entities/src/EntityItem.cpp
@@ -95,6 +95,8 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param
     requestedProperties += PROP_QUERY_AA_CUBE;
     requestedProperties += PROP_CAN_CAST_SHADOW;
     // requestedProperties += PROP_VISIBLE_IN_SECONDARY_CAMERA; // not sent over the wire
+    requestedProperties += PROP_RENDER_LAYER;
+    requestedProperties += PROP_PRIMITIVE_MODE;
     withReadLock([&] {
         requestedProperties += _grabProperties.getEntityProperties(params);
     });
@@ -263,8 +265,8 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet
         APPEND_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, getRegistrationPoint());
         APPEND_ENTITY_PROPERTY(PROP_CREATED, getCreated());
         APPEND_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, getLastEditedBy());
-        // APPEND_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, getEntityHostType());  // not sent over the wire
-        // APPEND_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, getOwningAvatarID());  // not sent over the wire
+        // APPEND_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, (uint32_t)getEntityHostType());  // not sent over the wire
+        // APPEND_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, getOwningAvatarID());            // not sent over the wire
         // convert AVATAR_SELF_ID to actual sessionUUID.
         QUuid actualParentID = getParentID();
         if (actualParentID == AVATAR_SELF_ID) {
@@ -276,6 +278,8 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet
         APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, getQueryAACube());
         APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, getCanCastShadow());
         // APPEND_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, getIsVisibleInSecondaryCamera()); // not sent over the wire
+        APPEND_ENTITY_PROPERTY(PROP_RENDER_LAYER, (uint32_t)getRenderLayer());
+        APPEND_ENTITY_PROPERTY(PROP_PRIMITIVE_MODE, (uint32_t)getPrimitiveMode());
         withReadLock([&] {
             _grabProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties,
                 propertyFlags, propertiesDidntFit, propertyCount, appendState);
@@ -770,7 +774,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
 
     auto lastEdited = lastEditedFromBufferAdjusted;
     bool otherOverwrites = overwriteLocalData && !weOwnSimulation;
-    auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) {
+    auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) {
+        if (stillHasGrabActions()) {
+            return false;
+        }
         bool simulationChanged = lastEdited > updatedTimestamp;
         return otherOverwrites && simulationChanged && (valueChanged || filterRejection);
     };
@@ -839,6 +846,8 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
     }
     READ_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow);
     // READ_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, bool, setIsVisibleInSecondaryCamera);  // not sent over the wire
+    READ_ENTITY_PROPERTY(PROP_RENDER_LAYER, RenderLayer, setRenderLayer);
+    READ_ENTITY_PROPERTY(PROP_PRIMITIVE_MODE, PrimitiveMode, setPrimitiveMode);
     withWriteLock([&] {
         int bytesFromGrab = _grabProperties.readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args,
             propertyFlags, overwriteLocalData,
@@ -1310,6 +1319,8 @@ EntityItemProperties EntityItem::getProperties(const EntityPropertyFlags& desire
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(queryAACube, getQueryAACube);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(isVisibleInSecondaryCamera, isVisibleInSecondaryCamera);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(renderLayer, getRenderLayer);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(primitiveMode, getPrimitiveMode);
     withReadLock([&] {
         _grabProperties.getProperties(properties);
     });
@@ -1454,6 +1465,8 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) {
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(isVisibleInSecondaryCamera, setIsVisibleInSecondaryCamera);
+    SET_ENTITY_PROPERTY_FROM_PROPERTIES(renderLayer, setRenderLayer);
+    SET_ENTITY_PROPERTY_FROM_PROPERTIES(primitiveMode, setPrimitiveMode);
     withWriteLock([&] {
         bool grabPropertiesChanged = _grabProperties.setProperties(properties);
         somethingChanged |= grabPropertiesChanged;
@@ -2931,6 +2944,46 @@ void EntityItem::setIsVisibleInSecondaryCamera(bool value) {
     }
 }
 
+RenderLayer EntityItem::getRenderLayer() const {
+    return resultWithReadLock<RenderLayer>([&] {
+        return _renderLayer;
+    });
+}
+
+void EntityItem::setRenderLayer(RenderLayer value) {
+    bool changed = false;
+    withWriteLock([&] {
+        if (_renderLayer != value) {
+            changed = true;
+            _renderLayer = value;
+        }
+    });
+
+    if (changed) {
+        emit requestRenderUpdate();
+    }
+}
+
+PrimitiveMode EntityItem::getPrimitiveMode() const {
+    return resultWithReadLock<PrimitiveMode>([&] {
+        return _primitiveMode;
+    });
+}
+
+void EntityItem::setPrimitiveMode(PrimitiveMode value) {
+    bool changed = false;
+    withWriteLock([&] {
+        if (_primitiveMode != value) {
+            changed = true;
+            _primitiveMode = value;
+        }
+    });
+
+    if (changed) {
+        emit requestRenderUpdate();
+    }
+}
+
 bool EntityItem::getCanCastShadow() const {
     bool result;
     withReadLock([&] {
@@ -3349,7 +3402,8 @@ void EntityItem::prepareForSimulationOwnershipBid(EntityItemProperties& properti
 }
 
 bool EntityItem::isWearable() const {
-    return isVisible() && (getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || getParentID() == AVATAR_SELF_ID);
+    return isVisible() &&
+        (getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || getParentID() == AVATAR_SELF_ID);
 }
 
 void EntityItem::addGrab(GrabPointer grab) {
@@ -3368,7 +3422,8 @@ void EntityItem::addGrab(GrabPointer grab) {
         EntityDynamicType dynamicType;
         QVariantMap arguments;
         int grabParentJointIndex =grab->getParentJointIndex();
-        if (grabParentJointIndex == FARGRAB_RIGHTHAND_INDEX || grabParentJointIndex == FARGRAB_LEFTHAND_INDEX) {
+        if (grabParentJointIndex == FARGRAB_RIGHTHAND_INDEX || grabParentJointIndex == FARGRAB_LEFTHAND_INDEX ||
+            grabParentJointIndex == FARGRAB_MOUSE_INDEX) {
             // add a far-grab action
             dynamicType = DYNAMIC_TYPE_FAR_GRAB;
             arguments["otherID"] = grab->getOwnerID();
diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h
index 826a9c34a0..cac4192cd5 100644
--- a/libraries/entities/src/EntityItem.h
+++ b/libraries/entities/src/EntityItem.h
@@ -293,6 +293,12 @@ public:
     bool isVisibleInSecondaryCamera() const;
     void setIsVisibleInSecondaryCamera(bool value);
 
+    RenderLayer getRenderLayer() const;
+    void setRenderLayer(RenderLayer value);
+
+    PrimitiveMode getPrimitiveMode() const;
+    void setPrimitiveMode(PrimitiveMode value);
+
     bool getCanCastShadow() const;
     void setCanCastShadow(bool value);
 
@@ -621,6 +627,8 @@ protected:
     float _angularDamping { ENTITY_ITEM_DEFAULT_ANGULAR_DAMPING };
     bool _visible { ENTITY_ITEM_DEFAULT_VISIBLE };
     bool _isVisibleInSecondaryCamera { ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA };
+    RenderLayer _renderLayer { RenderLayer::WORLD };
+    PrimitiveMode _primitiveMode { PrimitiveMode::SOLID };
     bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW };
     bool _collisionless { ENTITY_ITEM_DEFAULT_COLLISIONLESS };
     uint16_t _collisionMask { ENTITY_COLLISION_MASK_DEFAULT };
diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp
index 1fdff19e38..60592abe91 100644
--- a/libraries/entities/src/EntityItemProperties.cpp
+++ b/libraries/entities/src/EntityItemProperties.cpp
@@ -340,17 +340,70 @@ QString EntityItemProperties::getBillboardModeAsString() const {
     return BillboardModeHelpers::getNameForBillboardMode(_billboardMode);
 }
 
-void EntityItemProperties::setBillboardModeFromString(const QString& materialMappingMode) {
+void EntityItemProperties::setBillboardModeFromString(const QString& billboardMode) {
     if (stringToBillboardModeLookup.empty()) {
         buildStringToBillboardModeLookup();
     }
-    auto billboardModeItr = stringToBillboardModeLookup.find(materialMappingMode.toLower());
+    auto billboardModeItr = stringToBillboardModeLookup.find(billboardMode.toLower());
     if (billboardModeItr != stringToBillboardModeLookup.end()) {
         _billboardMode = billboardModeItr.value();
         _billboardModeChanged = true;
     }
 }
 
+QHash<QString, RenderLayer> stringToRenderLayerLookup;
+
+void addRenderLayer(RenderLayer mode) {
+    stringToRenderLayerLookup[RenderLayerHelpers::getNameForRenderLayer(mode)] = mode;
+}
+
+void buildStringToRenderLayerLookup() {
+    addRenderLayer(RenderLayer::WORLD);
+    addRenderLayer(RenderLayer::FRONT);
+    addRenderLayer(RenderLayer::HUD);
+}
+
+QString EntityItemProperties::getRenderLayerAsString() const {
+    return RenderLayerHelpers::getNameForRenderLayer(_renderLayer);
+}
+
+void EntityItemProperties::setRenderLayerFromString(const QString& renderLayer) {
+    if (stringToRenderLayerLookup.empty()) {
+        buildStringToRenderLayerLookup();
+    }
+    auto renderLayerItr = stringToRenderLayerLookup.find(renderLayer.toLower());
+    if (renderLayerItr != stringToRenderLayerLookup.end()) {
+        _renderLayer = renderLayerItr.value();
+        _renderLayerChanged = true;
+    }
+}
+
+QHash<QString, PrimitiveMode> stringToPrimitiveModeLookup;
+
+void addPrimitiveMode(PrimitiveMode mode) {
+    stringToPrimitiveModeLookup[PrimitiveModeHelpers::getNameForPrimitiveMode(mode)] = mode;
+}
+
+void buildStringToPrimitiveModeLookup() {
+    addPrimitiveMode(PrimitiveMode::SOLID);
+    addPrimitiveMode(PrimitiveMode::LINES);
+}
+
+QString EntityItemProperties::getPrimitiveModeAsString() const {
+    return PrimitiveModeHelpers::getNameForPrimitiveMode(_primitiveMode);
+}
+
+void EntityItemProperties::setPrimitiveModeFromString(const QString& primitiveMode) {
+    if (stringToPrimitiveModeLookup.empty()) {
+        buildStringToPrimitiveModeLookup();
+    }
+    auto primitiveModeItr = stringToPrimitiveModeLookup.find(primitiveMode.toLower());
+    if (primitiveModeItr != stringToPrimitiveModeLookup.end()) {
+        _primitiveMode = primitiveModeItr.value();
+        _primitiveModeChanged = true;
+    }
+}
+
 EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
     EntityPropertyFlags changedProperties;
 
@@ -375,6 +428,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
     CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube);
     CHECK_PROPERTY_CHANGE(PROP_CAN_CAST_SHADOW, canCastShadow);
     CHECK_PROPERTY_CHANGE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera);
+    CHECK_PROPERTY_CHANGE(PROP_RENDER_LAYER, renderLayer);
+    CHECK_PROPERTY_CHANGE(PROP_PRIMITIVE_MODE, primitiveMode);
     changedProperties += _grab.getChangedProperties();
 
     // Physics
@@ -474,6 +529,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
     CHECK_PROPERTY_CHANGE(PROP_JOINT_TRANSLATIONS_SET, jointTranslationsSet);
     CHECK_PROPERTY_CHANGE(PROP_JOINT_TRANSLATIONS, jointTranslations);
     CHECK_PROPERTY_CHANGE(PROP_RELAY_PARENT_JOINTS, relayParentJoints);
+    CHECK_PROPERTY_CHANGE(PROP_GROUP_CULLED, groupCulled);
     changedProperties += _animation.getChangedProperties();
 
     // Light
@@ -579,15 +635,15 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  *     the <code>shape</code> property set for entities of these types.) <em>Read-only.</em>
  * @property {EntityHostType} entityHostType="domain" - How this entity will behave, including if and how it is sent to other people.
  *     The value can only be set at entity creation by using the <code>entityHostType</code> parameter in
- *     {@link Entities.addEntity}.
+ *     {@link Entities.addEntity}.  Read-only.
  * @property {boolean} avatarEntity=false - If <code>true</code> then the entity is an avatar entity;  An avatar entity follows you to each domain you visit,
  *     rendering at the same world coordinates unless it's parented to your avatar. <em>Value cannot be changed after the entity is created.</em><br />
  *     The value can only be set at entity creation by using the <code>entityHostType</code> parameter in 
- *     {@link Entities.addEntity}.  <code>clientOnly</code> is an alias.
+ *     {@link Entities.addEntity}.  <code>clientOnly</code> is an alias.  Read-only.
  * @property {boolean} localEntity=false - If <code>true</code> then the entity is a local entity;  Local entities only render for you and are not sent over the wire.
  *     <em>Value cannot be changed after the entity is created.</em><br />
  *     The value can only be set at entity creation by using the <code>entityHostType</code> parameter in
- *     {@link Entities.addEntity}.
+ *     {@link Entities.addEntity}.  Read-only.
  * @property {Uuid} owningAvatarID=Uuid.NULL - The session ID of the owning avatar if <code>avatarEntity</code> is 
  *     <code>true</code>, otherwise {@link Uuid|Uuid.NULL}. <em>Read-only.</em>
  *
@@ -611,6 +667,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  *     {@link Entities.EntityType|Zone} entity with <code>castShadows</code> enabled in its 
  *     {@link Entities.EntityProperties-Zone|keyLight} property.
  * @property {boolean} isVisibleInSecondaryCamera=true - Whether or not the entity is rendered in the secondary camera. If <code>true</code> then the entity is rendered.
+ * @property {RenderLayer} renderLayer="world" - In which layer this entity renders.
+ * @property {PrimitiveMode} primitiveMode="solid" - How this entity's geometry is rendered.
  *
  * @property {Vec3} position=0,0,0 - The position of the entity.
  * @property {Quat} rotation=0,0,0,1 - The orientation of the entity with respect to world coordinates.
@@ -841,10 +899,14 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  * @property {number} priority=0 - The priority for applying the material to its parent. Only the highest priority material is 
  *     applied, with materials of the same priority randomly assigned. Materials that come with the model have a priority of 
  *     <code>0</code>.
- * @property {string|number} parentMaterialName="0" - Selects the submesh or submeshes within the parent to apply the material 
- *     to. If in the format <code>"mat::string"</code>, all submeshes with material name <code>"string"</code> are replaced. 
- *     Otherwise the property value is parsed as an unsigned integer, specifying the mesh index to modify. Invalid values are 
- *     parsed to <code>0</code>.
+ * @property {string} parentMaterialName="0" - Selects the mesh part or parts within the parent to which to apply the material.
+ *     If in the format <code>"mat::string"</code>, all mesh parts with material name <code>"string"</code> are replaced.
+ *     Otherwise the property value is parsed as an unsigned integer, specifying the mesh part index to modify.  If <code>"all"</code>,
+ *     all mesh parts will be replaced.  If an array (starts with <code>"["</code> and ends with <code>"]"</code>), the string will be
+ *     split at each <code>","</code> and each element will be parsed as either a number or a string if it starts with
+ *     <code>"mat::"</code>.  In other words, <code>"[0,1,mat::string,mat::string2]"</code> will replace mesh parts 0 and 1, and any
+ *     mesh parts with material <code>"string"</code> or <code>"string2"</code>.  Do not put spaces around the commas.  Invalid values
+ *     are parsed to <code>0</code>.
  * @property {string} materialMappingMode="uv" - How the material is mapped to the entity. Either <code>"uv"</code> or 
  *     <code>"projected"</code>. In "uv" mode, the material will be evaluated within the UV space of the mesh it is applied to.  In
  *     "projected" mode, the 3D transform of the Material Entity will be used to evaluate the texture coordinates for the material.
@@ -900,7 +962,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  *     parse the JSON string into a JavaScript object of name, URL pairs. <em>Read-only.</em>
  *
  * @property {ShapeType} shapeType="none" - The shape of the collision hull used if collisions are enabled.
- * @property {string} compoundShapeURL="" - The OBJ file to use for the compound shape if <code>shapeType</code> is
+ * @property {string} compoundShapeURL="" - The model file to use for the compound shape if <code>shapeType</code> is
  *     <code>"compound"</code>.
  *
  * @property {Entities.AnimationProperties} animation - An animation to play on the model.
@@ -925,6 +987,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  *     {@link Entities.getJointIndex|getJointIndex}.
  * @property {boolean} relayParentJoints=false - If <code>true</code> and the entity is parented to an avatar, then the 
  *     avatar's joint rotations are applied to the entity's joints.
+ * @property {boolean} groupCulled=false - If <code>true</code>, the mesh parts of the model are LOD culled as a group.
+ *     If <code>false</code>, separate mesh parts will be LOD culled individually.
  *
  * @example <caption>Rez a Vive tracker puck.</caption>
  * var entity = Entities.addEntity({
@@ -1238,7 +1302,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  * @property {ShapeType} shapeType="box" - The shape of the volume in which the zone's lighting effects and avatar 
  *     permissions have effect. Reverts to the default value if set to <code>"none"</code>, or set to <code>"compound"</code> 
  *     and <code>compoundShapeURL</code> is <code>""</code>.
-  * @property {string} compoundShapeURL="" - The OBJ file to use for the compound shape if <code>shapeType</code> is 
+  * @property {string} compoundShapeURL="" - The model file to use for the compound shape if <code>shapeType</code> is 
  *     <code>"compound"</code>.
  *
  * @property {string} keyLightMode="inherit" - Configures the key light in the zone. Possible values:<br />
@@ -1416,6 +1480,8 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_QUERY_AA_CUBE, queryAACube);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CAN_CAST_SHADOW, canCastShadow);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera);
+    COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_RENDER_LAYER, renderLayer, getRenderLayerAsString());
+    COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_PRIMITIVE_MODE, primitiveMode, getPrimitiveModeAsString());
     _grab.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
 
     // Physics
@@ -1532,6 +1598,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_TRANSLATIONS_SET, jointTranslationsSet);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_TRANSLATIONS, jointTranslations);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RELAY_PARENT_JOINTS, relayParentJoints);
+        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GROUP_CULLED, groupCulled);
         if (!psuedoPropertyFlagsButDesiredEmpty) {
             _animation.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
         }
@@ -1797,6 +1864,8 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
     COPY_PROPERTY_FROM_QSCRIPTVALUE(queryAACube, AACube, setQueryAACube); // TODO: should scripts be able to set this?
     COPY_PROPERTY_FROM_QSCRIPTVALUE(canCastShadow, bool, setCanCastShadow);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(isVisibleInSecondaryCamera, bool, setIsVisibleInSecondaryCamera);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(renderLayer, RenderLayer);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(primitiveMode, PrimitiveMode);
     _grab.copyFromScriptValue(object, _defaultSettings);
 
     // Physics
@@ -1901,6 +1970,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
     COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslations, qVectorVec3, setJointTranslations);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(relayParentJoints, bool, setRelayParentJoints);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE(groupCulled, bool, setGroupCulled);
     _animation.copyFromScriptValue(object, _defaultSettings);
 
     // Light
@@ -2068,6 +2138,8 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
     COPY_PROPERTY_IF_CHANGED(queryAACube);
     COPY_PROPERTY_IF_CHANGED(canCastShadow);
     COPY_PROPERTY_IF_CHANGED(isVisibleInSecondaryCamera);
+    COPY_PROPERTY_IF_CHANGED(renderLayer);
+    COPY_PROPERTY_IF_CHANGED(primitiveMode);
     _grab.merge(other._grab);
 
     // Physics
@@ -2167,6 +2239,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
     COPY_PROPERTY_IF_CHANGED(jointTranslationsSet);
     COPY_PROPERTY_IF_CHANGED(jointTranslations);
     COPY_PROPERTY_IF_CHANGED(relayParentJoints);
+    COPY_PROPERTY_IF_CHANGED(groupCulled);
     _animation.merge(other._animation);
 
     // Light
@@ -2338,6 +2411,8 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         ADD_PROPERTY_TO_MAP(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube);
         ADD_PROPERTY_TO_MAP(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool);
         ADD_PROPERTY_TO_MAP(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool);
+        ADD_PROPERTY_TO_MAP(PROP_RENDER_LAYER, RenderLayer, renderLayer, RenderLayer);
+        ADD_PROPERTY_TO_MAP(PROP_PRIMITIVE_MODE, PrimitiveMode, primitiveMode, PrimitiveMode);
         { // Grab
             ADD_GROUP_PROPERTY_TO_MAP(PROP_GRAB_GRABBABLE, Grab, grab, Grabbable, grabbable);
             ADD_GROUP_PROPERTY_TO_MAP(PROP_GRAB_KINEMATIC, Grab, grab, GrabKinematic, grabKinematic);
@@ -2491,6 +2566,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         ADD_PROPERTY_TO_MAP(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector<bool>);
         ADD_PROPERTY_TO_MAP(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector<vec3>);
         ADD_PROPERTY_TO_MAP(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool);
+        ADD_PROPERTY_TO_MAP(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool);
         { // Animation
             ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_URL, Animation, animation, URL, url);
             ADD_GROUP_PROPERTY_TO_MAP(PROP_ANIMATION_ALLOW_TRANSLATION, Animation, animation, AllowTranslation, allowTranslation);
@@ -2767,6 +2843,8 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
             APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, properties.getQueryAACube());
             APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, properties.getCanCastShadow());
             // APPEND_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, properties.getIsVisibleInSecondaryCamera()); // not sent over the wire
+            APPEND_ENTITY_PROPERTY(PROP_RENDER_LAYER, (uint32_t)properties.getRenderLayer());
+            APPEND_ENTITY_PROPERTY(PROP_PRIMITIVE_MODE, (uint32_t)properties.getPrimitiveMode());
             _staticGrab.setProperties(properties);
             _staticGrab.appendToEditPacket(packetData, requestedProperties, propertyFlags,
                                            propertiesDidntFit, propertyCount, appendState);
@@ -2873,6 +2951,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
                 APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, properties.getJointTranslationsSet());
                 APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, properties.getJointTranslations());
                 APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, properties.getRelayParentJoints());
+                APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, properties.getGroupCulled());
 
                 _staticAnimation.setProperties(properties);
                 _staticAnimation.appendToEditPacket(packetData, requestedProperties, propertyFlags, propertiesDidntFit, propertyCount, appendState);
@@ -3208,6 +3287,8 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_QUERY_AA_CUBE, AACube, setQueryAACube);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow);
     // READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_VISIBLE_IN_SECONDARY_CAMERA, bool, setIsVisibleInSecondaryCamera); // not sent over the wire
+    READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_RENDER_LAYER, RenderLayer, setRenderLayer);
+    READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PRIMITIVE_MODE, PrimitiveMode, setPrimitiveMode);
     properties.getGrab().decodeFromEditPacket(propertyFlags, dataAt, processedBytes);
 
     // Physics
@@ -3312,6 +3393,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_JOINT_TRANSLATIONS_SET, QVector<bool>, setJointTranslationsSet);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_JOINT_TRANSLATIONS, QVector<vec3>, setJointTranslations);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints);
+        READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GROUP_CULLED, bool, setGroupCulled);
 
         properties.getAnimation().decodeFromEditPacket(propertyFlags, dataAt, processedBytes);
     }
@@ -3590,6 +3672,8 @@ void EntityItemProperties::markAllChanged() {
     _queryAACubeChanged = true;
     _canCastShadowChanged = true;
     _isVisibleInSecondaryCameraChanged = true;
+    _renderLayerChanged = true;
+    _primitiveModeChanged = true;
     _grab.markAllChanged();
 
     // Physics
@@ -3682,6 +3766,7 @@ void EntityItemProperties::markAllChanged() {
     _jointTranslationsSetChanged = true;
     _jointTranslationsChanged = true;
     _relayParentJointsChanged = true;
+    _groupCulledChanged = true;
     _animation.markAllChanged();
 
     // Light
@@ -3962,6 +4047,12 @@ QList<QString> EntityItemProperties::listChangedProperties() {
     if (isVisibleInSecondaryCameraChanged()) {
         out += "isVisibleInSecondaryCamera";
     }
+    if (renderLayerChanged()) {
+        out += "renderLayer";
+    }
+    if (primitiveModeChanged()) {
+        out += "primitiveMode";
+    }
     getGrab().listChangedProperties(out);
 
     // Physics
@@ -4208,6 +4299,9 @@ QList<QString> EntityItemProperties::listChangedProperties() {
     if (relayParentJointsChanged()) {
         out += "relayParentJoints";
     }
+    if (groupCulledChanged()) {
+        out += "groupCulled";
+    }
     getAnimation().listChangedProperties(out);
 
     // Light
diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h
index bb4d8c5878..86150efe3a 100644
--- a/libraries/entities/src/EntityItemProperties.h
+++ b/libraries/entities/src/EntityItemProperties.h
@@ -51,6 +51,8 @@
 
 #include "MaterialMappingMode.h"
 #include "BillboardMode.h"
+#include "RenderLayer.h"
+#include "PrimitiveMode.h"
 
 const quint64 UNKNOWN_CREATED_TIME = 0;
 
@@ -169,6 +171,8 @@ public:
     DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube());
     DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW);
     DEFINE_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool, ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA);
+    DEFINE_PROPERTY_REF_ENUM(PROP_RENDER_LAYER, RenderLayer, renderLayer, RenderLayer, RenderLayer::WORLD);
+    DEFINE_PROPERTY_REF_ENUM(PROP_PRIMITIVE_MODE, PrimitiveMode, primitiveMode, PrimitiveMode, PrimitiveMode::SOLID);
     DEFINE_PROPERTY_GROUP(Grab, grab, GrabPropertyGroup);
 
     // Physics
@@ -268,6 +272,7 @@ public:
     DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector<bool>, QVector<bool>());
     DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS, JointTranslations, jointTranslations, QVector<glm::vec3>, ENTITY_ITEM_DEFAULT_EMPTY_VEC3_QVEC);
     DEFINE_PROPERTY(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool, ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS);
+    DEFINE_PROPERTY_REF(PROP_GROUP_CULLED, GroupCulled, groupCulled, bool, false);
     DEFINE_PROPERTY_GROUP(Animation, animation, AnimationPropertyGroup);
 
     // Light
diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h
index 5e4b27858c..a3d0d937cb 100644
--- a/libraries/entities/src/EntityPropertyFlags.h
+++ b/libraries/entities/src/EntityPropertyFlags.h
@@ -39,6 +39,8 @@ enum EntityPropertyList {
     PROP_QUERY_AA_CUBE,
     PROP_CAN_CAST_SHADOW,
     PROP_VISIBLE_IN_SECONDARY_CAMERA, // not sent over the wire
+    PROP_RENDER_LAYER,
+    PROP_PRIMITIVE_MODE,
     // Grab
     PROP_GRAB_GRABBABLE,
     PROP_GRAB_KINEMATIC,
@@ -198,16 +200,17 @@ enum EntityPropertyList {
     PROP_JOINT_TRANSLATIONS_SET = PROP_DERIVED_3,
     PROP_JOINT_TRANSLATIONS = PROP_DERIVED_4,
     PROP_RELAY_PARENT_JOINTS = PROP_DERIVED_5,
+    PROP_GROUP_CULLED = PROP_DERIVED_6,
     // Animation
-    PROP_ANIMATION_URL = PROP_DERIVED_6,
-    PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_7,
-    PROP_ANIMATION_FPS = PROP_DERIVED_8,
-    PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_9,
-    PROP_ANIMATION_PLAYING = PROP_DERIVED_10,
-    PROP_ANIMATION_LOOP = PROP_DERIVED_11,
-    PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_12,
-    PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_13,
-    PROP_ANIMATION_HOLD = PROP_DERIVED_14,
+    PROP_ANIMATION_URL = PROP_DERIVED_7,
+    PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_8,
+    PROP_ANIMATION_FPS = PROP_DERIVED_9,
+    PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_10,
+    PROP_ANIMATION_PLAYING = PROP_DERIVED_11,
+    PROP_ANIMATION_LOOP = PROP_DERIVED_12,
+    PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_13,
+    PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_14,
+    PROP_ANIMATION_HOLD = PROP_DERIVED_15,
 
     // Light
     PROP_IS_SPOTLIGHT = PROP_DERIVED_0,
diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp
index b8babc60b8..4c64a38e26 100644
--- a/libraries/entities/src/EntityTree.cpp
+++ b/libraries/entities/src/EntityTree.cpp
@@ -3008,8 +3008,8 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object,
     // if the queryBox has changed, tell the entity-server
     EntityItemPointer entity = std::dynamic_pointer_cast<EntityItem>(object);
     if (entity) {
-        bool tellServerThis = tellServer && (entity->getEntityHostType() != entity::HostType::AVATAR);
-        if ((entity->updateQueryAACube() || force)) {
+        // NOTE: we rely on side-effects of the entity->updateQueryAACube() call in the following if() conditional:
+        if (entity->updateQueryAACube() || force) {
             bool success;
             AACube newCube = entity->getQueryAACube(success);
             if (success) {
@@ -3017,7 +3017,7 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object,
             }
             // send an edit packet to update the entity-server about the queryAABox.  We do this for domain-hosted
             // entities as well as for avatar-entities; the packet-sender will route the update accordingly
-            if (tellServerThis && packetSender && (entity->isDomainEntity() || entity->isAvatarEntity())) {
+            if (tellServer && packetSender && (entity->isDomainEntity() || entity->isAvatarEntity())) {
                 quint64 now = usecTimestampNow();
                 EntityItemProperties properties = entity->getProperties();
                 properties.setQueryAACubeDirty();
@@ -3025,6 +3025,7 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object,
                 properties.setLastEdited(now);
 
                 packetSender->queueEditEntityMessage(PacketType::EntityEdit, getThisPointer(), entity->getID(), properties);
+                entity->setLastEdited(now); // so we ignore the echo from the server
                 entity->setLastBroadcast(now); // for debug/physics status icons
             }
 
diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp
index 55ae1c6c3b..ddbb028b6e 100644
--- a/libraries/entities/src/ModelEntityItem.cpp
+++ b/libraries/entities/src/ModelEntityItem.cpp
@@ -67,6 +67,7 @@ EntityItemProperties ModelEntityItem::getProperties(const EntityPropertyFlags& d
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslationsSet, getJointTranslationsSet);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslations, getJointTranslations);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(relayParentJoints, getRelayParentJoints);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(groupCulled, getGroupCulled);
     withReadLock([&] {
         _animationProperties.getProperties(properties);
     });
@@ -88,6 +89,7 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) {
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslationsSet, setJointTranslationsSet);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslations, setJointTranslations);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(relayParentJoints, setRelayParentJoints);
+    SET_ENTITY_PROPERTY_FROM_PROPERTIES(groupCulled, setGroupCulled);
 
     withWriteLock([&] {
         AnimationPropertyGroup animationProperties = _animationProperties;
@@ -130,6 +132,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
     READ_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, QVector<bool>, setJointTranslationsSet);
     READ_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, QVector<glm::vec3>, setJointTranslations);
     READ_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, bool, setRelayParentJoints);
+    READ_ENTITY_PROPERTY(PROP_GROUP_CULLED, bool, setGroupCulled);
 
     // grab a local copy of _animationProperties to avoid multiple locks
     int bytesFromAnimation;
@@ -166,6 +169,7 @@ EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams&
     requestedProperties += PROP_JOINT_TRANSLATIONS_SET;
     requestedProperties += PROP_JOINT_TRANSLATIONS;
     requestedProperties += PROP_RELAY_PARENT_JOINTS;
+    requestedProperties += PROP_GROUP_CULLED;
     requestedProperties += _animationProperties.getEntityProperties(params);
 
     return requestedProperties;
@@ -192,6 +196,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit
     APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, getJointTranslationsSet());
     APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS, getJointTranslations());
     APPEND_ENTITY_PROPERTY(PROP_RELAY_PARENT_JOINTS, getRelayParentJoints());
+    APPEND_ENTITY_PROPERTY(PROP_GROUP_CULLED, getGroupCulled());
 
     withReadLock([&] {
         _animationProperties.appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties,
@@ -548,6 +553,18 @@ bool ModelEntityItem::getRelayParentJoints() const {
     });
 }
 
+void ModelEntityItem::setGroupCulled(bool value) {
+    withWriteLock([&] {
+        _groupCulled = value;
+    });
+}
+
+bool ModelEntityItem::getGroupCulled() const {
+    return resultWithReadLock<bool>([&] {
+        return _groupCulled;
+    });
+}
+
 QString ModelEntityItem::getCompoundShapeURL() const {
     return _compoundShapeURL.get();
 }
diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h
index 5ca3e2caa1..8c9fbdc45f 100644
--- a/libraries/entities/src/ModelEntityItem.h
+++ b/libraries/entities/src/ModelEntityItem.h
@@ -100,6 +100,9 @@ public:
     void setRelayParentJoints(bool relayJoints);
     bool getRelayParentJoints() const;
 
+    void setGroupCulled(bool value);
+    bool getGroupCulled() const;
+
     bool getAnimationIsPlaying() const;
     float getAnimationCurrentFrame() const;
     float getAnimationFPS() const;
@@ -154,6 +157,7 @@ protected:
     glm::u8vec3 _color;
     QString _modelURL;
     bool _relayParentJoints;
+    bool _groupCulled { false };
 
     ThreadSafeValueCache<QString> _compoundShapeURL;
 
diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp
index 68019c2f4b..4c82b4f5d7 100644
--- a/libraries/fbx/src/FBXSerializer.cpp
+++ b/libraries/fbx/src/FBXSerializer.cpp
@@ -131,6 +131,7 @@ public:
     glm::vec3 geometricTranslation;
     glm::quat geometricRotation;
     glm::vec3 geometricScaling;
+    bool isLimbNode;  // is this FBXModel transform is a "LimbNode" i.e. a joint
 };
 
 glm::mat4 getGlobalTransform(const QMultiMap<QString, QString>& _connectionParentMap,
@@ -559,9 +560,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
                     glm::vec3 geometricRotation;
 
                     glm::vec3 rotationMin, rotationMax;
+
+                    bool isLimbNode = object.properties.size() >= 3 && object.properties.at(2) == "LimbNode";
                     FBXModel fbxModel = { name, -1, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(),
-                                       glm::mat4(), glm::vec3(), glm::vec3(),
-                                       false, glm::vec3(), glm::quat(), glm::vec3(1.0f) };
+                                          glm::mat4(), glm::vec3(), glm::vec3(),
+                                          false, glm::vec3(), glm::quat(), glm::vec3(1.0f), isLimbNode };
                     ExtractedMesh* mesh = NULL;
                     QVector<ExtractedBlendshape> blendshapes;
                     foreach (const FBXNode& subobject, object.children) {
@@ -1258,6 +1261,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
     // convert the models to joints
     QVariantList freeJoints = mapping.values("freeJoint");
     hfmModel.hasSkeletonJoints = false;
+
     foreach (const QString& modelID, modelIDs) {
         const FBXModel& fbxModel = fbxModels[modelID];
         HFMJoint joint;
@@ -1288,6 +1292,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
         joint.geometricTranslation = fbxModel.geometricTranslation;
         joint.geometricRotation = fbxModel.geometricRotation;
         joint.geometricScaling = fbxModel.geometricScaling;
+        joint.isSkeletonJoint = fbxModel.isLimbNode;
+        hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint);
 
         glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation;
 
@@ -1311,14 +1317,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
             joint.name = hfmModel.hfmToHifiJointNameMapping.key(fbxModel.name);
         }
 
-        foreach (const QString& childID, _connectionChildMap.values(modelID)) {
-            QString type = typeFlags.value(childID);
-            if (!type.isEmpty()) {
-                hfmModel.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton"));
-                break;
-            }
-        }
-
         joint.bindTransformFoundInCluster = false;
 
         hfmModel.joints.append(joint);
diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp
index 418dc8f417..7ab2296935 100644
--- a/libraries/networking/src/udt/Connection.cpp
+++ b/libraries/networking/src/udt/Connection.cpp
@@ -31,6 +31,7 @@ using namespace udt;
 using namespace std::chrono;
 
 Connection::Connection(Socket* parentSocket, HifiSockAddr destination, std::unique_ptr<CongestionControl> congestionControl) :
+    QObject(parentSocket),
     _parentSocket(parentSocket),
     _destination(destination),
     _congestionControl(move(congestionControl))
diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp
index 642914cd56..7f20f881da 100644
--- a/libraries/networking/src/udt/PacketHeaders.cpp
+++ b/libraries/networking/src/udt/PacketHeaders.cpp
@@ -33,15 +33,15 @@ PacketVersion versionForPacketType(PacketType packetType) {
         case PacketType::EntityEdit:
         case PacketType::EntityData:
         case PacketType::EntityPhysics:
-            return static_cast<PacketVersion>(EntityVersion::FixProtocolVersionBumpMismatch);
+            return static_cast<PacketVersion>(EntityVersion::LAST_PACKET_TYPE);
         case PacketType::EntityQuery:
             return static_cast<PacketVersion>(EntityQueryPacketVersion::ConicalFrustums);
         case PacketType::AvatarIdentity:
         case PacketType::AvatarData:
-            return static_cast<PacketVersion>(AvatarMixerPacketVersion::CollisionFlag);
+            return static_cast<PacketVersion>(AvatarMixerPacketVersion::SendMaxTranslationDimension);
         case PacketType::BulkAvatarData:
         case PacketType::KillAvatar:
-            return static_cast<PacketVersion>(AvatarMixerPacketVersion::FasterAvatarEntities);
+            return static_cast<PacketVersion>(AvatarMixerPacketVersion::SendMaxTranslationDimension);
         case PacketType::MessagesData:
             return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
         // ICE packets
@@ -93,8 +93,6 @@ PacketVersion versionForPacketType(PacketType packetType) {
             return static_cast<PacketVersion>(PingVersion::IncludeConnectionID);
         case PacketType::AvatarQuery:
             return static_cast<PacketVersion>(AvatarQueryVersion::ConicalFrustums);
-        case PacketType::AvatarIdentityRequest:
-            return 22;
         case PacketType::EntityQueryInitialResultsComplete:
             return static_cast<PacketVersion>(EntityVersion::ParticleSpin);
         case PacketType::BulkAvatarTraitsAck:
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index f53a287d71..2425d6f689 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -57,7 +57,7 @@ public:
         ICEServerQuery,
         OctreeStats,
         SetAvatarTraits,
-        AvatarIdentityRequest,
+        UNUSED_PACKET_TYPE,
         AssignmentClientStatus,
         NoisyMute,
         AvatarIdentity,
@@ -255,7 +255,12 @@ enum class EntityVersion : PacketVersion {
     MorePropertiesCleanup,
     FixPropertiesFromCleanup,
     UpdatedPolyLines,
-    FixProtocolVersionBumpMismatch
+    FixProtocolVersionBumpMismatch,
+    MigrateOverlayRenderProperties,
+
+    // Add new versions above here
+    NUM_PACKET_TYPE,
+    LAST_PACKET_TYPE = NUM_PACKET_TYPE - 1
 };
 
 enum class EntityScriptCallMethodVersion : PacketVersion {
@@ -312,7 +317,8 @@ enum class AvatarMixerPacketVersion : PacketVersion {
     GrabTraits,
     CollisionFlag,
     AvatarTraitsAck,
-    FasterAvatarEntities
+    FasterAvatarEntities,
+    SendMaxTranslationDimension
 };
 
 enum class DomainConnectRequestVersion : PacketVersion {
diff --git a/libraries/octree/src/OctreePacketData.h b/libraries/octree/src/OctreePacketData.h
index bd1abf8744..debc6786f1 100644
--- a/libraries/octree/src/OctreePacketData.h
+++ b/libraries/octree/src/OctreePacketData.h
@@ -35,6 +35,8 @@
 
 #include "MaterialMappingMode.h"
 #include "BillboardMode.h"
+#include "RenderLayer.h"
+#include "PrimitiveMode.h"
 
 #include "OctreeConstants.h"
 #include "OctreeElement.h"
@@ -263,6 +265,8 @@ public:
     static int unpackDataFromBytes(const unsigned char* dataBytes, ShapeType& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); }
     static int unpackDataFromBytes(const unsigned char* dataBytes, MaterialMappingMode& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); }
     static int unpackDataFromBytes(const unsigned char* dataBytes, BillboardMode& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); }
+    static int unpackDataFromBytes(const unsigned char* dataBytes, RenderLayer& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); }
+    static int unpackDataFromBytes(const unsigned char* dataBytes, PrimitiveMode& result) { memcpy(&result, dataBytes, sizeof(result)); return sizeof(result); }
     static int unpackDataFromBytes(const unsigned char* dataBytes, glm::vec2& result);
     static int unpackDataFromBytes(const unsigned char* dataBytes, glm::vec3& result);
     static int unpackDataFromBytes(const unsigned char* dataBytes, glm::u8vec3& result);
diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp
index 8fd6d4eada..d5ded6f909 100755
--- a/libraries/physics/src/CharacterController.cpp
+++ b/libraries/physics/src/CharacterController.cpp
@@ -109,7 +109,8 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
             }
             _dynamicsWorld = nullptr;
         }
-        int32_t collisionGroup = computeCollisionGroup();
+        int32_t collisionMask = computeCollisionMask();
+        int32_t collisionGroup = BULLET_COLLISION_GROUP_MY_AVATAR; 
         if (_rigidBody) {
             updateMassProperties();
         }
@@ -117,7 +118,7 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
             // add to new world
             _dynamicsWorld = world;
             _pendingFlags &= ~PENDING_FLAG_JUMP;
-            _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
+            _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, collisionMask); 
             _dynamicsWorld->addAction(this);
             // restore gravity settings because adding an object to the world overwrites its gravity setting
             _rigidBody->setGravity(_currentGravity * _currentUp);
@@ -127,7 +128,7 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) {
             assert(shape && shape->getShapeType() == CONVEX_HULL_SHAPE_PROXYTYPE);
             _ghost.setCharacterShape(static_cast<btConvexHullShape*>(shape));
         }
-        _ghost.setCollisionGroupAndMask(collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR & (~ collisionGroup));
+        _ghost.setCollisionGroupAndMask(collisionGroup, collisionMask & (~ collisionGroup)); 
         _ghost.setCollisionWorld(_dynamicsWorld);
         _ghost.setRadiusAndHalfHeight(_radius, _halfHeight);
         if (_rigidBody) {
@@ -384,8 +385,8 @@ static const char* stateToStr(CharacterController::State state) {
 #endif // #ifdef DEBUG_STATE_CHANGE
 
 void CharacterController::updateCurrentGravity() {
-    int32_t collisionGroup = computeCollisionGroup();
-    if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) {
+    int32_t collisionMask = computeCollisionMask();
+    if (_state == State::Hover || collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS) {
         _currentGravity = 0.0f;
     } else {
         _currentGravity = _gravity;
@@ -458,28 +459,7 @@ void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const
 void CharacterController::setCollisionless(bool collisionless) {
     if (collisionless != _collisionless) {
         _collisionless = collisionless;
-        _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP;
-    }
-}
-
-int32_t CharacterController::computeCollisionGroup() const {
-    if (_collisionless) {
-        return _collisionlessAllowed ? BULLET_COLLISION_GROUP_COLLISIONLESS : BULLET_COLLISION_GROUP_MY_AVATAR;
-    } else {
-        return BULLET_COLLISION_GROUP_MY_AVATAR;
-    }
-}
-
-void CharacterController::handleChangedCollisionGroup() {
-    if (_pendingFlags & PENDING_FLAG_UPDATE_COLLISION_GROUP) {
-        // ATM the easiest way to update collision groups is to remove/re-add the RigidBody
-        if (_dynamicsWorld) {
-            _dynamicsWorld->removeRigidBody(_rigidBody);
-            int32_t collisionGroup = computeCollisionGroup();
-            _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR);
-        }
-        _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP;
-        updateCurrentGravity();
+        _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK;
     }
 }
 
@@ -567,8 +547,8 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel
     btScalar angle = motor.rotation.getAngle();
     btVector3 velocity = worldVelocity.rotate(axis, -angle);
 
-    int32_t collisionGroup = computeCollisionGroup();
-    if (collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ||
+    int32_t collisionMask = computeCollisionMask();
+    if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS ||
             _state == State::Hover || motor.hTimescale == motor.vTimescale) {
         // modify velocity
         btScalar tau = dt / motor.hTimescale;
@@ -708,11 +688,11 @@ void CharacterController::updateState() {
     btVector3 rayStart = _position;
 
     btScalar rayLength = _radius;
-    int32_t collisionGroup = computeCollisionGroup();
-    if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) {
-        rayLength += _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT;
-    } else {
+    int32_t collisionMask = computeCollisionMask();
+    if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS) {
         rayLength += MIN_HOVER_HEIGHT;
+    } else {
+        rayLength += _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT;
     }
     btVector3 rayEnd = rayStart - rayLength * _currentUp;
 
@@ -746,69 +726,7 @@ void CharacterController::updateState() {
 
     // disable normal state transitions while collisionless
     const btScalar MAX_WALKING_SPEED = 2.65f;
-    if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) {
-        switch (_state) {
-        case State::Ground:
-            if (!rayHasHit && !_hasSupport) {
-                SET_STATE(State::Hover, "no ground detected");
-            } else if (_pendingFlags & PENDING_FLAG_JUMP && _jumpButtonDownCount != _takeoffJumpButtonID) {
-                _takeoffJumpButtonID = _jumpButtonDownCount;
-                _takeoffToInAirStartTime = now;
-                SET_STATE(State::Takeoff, "jump pressed");
-            } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_FLY_THRESHOLD) {
-                SET_STATE(State::InAir, "falling");
-            }
-            break;
-        case State::Takeoff:
-            if (!rayHasHit && !_hasSupport) {
-                SET_STATE(State::Hover, "no ground");
-            } else if ((now - _takeoffToInAirStartTime) > TAKE_OFF_TO_IN_AIR_PERIOD) {
-                SET_STATE(State::InAir, "takeoff done");
-
-                // compute jumpSpeed based on the scaled jump height for the default avatar in default gravity.
-                const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT);
-                const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
-                velocity += jumpSpeed * _currentUp;
-                _rigidBody->setLinearVelocity(velocity);
-            }
-            break;
-        case State::InAir: {
-            const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT);
-            const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
-            if ((velocity.dot(_currentUp) <= (jumpSpeed / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) {
-                SET_STATE(State::Ground, "hit ground");
-            } else if (_flyingAllowed) {
-                btVector3 desiredVelocity = _targetVelocity;
-                if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) {
-                    desiredVelocity = btVector3(0.0f, 0.0f, 0.0f);
-                }
-                bool vertTargetSpeedIsNonZero = desiredVelocity.dot(_currentUp) > MIN_TARGET_SPEED;
-                if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (_takeoffJumpButtonID != _jumpButtonDownCount)) {
-                    SET_STATE(State::Hover, "double jump button");
-                } else if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) {
-                    SET_STATE(State::Hover, "jump button held");
-                } else if (_floorDistance > _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT) {
-                    // Transition to hover if we are above the fall threshold
-                    SET_STATE(State::Hover, "above fall threshold");
-                }
-            } else if (!rayHasHit && !_hasSupport) {
-                SET_STATE(State::Hover, "no ground detected");
-            }
-            break;
-        }
-        case State::Hover:
-            btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length();
-            bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f);
-            if (!_flyingAllowed && rayHasHit) {
-                SET_STATE(State::InAir, "flying not allowed");
-            } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
-                SET_STATE(State::InAir, "near ground");
-            } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
-                SET_STATE(State::Ground, "touching ground");
-            }
-            break;
-        }
-    } else {
+    if (collisionMask == BULLET_COLLISION_MASK_COLLISIONLESS) {
         // when collisionless: only switch between State::Ground and State::Hover
         // and bypass state debugging
         if (rayHasHit) {
@@ -820,6 +738,68 @@ void CharacterController::updateState() {
         } else {
             _state = State::Hover;
         }
+    } else {
+        switch (_state) {
+            case State::Ground:
+                if (!rayHasHit && !_hasSupport) {
+                    SET_STATE(State::Hover, "no ground detected");
+                } else if (_pendingFlags & PENDING_FLAG_JUMP && _jumpButtonDownCount != _takeoffJumpButtonID) {
+                    _takeoffJumpButtonID = _jumpButtonDownCount;
+                    _takeoffToInAirStartTime = now;
+                    SET_STATE(State::Takeoff, "jump pressed");
+                } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_FLY_THRESHOLD) {
+                    SET_STATE(State::InAir, "falling");
+                }
+                break;
+            case State::Takeoff:
+                if (!rayHasHit && !_hasSupport) {
+                    SET_STATE(State::Hover, "no ground");
+                } else if ((now - _takeoffToInAirStartTime) > TAKE_OFF_TO_IN_AIR_PERIOD) {
+                    SET_STATE(State::InAir, "takeoff done");
+
+                    // compute jumpSpeed based on the scaled jump height for the default avatar in default gravity.
+                    const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT);
+                    const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
+                    velocity += jumpSpeed * _currentUp;
+                    _rigidBody->setLinearVelocity(velocity);
+                }
+                break;
+            case State::InAir: {
+                const float jumpHeight = std::max(_scaleFactor * DEFAULT_AVATAR_JUMP_HEIGHT, DEFAULT_AVATAR_MIN_JUMP_HEIGHT);
+                const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
+                if ((velocity.dot(_currentUp) <= (jumpSpeed / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) {
+                    SET_STATE(State::Ground, "hit ground");
+                } else if (_flyingAllowed) {
+                    btVector3 desiredVelocity = _targetVelocity;
+                    if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) {
+                        desiredVelocity = btVector3(0.0f, 0.0f, 0.0f);
+                    }
+                    bool vertTargetSpeedIsNonZero = desiredVelocity.dot(_currentUp) > MIN_TARGET_SPEED;
+                    if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (_takeoffJumpButtonID != _jumpButtonDownCount)) {
+                        SET_STATE(State::Hover, "double jump button");
+                    } else if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) {
+                        SET_STATE(State::Hover, "jump button held");
+                    } else if (_floorDistance > _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT) {
+                        // Transition to hover if we are above the fall threshold
+                        SET_STATE(State::Hover, "above fall threshold");
+                    }
+                } else if (!rayHasHit && !_hasSupport) {
+                    SET_STATE(State::Hover, "no ground detected");
+                }
+                break;
+            }
+            case State::Hover:
+                btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length();
+                bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f);
+                if (!_flyingAllowed && rayHasHit) {
+                    SET_STATE(State::InAir, "flying not allowed");
+                } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
+                    SET_STATE(State::InAir, "near ground");
+                } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
+                    SET_STATE(State::Ground, "touching ground");
+                }
+                break;
+        }
     }
 }
 
@@ -866,6 +846,6 @@ void CharacterController::setFlyingAllowed(bool value) {
 void CharacterController::setCollisionlessAllowed(bool value) {
     if (value != _collisionlessAllowed) {
         _collisionlessAllowed = value;
-        _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_GROUP;
+        _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK;
     }
 }
diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h
old mode 100644
new mode 100755
index 50db2bea12..cac37da0b9
--- a/libraries/physics/src/CharacterController.h
+++ b/libraries/physics/src/CharacterController.h
@@ -30,7 +30,7 @@ const uint32_t PENDING_FLAG_ADD_TO_SIMULATION = 1U << 0;
 const uint32_t PENDING_FLAG_REMOVE_FROM_SIMULATION = 1U << 1;
 const uint32_t PENDING_FLAG_UPDATE_SHAPE = 1U << 2;
 const uint32_t PENDING_FLAG_JUMP = 1U << 3;
-const uint32_t PENDING_FLAG_UPDATE_COLLISION_GROUP = 1U << 4;
+const uint32_t PENDING_FLAG_UPDATE_COLLISION_MASK = 1U << 4;
 const uint32_t PENDING_FLAG_RECOMPUTE_FLYING = 1U << 5;
 const float DEFAULT_MIN_FLOOR_NORMAL_DOT_UP = cosf(PI / 3.0f);
 
@@ -120,14 +120,16 @@ public:
     bool isStuck() const { return _isStuck; }
 
     void setCollisionless(bool collisionless);
-    int32_t computeCollisionGroup() const;
-    void handleChangedCollisionGroup();
+
+    virtual int32_t computeCollisionMask() const = 0;
+    virtual void handleChangedCollisionMask() = 0;
 
     bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation);
 
     void setFlyingAllowed(bool value);
     void setCollisionlessAllowed(bool value);
 
+    void setPendingFlagsUpdateCollisionMask(){ _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK; }
 
 protected:
 #ifdef DEBUG_STATE_CHANGE
diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp
index a546f69d23..0d0209e35f 100644
--- a/libraries/plugins/src/plugins/PluginManager.cpp
+++ b/libraries/plugins/src/plugins/PluginManager.cpp
@@ -14,6 +14,11 @@
 #include <QtCore/QDebug>
 #include <QtCore/QPluginLoader>
 
+//#define HIFI_PLUGINMANAGER_DEBUG
+#if defined(HIFI_PLUGINMANAGER_DEBUG)
+#include <QJsonDocument>
+#endif
+
 #include <DependencyManager.h>
 #include <UserActivityLogger.h>
 
@@ -79,10 +84,7 @@ bool isDisabled(QJsonObject metaData) {
     return false;
 }
 
-using Loader = QSharedPointer<QPluginLoader>;
-using LoaderList = QList<Loader>;
-
-const LoaderList& getLoadedPlugins() {
+ auto PluginManager::getLoadedPlugins() const -> const LoaderList& {
     static std::once_flag once;
     static LoaderList loadedPlugins;
     std::call_once(once, [&] {
@@ -106,15 +108,25 @@ const LoaderList& getLoadedPlugins() {
             for (auto plugin : candidates) {
                 qCDebug(plugins) << "Attempting plugin" << qPrintable(plugin);
                 QSharedPointer<QPluginLoader> loader(new QPluginLoader(pluginPath + plugin));
-
-                if (isDisabled(loader->metaData())) {
+                const QJsonObject pluginMetaData = loader->metaData();
+#if defined(HIFI_PLUGINMANAGER_DEBUG)
+                QJsonDocument metaDataDoc(pluginMetaData);
+                qCInfo(plugins) << "Metadata for " << qPrintable(plugin) << ": " << QString(metaDataDoc.toJson());
+#endif
+                if (isDisabled(pluginMetaData)) {
                     qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "is disabled";
                     // Skip this one, it's disabled
                     continue;
                 }
-                if (getPluginInterfaceVersionFromMetaData(loader->metaData()) != HIFI_PLUGIN_INTERFACE_VERSION) {
+
+                if (!_pluginFilter(pluginMetaData)) {
+                    qCDebug(plugins) << "Plugin" << qPrintable(plugin) << "doesn't pass provided filter";
+                    continue;
+                }
+
+                if (getPluginInterfaceVersionFromMetaData(pluginMetaData) != HIFI_PLUGIN_INTERFACE_VERSION) {
                     qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "interface version doesn't match, not loading:"
-                                       << getPluginInterfaceVersionFromMetaData(loader->metaData())
+                                       << getPluginInterfaceVersionFromMetaData(pluginMetaData)
                                        << "doesn't match" << HIFI_PLUGIN_INTERFACE_VERSION;
                     continue;
                 }
diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h
index 593daeca11..1a578c7406 100644
--- a/libraries/plugins/src/plugins/PluginManager.h
+++ b/libraries/plugins/src/plugins/PluginManager.h
@@ -13,8 +13,7 @@
 
 #include "Forward.h"
 
-
-class PluginManager;
+class QPluginLoader;
 using PluginManagerPointer = QSharedPointer<PluginManager>;
 
 class PluginManager : public QObject, public Dependency {
@@ -48,6 +47,9 @@ public:
     void setInputPluginSettingsPersister(const InputPluginSettingsPersister& persister);
     QStringList getRunningInputDeviceNames() const;
 
+    using PluginFilter = std::function<bool(const QJsonObject&)>;
+    void setPluginFilter(PluginFilter pluginFilter) { _pluginFilter = pluginFilter; }
+
 signals:
     void inputDeviceRunningChanged(const QString& pluginName, bool isRunning, const QStringList& runningDevices);
     
@@ -61,6 +63,12 @@ private:
     PluginContainer* _container { nullptr };
     DisplayPluginList _displayPlugins;
     InputPluginList _inputPlugins;
+    PluginFilter _pluginFilter { [](const QJsonObject&) { return true; } };
+
+    using Loader = QSharedPointer<QPluginLoader>;
+    using LoaderList = QList<Loader>;
+
+    const LoaderList& getLoadedPlugins() const;
 };
 
 // TODO: we should define this value in CMake, and then use CMake
diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp
index 3e32d19b49..81a81c5602 100644
--- a/libraries/render-utils/src/CauterizedModel.cpp
+++ b/libraries/render-utils/src/CauterizedModel.cpp
@@ -215,7 +215,7 @@ void CauterizedModel::updateRenderItems() {
             modelTransform.setTranslation(self->getTranslation());
             modelTransform.setRotation(self->getRotation());
 
-            bool isWireframe = self->isWireframe();
+            PrimitiveMode primitiveMode = self->getPrimitiveMode();
             auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags();
             bool enableCauterization = self->getEnableCauterization();
 
@@ -232,7 +232,7 @@ void CauterizedModel::updateRenderItems() {
                 bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning();
 
                 transaction.updateItem<ModelMeshPartPayload>(itemID, [modelTransform, meshState, useDualQuaternionSkinning, cauterizedMeshState, invalidatePayloadShapeKey,
-                        isWireframe, renderItemKeyGlobalFlags, enableCauterization](ModelMeshPartPayload& mmppData) {
+                        primitiveMode, renderItemKeyGlobalFlags, enableCauterization](ModelMeshPartPayload& mmppData) {
                     CauterizedMeshPartPayload& data = static_cast<CauterizedMeshPartPayload&>(mmppData);
                     if (useDualQuaternionSkinning) {
                         data.updateClusterBuffer(meshState.clusterDualQuaternions,
@@ -276,7 +276,7 @@ void CauterizedModel::updateRenderItems() {
 
                     data.setEnableCauterization(enableCauterization);
                     data.updateKey(renderItemKeyGlobalFlags);
-                    data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning);
+                    data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning);
                 });
             }
 
diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp
index d6d6f4903e..02f51e558a 100644
--- a/libraries/render-utils/src/GeometryCache.cpp
+++ b/libraries/render-utils/src/GeometryCache.cpp
@@ -721,11 +721,16 @@ QHash<SimpleProgramKey, gpu::PipelinePointer> GeometryCache::_simplePrograms;
 gpu::ShaderPointer GeometryCache::_simpleShader;
 gpu::ShaderPointer GeometryCache::_transparentShader;
 gpu::ShaderPointer GeometryCache::_unlitShader;
+gpu::ShaderPointer GeometryCache::_forwardSimpleShader;
+gpu::ShaderPointer GeometryCache::_forwardTransparentShader;
+gpu::ShaderPointer GeometryCache::_forwardUnlitShader;
 gpu::ShaderPointer GeometryCache::_simpleFadeShader;
 gpu::ShaderPointer GeometryCache::_unlitFadeShader;
 
 render::ShapePipelinePointer GeometryCache::_simpleOpaquePipeline;
 render::ShapePipelinePointer GeometryCache::_simpleTransparentPipeline;
+render::ShapePipelinePointer GeometryCache::_forwardSimpleOpaquePipeline;
+render::ShapePipelinePointer GeometryCache::_forwardSimpleTransparentPipeline;
 render::ShapePipelinePointer GeometryCache::_simpleOpaqueFadePipeline;
 render::ShapePipelinePointer GeometryCache::_simpleTransparentFadePipeline;
 render::ShapePipelinePointer GeometryCache::_simpleWirePipeline;
@@ -805,6 +810,8 @@ void GeometryCache::initializeShapePipelines() {
     if (!_simpleOpaquePipeline) {
         _simpleOpaquePipeline = getShapePipeline(false, false, true, false);
         _simpleTransparentPipeline = getShapePipeline(false, true, true, false);
+        _forwardSimpleOpaquePipeline = getShapePipeline(false, false, true, false, false, true);
+        _forwardSimpleTransparentPipeline = getShapePipeline(false, true, true, false, false, true);
         _simpleOpaqueFadePipeline = getFadingShapePipeline(false, false, false, false, false);
         _simpleTransparentFadePipeline = getFadingShapePipeline(false, true, false, false, false);
         _simpleWirePipeline = getShapePipeline(false, false, true, true);
@@ -812,9 +819,9 @@ void GeometryCache::initializeShapePipelines() {
 }
 
 render::ShapePipelinePointer GeometryCache::getShapePipeline(bool textured, bool transparent, bool culled,
-    bool unlit, bool depthBias) {
+    bool unlit, bool depthBias, bool forward) {
 
-    return std::make_shared<render::ShapePipeline>(getSimplePipeline(textured, transparent, culled, unlit, depthBias, false, true), nullptr,
+    return std::make_shared<render::ShapePipeline>(getSimplePipeline(textured, transparent, culled, unlit, depthBias, false, true, forward), nullptr,
         [](const render::ShapePipeline& pipeline, gpu::Batch& batch, render::Args* args) {
             batch.setResourceTexture(gr::Texture::MaterialAlbedo, DependencyManager::get<TextureCache>()->getWhiteTexture());
             DependencyManager::get<DeferredLightingEffect>()->setupKeyLightBatch(args, batch);
@@ -2165,6 +2172,7 @@ public:
         HAS_DEPTH_BIAS_FLAG,
         IS_FADING_FLAG,
         IS_ANTIALIASED_FLAG,
+        IS_FORWARD_FLAG,
 
         NUM_FLAGS,
     };
@@ -2177,6 +2185,7 @@ public:
         HAS_DEPTH_BIAS = (1 << HAS_DEPTH_BIAS_FLAG),
         IS_FADING = (1 << IS_FADING_FLAG),
         IS_ANTIALIASED = (1 << IS_ANTIALIASED_FLAG),
+        IS_FORWARD = (1 << IS_FORWARD_FLAG),
     };
     typedef unsigned short Flags;
 
@@ -2189,6 +2198,7 @@ public:
     bool hasDepthBias() const { return isFlag(HAS_DEPTH_BIAS); }
     bool isFading() const { return isFlag(IS_FADING); }
     bool isAntiAliased() const { return isFlag(IS_ANTIALIASED); }
+    bool isForward() const { return isFlag(IS_FORWARD); }
 
     Flags _flags = 0;
 #if defined(__clang__)
@@ -2200,9 +2210,9 @@ public:
 
 
     SimpleProgramKey(bool textured = false, bool transparent = false, bool culled = true,
-        bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true) {
+        bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true, bool forward = false) {
         _flags = (textured ? IS_TEXTURED : 0) | (transparent ? IS_TRANSPARENT : 0) | (culled ? IS_CULLED : 0) |
-            (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAntiAliased ? IS_ANTIALIASED : 0);
+            (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0) | (fading ? IS_FADING : 0) | (isAntiAliased ? IS_ANTIALIASED : 0) | (forward ? IS_FORWARD : 0);
     }
 
     SimpleProgramKey(int bitmask) : _flags(bitmask) {}
@@ -2255,8 +2265,8 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool tra
     }
 }
 
-gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAntiAliased) {
-    SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAntiAliased };
+gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased, bool fading, bool isAntiAliased, bool forward) {
+    SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased, fading, isAntiAliased, forward };
 
     // If the pipeline already exists, return it
     auto it = _simplePrograms.find(config);
@@ -2269,13 +2279,20 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp
         static std::once_flag once;
         std::call_once(once, [&]() {
             using namespace shader::render_utils::program;
-            auto PS = DISABLE_DEFERRED ? forward_simple_textured : simple_textured;
-            // Use the forward pipeline for both here, otherwise transparents will be unlit
-            auto PSTransparent = DISABLE_DEFERRED ? forward_simple_textured_transparent : forward_simple_textured_transparent;
-            auto PSUnlit = DISABLE_DEFERRED ? forward_simple_textured_unlit : simple_textured_unlit;
-            _simpleShader = gpu::Shader::createProgram(PS);
-            _transparentShader = gpu::Shader::createProgram(PSTransparent);
-            _unlitShader = gpu::Shader::createProgram(PSUnlit);
+
+            _forwardSimpleShader = gpu::Shader::createProgram(forward_simple_textured);
+            _forwardTransparentShader = gpu::Shader::createProgram(forward_simple_textured_transparent);
+            _forwardUnlitShader = gpu::Shader::createProgram(forward_simple_textured_unlit);
+            if (DISABLE_DEFERRED) {
+                _simpleShader = _forwardSimpleShader;
+                _transparentShader = _forwardTransparentShader;
+                _unlitShader = _forwardUnlitShader;
+            } else {
+                _simpleShader = gpu::Shader::createProgram(simple_textured);
+                // Use the forward pipeline for both here, otherwise transparents will be unlit
+                _transparentShader = gpu::Shader::createProgram(forward_simple_textured_transparent);
+                _unlitShader = gpu::Shader::createProgram(simple_textured_unlit);
+            }
         });
     } else {
         static std::once_flag once;
@@ -2308,8 +2325,14 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp
         PrepareStencil::testMaskDrawShapeNoAA(*state);
     }
 
-    gpu::ShaderPointer program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _unlitShader) :
-                                                      (config.isFading() ? _simpleFadeShader : (config.isTransparent() ? _transparentShader : _simpleShader));
+    gpu::ShaderPointer program;
+    if (config.isForward()) {
+        program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _forwardUnlitShader) :
+                                       (config.isFading() ? _simpleFadeShader : (config.isTransparent() ? _forwardTransparentShader : _forwardSimpleShader));
+    } else {
+        program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _unlitShader) :
+                                       (config.isFading() ? _simpleFadeShader : (config.isTransparent() ? _transparentShader : _simpleShader));
+    }
     gpu::PipelinePointer pipeline = gpu::Pipeline::create(program, state);
     _simplePrograms.insert(config, pipeline);
     return pipeline;
diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h
index 589be4dcc1..e46bb4a899 100644
--- a/libraries/render-utils/src/GeometryCache.h
+++ b/libraries/render-utils/src/GeometryCache.h
@@ -174,7 +174,7 @@ public:
                                           bool unlit = false, bool depthBias = false, bool isAntiAliased = true);
     // Get the pipeline to render static geometry
     static gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool culled = true,
-                                          bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true);
+                                          bool unlit = false, bool depthBias = false, bool fading = false, bool isAntiAliased = true, bool forward = false);
 
     void bindWebBrowserProgram(gpu::Batch& batch, bool transparent = false);
     gpu::PipelinePointer getWebBrowserProgram(bool transparent);
@@ -183,6 +183,8 @@ public:
 
     render::ShapePipelinePointer getOpaqueShapePipeline() { assert(_simpleOpaquePipeline != nullptr); return _simpleOpaquePipeline; }
     render::ShapePipelinePointer getTransparentShapePipeline() { assert(_simpleTransparentPipeline != nullptr); return _simpleTransparentPipeline; }
+    render::ShapePipelinePointer getForwardOpaqueShapePipeline() { assert(_forwardSimpleOpaquePipeline != nullptr); return _forwardSimpleOpaquePipeline; }
+    render::ShapePipelinePointer getForwardTransparentShapePipeline() { assert(_forwardSimpleTransparentPipeline != nullptr); return _forwardSimpleTransparentPipeline; }
     render::ShapePipelinePointer getOpaqueFadeShapePipeline() { assert(_simpleOpaqueFadePipeline != nullptr); return _simpleOpaqueFadePipeline; }
     render::ShapePipelinePointer getTransparentFadeShapePipeline() { assert(_simpleTransparentFadePipeline != nullptr); return _simpleTransparentFadePipeline; }
     render::ShapePipelinePointer getOpaqueShapePipeline(bool isFading);
@@ -465,13 +467,19 @@ private:
     QHash<int, Vec2FloatPairPair> _lastRegisteredGridBuffer;
     QHash<int, GridBuffer> _registeredGridBuffers;
 
+    // FIXME: clean these up
     static gpu::ShaderPointer _simpleShader;
     static gpu::ShaderPointer _transparentShader;
     static gpu::ShaderPointer _unlitShader;
+    static gpu::ShaderPointer _forwardSimpleShader;
+    static gpu::ShaderPointer _forwardTransparentShader;
+    static gpu::ShaderPointer _forwardUnlitShader;
     static gpu::ShaderPointer _simpleFadeShader;
     static gpu::ShaderPointer _unlitFadeShader;
     static render::ShapePipelinePointer _simpleOpaquePipeline;
     static render::ShapePipelinePointer _simpleTransparentPipeline;
+    static render::ShapePipelinePointer _forwardSimpleOpaquePipeline;
+    static render::ShapePipelinePointer _forwardSimpleTransparentPipeline;
     static render::ShapePipelinePointer _simpleOpaqueFadePipeline;
     static render::ShapePipelinePointer _simpleTransparentFadePipeline;
     static render::ShapePipelinePointer _simpleWirePipeline;
@@ -485,7 +493,7 @@ private:
     gpu::PipelinePointer _simpleTransparentWebBrowserPipeline;
 
     static render::ShapePipelinePointer getShapePipeline(bool textured = false, bool transparent = false, bool culled = true,
-        bool unlit = false, bool depthBias = false);
+        bool unlit = false, bool depthBias = false, bool forward = false);
     static render::ShapePipelinePointer getFadingShapePipeline(bool textured = false, bool transparent = false, bool culled = true,
         bool unlit = false, bool depthBias = false);
 };
diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp
index ca2e56862d..7a52ad77da 100644
--- a/libraries/render-utils/src/MeshPartPayload.cpp
+++ b/libraries/render-utils/src/MeshPartPayload.cpp
@@ -342,7 +342,7 @@ void ModelMeshPartPayload::updateKey(const render::ItemKey& key) {
     _itemKey = builder.build();
 }
 
-void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning) {
+void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, PrimitiveMode primitiveMode, bool useDualQuaternionSkinning) {
     if (invalidateShapeKey) {
         _shapeKey = ShapeKey::Builder::invalid();
         return;
@@ -359,6 +359,7 @@ void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe
     bool isUnlit = drawMaterialKey.isUnlit();
 
     bool isDeformed = _isBlendShaped || _isSkinned;
+    bool isWireframe = primitiveMode == PrimitiveMode::LINES;
 
     if (isWireframe) {
         isTranslucent = hasTangents = hasLightmap = false;
diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h
index 29c0091f11..3b0590b4a9 100644
--- a/libraries/render-utils/src/MeshPartPayload.h
+++ b/libraries/render-utils/src/MeshPartPayload.h
@@ -109,7 +109,7 @@ public:
     render::ShapeKey getShapeKey() const override; // shape interface
     void render(RenderArgs* args) override;
 
-    void setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning);
+    void setShapeKey(bool invalidateShapeKey, PrimitiveMode primitiveMode, bool useDualQuaternionSkinning);
 
     // ModelMeshPartPayload functions to perform render
     void bindMesh(gpu::Batch& batch) override;
diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp
index ec29fb009e..da8dceb176 100644
--- a/libraries/render-utils/src/Model.cpp
+++ b/libraries/render-utils/src/Model.cpp
@@ -62,7 +62,6 @@ Model::Model(QObject* parent, SpatiallyNestable* spatiallyNestableOverride) :
     _snapModelToRegistrationPoint(false),
     _snappedToRegistrationPoint(false),
     _url(HTTP_INVALID_COM),
-    _isWireframe(false),
     _renderItemKeyGlobalFlags(render::ItemKey::Builder().withVisible().withTagBits(render::hifi::TAG_ALL_VIEWS).build())
 {
     // we may have been created in the network thread, but we live in the main thread
@@ -223,7 +222,7 @@ void Model::updateRenderItems() {
         Transform modelTransform = self->getTransform();
         modelTransform.setScale(glm::vec3(1.0f));
 
-        bool isWireframe = self->isWireframe();
+        PrimitiveMode primitiveMode = self->getPrimitiveMode();
         auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags();
 
         render::Transaction transaction;
@@ -238,7 +237,7 @@ void Model::updateRenderItems() {
             bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning();
 
             transaction.updateItem<ModelMeshPartPayload>(itemID, [modelTransform, meshState, useDualQuaternionSkinning,
-                                                                  invalidatePayloadShapeKey, isWireframe, renderItemKeyGlobalFlags](ModelMeshPartPayload& data) {
+                                                                  invalidatePayloadShapeKey, primitiveMode, renderItemKeyGlobalFlags](ModelMeshPartPayload& data) {
                 if (useDualQuaternionSkinning) {
                     data.updateClusterBuffer(meshState.clusterDualQuaternions);
                 } else {
@@ -263,7 +262,7 @@ void Model::updateRenderItems() {
                 data.updateTransformForSkinnedMesh(renderTransform, modelTransform);
 
                 data.updateKey(renderItemKeyGlobalFlags);
-                data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning);
+                data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning);
             });
         }
 
@@ -276,6 +275,11 @@ void Model::setRenderItemsNeedUpdate() {
     emit requestRenderUpdate();
 }
 
+void Model::setPrimitiveMode(PrimitiveMode primitiveMode) {
+    _primitiveMode = primitiveMode;
+    setRenderItemsNeedUpdate();
+}
+
 void Model::reset() {
     if (isLoaded()) {
         const HFMModel& hfmModel = getHFMModel();
@@ -878,30 +882,14 @@ bool Model::canCastShadow() const {
     return _renderItemKeyGlobalFlags.isShadowCaster();
 }
 
-void Model::setLayeredInFront(bool layeredInFront, const render::ScenePointer& scene) {
-    if (Model::isLayeredInFront() != layeredInFront) {
+void Model::setHifiRenderLayer(render::hifi::Layer renderLayer, const render::ScenePointer& scene) {
+    if (_renderItemKeyGlobalFlags.getLayer() != renderLayer) {
         auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags);
-        _renderItemKeyGlobalFlags = (layeredInFront ? keyBuilder.withLayer(render::hifi::LAYER_3D_FRONT) : keyBuilder.withoutLayer());
+        _renderItemKeyGlobalFlags = keyBuilder.withLayer(renderLayer);
         updateRenderItemsKey(scene);
     }
 }
 
-bool Model::isLayeredInFront() const {
-    return _renderItemKeyGlobalFlags.isLayer(render::hifi::LAYER_3D_FRONT);
-}
-
-void Model::setLayeredInHUD(bool layeredInHUD, const render::ScenePointer& scene) {
-    if (Model::isLayeredInHUD() != layeredInHUD) {
-        auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags);
-        _renderItemKeyGlobalFlags = (layeredInHUD ? keyBuilder.withLayer(render::hifi::LAYER_3D_HUD) : keyBuilder.withoutLayer());
-        updateRenderItemsKey(scene);
-    }
-}
-
-bool Model::isLayeredInHUD() const {
-    return _renderItemKeyGlobalFlags.isLayer(render::hifi::LAYER_3D_HUD);
-}
-
 void Model::setTagMask(uint8_t mask, const render::ScenePointer& scene) {
     if (Model::getTagMask() != mask) {
         auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags);
@@ -920,6 +908,7 @@ void Model::setGroupCulled(bool groupCulled, const render::ScenePointer& scene)
         updateRenderItemsKey(scene);
     }
 }
+
 bool Model::isGroupCulled() const {
     return _renderItemKeyGlobalFlags.isSubMetaCulled();
 }
@@ -1486,44 +1475,71 @@ bool Model::isRenderable() const {
     return !_meshStates.empty() || (isLoaded() && _renderGeometry->getMeshes().empty());
 }
 
-std::vector<unsigned int> Model::getMeshIDsFromMaterialID(QString parentMaterialName) {
-    // try to find all meshes with materials that match parentMaterialName as a string
-    // if none, return parentMaterialName as a uint
-    std::vector<unsigned int> toReturn;
-    const QString MATERIAL_NAME_PREFIX = "mat::";
-    if (parentMaterialName.startsWith(MATERIAL_NAME_PREFIX)) {
-        parentMaterialName.replace(0, MATERIAL_NAME_PREFIX.size(), QString(""));
-        for (unsigned int i = 0; i < (unsigned int)_modelMeshMaterialNames.size(); i++) {
-            if (_modelMeshMaterialNames[i] == parentMaterialName.toStdString()) {
-                toReturn.push_back(i);
-            }
-        }
-    }
+std::set<unsigned int> Model::getMeshIDsFromMaterialID(QString parentMaterialName) {
+    std::set<unsigned int> toReturn;
 
-    if (toReturn.empty()) {
-        toReturn.push_back(parentMaterialName.toUInt());
+    const QString all("all");
+    if (parentMaterialName == all) {
+        for (unsigned int i = 0; i < (unsigned int)_modelMeshRenderItemIDs.size(); i++) {
+            toReturn.insert(i);
+        }
+    } else if (!parentMaterialName.isEmpty()) {
+        auto parseFunc = [this, &toReturn] (QString& target) {
+            if (target.isEmpty()) {
+                return;
+            }
+            // if target starts with "mat::", try to find all meshes with materials that match target as a string
+            // otherwise, return target as a uint
+            const QString MATERIAL_NAME_PREFIX("mat::");
+            if (target.startsWith(MATERIAL_NAME_PREFIX)) {
+                std::string targetStdString = target.replace(0, MATERIAL_NAME_PREFIX.size(), "").toStdString();
+                for (unsigned int i = 0; i < (unsigned int)_modelMeshMaterialNames.size(); i++) {
+                    if (_modelMeshMaterialNames[i] == targetStdString) {
+                        toReturn.insert(i);
+                    }
+                }
+                return;
+            }
+            toReturn.insert(target.toUInt());
+        };
+
+        if (parentMaterialName.length() > 2 && parentMaterialName.startsWith("[") && parentMaterialName.endsWith("]")) {
+            QStringList list = parentMaterialName.split(",", QString::SkipEmptyParts);
+            for (int i = 0; i < list.length(); i++) {
+                auto& target = list[i];
+                if (i == 0) {
+                    target = target.replace(0, 1, "");
+                }
+                if (i == list.length() - 1) {
+                    target = target.replace(target.length() - 1, 1, "");
+                }
+                parseFunc(target);
+            }
+        } else {
+            parseFunc(parentMaterialName);
+        }
     }
 
     return toReturn;
 }
 
 void Model::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) {
-    std::vector<unsigned int> shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str()));
+    std::set<unsigned int> shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str()));
     render::Transaction transaction;
     for (auto shapeID : shapeIDs) {
         if (shapeID < _modelMeshRenderItemIDs.size()) {
             auto itemID = _modelMeshRenderItemIDs[shapeID];
             auto renderItemsKey = _renderItemKeyGlobalFlags;
-            bool wireframe = isWireframe();
+            PrimitiveMode primitiveMode = getPrimitiveMode();
             auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex;
             bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex);
             bool useDualQuaternionSkinning = _useDualQuaternionSkinning;
             transaction.updateItem<ModelMeshPartPayload>(itemID, [material, renderItemsKey,
-                invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) {
+                invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) {
                 data.addMaterial(material);
                 // if the material changed, we might need to update our item key or shape key
                 data.updateKey(renderItemsKey);
-                data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning);
+                data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning);
             });
         }
     }
@@ -1531,22 +1547,22 @@ void Model::addMaterial(graphics::MaterialLayer material, const std::string& par
 }
 
 void Model::removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) {
-    std::vector<unsigned int> shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str()));
+    std::set<unsigned int> shapeIDs = getMeshIDsFromMaterialID(QString(parentMaterialName.c_str()));
     render::Transaction transaction;
     for (auto shapeID : shapeIDs) {
         if (shapeID < _modelMeshRenderItemIDs.size()) {
             auto itemID = _modelMeshRenderItemIDs[shapeID];
             auto renderItemsKey = _renderItemKeyGlobalFlags;
-            bool wireframe = isWireframe();
+            PrimitiveMode primitiveMode = getPrimitiveMode();
             auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex;
             bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex);
             bool useDualQuaternionSkinning = _useDualQuaternionSkinning;
             transaction.updateItem<ModelMeshPartPayload>(itemID, [material, renderItemsKey,
-                invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) {
+                invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) {
                 data.removeMaterial(material);
                 // if the material changed, we might need to update our item key or shape key
                 data.updateKey(renderItemsKey);
-                data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning);
+                data.setShapeKey(invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning);
             });
         }
     }
diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h
index 0f8eb782c3..16e08c2b23 100644
--- a/libraries/render-utils/src/Model.h
+++ b/libraries/render-utils/src/Model.h
@@ -37,6 +37,7 @@
 #include "GeometryCache.h"
 #include "TextureCache.h"
 #include "Rig.h"
+#include "PrimitiveMode.h"
 
 // Use dual quaternion skinning!
 // Must match define in Skinning.slh
@@ -123,11 +124,7 @@ public:
     bool canCastShadow() const;
     void setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene = nullptr);
 
-    void setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene = nullptr);
-    void setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& scene = nullptr);
-
-    bool isLayeredInFront() const;
-    bool isLayeredInHUD() const;
+    void setHifiRenderLayer(render::hifi::Layer layer, const render::ScenePointer& scene = nullptr);
 
     // Access the current RenderItemKey Global Flags used by the model and applied to the render items  representing the parts of the model.
     const render::ItemKey getRenderItemKeyGlobalFlags() const;
@@ -166,8 +163,8 @@ public:
     bool isLoaded() const { return (bool)_renderGeometry && _renderGeometry->isHFMModelLoaded(); }
     bool isAddedToScene() const { return _addedToScene; }
 
-    void setIsWireframe(bool isWireframe) { _isWireframe = isWireframe; }
-    bool isWireframe() const { return _isWireframe; }
+    void setPrimitiveMode(PrimitiveMode primitiveMode);
+    PrimitiveMode getPrimitiveMode() const { return _primitiveMode; }
 
     void reset();
 
@@ -455,7 +452,7 @@ protected:
 
     virtual void createRenderItemSet();
 
-    bool _isWireframe;
+    PrimitiveMode _primitiveMode { PrimitiveMode::SOLID };
     bool _useDualQuaternionSkinning { false };
 
     // debug rendering support
@@ -513,7 +510,7 @@ private:
 
     void calculateTextureInfo();
 
-    std::vector<unsigned int> getMeshIDsFromMaterialID(QString parentMaterialName);
+    std::set<unsigned int> getMeshIDsFromMaterialID(QString parentMaterialName);
 };
 
 Q_DECLARE_METATYPE(ModelPointer)
diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf
index 582549ade1..469c0976aa 100644
--- a/libraries/render-utils/src/simple.slf
+++ b/libraries/render-utils/src/simple.slf
@@ -91,14 +91,14 @@ void main(void) {
         position.xyz,
 #endif
         normal,
-        vec3(0.0),
+        diffuse,
         DEFAULT_SPECULAR,
-        DEFAULT_EMISSIVE,
+        emissive,
         1.0,
-        DEFAULT_ROUGHNESS,
-        DEFAULT_METALLIC,
-        DEFAULT_OCCLUSION,
-        DEFAULT_SCATTERING
+        roughness,
+        metallic,
+        occlusion,
+        scattering
     );
 
 #if defined(PROCEDURAL_V3)
diff --git a/libraries/render-utils/src/simple_transparent.slf b/libraries/render-utils/src/simple_transparent.slf
index ea444d6113..6d8348f50c 100644
--- a/libraries/render-utils/src/simple_transparent.slf
+++ b/libraries/render-utils/src/simple_transparent.slf
@@ -92,22 +92,22 @@ void main(void) {
     emissive = vec3(clamp(emissiveAmount, 0.0, 1.0));
 #elif defined(PROCEDURAL_V3) || defined(PROCEDURAL_V4)
 #if defined(PROCEDURAL_V3)
-    ProceduralFragment proceduralData = {
+    ProceduralFragment proceduralData = ProceduralFragment(
 #else
     vec4 position = cam._viewInverse * _positionES;
-    ProceduralFragmentWithPosition proceduralData = {
+    ProceduralFragmentWithPosition proceduralData = ProceduralFragmentWithPosition(
         position.xyz,
 #endif
         normal,
-        vec3(0.0),
-        DEFAULT_SPECULAR,
-        DEFAULT_EMISSIVE,
-        1.0,
-        DEFAULT_ROUGHNESS,
-        DEFAULT_METALLIC,
-        DEFAULT_OCCLUSION,
+        diffuse,
+        fresnel,
+        emissive,
+        alpha,
+        roughness,
+        metallic,
+        occlusion,
         DEFAULT_SCATTERING
-    };
+    );
 
 #if defined(PROCEDURAL_V3)
     emissiveAmount = getProceduralFragment(proceduralData);
diff --git a/libraries/shared/src/BillboardMode.cpp b/libraries/shared/src/BillboardMode.cpp
index 56251f53f2..4b6af5db33 100644
--- a/libraries/shared/src/BillboardMode.cpp
+++ b/libraries/shared/src/BillboardMode.cpp
@@ -14,10 +14,10 @@ const char* billboardModeNames[] = {
     "full"
 };
 
-static const size_t MATERIAL_MODE_NAMES = (sizeof(billboardModeNames) / sizeof((billboardModeNames)[0]));
+static const size_t BILLBOARD_MODE_NAMES = (sizeof(billboardModeNames) / sizeof(billboardModeNames[0]));
 
 QString BillboardModeHelpers::getNameForBillboardMode(BillboardMode mode) {
-    if (((int)mode <= 0) || ((int)mode >= (int)MATERIAL_MODE_NAMES)) {
+    if (((int)mode <= 0) || ((int)mode >= (int)BILLBOARD_MODE_NAMES)) {
         mode = (BillboardMode)0;
     }
 
diff --git a/libraries/shared/src/JointData.h b/libraries/shared/src/JointData.h
index f4c8b89e7a..7a2420262a 100644
--- a/libraries/shared/src/JointData.h
+++ b/libraries/shared/src/JointData.h
@@ -14,7 +14,7 @@ public:
 };
 
 // Used by the avatar mixer to describe a single joint
-// Translations relative to their parent and are in meters.
+// Translations relative to their parent joint
 // Rotations are absolute (i.e. not relative to parent) and are in rig space.
 class JointData {
 public:
diff --git a/libraries/shared/src/MaterialMappingMode.cpp b/libraries/shared/src/MaterialMappingMode.cpp
index 1ddad178a2..09a7cfd6d9 100644
--- a/libraries/shared/src/MaterialMappingMode.cpp
+++ b/libraries/shared/src/MaterialMappingMode.cpp
@@ -13,7 +13,7 @@ const char* materialMappingModeNames[] = {
     "projected"
 };
 
-static const size_t MATERIAL_MODE_NAMES = (sizeof(materialMappingModeNames) / sizeof((materialMappingModeNames)[0]));
+static const size_t MATERIAL_MODE_NAMES = (sizeof(materialMappingModeNames) / sizeof(materialMappingModeNames[0]));
 
 QString MaterialMappingModeHelpers::getNameForMaterialMappingMode(MaterialMappingMode mode) {
     if (((int)mode <= 0) || ((int)mode >= (int)MATERIAL_MODE_NAMES)) {
diff --git a/libraries/shared/src/PrimitiveMode.cpp b/libraries/shared/src/PrimitiveMode.cpp
new file mode 100644
index 0000000000..c426f896b9
--- /dev/null
+++ b/libraries/shared/src/PrimitiveMode.cpp
@@ -0,0 +1,24 @@
+//
+//  Created by Sam Gondelman on 1/7/19
+//  Copyright 2019 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 "PrimitiveMode.h"
+
+const char* primitiveModeNames[] = {
+    "solid",
+    "lines"
+};
+
+static const size_t PRIMITIVE_MODE_NAMES = (sizeof(primitiveModeNames) / sizeof(primitiveModeNames[0]));
+
+QString PrimitiveModeHelpers::getNameForPrimitiveMode(PrimitiveMode mode) {
+    if (((int)mode <= 0) || ((int)mode >= (int)PRIMITIVE_MODE_NAMES)) {
+        mode = (PrimitiveMode)0;
+    }
+
+    return primitiveModeNames[(int)mode];
+}
\ No newline at end of file
diff --git a/libraries/shared/src/PrimitiveMode.h b/libraries/shared/src/PrimitiveMode.h
new file mode 100644
index 0000000000..6072f24fb9
--- /dev/null
+++ b/libraries/shared/src/PrimitiveMode.h
@@ -0,0 +1,39 @@
+//
+//  Created by Sam Gondelman on 1/7/19.
+//  Copyright 2019 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_PrimitiveMode_h
+#define hifi_PrimitiveMode_h
+
+#include "QString"
+
+/**jsdoc
+ * <p>How the geometry of the entity is rendered.</p>
+ * <table>
+ *   <thead>
+ *     <tr><th>Value</th><th>Description</th></tr>
+ *   </thead>
+ *   <tbody>
+ *     <tr><td><code>solid</code></td><td>The entity will be drawn as a solid shape.</td></tr>
+ *     <tr><td><code>lines</code></td><td>The entity will be drawn as wireframe.</td></tr>
+ *   </tbody>
+ * </table>
+ * @typedef {string} PrimitiveMode
+ */
+
+enum class PrimitiveMode {
+    SOLID = 0,
+    LINES
+};
+
+class PrimitiveModeHelpers {
+public:
+    static QString getNameForPrimitiveMode(PrimitiveMode mode);
+};
+
+#endif // hifi_PrimitiveMode_h
+
diff --git a/libraries/shared/src/RenderLayer.cpp b/libraries/shared/src/RenderLayer.cpp
new file mode 100644
index 0000000000..7e072631e0
--- /dev/null
+++ b/libraries/shared/src/RenderLayer.cpp
@@ -0,0 +1,25 @@
+//
+//  Created by Sam Gondelman on 1/3/19
+//  Copyright 2019 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 "RenderLayer.h"
+
+const char* renderLayerNames[] = {
+    "world",
+    "front",
+    "hud"
+};
+
+static const size_t RENDER_LAYER_NAMES = (sizeof(renderLayerNames) / sizeof(renderLayerNames[0]));
+
+QString RenderLayerHelpers::getNameForRenderLayer(RenderLayer mode) {
+    if (((int)mode <= 0) || ((int)mode >= (int)RENDER_LAYER_NAMES)) {
+        mode = (RenderLayer)0;
+    }
+
+    return renderLayerNames[(int)mode];
+}
\ No newline at end of file
diff --git a/libraries/shared/src/RenderLayer.h b/libraries/shared/src/RenderLayer.h
new file mode 100644
index 0000000000..b5bf885616
--- /dev/null
+++ b/libraries/shared/src/RenderLayer.h
@@ -0,0 +1,41 @@
+//
+//  Created by Sam Gondelman on 1/3/19.
+//  Copyright 2019 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_RenderLayer_h
+#define hifi_RenderLayer_h
+
+#include "QString"
+
+/**jsdoc
+ * <p>In which layer an entity is rendered.</p>
+ * <table>
+ *   <thead>
+ *     <tr><th>Value</th><th>Description</th></tr>
+ *   </thead>
+ *   <tbody>
+ *     <tr><td><code>world</code></td><td>The entity will be drawn in the world with everything else.</td></tr>
+ *     <tr><td><code>front</code></td><td>The entity will be drawn on top of the world layer, but behind the HUD sphere.</td></tr>
+ *     <tr><td><code>hud</code></td><td>The entity will be drawn on top of other layers and the HUD sphere.</td></tr>
+ *   </tbody>
+ * </table>
+ * @typedef {string} RenderLayer
+ */
+
+enum class RenderLayer {
+    WORLD = 0,
+    FRONT,
+    HUD
+};
+
+class RenderLayerHelpers {
+public:
+    static QString getNameForRenderLayer(RenderLayer mode);
+};
+
+#endif // hifi_RenderLayer_h
+
diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp
index 62ac8fad52..2693b9ee7e 100644
--- a/plugins/oculus/src/OculusHelpers.cpp
+++ b/plugins/oculus/src/OculusHelpers.cpp
@@ -82,15 +82,18 @@ private:
         }
 
 #ifdef OCULUS_APP_ID
-        if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) {
-            if (ovr_PlatformInitializeWindows(OCULUS_APP_ID) != ovrPlatformInitialize_Success) {
-                qCWarning(oculusLog) << "Unable to initialize the platform for entitlement check - fail the check" << ovr::getError();
-                return;
-            } else {
-                qCDebug(oculusLog) << "Performing Oculus Platform entitlement check";
-                ovr_Entitlement_GetIsViewerEntitled();
+        static std::once_flag once;
+        std::call_once(once, []() {
+            if (qApp->property(hifi::properties::OCULUS_STORE).toBool()) {
+                if (ovr_PlatformInitializeWindows(OCULUS_APP_ID) != ovrPlatformInitialize_Success) {
+                    qCWarning(oculusLog) << "Unable to initialize the platform for entitlement check - fail the check" << ovr::getError();
+                    return;
+                } else {
+                    qCDebug(oculusLog) << "Performing Oculus Platform entitlement check";
+                    ovr_Entitlement_GetIsViewerEntitled();
+                }
             }
-        }
+        });
 #endif
 
         ovrGraphicsLuid luid;
diff --git a/prebuild.py b/prebuild.py
index a758dcbea2..fb54b8d6fe 100644
--- a/prebuild.py
+++ b/prebuild.py
@@ -43,8 +43,7 @@ def parse_args():
     defaultPortsPath = hifi_utils.scriptRelative('cmake', 'ports')
     from argparse import ArgumentParser
     parser = ArgumentParser(description='Prepare build dependencies.')
-    parser.add_argument('--android', action='store_true')
-    #parser.add_argument('--android', type=str)
+    parser.add_argument('--android', type=str)
     parser.add_argument('--debug', action='store_true')
     parser.add_argument('--force-bootstrap', action='store_true')
     parser.add_argument('--force-build', action='store_true')
@@ -87,6 +86,17 @@ def main():
         # here shouldn't invalidte the vcpkg install)
         pm.cleanBuilds()
 
+        # If we're running in android mode, we also need to grab a bunch of additional binaries
+        # (this logic is all migrated from the old setupDependencies tasks in gradle)
+        if args.android:
+            # Find the target location
+            appPath = hifi_utils.scriptRelative('android/apps/' + args.android)
+            # Copy the non-Qt libraries specified in the config in hifi_android.py
+            hifi_android.copyAndroidLibs(pm.androidPackagePath, appPath)
+            # Determine the Qt package path
+            qtPath = os.path.join(pm.androidPackagePath, 'qt')
+            hifi_android.QtPackager(appPath, qtPath).bundle()
+
         # Write the vcpkg config to the build directory last
         pm.writeConfig()
 
diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json
index cb194c9d66..bf3ff3f324 100644
--- a/scripts/system/assets/data/createAppTooltips.json
+++ b/scripts/system/assets/data/createAppTooltips.json
@@ -39,6 +39,14 @@
     "leftMargin": {
         "tooltip": "The left margin, in meters."
     },
+    "zoneShapeType": {
+        "tooltip": "The shape of the volume in which the zone's lighting effects and avatar permissions have effect.",
+        "jsPropertyName": "shapeType"
+    },
+    "zoneCompoundShapeURL": {
+        "tooltip": "The model file to use for the compound shape if Shape Type is \"Use Compound Shape URL\".",
+        "jsPropertyName": "compoundShapeURL"
+    },
     "flyingAllowed": {
         "tooltip": "If enabled, users can fly in the zone."
     },
@@ -133,7 +141,7 @@
         "tooltip": "The shape of the collision hull used if collisions are enabled. This affects how an entity collides."
     },
     "compoundShapeURL": {
-        "tooltip": "The OBJ file to use for the compound shape if Collision Shape is \"compound\"."
+        "tooltip": "The model file to use for the compound shape if Collision Shape is \"Compound\"."
     },
     "animation.url": {
         "tooltip": "An animation to play on the model."
diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js
index c61e46c8eb..b1c1bc7765 100644
--- a/scripts/system/controllers/controllerModules/equipEntity.js
+++ b/scripts/system/controllers/controllerModules/equipEntity.js
@@ -6,11 +6,11 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 
 
-/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print,
-   getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther,
-   Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions,
-   Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable,
-   cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData
+/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex,
+   enableDispatcherModule, disableDispatcherModule, entityIsFarGrabbedByOther, Messages, makeDispatcherModuleParameters,
+   makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic,
+   entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, unhighlightTargetEntity, isInEditMode, getGrabbableData,
+   entityIsEquippable
 */
 
 Script.include("/~/system/libraries/Xform.js");
@@ -767,7 +767,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
             var entityProperties = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
             entityProperties.id = entityID;
             var hasEquipData = getWearableData(entityProperties);
-            if (hasEquipData && entityProperties.parentID === EMPTY_PARENT_ID && !entityIsFarGrabbedByOther(entityID)) {
+            if (hasEquipData && entityIsEquippable(entityProperties)) {
                 entityProperties.id = entityID;
                 var rightHandPosition = MyAvatar.getJointPosition("RightHand");
                 var leftHandPosition = MyAvatar.getJointPosition("LeftHand");
diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js b/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js
deleted file mode 100644
index 0ba3dd6e6b..0000000000
--- a/scripts/system/controllers/controllerModules/farActionGrabEntityDynOnly.js
+++ /dev/null
@@ -1,572 +0,0 @@
-"use strict";
-
-//  farActionGrabEntity.js
-//
-//  Distributed under the Apache License, Version 2.0.
-//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-
-/* jslint bitwise: true */
-
-/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Camera, Quat, getEnabledModuleByName,
-   makeRunningValues, Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable,
-   makeDispatcherModuleParameters, MSECS_PER_SEC, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE,
-   TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation, projectOntoEntityXYPlane, ContextOverlay, HMD,
-   Picks, makeLaserLockInfo, makeLaserParams, AddressManager, getEntityParents, Selection, DISPATCHER_HOVERING_LIST,
-   Uuid, worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES
-*/
-
-Script.include("/~/system/libraries/controllerDispatcherUtils.js");
-Script.include("/~/system/libraries/controllers.js");
-
-(function() {
-
-    var MARGIN = 25;
-
-    function TargetObject(entityID, entityProps) {
-        this.entityID = entityID;
-        this.entityProps = entityProps;
-        this.targetEntityID = null;
-        this.targetEntityProps = null;
-
-        this.getTargetEntity = function() {
-            var parentPropsLength = this.parentProps.length;
-            if (parentPropsLength !== 0) {
-                var targetEntity = {
-                    id: this.parentProps[parentPropsLength - 1].id,
-                    props: this.parentProps[parentPropsLength - 1]};
-                this.targetEntityID = targetEntity.id;
-                this.targetEntityProps = targetEntity.props;
-                return targetEntity;
-            }
-            this.targetEntityID = this.entityID;
-            this.targetEntityProps = this.entityProps;
-            return {
-                id: this.entityID,
-                props: this.entityProps};
-        };
-    }
-
-    function FarActionGrabEntity(hand) {
-        this.hand = hand;
-        this.grabbing = false;
-        this.grabbedThingID = null;
-        this.targetObject = null;
-        this.actionID = null; // action this script created...
-        this.entityToLockOnto = null;
-        this.potentialEntityWithContextOverlay = false;
-        this.entityWithContextOverlay = false;
-        this.contextOverlayTimer = false;
-        this.locked = false;
-        this.highlightedEntity = null;
-        this.reticleMinX = MARGIN;
-        this.reticleMaxX = 0;
-        this.reticleMinY = MARGIN;
-        this.reticleMaxY = 0;
-
-        var ACTION_TTL = 15; // seconds
-
-        var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object
-        var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
-        var DISTANCE_HOLDING_UNITY_MASS = 1200; //  The mass at which the distance holding action timeframe is unmodified
-        var DISTANCE_HOLDING_UNITY_DISTANCE = 6; //  The distance at which the distance holding action timeframe is unmodified
-
-        this.parameters = makeDispatcherModuleParameters(
-            550,
-            this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
-            [],
-            100,
-            makeLaserParams(this.hand, false));
-
-
-        this.handToController = function() {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
-        };
-
-        this.distanceGrabTimescale = function(mass, distance) {
-            var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass /
-                DISTANCE_HOLDING_UNITY_MASS * distance /
-                DISTANCE_HOLDING_UNITY_DISTANCE;
-            if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) {
-                timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
-            }
-            return timeScale;
-        };
-
-        this.getMass = function(dimensions, density) {
-            return (dimensions.x * dimensions.y * dimensions.z) * density;
-        };
-
-        this.startFarGrabAction = function (controllerData, grabbedProperties) {
-            var controllerLocation = controllerData.controllerLocations[this.hand];
-            var worldControllerPosition = controllerLocation.position;
-            var worldControllerRotation = controllerLocation.orientation;
-
-            // transform the position into room space
-            var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
-            var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
-
-            var now = Date.now();
-
-            // add the action and initialize some variables
-            this.currentObjectPosition = grabbedProperties.position;
-            this.currentObjectRotation = grabbedProperties.rotation;
-            this.currentObjectTime = now;
-            this.currentCameraOrientation = Camera.orientation;
-
-            this.grabRadius = this.grabbedDistance;
-            this.grabRadialVelocity = 0.0;
-
-            // offset between controller vector at the grab radius and the entity position
-            var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
-            targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
-            this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
-
-            // compute a constant based on the initial conditions which we use below to exaggerate hand motion
-            // onto the held object
-            this.radiusScalar = Math.log(this.grabRadius + 1.0);
-            if (this.radiusScalar < 1.0) {
-                this.radiusScalar = 1.0;
-            }
-
-            // compute the mass for the purpose of energy and how quickly to move object
-            this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density);
-            var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, grabbedProperties.position));
-            var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject);
-            this.linearTimeScale = timeScale;
-            this.actionID = Entities.addAction("far-grab", this.grabbedThingID, {
-                targetPosition: this.currentObjectPosition,
-                linearTimeScale: timeScale,
-                targetRotation: this.currentObjectRotation,
-                angularTimeScale: timeScale,
-                tag: "far-grab-" + MyAvatar.sessionUUID,
-                ttl: ACTION_TTL
-            });
-            if (this.actionID === Uuid.NULL) {
-                this.actionID = null;
-            }
-
-            if (this.actionID !== null) {
-                var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-                Entities.callEntityMethod(this.grabbedThingID, "startDistanceGrab", args);
-            }
-
-            Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
-            this.previousRoomControllerPosition = roomControllerPosition;
-            this.grabbing = true;
-        };
-
-        this.continueDistanceHolding = function(controllerData) {
-            var controllerLocation = controllerData.controllerLocations[this.hand];
-            var worldControllerPosition = controllerLocation.position;
-            var worldControllerRotation = controllerLocation.orientation;
-
-            // also transform the position into room space
-            var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
-            var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
-
-            var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES);
-            var now = Date.now();
-            var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
-            this.currentObjectTime = now;
-
-            // the action was set up when this.distanceHolding was called.  update the targets.
-            var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) *
-                this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR;
-            if (radius < 1.0) {
-                radius = 1.0;
-            }
-
-            var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition);
-            var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta);
-            var handMoved = Vec3.multiply(worldHandDelta, radius);
-            this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(this.grabbedThingID, "continueDistanceGrab", args);
-
-            //  Update radialVelocity
-            var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime);
-            var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition));
-            var newRadialVelocity = Vec3.dot(lastVelocity, delta);
-
-            var VELOCITY_AVERAGING_TIME = 0.016;
-            var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME;
-            if (blendFactor < 0.0) {
-                blendFactor = 0.0;
-            } else if (blendFactor > 1.0) {
-                blendFactor = 1.0;
-            }
-            this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity;
-
-            var RADIAL_GRAB_AMPLIFIER = 10.0;
-            if (Math.abs(this.grabRadialVelocity) > 0.0) {
-                this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime *
-                                                     this.grabRadius * RADIAL_GRAB_AMPLIFIER);
-            }
-
-            // don't let grabRadius go all the way to zero, because it can't come back from that
-            var MINIMUM_GRAB_RADIUS = 0.1;
-            if (this.grabRadius < MINIMUM_GRAB_RADIUS) {
-                this.grabRadius = MINIMUM_GRAB_RADIUS;
-            }
-            var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
-            newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition);
-            newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition);
-
-            // XXX
-            // this.maybeScale(grabbedProperties);
-
-            var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition));
-
-            this.linearTimeScale = (this.linearTimeScale / 2);
-            if (this.linearTimeScale <= DISTANCE_HOLDING_ACTION_TIMEFRAME) {
-                this.linearTimeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
-            }
-            var success = Entities.updateAction(this.grabbedThingID, this.actionID, {
-                targetPosition: newTargetPosition,
-                linearTimeScale: this.linearTimeScale,
-                targetRotation: this.currentObjectRotation,
-                angularTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject),
-                ttl: ACTION_TTL
-            });
-            if (!success) {
-                print("farActionGrabEntity continueDistanceHolding -- updateAction failed: " + this.actionID);
-                this.actionID = null;
-            }
-
-            this.previousRoomControllerPosition = roomControllerPosition;
-        };
-
-        this.endFarGrabAction = function () {
-            this.distanceHolding = false;
-            this.distanceRotating = false;
-            Entities.deleteAction(this.grabbedThingID, this.actionID);
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(this.grabbedThingID, "releaseGrab", args);
-            this.actionID = null;
-            this.grabbedThingID = null;
-            this.targetObject = null;
-            this.potentialEntityWithContextOverlay = false;
-            this.grabbing = false;
-        };
-
-        this.updateRecommendedArea = function() {
-            var dims = Controller.getViewportDimensions();
-            this.reticleMaxX = dims.x - MARGIN;
-            this.reticleMaxY = dims.y - MARGIN;
-        };
-
-        this.calculateNewReticlePosition = function(intersection) {
-            this.updateRecommendedArea();
-            var point2d = HMD.overlayFromWorldPoint(intersection);
-            point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
-            point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
-            return point2d;
-        };
-
-        this.notPointingAtEntity = function(controllerData) {
-            var intersection = controllerData.rayPicks[this.hand];
-            var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
-            var entityType = entityProperty.type;
-            var hudRayPick = controllerData.hudRayPicks[this.hand];
-            var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
-            if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") ||
-                intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) {
-                return true;
-            }
-            return false;
-        };
-
-        this.distanceRotate = function(otherFarGrabModule) {
-            this.distanceRotating = true;
-            this.distanceHolding = false;
-
-            var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation;
-            var controllerRotationDelta =
-                Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation));
-            // Rotate entity by twice the delta rotation.
-            controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta);
-
-            // Perform the rotation in the translation controller's action update.
-            otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta,
-                otherFarGrabModule.currentObjectRotation);
-
-            this.previousWorldControllerRotation = worldControllerRotation;
-        };
-
-        this.prepareDistanceRotatingData = function(controllerData) {
-            var intersection = controllerData.rayPicks[this.hand];
-
-            var controllerLocation = getControllerWorldLocation(this.handToController(), true);
-            var worldControllerPosition = controllerLocation.position;
-            var worldControllerRotation = controllerLocation.orientation;
-
-            var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
-            this.currentObjectPosition = grabbedProperties.position;
-            this.grabRadius = intersection.distance;
-
-            // Offset between controller vector at the grab radius and the entity position.
-            var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
-            targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
-            this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
-
-            // Initial controller rotation.
-            this.previousWorldControllerRotation = worldControllerRotation;
-        };
-
-        this.destroyContextOverlay = function(controllerData) {
-            if (this.entityWithContextOverlay) {
-                ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay);
-                this.entityWithContextOverlay = false;
-                this.potentialEntityWithContextOverlay = false;
-            }
-        };
-
-        this.targetIsNull = function() {
-            var properties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES);
-            if (Object.keys(properties).length === 0 && this.distanceHolding) {
-                return true;
-            }
-            return false;
-        };
-
-        this.getTargetProps = function (controllerData) {
-            var targetEntityID = controllerData.rayPicks[this.hand].objectID;
-            if (targetEntityID) {
-                return Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES);
-            }
-            return null;
-        };
-
-        this.isReady = function (controllerData) {
-            if (HMD.active) {
-                if (this.notPointingAtEntity(controllerData)) {
-                    return makeRunningValues(false, [], []);
-                }
-
-                this.distanceHolding = false;
-                this.distanceRotating = false;
-
-                if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
-                    this.prepareDistanceRotatingData(controllerData);
-                    return makeRunningValues(true, [], []);
-                } else {
-                    this.destroyContextOverlay();
-                    return makeRunningValues(false, [], []);
-                }
-            }
-            return makeRunningValues(false, [], []);
-        };
-
-        this.run = function (controllerData) {
-            if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) {
-                this.endFarGrabAction();
-                Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
-                    this.highlightedEntity);
-                this.highlightedEntity = null;
-                return makeRunningValues(false, [], []);
-            }
-            this.intersectionDistance = controllerData.rayPicks[this.hand].distance;
-
-            var otherModuleName = this.hand === RIGHT_HAND ? "LeftFarActionGrabEntity" : "RightFarActionGrabEntity";
-            var otherFarGrabModule = getEnabledModuleByName(otherModuleName);
-
-            // gather up the readiness of the near-grab modules
-            var nearGrabNames = [
-                this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar",
-                this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity",
-                this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity",
-                this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"
-            ];
-            if (!this.grabbing) {
-                nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay");
-                nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
-            }
-
-            var nearGrabReadiness = [];
-            for (var i = 0; i < nearGrabNames.length; i++) {
-                var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]);
-                var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []);
-                nearGrabReadiness.push(ready);
-            }
-
-            if (this.actionID) {
-                // if we are doing a distance grab and the object or tablet gets close enough to the controller,
-                // stop the far-grab so the near-grab or equip can take over.
-                for (var k = 0; k < nearGrabReadiness.length; k++) {
-                    if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.grabbedThingID ||
-                                                        HMD.tabletID && nearGrabReadiness[k].targets[0] === HMD.tabletID)) {
-                        this.endFarGrabAction();
-                        return makeRunningValues(false, [], []);
-                    }
-                }
-
-                this.continueDistanceHolding(controllerData);
-            } else {
-                // if we are doing a distance search and this controller moves into a position
-                // where it could near-grab something, stop searching.
-                for (var j = 0; j < nearGrabReadiness.length; j++) {
-                    if (nearGrabReadiness[j].active) {
-                        this.endFarGrabAction();
-                        return makeRunningValues(false, [], []);
-                    }
-                }
-
-                var rayPickInfo = controllerData.rayPicks[this.hand];
-                if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) {
-                    if (controllerData.triggerClicks[this.hand]) {
-                        var entityID = rayPickInfo.objectID;
-                        Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
-                            this.highlightedEntity);
-                        this.highlightedEntity = null;
-                        var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
-                        if (targetProps.href !== "") {
-                            AddressManager.handleLookupString(targetProps.href);
-                            return makeRunningValues(false, [], []);
-                        }
-
-                        this.targetObject = new TargetObject(entityID, targetProps);
-                        this.targetObject.parentProps = getEntityParents(targetProps);
-
-                        if (this.contextOverlayTimer) {
-                            Script.clearTimeout(this.contextOverlayTimer);
-                        }
-                        this.contextOverlayTimer = false;
-                        if (entityID === this.entityWithContextOverlay) {
-                            this.destroyContextOverlay();
-                        } else {
-                            Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID);
-                        }
-
-                        var targetEntity = this.targetObject.getTargetEntity();
-                        entityID = targetEntity.id;
-                        targetProps = targetEntity.props;
-
-                        if (!targetProps.dynamic && !this.targetObject.entityProps.dynamic) {
-                            // let farParentGrabEntity handle it
-                            return makeRunningValues(false, [], []);
-                        }
-
-                        if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) {
-                            if (!this.distanceRotating) {
-                                this.grabbedThingID = entityID;
-                                this.grabbedDistance = rayPickInfo.distance;
-                            }
-
-                            if (otherFarGrabModule.grabbedThingID === this.grabbedThingID &&
-                                otherFarGrabModule.distanceHolding) {
-                                this.prepareDistanceRotatingData(controllerData);
-                                this.distanceRotate(otherFarGrabModule);
-                            } else {
-                                this.distanceHolding = true;
-                                this.distanceRotating = false;
-                                this.startFarGrabAction(controllerData, targetProps);
-                            }
-                        }
-                    } else {
-                        var targetEntityID = rayPickInfo.objectID;
-                        if (this.highlightedEntity !== targetEntityID) {
-                            Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
-                                this.highlightedEntity);
-                            var selectionTargetProps = Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES);
-
-                            var selectionTargetObject = new TargetObject(targetEntityID, selectionTargetProps);
-                            selectionTargetObject.parentProps = getEntityParents(selectionTargetProps);
-                            var selectionTargetEntity = selectionTargetObject.getTargetEntity();
-
-                            if (entityIsGrabbable(selectionTargetEntity.props) ||
-                                entityIsGrabbable(selectionTargetObject.entityProps)) {
-
-                                Selection.addToSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", rayPickInfo.objectID);
-                            }
-                            this.highlightedEntity = rayPickInfo.objectID;
-                        }
-
-                        if (!this.entityWithContextOverlay) {
-                            var _this = this;
-
-                            if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) {
-                                if (_this.contextOverlayTimer) {
-                                    Script.clearTimeout(_this.contextOverlayTimer);
-                                }
-                                _this.contextOverlayTimer = false;
-                                _this.potentialEntityWithContextOverlay = rayPickInfo.objectID;
-                            }
-
-                            if (!_this.contextOverlayTimer) {
-                                _this.contextOverlayTimer = Script.setTimeout(function () {
-                                    if (!_this.entityWithContextOverlay &&
-                                        _this.contextOverlayTimer &&
-                                        _this.potentialEntityWithContextOverlay === rayPickInfo.objectID) {
-                                        var pEvProps = Entities.getEntityProperties(rayPickInfo.objectID,
-                                                                                    DISPATCHER_PROPERTIES);
-                                        var pointerEvent = {
-                                            type: "Move",
-                                            id: _this.hand + 1, // 0 is reserved for hardware mouse
-                                            pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID,
-                                                                            rayPickInfo.intersection, pEvProps),
-                                            pos3D: rayPickInfo.intersection,
-                                            normal: rayPickInfo.surfaceNormal,
-                                            direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal),
-                                            button: "Secondary"
-                                        };
-                                        if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) {
-                                            _this.entityWithContextOverlay = rayPickInfo.objectID;
-                                        }
-                                    }
-                                    _this.contextOverlayTimer = false;
-                                }, 500);
-                            }
-                        }
-                    }
-                } else if (this.distanceRotating) {
-                    this.distanceRotate(otherFarGrabModule);
-                } else if (this.highlightedEntity) {
-                    Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
-                    this.highlightedEntity = null;
-                }
-            }
-            return this.exitIfDisabled(controllerData);
-        };
-
-        this.exitIfDisabled = function(controllerData) {
-            var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules";
-            var disableModule = getEnabledModuleByName(moduleName);
-            if (disableModule) {
-                if (disableModule.disableModules) {
-                    this.endFarGrabAction();
-                    Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity",
-                        this.highlightedEntity);
-                    this.highlightedEntity = null;
-                    return makeRunningValues(false, [], []);
-                }
-            }
-            var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null;
-            var offset = this.calculateOffset(controllerData);
-            var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset);
-            return makeRunningValues(true, [], [], laserLockInfo);
-        };
-
-        this.calculateOffset = function(controllerData) {
-            if (this.distanceHolding || this.distanceRotating) {
-                var targetProps = Entities.getEntityProperties(this.targetObject.entityID,
-                                                               [ "position", "rotation", "registrationPoint", "dimensions" ]);
-                return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection);
-            }
-            return undefined;
-        };
-    }
-
-    var leftFarActionGrabEntity = new FarActionGrabEntity(LEFT_HAND);
-    var rightFarActionGrabEntity = new FarActionGrabEntity(RIGHT_HAND);
-
-    enableDispatcherModule("LeftFarActionGrabEntity", leftFarActionGrabEntity);
-    enableDispatcherModule("RightFarActionGrabEntity", rightFarActionGrabEntity);
-
-    function cleanup() {
-        disableDispatcherModule("LeftFarActionGrabEntity");
-        disableDispatcherModule("RightFarActionGrabEntity");
-    }
-    Script.scriptEnding.connect(cleanup);
-}());
diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js
index dab1aa97af..197a809e91 100644
--- a/scripts/system/controllers/controllerModules/farGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/farGrabEntity.js
@@ -11,7 +11,7 @@
    Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC,
    HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC,
    projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager,
-   getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGroupParent,
+   getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, findGrabbableGroupParent,
    worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES
 */
 
@@ -308,7 +308,7 @@ Script.include("/~/system/libraries/controllers.js");
                 var gtProps = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES);
                 if (entityIsGrabbable(gtProps)) {
                     // if we've attempted to grab a child, roll up to the root of the tree
-                    var groupRootProps = findGroupParent(controllerData, gtProps);
+                    var groupRootProps = findGrabbableGroupParent(controllerData, gtProps);
                     if (entityIsGrabbable(groupRootProps)) {
                         return groupRootProps;
                     }
diff --git a/scripts/system/controllers/controllerModules/farParentGrabEntity.js b/scripts/system/controllers/controllerModules/farParentGrabEntity.js
deleted file mode 100644
index 9960b08292..0000000000
--- a/scripts/system/controllers/controllerModules/farParentGrabEntity.js
+++ /dev/null
@@ -1,664 +0,0 @@
-"use strict";
-
-//  farParentGrabEntity.js
-//
-//  Distributed under the Apache License, Version 2.0.
-//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-
-/* jslint bitwise: true */
-
-/* global Script, Controller, RIGHT_HAND, LEFT_HAND, Mat4, MyAvatar, Vec3, Quat, getEnabledModuleByName, makeRunningValues,
-   Entities, enableDispatcherModule, disableDispatcherModule, entityIsGrabbable, makeDispatcherModuleParameters, MSECS_PER_SEC,
-   HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE, TRIGGER_ON_VALUE, ZERO_VEC, getControllerWorldLocation,
-   projectOntoEntityXYPlane, ContextOverlay, HMD, Picks, makeLaserLockInfo, makeLaserParams, AddressManager,
-   getEntityParents, Selection, DISPATCHER_HOVERING_LIST, unhighlightTargetEntity, Messages, Uuid, findGroupParent,
-   worldPositionToRegistrationFrameMatrix, DISPATCHER_PROPERTIES, findFarGrabJointChildEntities
-*/
-
-Script.include("/~/system/libraries/controllerDispatcherUtils.js");
-Script.include("/~/system/libraries/controllers.js");
-
-(function() {
-    var MARGIN = 25;
-
-    function TargetObject(entityID, entityProps) {
-        this.entityID = entityID;
-        this.entityProps = entityProps;
-        this.targetEntityID = null;
-        this.targetEntityProps = null;
-
-        this.getTargetEntity = function() {
-            var parentPropsLength = this.parentProps.length;
-            if (parentPropsLength !== 0) {
-                var targetEntity = {
-                    id: this.parentProps[parentPropsLength - 1].id,
-                    props: this.parentProps[parentPropsLength - 1]};
-                this.targetEntityID = targetEntity.id;
-                this.targetEntityProps = targetEntity.props;
-                return targetEntity;
-            }
-            this.targetEntityID = this.entityID;
-            this.targetEntityProps = this.entityProps;
-            return {
-                id: this.entityID,
-                props: this.entityProps};
-        };
-    }
-
-    function FarParentGrabEntity(hand) {
-        this.hand = hand;
-        this.grabbing = false;
-        this.targetEntityID = null;
-        this.targetObject = null;
-        this.previouslyUnhooked = {};
-        this.previousParentID = {};
-        this.previousParentJointIndex = {};
-        this.potentialEntityWithContextOverlay = false;
-        this.entityWithContextOverlay = false;
-        this.contextOverlayTimer = false;
-        this.highlightedEntity = null;
-        this.reticleMinX = MARGIN;
-        this.reticleMaxX = 0;
-        this.reticleMinY = MARGIN;
-        this.reticleMaxY = 0;
-        this.lastUnexpectedChildrenCheckTime = 0;
-        this.endedGrab = 0;
-        this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms
-
-        var FAR_GRAB_JOINTS = [65527, 65528]; // FARGRAB_LEFTHAND_INDEX, FARGRAB_RIGHTHAND_INDEX
-
-        var DISTANCE_HOLDING_RADIUS_FACTOR = 3.5; // multiplied by distance between hand and object
-        var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
-        var DISTANCE_HOLDING_UNITY_MASS = 1200; //  The mass at which the distance holding action timeframe is unmodified
-        var DISTANCE_HOLDING_UNITY_DISTANCE = 6; //  The distance at which the distance holding action timeframe is unmodified
-
-        this.parameters = makeDispatcherModuleParameters(
-            540,
-            this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
-            [],
-            100,
-            makeLaserParams(this.hand, false));
-
-
-        this.handToController = function() {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
-        };
-
-        this.distanceGrabTimescale = function(mass, distance) {
-            var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass /
-                DISTANCE_HOLDING_UNITY_MASS * distance /
-                DISTANCE_HOLDING_UNITY_DISTANCE;
-            if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) {
-                timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
-            }
-            return timeScale;
-        };
-
-        this.getMass = function(dimensions, density) {
-            return (dimensions.x * dimensions.y * dimensions.z) * density;
-        };
-
-        this.thisFarGrabJointIsParent = function(isParentProps) {
-            if (!isParentProps) {
-                return false;
-            }
-
-            if (isParentProps.parentID !== MyAvatar.sessionUUID && isParentProps.parentID !== MyAvatar.SELF_ID) {
-                return false;
-            }
-
-            if (isParentProps.parentJointIndex === FAR_GRAB_JOINTS[this.hand]) {
-                return true;
-            }
-
-            return false;
-        };
-
-        this.startFarParentGrab = function (controllerData, grabbedProperties) {
-            var controllerLocation = controllerData.controllerLocations[this.hand];
-            var worldControllerPosition = controllerLocation.position;
-            var worldControllerRotation = controllerLocation.orientation;
-            // transform the position into room space
-            var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
-            var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
-
-            var now = Date.now();
-
-            // add the action and initialize some variables
-            this.currentObjectPosition = grabbedProperties.position;
-            this.currentObjectRotation = grabbedProperties.rotation;
-            this.currentObjectTime = now;
-
-            this.grabRadius = this.grabbedDistance;
-            this.grabRadialVelocity = 0.0;
-
-            // offset between controller vector at the grab radius and the entity position
-            var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
-            targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
-            this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
-
-            // compute a constant based on the initial conditions which we use below to exaggerate hand motion
-            // onto the held object
-            this.radiusScalar = Math.log(this.grabRadius + 1.0);
-            if (this.radiusScalar < 1.0) {
-                this.radiusScalar = 1.0;
-            }
-
-            // compute the mass for the purpose of energy and how quickly to move object
-            this.mass = this.getMass(grabbedProperties.dimensions, grabbedProperties.density);
-
-            // Debounce haptic pules. Can occur as near grab controller module vacillates between being ready or not due to
-            // changing positions and floating point rounding.
-            if (Date.now() - this.endedGrab > this.MIN_HAPTIC_PULSE_INTERVAL) {
-                Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
-            }
-
-            unhighlightTargetEntity(this.targetEntityID);
-            var message = {
-                hand: this.hand,
-                entityID: this.targetEntityID
-            };
-
-            Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message));
-
-            var newTargetPosLocal = MyAvatar.worldToJointPoint(grabbedProperties.position);
-            MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(grabbedProperties.id, "startNearGrab", args);
-
-            var reparentProps = {
-                parentID: MyAvatar.SELF_ID,
-                parentJointIndex: FAR_GRAB_JOINTS[this.hand],
-                localVelocity: {x: 0, y: 0, z: 0},
-                localAngularVelocity: {x: 0, y: 0, z: 0}
-            };
-
-            if (this.thisFarGrabJointIsParent(grabbedProperties)) {
-                // this should never happen, but if it does, don't set previous parent to be this hand.
-                this.previousParentID[grabbedProperties.id] = null;
-                this.previousParentJointIndex[grabbedProperties.id] = -1;
-            } else {
-                this.previousParentID[grabbedProperties.id] = grabbedProperties.parentID;
-                this.previousParentJointIndex[grabbedProperties.id] = grabbedProperties.parentJointIndex;
-            }
-
-            this.targetEntityID = grabbedProperties.id;
-            Entities.editEntity(grabbedProperties.id, reparentProps);
-
-            Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
-                action: 'grab',
-                grabbedEntity: grabbedProperties.id,
-                joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
-            }));
-            this.grabbing = true;
-
-            this.previousRoomControllerPosition = roomControllerPosition;
-        };
-
-        this.continueDistanceHolding = function(controllerData) {
-            var controllerLocation = controllerData.controllerLocations[this.hand];
-            var worldControllerPosition = controllerLocation.position;
-            var worldControllerRotation = controllerLocation.orientation;
-
-            // also transform the position into room space
-            var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
-            var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
-
-            var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
-            var now = Date.now();
-            var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
-            this.currentObjectTime = now;
-
-            // the action was set up when this.distanceHolding was called.  update the targets.
-            var radius = Vec3.distance(this.currentObjectPosition, worldControllerPosition) *
-                this.radiusScalar * DISTANCE_HOLDING_RADIUS_FACTOR;
-            if (radius < 1.0) {
-                radius = 1.0;
-            }
-
-            var roomHandDelta = Vec3.subtract(roomControllerPosition, this.previousRoomControllerPosition);
-            var worldHandDelta = Mat4.transformVector(MyAvatar.getSensorToWorldMatrix(), roomHandDelta);
-            var handMoved = Vec3.multiply(worldHandDelta, radius);
-            this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, handMoved);
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(this.targetEntityID, "continueDistanceGrab", args);
-
-            //  Update radialVelocity
-            var lastVelocity = Vec3.multiply(worldHandDelta, 1.0 / deltaObjectTime);
-            var delta = Vec3.normalize(Vec3.subtract(grabbedProperties.position, worldControllerPosition));
-            var newRadialVelocity = Vec3.dot(lastVelocity, delta);
-
-            var VELOCITY_AVERAGING_TIME = 0.016;
-            var blendFactor = deltaObjectTime / VELOCITY_AVERAGING_TIME;
-            if (blendFactor < 0.0) {
-                blendFactor = 0.0;
-            } else if (blendFactor > 1.0) {
-                blendFactor = 1.0;
-            }
-            this.grabRadialVelocity = blendFactor * newRadialVelocity + (1.0 - blendFactor) * this.grabRadialVelocity;
-
-            var RADIAL_GRAB_AMPLIFIER = 10.0;
-            if (Math.abs(this.grabRadialVelocity) > 0.0) {
-                this.grabRadius = this.grabRadius + (this.grabRadialVelocity * deltaObjectTime *
-                                                     this.grabRadius * RADIAL_GRAB_AMPLIFIER);
-            }
-
-            // don't let grabRadius go all the way to zero, because it can't come back from that
-            var MINIMUM_GRAB_RADIUS = 0.1;
-            if (this.grabRadius < MINIMUM_GRAB_RADIUS) {
-                this.grabRadius = MINIMUM_GRAB_RADIUS;
-            }
-            var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
-            newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition);
-            newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition);
-
-            // MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], MyAvatar.worldToJointPoint(newTargetPosition));
-
-            // var newTargetPosLocal = Mat4.transformPoint(MyAvatar.getSensorToWorldMatrix(), newTargetPosition);
-            var newTargetPosLocal = MyAvatar.worldToJointPoint(newTargetPosition);
-            MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
-
-            this.previousRoomControllerPosition = roomControllerPosition;
-        };
-
-        this.endFarParentGrab = function (controllerData) {
-            this.endedGrab = Date.now();
-            // var endProps = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
-            var endProps = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
-            if (this.thisFarGrabJointIsParent(endProps)) {
-                Entities.editEntity(this.targetEntityID, {
-                    parentID: this.previousParentID[this.targetEntityID],
-                    parentJointIndex: this.previousParentJointIndex[this.targetEntityID],
-                    localVelocity: {x: 0, y: 0, z: 0},
-                    localAngularVelocity: {x: 0, y: 0, z: 0}
-                });
-            }
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
-            Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
-                action: 'release',
-                grabbedEntity: this.targetEntityID,
-                joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
-            }));
-            unhighlightTargetEntity(this.targetEntityID);
-            this.grabbing = false;
-            this.targetEntityID = null;
-            this.potentialEntityWithContextOverlay = false;
-            MyAvatar.clearJointData(FAR_GRAB_JOINTS[this.hand]);
-        };
-
-        this.updateRecommendedArea = function() {
-            var dims = Controller.getViewportDimensions();
-            this.reticleMaxX = dims.x - MARGIN;
-            this.reticleMaxY = dims.y - MARGIN;
-        };
-
-        this.calculateNewReticlePosition = function(intersection) {
-            this.updateRecommendedArea();
-            var point2d = HMD.overlayFromWorldPoint(intersection);
-            point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX));
-            point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY));
-            return point2d;
-        };
-
-        this.notPointingAtEntity = function(controllerData) {
-            var intersection = controllerData.rayPicks[this.hand];
-            var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
-            var entityType = entityProperty.type;
-            var hudRayPick = controllerData.hudRayPicks[this.hand];
-            var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
-            if ((intersection.type === Picks.INTERSECTED_ENTITY && entityType === "Web") ||
-                intersection.type === Picks.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) {
-                return true;
-            }
-            return false;
-        };
-
-        this.distanceRotate = function(otherFarGrabModule) {
-            this.distanceRotating = true;
-            this.distanceHolding = false;
-
-            var worldControllerRotation = getControllerWorldLocation(this.handToController(), true).orientation;
-            var controllerRotationDelta =
-                Quat.multiply(worldControllerRotation, Quat.inverse(this.previousWorldControllerRotation));
-            // Rotate entity by twice the delta rotation.
-            controllerRotationDelta = Quat.multiply(controllerRotationDelta, controllerRotationDelta);
-
-            // Perform the rotation in the translation controller's action update.
-            otherFarGrabModule.currentObjectRotation = Quat.multiply(controllerRotationDelta,
-                otherFarGrabModule.currentObjectRotation);
-
-            this.previousWorldControllerRotation = worldControllerRotation;
-        };
-
-        this.prepareDistanceRotatingData = function(controllerData) {
-            var intersection = controllerData.rayPicks[this.hand];
-
-            var controllerLocation = getControllerWorldLocation(this.handToController(), true);
-            var worldControllerPosition = controllerLocation.position;
-            var worldControllerRotation = controllerLocation.orientation;
-
-            var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
-            this.currentObjectPosition = grabbedProperties.position;
-            this.grabRadius = intersection.distance;
-
-            // Offset between controller vector at the grab radius and the entity position.
-            var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation));
-            targetPosition = Vec3.sum(targetPosition, worldControllerPosition);
-            this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition);
-
-            // Initial controller rotation.
-            this.previousWorldControllerRotation = worldControllerRotation;
-        };
-
-        this.destroyContextOverlay = function(controllerData) {
-            if (this.entityWithContextOverlay) {
-                ContextOverlay.destroyContextOverlay(this.entityWithContextOverlay);
-                this.entityWithContextOverlay = false;
-                this.potentialEntityWithContextOverlay = false;
-            }
-        };
-
-        this.checkForUnexpectedChildren = function (controllerData) {
-            // sometimes things can get parented to a hand and this script is unaware.  Search for such entities and
-            // unhook them.
-
-            var now = Date.now();
-            var UNEXPECTED_CHILDREN_CHECK_TIME = 0.1; // seconds
-            if (now - this.lastUnexpectedChildrenCheckTime > MSECS_PER_SEC * UNEXPECTED_CHILDREN_CHECK_TIME) {
-                this.lastUnexpectedChildrenCheckTime = now;
-
-                var children = findFarGrabJointChildEntities(this.hand);
-                var _this = this;
-
-                children.forEach(function(childID) {
-                    // we appear to be holding something and this script isn't in a state that would be holding something.
-                    // unhook it.  if we previously took note of this entity's parent, put it back where it was.  This
-                    // works around some problems that happen when more than one hand or avatar is passing something around.
-                    if (_this.previousParentID[childID]) {
-                        var previousParentID = _this.previousParentID[childID];
-                        var previousParentJointIndex = _this.previousParentJointIndex[childID];
-
-                        // The main flaw with keeping track of previous parentage in individual scripts is:
-                        // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it
-                        // now A and B will take turns passing it back to the other.  Detect this and stop the loop here...
-                        var UNHOOK_LOOP_DETECT_MS = 200;
-                        if (_this.previouslyUnhooked[childID]) {
-                            if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) {
-                                previousParentID = Uuid.NULL;
-                                previousParentJointIndex = -1;
-                            }
-                        }
-                        _this.previouslyUnhooked[childID] = now;
-
-                        Entities.editEntity(childID, {
-                            parentID: previousParentID,
-                            parentJointIndex: previousParentJointIndex
-                        });
-                    } else {
-                        Entities.editEntity(childID, { parentID: Uuid.NULL });
-                    }
-                });
-            }
-        };
-
-        this.targetIsNull = function() {
-            var properties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
-            if (Object.keys(properties).length === 0 && this.distanceHolding) {
-                return true;
-            }
-            return false;
-        };
-
-        this.getTargetProps = function (controllerData) {
-            var targetEntity = controllerData.rayPicks[this.hand].objectID;
-            if (targetEntity) {
-                var gtProps = Entities.getEntityProperties(targetEntity, DISPATCHER_PROPERTIES);
-                if (entityIsGrabbable(gtProps)) {
-                    // if we've attempted to grab a child, roll up to the root of the tree
-                    var groupRootProps = findGroupParent(controllerData, gtProps);
-                    if (entityIsGrabbable(groupRootProps)) {
-                        return groupRootProps;
-                    }
-                    return gtProps;
-                }
-            }
-            return null;
-        };
-
-        this.isReady = function (controllerData) {
-            if (HMD.active) {
-                if (this.notPointingAtEntity(controllerData)) {
-                    return makeRunningValues(false, [], []);
-                }
-
-                this.distanceHolding = false;
-                this.distanceRotating = false;
-
-                if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
-                    var targetProps = this.getTargetProps(controllerData);
-                    if (targetProps && (targetProps.dynamic && targetProps.parentID === Uuid.NULL)) {
-                        return makeRunningValues(false, [], []); // let farActionGrabEntity handle it
-                    } else {
-                        this.prepareDistanceRotatingData(controllerData);
-                        return makeRunningValues(true, [], []);
-                    }
-                } else {
-                    this.checkForUnexpectedChildren(controllerData);
-                    this.destroyContextOverlay();
-                    return makeRunningValues(false, [], []);
-                }
-            }
-            return makeRunningValues(false, [], []);
-        };
-
-        this.run = function (controllerData) {
-            if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) {
-                this.endFarParentGrab(controllerData);
-                Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
-                this.highlightedEntity = null;
-                return makeRunningValues(false, [], []);
-            }
-            this.intersectionDistance = controllerData.rayPicks[this.hand].distance;
-
-            var otherModuleName = this.hand === RIGHT_HAND ? "LeftFarParentGrabEntity" : "RightFarParentGrabEntity";
-            var otherFarGrabModule = getEnabledModuleByName(otherModuleName);
-
-            // gather up the readiness of the near-grab modules
-            var nearGrabNames = [
-                this.hand === RIGHT_HAND ? "RightScaleAvatar" : "LeftScaleAvatar",
-                this.hand === RIGHT_HAND ? "RightFarTriggerEntity" : "LeftFarTriggerEntity",
-                this.hand === RIGHT_HAND ? "RightNearActionGrabEntity" : "LeftNearActionGrabEntity",
-                this.hand === RIGHT_HAND ? "RightNearParentingGrabEntity" : "LeftNearParentingGrabEntity"
-            ];
-            if (!this.grabbing) {
-                nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay");
-                nearGrabNames.push(this.hand === RIGHT_HAND ? "RightNearTabletHighlight" : "LeftNearTabletHighlight");
-            }
-
-            var nearGrabReadiness = [];
-            for (var i = 0; i < nearGrabNames.length; i++) {
-                var nearGrabModule = getEnabledModuleByName(nearGrabNames[i]);
-                var ready = nearGrabModule ? nearGrabModule.isReady(controllerData) : makeRunningValues(false, [], []);
-                nearGrabReadiness.push(ready);
-            }
-
-            if (this.targetEntityID) {
-                // if we are doing a distance grab and the object gets close enough to the controller,
-                // stop the far-grab so the near-grab or equip can take over.
-                for (var k = 0; k < nearGrabReadiness.length; k++) {
-                    if (nearGrabReadiness[k].active && (nearGrabReadiness[k].targets[0] === this.targetEntityID)) {
-                        this.endFarParentGrab(controllerData);
-                        return makeRunningValues(false, [], []);
-                    }
-                }
-
-                this.continueDistanceHolding(controllerData);
-            } else {
-                // if we are doing a distance search and this controller moves into a position
-                // where it could near-grab something, stop searching.
-                for (var j = 0; j < nearGrabReadiness.length; j++) {
-                    if (nearGrabReadiness[j].active) {
-                        this.endFarParentGrab(controllerData);
-                        return makeRunningValues(false, [], []);
-                    }
-                }
-
-                var rayPickInfo = controllerData.rayPicks[this.hand];
-                if (rayPickInfo.type === Picks.INTERSECTED_ENTITY) {
-                    if (controllerData.triggerClicks[this.hand]) {
-                        var entityID = rayPickInfo.objectID;
-                        Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
-                        this.highlightedEntity = null;
-                        var targetProps = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
-                        if (targetProps.href !== "") {
-                            AddressManager.handleLookupString(targetProps.href);
-                            return makeRunningValues(false, [], []);
-                        }
-
-                        this.targetObject = new TargetObject(entityID, targetProps);
-                        this.targetObject.parentProps = getEntityParents(targetProps);
-
-                        if (this.contextOverlayTimer) {
-                            Script.clearTimeout(this.contextOverlayTimer);
-                        }
-                        this.contextOverlayTimer = false;
-                        if (entityID === this.entityWithContextOverlay) {
-                            this.destroyContextOverlay();
-                        } else {
-                            Selection.removeFromSelectedItemsList("contextOverlayHighlightList", "entity", entityID);
-                        }
-
-                        var targetEntity = this.targetObject.getTargetEntity();
-                        entityID = targetEntity.id;
-                        targetProps = targetEntity.props;
-
-                        if (targetProps.dynamic || this.targetObject.entityProps.dynamic) {
-                            // let farActionGrabEntity handle it
-                            return makeRunningValues(false, [], []);
-                        }
-
-                        if (entityIsGrabbable(targetProps) || entityIsGrabbable(this.targetObject.entityProps)) {
-
-                            if (!this.distanceRotating) {
-                                this.targetEntityID = entityID;
-                                this.grabbedDistance = rayPickInfo.distance;
-                            }
-
-                            if (otherFarGrabModule.targetEntityID === this.targetEntityID &&
-                                otherFarGrabModule.distanceHolding) {
-                                this.prepareDistanceRotatingData(controllerData);
-                                this.distanceRotate(otherFarGrabModule);
-                            } else {
-                                this.distanceHolding = true;
-                                this.distanceRotating = false;
-                                this.startFarParentGrab(controllerData, targetProps);
-                            }
-                        }
-                    } else {
-                        var targetEntityID = rayPickInfo.objectID;
-                        if (this.highlightedEntity !== targetEntityID) {
-                            Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
-                            var selectionTargetProps = Entities.getEntityProperties(targetEntityID, DISPATCHER_PROPERTIES);
-
-                            var selectionTargetObject = new TargetObject(targetEntityID, selectionTargetProps);
-                            selectionTargetObject.parentProps = getEntityParents(selectionTargetProps);
-                            var selectionTargetEntity = selectionTargetObject.getTargetEntity();
-
-                            if (entityIsGrabbable(selectionTargetEntity.props) ||
-                                entityIsGrabbable(selectionTargetObject.entityProps)) {
-
-                                Selection.addToSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", rayPickInfo.objectID);
-                            }
-                            this.highlightedEntity = rayPickInfo.objectID;
-                        }
-
-                        if (!this.entityWithContextOverlay) {
-                            var _this = this;
-
-                            if (_this.potentialEntityWithContextOverlay !== rayPickInfo.objectID) {
-                                if (_this.contextOverlayTimer) {
-                                    Script.clearTimeout(_this.contextOverlayTimer);
-                                }
-                                _this.contextOverlayTimer = false;
-                                _this.potentialEntityWithContextOverlay = rayPickInfo.objectID;
-                            }
-
-                            if (!_this.contextOverlayTimer) {
-                                _this.contextOverlayTimer = Script.setTimeout(function () {
-                                    if (!_this.entityWithContextOverlay &&
-                                        _this.contextOverlayTimer &&
-                                        _this.potentialEntityWithContextOverlay === rayPickInfo.objectID) {
-                                        var cotProps = Entities.getEntityProperties(rayPickInfo.objectID,
-                                                                                    DISPATCHER_PROPERTIES);
-                                        var pointerEvent = {
-                                            type: "Move",
-                                            id: _this.hand + 1, // 0 is reserved for hardware mouse
-                                            pos2D: projectOntoEntityXYPlane(rayPickInfo.objectID,
-                                                                            rayPickInfo.intersection, cotProps),
-                                            pos3D: rayPickInfo.intersection,
-                                            normal: rayPickInfo.surfaceNormal,
-                                            direction: Vec3.subtract(ZERO_VEC, rayPickInfo.surfaceNormal),
-                                            button: "Secondary"
-                                        };
-                                        if (ContextOverlay.createOrDestroyContextOverlay(rayPickInfo.objectID, pointerEvent)) {
-                                            _this.entityWithContextOverlay = rayPickInfo.objectID;
-                                        }
-                                    }
-                                    _this.contextOverlayTimer = false;
-                                }, 500);
-                            }
-                        }
-                    }
-                } else if (this.distanceRotating) {
-                    this.distanceRotate(otherFarGrabModule);
-                } else if (this.highlightedEntity) {
-                    Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
-                    this.highlightedEntity = null;
-                }
-            }
-            return this.exitIfDisabled(controllerData);
-        };
-
-        this.exitIfDisabled = function(controllerData) {
-            var moduleName = this.hand === RIGHT_HAND ? "RightDisableModules" : "LeftDisableModules";
-            var disableModule = getEnabledModuleByName(moduleName);
-            if (disableModule) {
-                if (disableModule.disableModules) {
-                    this.endFarParentGrab(controllerData);
-                    Selection.removeFromSelectedItemsList(DISPATCHER_HOVERING_LIST, "entity", this.highlightedEntity);
-                    this.highlightedEntity = null;
-                    return makeRunningValues(false, [], []);
-                }
-            }
-            var grabbedThing = (this.distanceHolding || this.distanceRotating) ? this.targetObject.entityID : null;
-            var offset = this.calculateOffset(controllerData);
-            var laserLockInfo = makeLaserLockInfo(grabbedThing, false, this.hand, offset);
-            return makeRunningValues(true, [], [], laserLockInfo);
-        };
-
-        this.calculateOffset = function(controllerData) {
-            if (this.distanceHolding || this.distanceRotating) {
-                var targetProps = Entities.getEntityProperties(this.targetObject.entityID,
-                                                               [ "position", "rotation", "registrationPoint", "dimensions" ]);
-                return worldPositionToRegistrationFrameMatrix(targetProps, controllerData.rayPicks[this.hand].intersection);
-            }
-            return undefined;
-        };
-    }
-
-    var leftFarParentGrabEntity = new FarParentGrabEntity(LEFT_HAND);
-    var rightFarParentGrabEntity = new FarParentGrabEntity(RIGHT_HAND);
-
-    enableDispatcherModule("LeftFarParentGrabEntity", leftFarParentGrabEntity);
-    enableDispatcherModule("RightFarParentGrabEntity", rightFarParentGrabEntity);
-
-    function cleanup() {
-        disableDispatcherModule("LeftFarParentGrabEntity");
-        disableDispatcherModule("RightFarParentGrabEntity");
-    }
-    Script.scriptEnding.connect(cleanup);
-}());
diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
deleted file mode 100644
index ddff35b9e7..0000000000
--- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js
+++ /dev/null
@@ -1,250 +0,0 @@
-"use strict";
-
-//  nearActionGrabEntity.js
-//
-//  Distributed under the Apache License, Version 2.0.
-//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-
-/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND,
-   getControllerJointIndex, getGrabbableData, enableDispatcherModule, disableDispatcherModule,
-   propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, entityIsGrabbable,
-   MSECS_PER_SEC, makeDispatcherModuleParameters, makeRunningValues,
-   TRIGGER_OFF_VALUE, NEAR_GRAB_RADIUS, findGroupParent, entityIsCloneable, propsAreCloneDynamic, cloneEntity,
-   HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, unhighlightTargetEntity, Uuid,
-   DISPATCHER_PROPERTIES, HMD
-*/
-
-Script.include("/~/system/libraries/controllerDispatcherUtils.js");
-Script.include("/~/system/libraries/controllers.js");
-Script.include("/~/system/libraries/cloneEntityUtils.js");
-
-(function() {
-
-    function NearActionGrabEntity(hand) {
-        this.hand = hand;
-        this.targetEntityID = null;
-        this.actionID = null; // action this script created...
-
-        this.parameters = makeDispatcherModuleParameters(
-            500,
-            this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
-            [],
-            100);
-
-        var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
-        var ACTION_TTL = 15; // seconds
-        var ACTION_TTL_REFRESH = 5;
-
-        // XXX does handJointIndex change if the avatar changes?
-        this.handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
-        this.controllerJointIndex = getControllerJointIndex(this.hand);
-
-
-        // handPosition is where the avatar's hand appears to be, in-world.
-        this.getHandPosition = function () {
-            if (this.hand === RIGHT_HAND) {
-                return MyAvatar.getRightPalmPosition();
-            } else {
-                return MyAvatar.getLeftPalmPosition();
-            }
-        };
-
-        this.getHandRotation = function () {
-            if (this.hand === RIGHT_HAND) {
-                return MyAvatar.getRightPalmRotation();
-            } else {
-                return MyAvatar.getLeftPalmRotation();
-            }
-        };
-
-
-        this.startNearGrabAction = function (controllerData, targetProps) {
-            Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
-
-            var grabbableData = getGrabbableData(targetProps);
-            this.grabFollowsController = grabbableData.grabFollowsController;
-            this.kinematicGrab = grabbableData.grabKinematic;
-
-            var handJointIndex;
-            if (HMD.mounted && HMD.isHandControllerAvailable() && grabbableData.grabFollowsController) {
-                handJointIndex = getControllerJointIndex(this.hand);
-            } else {
-                handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
-            }
-            this.offsetPosition = Entities.worldToLocalPosition(targetProps.position, MyAvatar.SELF_ID, handJointIndex);
-            this.offsetRotation = Entities.worldToLocalRotation(targetProps.rotation, MyAvatar.SELF_ID, handJointIndex);
-
-            var now = Date.now();
-            this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
-
-            if (this.actionID) {
-                Entities.deleteAction(this.targetEntityID, this.actionID);
-            }
-            this.actionID = Entities.addAction("hold", this.targetEntityID, {
-                hand: this.hand === RIGHT_HAND ? "right" : "left",
-                timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
-                relativePosition: this.offsetPosition,
-                relativeRotation: this.offsetRotation,
-                ttl: ACTION_TTL,
-                kinematic: this.kinematicGrab,
-                kinematicSetVelocity: true,
-                ignoreIK: this.grabFollowsController
-            });
-            if (this.actionID === Uuid.NULL) {
-                this.actionID = null;
-                return;
-            }
-
-            Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
-                action: 'grab',
-                grabbedEntity: this.targetEntityID,
-                joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
-            }));
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(this.targetEntityID, "startNearGrab", args);
-        };
-
-        // this is for when the action is going to time-out
-        this.refreshNearGrabAction = function (controllerData) {
-            var now = Date.now();
-            if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) {
-                // if less than a 5 seconds left, refresh the actions ttl
-                var success = Entities.updateAction(this.targetEntityID, this.actionID, {
-                    hand: this.hand === RIGHT_HAND ? "right" : "left",
-                    timeScale: NEAR_GRABBING_ACTION_TIMEFRAME,
-                    relativePosition: this.offsetPosition,
-                    relativeRotation: this.offsetRotation,
-                    ttl: ACTION_TTL,
-                    kinematic: this.kinematicGrab,
-                    kinematicSetVelocity: true,
-                    ignoreIK: this.grabFollowsController
-                });
-                if (success) {
-                    this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC);
-                }
-            }
-        };
-
-        this.endNearGrabAction = function () {
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
-
-            Entities.deleteAction(this.targetEntityID, this.actionID);
-            this.actionID = null;
-
-            Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
-                action: 'release',
-                grabbedEntity: this.targetEntityID,
-                joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
-            }));
-
-            this.targetEntityID = null;
-        };
-
-        this.getTargetProps = function (controllerData) {
-            // nearbyEntityProperties is already sorted by distance from controller
-            var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
-            var sensorScaleFactor = MyAvatar.sensorToWorldScale;
-            for (var i = 0; i < nearbyEntityProperties.length; i++) {
-                var props = nearbyEntityProperties[i];
-                if (props.distance > NEAR_GRAB_RADIUS * sensorScaleFactor) {
-                    break;
-                }
-                if (entityIsGrabbable(props) || entityIsCloneable(props)) {
-                    if (!entityIsCloneable(props)) {
-                        // if we've attempted to grab a non-cloneable child, roll up to the root of the tree
-                        var groupRootProps = findGroupParent(controllerData, props);
-                        if (entityIsGrabbable(groupRootProps)) {
-                            return groupRootProps;
-                        }
-                    }
-                    return props;
-                }
-            }
-            return null;
-        };
-
-        this.isReady = function (controllerData) {
-            this.targetEntityID = null;
-
-            var targetProps = this.getTargetProps(controllerData);
-            if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE &&
-                controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
-                return makeRunningValues(false, [], []);
-            }
-
-            if (targetProps) {
-                if ((!propsArePhysical(targetProps) && !propsAreCloneDynamic(targetProps)) ||
-                    targetProps.parentID !== Uuid.NULL) {
-                    return makeRunningValues(false, [], []); // let nearParentGrabEntity handle it
-                } else {
-                    this.targetEntityID = targetProps.id;
-                    return makeRunningValues(true, [this.targetEntityID], []);
-                }
-            } else {
-                return makeRunningValues(false, [], []);
-            }
-        };
-
-        this.run = function (controllerData) {
-            if (this.actionID) {
-                if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE &&
-                    controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
-                    this.endNearGrabAction();
-                    return makeRunningValues(false, [], []);
-                }
-
-                this.refreshNearGrabAction(controllerData);
-                var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-                Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args);
-            } else {
-
-                // still searching / highlighting
-                var readiness = this.isReady (controllerData);
-                if (!readiness.active) {
-                    return readiness;
-                }
-
-                var targetProps = this.getTargetProps(controllerData);
-                if (targetProps) {
-                    if (controllerData.triggerClicks[this.hand] ||
-                        controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
-                        // switch to grabbing
-                        var targetCloneable = entityIsCloneable(targetProps);
-                        if (targetCloneable) {
-                            var cloneID = cloneEntity(targetProps);
-                            var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES);
-                            this.targetEntityID = cloneID;
-                            this.startNearGrabAction(controllerData, cloneProps);
-                        } else {
-                            this.startNearGrabAction(controllerData, targetProps);
-                        }
-                    }
-                }
-            }
-
-            return makeRunningValues(true, [this.targetEntityID], []);
-        };
-
-        this.cleanup = function () {
-            if (this.targetEntityID) {
-                this.endNearGrabAction();
-            }
-        };
-    }
-
-    var leftNearActionGrabEntity = new NearActionGrabEntity(LEFT_HAND);
-    var rightNearActionGrabEntity = new NearActionGrabEntity(RIGHT_HAND);
-
-    enableDispatcherModule("LeftNearActionGrabEntity", leftNearActionGrabEntity);
-    enableDispatcherModule("RightNearActionGrabEntity", rightNearActionGrabEntity);
-
-    function cleanup() {
-        leftNearActionGrabEntity.cleanup();
-        rightNearActionGrabEntity.cleanup();
-        disableDispatcherModule("LeftNearActionGrabEntity");
-        disableDispatcherModule("RightNearActionGrabEntity");
-    }
-    Script.scriptEnding.connect(cleanup);
-}());
diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js
index 60a5781ca4..0f8071677c 100644
--- a/scripts/system/controllers/controllerModules/nearGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js
@@ -8,9 +8,10 @@
 
 /* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex, enableDispatcherModule,
    disableDispatcherModule, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, TRIGGER_OFF_VALUE,
-   makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGroupParent, Vec3, cloneEntity,
-   entityIsCloneable, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, distanceBetweenPointAndEntityBoundingBox,
-   getGrabbableData, getEnabledModuleByName, DISPATCHER_PROPERTIES, HMD, NEAR_GRAB_DISTANCE
+   makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS, findGrabbableGroupParent, Vec3,
+   cloneEntity, entityIsCloneable, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE,
+   distanceBetweenPointAndEntityBoundingBox, getGrabbableData, getEnabledModuleByName, DISPATCHER_PROPERTIES, HMD,
+   NEAR_GRAB_DISTANCE
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -80,9 +81,6 @@ Script.include("/~/system/libraries/controllers.js");
         this.endNearGrabEntity = function () {
             this.endGrab();
 
-            this.grabbing = false;
-            this.targetEntityID = null;
-
             var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
             Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
             Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
@@ -90,6 +88,9 @@ Script.include("/~/system/libraries/controllers.js");
                 grabbedEntity: this.targetEntityID,
                 joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
             }));
+
+            this.grabbing = false;
+            this.targetEntityID = null;
         };
 
         this.getTargetProps = function (controllerData) {
@@ -110,7 +111,7 @@ Script.include("/~/system/libraries/controllers.js");
                 if (entityIsGrabbable(props) || entityIsCloneable(props)) {
                     if (!entityIsCloneable(props)) {
                         // if we've attempted to grab a non-cloneable child, roll up to the root of the tree
-                        var groupRootProps = findGroupParent(controllerData, props);
+                        var groupRootProps = findGrabbableGroupParent(controllerData, props);
                         if (entityIsGrabbable(groupRootProps)) {
                             return groupRootProps;
                         }
diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
deleted file mode 100644
index 13557bdb7e..0000000000
--- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js
+++ /dev/null
@@ -1,359 +0,0 @@
-"use strict";
-
-//  nearParentGrabEntity.js
-//
-//  Distributed under the Apache License, Version 2.0.
-//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-
-
-/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, getControllerJointIndex,
-   enableDispatcherModule, disableDispatcherModule, propsArePhysical, Messages, HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION,
-   TRIGGER_OFF_VALUE, makeDispatcherModuleParameters, entityIsGrabbable, makeRunningValues, NEAR_GRAB_RADIUS,
-   findGroupParent, Vec3, cloneEntity, entityIsCloneable, propsAreCloneDynamic, HAPTIC_PULSE_STRENGTH,
-   HAPTIC_PULSE_DURATION, BUMPER_ON_VALUE, findHandChildEntities, TEAR_AWAY_DISTANCE, MSECS_PER_SEC, TEAR_AWAY_CHECK_TIME,
-   TEAR_AWAY_COUNT, distanceBetweenPointAndEntityBoundingBox, print, Uuid, NEAR_GRAB_DISTANCE,
-   distanceBetweenEntityLocalPositionAndBoundingBox, getGrabbableData, getGrabPointSphereOffset, DISPATCHER_PROPERTIES
-*/
-
-Script.include("/~/system/libraries/controllerDispatcherUtils.js");
-Script.include("/~/system/libraries/cloneEntityUtils.js");
-Script.include("/~/system/libraries/controllers.js");
-
-(function() {
-
-    // XXX this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true;
-    // XXX this.kinematicGrab = (grabbableData.kinematic !== undefined) ? grabbableData.kinematic : NEAR_GRABBING_KINEMATIC;
-
-    function NearParentingGrabEntity(hand) {
-        this.hand = hand;
-        this.targetEntityID = null;
-        this.grabbing = false;
-        this.previousParentID = {};
-        this.previousParentJointIndex = {};
-        this.previouslyUnhooked = {};
-        this.lastUnequipCheckTime = 0;
-        this.autoUnequipCounter = 0;
-        this.lastUnexpectedChildrenCheckTime = 0;
-        this.robbed = false;
-        this.cloneAllowed = true;
-
-        this.parameters = makeDispatcherModuleParameters(
-            500,
-            this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"],
-            [],
-            100);
-
-        this.thisHandIsParent = function(props) {
-            if (!props) {
-                return false;
-            }
-
-            if (props.parentID !== MyAvatar.sessionUUID && props.parentID !== MyAvatar.SELF_ID) {
-                return false;
-            }
-
-            var handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
-            if (props.parentJointIndex === handJointIndex) {
-                return true;
-            }
-
-            if (props.parentJointIndex === getControllerJointIndex(this.hand)) {
-                return true;
-            }
-
-            var controllerCRJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ?
-                "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
-                "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
-
-            if (props.parentJointIndex === controllerCRJointIndex) {
-                return true;
-            }
-
-            return false;
-        };
-
-        this.getOtherModule = function() {
-            return this.hand === RIGHT_HAND ? leftNearParentingGrabEntity : rightNearParentingGrabEntity;
-        };
-
-        this.otherHandIsParent = function(props) {
-            var otherModule = this.getOtherModule();
-            return (otherModule.thisHandIsParent(props) && otherModule.grabbing);
-        };
-
-        this.startNearParentingGrabEntity = function (controllerData, targetProps) {
-            var grabData = getGrabbableData(targetProps);
-            Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
-
-            var handJointIndex;
-            if (grabData.grabFollowsController) {
-                handJointIndex = getControllerJointIndex(this.hand);
-            } else {
-                handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
-            }
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(targetProps.id, "startNearGrab", args);
-
-            var reparentProps = {
-                parentID: MyAvatar.SELF_ID,
-                parentJointIndex: handJointIndex,
-                localVelocity: {x: 0, y: 0, z: 0},
-                localAngularVelocity: {x: 0, y: 0, z: 0}
-            };
-
-            if (this.thisHandIsParent(targetProps)) {
-                // this should never happen, but if it does, don't set previous parent to be this hand.
-                this.previousParentID[targetProps.id] = null;
-                this.previousParentJointIndex[targetProps.id] = -1;
-            } else if (this.otherHandIsParent(targetProps)) {
-                var otherModule = this.getOtherModule();
-                this.previousParentID[this.grabbedThingID] = otherModule.previousParentID[this.grabbedThingID];
-                this.previousParentJointIndex[this.grabbedThingID] = otherModule.previousParentJointIndex[this.grabbedThingID];
-                otherModule.robbed = true;
-            } else {
-                this.previousParentID[targetProps.id] = targetProps.parentID;
-                this.previousParentJointIndex[targetProps.id] = targetProps.parentJointIndex;
-            }
-
-            this.targetEntityID = targetProps.id;
-            Entities.editEntity(targetProps.id, reparentProps);
-
-            Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
-                action: 'grab',
-                grabbedEntity: targetProps.id,
-                joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
-            }));
-            this.grabbing = true;
-        };
-
-        this.endNearParentingGrabEntity = function (controllerData) {
-            var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
-            if (this.thisHandIsParent(props) && !this.robbed) {
-                Entities.editEntity(this.targetEntityID, {
-                    parentID: this.previousParentID[this.targetEntityID],
-                    parentJointIndex: this.previousParentJointIndex[this.targetEntityID],
-                    localVelocity: {x: 0, y: 0, z: 0},
-                    localAngularVelocity: {x: 0, y: 0, z: 0}
-                });
-            }
-
-            var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-            Entities.callEntityMethod(this.targetEntityID, "releaseGrab", args);
-            Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
-                action: 'release',
-                grabbedEntity: this.targetEntityID,
-                joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
-            }));
-
-            this.grabbing = false;
-            this.targetEntityID = null;
-            this.robbed = false;
-        };
-
-        this.checkForChildTooFarAway = function (controllerData) {
-            var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
-            var now = Date.now();
-            if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * TEAR_AWAY_CHECK_TIME) {
-                this.lastUnequipCheckTime = now;
-                if (props.parentID === MyAvatar.SELF_ID) {
-                    var tearAwayDistance = TEAR_AWAY_DISTANCE * MyAvatar.sensorToWorldScale;
-                    var controllerIndex =
-                        this.hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
-                    var controllerGrabOffset = getGrabPointSphereOffset(controllerIndex, true);
-                    controllerGrabOffset = Vec3.multiply(-MyAvatar.sensorToWorldScale, controllerGrabOffset);
-                    var distance = distanceBetweenEntityLocalPositionAndBoundingBox(props, controllerGrabOffset);
-                    if (distance > tearAwayDistance) {
-                        this.autoUnequipCounter++;
-                    } else {
-                        this.autoUnequipCounter = 0;
-                    }
-                    if (this.autoUnequipCounter >= TEAR_AWAY_COUNT) {
-                        return true;
-                    }
-                }
-            }
-            return false;
-        };
-
-
-        this.checkForUnexpectedChildren = function (controllerData) {
-            // sometimes things can get parented to a hand and this script is unaware.  Search for such entities and
-            // unhook them.
-
-            var now = Date.now();
-            var UNEXPECTED_CHILDREN_CHECK_TIME = 0.1; // seconds
-            if (now - this.lastUnexpectedChildrenCheckTime > MSECS_PER_SEC * UNEXPECTED_CHILDREN_CHECK_TIME) {
-                this.lastUnexpectedChildrenCheckTime = now;
-
-                var children = findHandChildEntities(this.hand);
-                var _this = this;
-
-                children.forEach(function(childID) {
-                    // we appear to be holding something and this script isn't in a state that would be holding something.
-                    // unhook it.  if we previously took note of this entity's parent, put it back where it was.  This
-                    // works around some problems that happen when more than one hand or avatar is passing something around.
-                    if (_this.previousParentID[childID]) {
-                        var previousParentID = _this.previousParentID[childID];
-                        var previousParentJointIndex = _this.previousParentJointIndex[childID];
-
-                        // The main flaw with keeping track of previous parentage in individual scripts is:
-                        // (1) A grabs something (2) B takes it from A (3) A takes it from B (4) A releases it
-                        // now A and B will take turns passing it back to the other.  Detect this and stop the loop here...
-                        var UNHOOK_LOOP_DETECT_MS = 200;
-                        if (_this.previouslyUnhooked[childID]) {
-                            if (now - _this.previouslyUnhooked[childID] < UNHOOK_LOOP_DETECT_MS) {
-                                previousParentID = Uuid.NULL;
-                                previousParentJointIndex = -1;
-                            }
-                        }
-                        _this.previouslyUnhooked[childID] = now;
-
-                        Entities.editEntity(childID, {
-                            parentID: previousParentID,
-                            parentJointIndex: previousParentJointIndex
-                        });
-                    } else {
-                        Entities.editEntity(childID, { parentID: Uuid.NULL });
-                    }
-                });
-            }
-        };
-
-        this.getTargetProps = function (controllerData) {
-            // nearbyEntityProperties is already sorted by length from controller
-            var nearbyEntityProperties = controllerData.nearbyEntityProperties[this.hand];
-            var sensorScaleFactor = MyAvatar.sensorToWorldScale;
-            var nearGrabDistance = NEAR_GRAB_DISTANCE * sensorScaleFactor;
-            var nearGrabRadius = NEAR_GRAB_RADIUS * sensorScaleFactor;
-            for (var i = 0; i < nearbyEntityProperties.length; i++) {
-                var props = nearbyEntityProperties[i];
-                var grabPosition = controllerData.controllerLocations[this.hand].position; // Is offset from hand position.
-                var dist = distanceBetweenPointAndEntityBoundingBox(grabPosition, props);
-                var distance = Vec3.distance(grabPosition, props.position);
-                if ((dist > nearGrabDistance) ||
-                    (distance > nearGrabRadius)) { // Only smallish entities can be near grabbed.
-                    continue;
-                }
-                if (entityIsGrabbable(props) || entityIsCloneable(props)) {
-                    if (!entityIsCloneable(props)) {
-                        // if we've attempted to grab a non-cloneable child, roll up to the root of the tree
-                        var groupRootProps = findGroupParent(controllerData, props);
-                        if (entityIsGrabbable(groupRootProps)) {
-                            return groupRootProps;
-                        }
-                    }
-                    return props;
-                }
-            }
-            return null;
-        };
-
-        this.isReady = function (controllerData, deltaTime) {
-            this.targetEntityID = null;
-            this.grabbing = false;
-
-            var targetProps = this.getTargetProps(controllerData);
-            if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE &&
-                controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
-                this.checkForUnexpectedChildren(controllerData);
-                this.robbed = false;
-                this.cloneAllowed = true;
-                return makeRunningValues(false, [], []);
-            }
-
-            if (targetProps) {
-                if ((propsArePhysical(targetProps) || propsAreCloneDynamic(targetProps)) &&
-                    targetProps.parentID === Uuid.NULL) {
-                    this.robbed = false;
-                    return makeRunningValues(false, [], []); // let nearActionGrabEntity handle it
-                } else {
-                    this.targetEntityID = targetProps.id;
-                    return makeRunningValues(true, [this.targetEntityID], []);
-                }
-            } else {
-                this.robbed = false;
-                return makeRunningValues(false, [], []);
-            }
-        };
-
-        this.run = function (controllerData, deltaTime) {
-            if (this.grabbing) {
-                if (controllerData.triggerClicks[this.hand] < TRIGGER_OFF_VALUE &&
-                    controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
-                    this.endNearParentingGrabEntity(controllerData);
-                    return makeRunningValues(false, [], []);
-                }
-
-                var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
-                if (!props) {
-                    // entity was deleted
-                    this.grabbing = false;
-                    this.targetEntityID = null;
-                    this.robbed = false;
-                    return makeRunningValues(false, [], []);
-                }
-
-                if (this.checkForChildTooFarAway(controllerData)) {
-                    // if the held entity moves too far from the hand, release it
-                    print("nearParentGrabEntity -- autoreleasing held item because it is far from hand");
-                    this.endNearParentingGrabEntity(controllerData);
-                    return makeRunningValues(false, [], []);
-                }
-
-                var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
-                Entities.callEntityMethod(this.targetEntityID, "continueNearGrab", args);
-            } else {
-                // still searching
-                var readiness = this.isReady(controllerData);
-                if (!readiness.active) {
-                    this.robbed = false;
-                    return readiness;
-                }
-                if (controllerData.triggerClicks[this.hand] || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
-                    // switch to grab
-                    var targetProps = this.getTargetProps(controllerData);
-                    var targetCloneable = entityIsCloneable(targetProps);
-
-                    if (targetCloneable) {
-                        if (this.cloneAllowed) {
-                            var cloneID = cloneEntity(targetProps);
-                            if (cloneID !== null) {
-                                var cloneProps = Entities.getEntityProperties(cloneID, DISPATCHER_PROPERTIES);
-                                this.grabbing = true;
-                                this.targetEntityID = cloneID;
-                                this.startNearParentingGrabEntity(controllerData, cloneProps);
-                                this.cloneAllowed = false; // prevent another clone call until inputs released
-                            }
-                        }
-                    } else if (targetProps) {
-                        this.grabbing = true;
-                        this.startNearParentingGrabEntity(controllerData, targetProps);
-                    }
-                }
-            }
-
-            return makeRunningValues(true, [this.targetEntityID], []);
-        };
-
-        this.cleanup = function () {
-            if (this.targetEntityID) {
-                this.endNearParentingGrabEntity();
-            }
-        };
-    }
-
-    var leftNearParentingGrabEntity = new NearParentingGrabEntity(LEFT_HAND);
-    var rightNearParentingGrabEntity = new NearParentingGrabEntity(RIGHT_HAND);
-
-    enableDispatcherModule("LeftNearParentingGrabEntity", leftNearParentingGrabEntity);
-    enableDispatcherModule("RightNearParentingGrabEntity", rightNearParentingGrabEntity);
-
-    function cleanup() {
-        leftNearParentingGrabEntity.cleanup();
-        rightNearParentingGrabEntity.cleanup();
-        disableDispatcherModule("LeftNearParentingGrabEntity");
-        disableDispatcherModule("RightNearParentingGrabEntity");
-    }
-    Script.scriptEnding.connect(cleanup);
-}());
diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js
index 2114f2c0b2..86ff7701c3 100644
--- a/scripts/system/controllers/controllerScripts.js
+++ b/scripts/system/controllers/controllerScripts.js
@@ -32,22 +32,13 @@ var CONTOLLER_SCRIPTS = [
     "controllerModules/mouseHMD.js",
     "controllerModules/scaleEntity.js",
     "controllerModules/nearGrabHyperLinkEntity.js",
-    "controllerModules/nearTabletHighlight.js"
+    "controllerModules/nearTabletHighlight.js",
+    "controllerModules/nearGrabEntity.js",
+    "controllerModules/farGrabEntity.js"
 ];
 
-if (Settings.getValue("useTraitsGrab", true)) {
-    CONTOLLER_SCRIPTS.push("controllerModules/nearGrabEntity.js");
-    CONTOLLER_SCRIPTS.push("controllerModules/farGrabEntity.js");
-} else {
-    CONTOLLER_SCRIPTS.push("controllerModules/nearParentGrabEntity.js");
-    CONTOLLER_SCRIPTS.push("controllerModules/nearActionGrabEntity.js");
-    CONTOLLER_SCRIPTS.push("controllerModules/farActionGrabEntityDynOnly.js");
-    CONTOLLER_SCRIPTS.push("controllerModules/farParentGrabEntity.js");
-}
-
 var DEBUG_MENU_ITEM = "Debug defaultScripts.js";
 
-
 function runDefaultsTogether() {
     for (var j in CONTOLLER_SCRIPTS) {
         if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) {
diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js
index a78a2971e9..1fb82d3843 100644
--- a/scripts/system/controllers/grab.js
+++ b/scripts/system/controllers/grab.js
@@ -14,79 +14,25 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 
-/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, getEntityCustomData, Messages, Quat, Controller,
+/* global MyAvatar, Entities, Script, HMD, Camera, Vec3, Reticle, Overlays, Messages, Quat, Controller,
    isInEditMode, entityIsGrabbable, Picks, PickType, Pointers, unhighlightTargetEntity, DISPATCHER_PROPERTIES,
-   entityIsGrabbable, entityIsEquipped, getMainTabletIDs
+   entityIsGrabbable, getMainTabletIDs
 */
 /* jslint bitwise: true */
 
 (function() { // BEGIN LOCAL_SCOPE
 
-    Script.include("/~/system/libraries/utils.js");
-    Script.include("/~/system/libraries/controllerDispatcherUtils.js");
+Script.include("/~/system/libraries/utils.js");
+Script.include("/~/system/libraries/controllerDispatcherUtils.js");
+
+var MOUSE_GRAB_JOINT = 65526; // FARGRAB_MOUSE_INDEX
+
 var MAX_SOLID_ANGLE = 0.01; // objects that appear smaller than this can't be grabbed
 
 var DELAY_FOR_30HZ = 33; // milliseconds
 
-var ZERO_VEC3 = {
-    x: 0,
-    y: 0,
-    z: 0
-};
-var IDENTITY_QUAT = {
-    x: 0,
-    y: 0,
-    z: 0,
-    w: 0
-};
-
-var DEFAULT_GRABBABLE_DATA = {
-    grabbable: true,
-    invertSolidWhileHeld: false
-};
-
-
-var ACTION_TTL = 10; // seconds
-
-function getTag() {
-    return "grab-" + MyAvatar.sessionUUID;
-}
-
-var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
-var DISTANCE_HOLDING_UNITY_MASS = 1200; //  The mass at which the distance holding action timeframe is unmodified
-var DISTANCE_HOLDING_UNITY_DISTANCE = 6; //  The distance at which the distance holding action timeframe is unmodified
-
-function distanceGrabTimescale(mass, distance) {
-    var timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME * mass /
-        DISTANCE_HOLDING_UNITY_MASS * distance /
-        DISTANCE_HOLDING_UNITY_DISTANCE;
-    if (timeScale < DISTANCE_HOLDING_ACTION_TIMEFRAME) {
-        timeScale = DISTANCE_HOLDING_ACTION_TIMEFRAME;
-    }
-    return timeScale;
-}
-function getMass(dimensions, density) {
-    return (dimensions.x * dimensions.y * dimensions.z) * density;
-}
-
-function entityIsGrabbedByOther(entityID) {
-    // by convention, a distance grab sets the tag of its action to be grab-*owner-session-id*.
-    var actionIDs = Entities.getActionIDs(entityID);
-    for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) {
-        var actionID = actionIDs[actionIndex];
-        var actionArguments = Entities.getActionArguments(entityID, actionID);
-        var tag = actionArguments.tag;
-        if (tag == getTag()) {
-            // we see a grab-*uuid* shaped tag, but it's our tag, so that's okay.
-            continue;
-        }
-        if (tag.slice(0, 5) == "grab-") {
-            // we see a grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it.
-            return true;
-        }
-    }
-    return false;
-}
+var ZERO_VEC3 = { x: 0, y: 0, z: 0 };
+var IDENTITY_QUAT = { x: 0, y: 0, z: 0, w: 1 };
 
 // helper function
 function mouseIntersectionWithPlane(pointOnPlane, planeNormal, event, maxDistance) {
@@ -227,7 +173,6 @@ var beacon = {
 function Grabber() {
     this.isGrabbing = false;
     this.entityID = null;
-    this.actionID = null;
     this.startPosition = ZERO_VEC3;
     this.lastRotation = IDENTITY_QUAT;
     this.currentPosition = ZERO_VEC3;
@@ -253,9 +198,6 @@ function Grabber() {
         z: 0
     };
 
-    this.targetPosition = null;
-    this.targetRotation = null;
-
     this.liftKey = false; // SHIFT
     this.rotateKey = false; // CONTROL
 
@@ -305,7 +247,7 @@ Grabber.prototype.computeNewGrabPlane = function() {
         }
     }
 
-    this.pointOnPlane = Vec3.sum(this.currentPosition, this.offset);
+    this.pointOnPlane = Vec3.subtract(this.currentPosition, this.offset);
     var xzOffset = Vec3.subtract(this.pointOnPlane, Camera.getPosition());
     xzOffset.y = 0;
     this.xzDistanceToGrab = Vec3.length(xzOffset);
@@ -315,15 +257,12 @@ Grabber.prototype.pressEvent = function(event) {
     if (isInEditMode() || HMD.active) {
         return;
     }
-
     if (event.button !== "LEFT") {
         return;
     }
-
     if (event.isAlt || event.isMeta) {
         return;
     }
-
     if (Overlays.getOverlayAtPoint(Reticle.position) > 0) {
         // the mouse is pointing at an overlay; don't look for entities underneath the overlay.
         return;
@@ -341,13 +280,12 @@ Grabber.prototype.pressEvent = function(event) {
     }
 
     var props = Entities.getEntityProperties(pickResults.objectID, DISPATCHER_PROPERTIES);
-    var isDynamic = props.dynamic;
     if (!entityIsGrabbable(props)) {
         // only grab grabbable objects
         return;
     }
-
-    if (!props.grab.grabbable) {
+    if (props.grab.equippable) {
+        // don't mouse-grab click-to-equip entities (let equipEntity.js handle these)
         return;
     }
 
@@ -361,7 +299,6 @@ Grabber.prototype.pressEvent = function(event) {
     var entityProperties = Entities.getEntityProperties(clickedEntity, DISPATCHER_PROPERTIES);
     this.startPosition = entityProperties.position;
     this.lastRotation = entityProperties.rotation;
-    this.madeDynamic = false;
     var cameraPosition = Camera.getPosition();
 
     var objectBoundingDiameter = Vec3.length(entityProperties.dimensions);
@@ -373,21 +310,10 @@ Grabber.prototype.pressEvent = function(event) {
         return;
     }
 
-    if (entityIsGrabbable(props) && !isDynamic) {
-        entityProperties.dynamic = true;
-        Entities.editEntity(clickedEntity, entityProperties);
-        this.madeDynamic = true;
-    }
-    // this.activateEntity(clickedEntity, entityProperties);
     this.isGrabbing = true;
 
     this.entityID = clickedEntity;
     this.currentPosition = entityProperties.position;
-    this.targetPosition = {
-        x: this.startPosition.x,
-        y: this.startPosition.y,
-        z: this.startPosition.z
-    };
 
     // compute the grab point
     var pickRay = Camera.computePickRay(event.x, event.y);
@@ -396,14 +322,13 @@ Grabber.prototype.pressEvent = function(event) {
     nearestPoint = Vec3.multiply(distanceToGrab, pickRay.direction);
     this.pointOnPlane = Vec3.sum(cameraPosition, nearestPoint);
 
-    // compute the grab offset (points from object center to point of grab)
-    this.offset = Vec3.subtract(this.pointOnPlane, this.startPosition);
+    // compute the grab offset (points from point of grab to object center)
+    this.offset = Vec3.subtract(this.startPosition, this.pointOnPlane); // offset in world-space
+    MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(this.startPosition));
+    MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation));
 
     this.computeNewGrabPlane();
-
-    if (!entityIsGrabbedByOther(this.entityID)) {
-      this.moveEvent(event);
-    }
+    this.moveEvent(event);
 
     var args = "mouse";
     Entities.callEntityMethod(this.entityID, "startDistanceGrab", args);
@@ -413,6 +338,12 @@ Grabber.prototype.pressEvent = function(event) {
         grabbedEntity: this.entityID
     }));
 
+    if (this.grabID) {
+        MyAvatar.releaseGrab(this.grabID);
+        this.grabID = null;
+    }
+    this.grabID = MyAvatar.grab(this.entityID, MOUSE_GRAB_JOINT, ZERO_VEC3, IDENTITY_QUAT);
+
     // TODO: play sounds again when we aren't leaking AudioInjector threads
     //Audio.playSound(grabSound, { position: entityProperties.position, volume: VOLUME });
 };
@@ -428,20 +359,7 @@ Grabber.prototype.releaseEvent = function(event) {
     }
 
     if (this.isGrabbing) {
-        // this.deactivateEntity(this.entityID);
         this.isGrabbing = false;
-        if (this.actionID) {
-            Entities.deleteAction(this.entityID, this.actionID);
-        }
-
-        if (this.madeDynamic) {
-            var entityProps = {};
-            entityProps.dynamic = false;
-            entityProps.localVelocity = {x: 0, y: 0, z: 0};
-            Entities.editEntity(this.entityID, entityProps);
-        }
-
-        this.actionID = null;
 
         Pointers.setRenderState(this.mouseRayEntities, "");
         Pointers.setLockEndUUID(this.mouseRayEntities, null, false);
@@ -455,6 +373,13 @@ Grabber.prototype.releaseEvent = function(event) {
             joint: "mouse"
         }));
 
+        if (this.grabID) {
+            MyAvatar.releaseGrab(this.grabID);
+            this.grabID = null;
+        }
+
+        MyAvatar.clearJointData(MOUSE_GRAB_JOINT);
+
         // TODO: play sounds again when we aren't leaking AudioInjector threads
         //Audio.playSound(releaseSound, { position: entityProperties.position, volume: VOLUME });
     }
@@ -482,23 +407,12 @@ Grabber.prototype.moveEvent = function(event) {
 
 Grabber.prototype.moveEventProcess = function() {
     this.moveEventTimer = null;
-    // see if something added/restored gravity
     var entityProperties = Entities.getEntityProperties(this.entityID, DISPATCHER_PROPERTIES);
-    if (!entityProperties || !entityProperties.gravity || HMD.active) {
+    if (!entityProperties || HMD.active) {
         return;
     }
 
-    if (Vec3.length(entityProperties.gravity) !== 0.0) {
-        this.originalGravity = entityProperties.gravity;
-    }
     this.currentPosition = entityProperties.position;
-    this.mass = getMass(entityProperties.dimensions, entityProperties.density);
-    var cameraPosition = Camera.getPosition();
-
-    var actionArgs = {
-        tag: getTag(),
-        ttl: ACTION_TTL
-    };
 
     if (this.mode === "rotate") {
         var drag = mouse.getDrag();
@@ -510,19 +424,9 @@ Grabber.prototype.moveEventProcess = function() {
         var ROTATE_STRENGTH = 0.4; // magic number tuned by hand
         var angle = ROTATE_STRENGTH * Math.sqrt((drag.x * drag.x) + (drag.y * drag.y));
         var deltaQ = Quat.angleAxis(angle, axis);
-        // var qZero = entityProperties.rotation;
-        //var qZero = this.lastRotation;
+
         this.lastRotation = Quat.multiply(deltaQ, this.lastRotation);
-
-        var distanceToCameraR = Vec3.length(Vec3.subtract(this.currentPosition, cameraPosition));
-        var angularTimeScale = distanceGrabTimescale(this.mass, distanceToCameraR);
-
-        actionArgs = {
-            targetRotation: this.lastRotation,
-            angularTimeScale: angularTimeScale,
-            tag: getTag(),
-            ttl: ACTION_TTL
-        };
+        MyAvatar.setJointRotation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointRotation(this.lastRotation));
 
     } else {
         var newPointOnPlane;
@@ -534,17 +438,10 @@ Grabber.prototype.moveEventProcess = function() {
             planeNormal = Vec3.normalize(planeNormal);
             var pointOnCylinder = Vec3.multiply(planeNormal, this.xzDistanceToGrab);
             pointOnCylinder = Vec3.sum(Camera.getPosition(), pointOnCylinder);
-            this.pointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance);
-            newPointOnPlane = {
-                x: this.pointOnPlane.x,
-                y: this.pointOnPlane.y,
-                z: this.pointOnPlane.z
-            };
-
+            newPointOnPlane = mouseIntersectionWithPlane(pointOnCylinder, planeNormal, mouse.current, this.maxDistance);
         } else {
-
-            newPointOnPlane = mouseIntersectionWithPlane(
-                    this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance);
+            var cameraPosition = Camera.getPosition();
+            newPointOnPlane = mouseIntersectionWithPlane(this.pointOnPlane, this.planeNormal, mouse.current, this.maxDistance);
             var relativePosition = Vec3.subtract(newPointOnPlane, cameraPosition);
             var distance = Vec3.length(relativePosition);
             if (distance > this.maxDistance) {
@@ -553,26 +450,8 @@ Grabber.prototype.moveEventProcess = function() {
                 newPointOnPlane = Vec3.sum(relativePosition, cameraPosition);
             }
         }
-        this.targetPosition = Vec3.subtract(newPointOnPlane, this.offset);
 
-        var distanceToCameraL = Vec3.length(Vec3.subtract(this.targetPosition, cameraPosition));
-        var linearTimeScale = distanceGrabTimescale(this.mass, distanceToCameraL);
-
-        actionArgs = {
-            targetPosition: this.targetPosition,
-            linearTimeScale: linearTimeScale,
-            tag: getTag(),
-            ttl: ACTION_TTL
-        };
-
-    }
-
-    if (!this.actionID) {
-        if (!entityIsGrabbedByOther(this.entityID) && !entityIsEquipped(this.entityID)) {
-            this.actionID = Entities.addAction("far-grab", this.entityID, actionArgs);
-        }
-    } else {
-        Entities.updateAction(this.entityID, this.actionID, actionArgs);
+        MyAvatar.setJointTranslation(MOUSE_GRAB_JOINT, MyAvatar.worldToJointPoint(Vec3.sum(newPointOnPlane, this.offset)));
     }
 
     this.scheduleMouseMoveProcessor();
@@ -601,6 +480,10 @@ Grabber.prototype.keyPressEvent = function(event) {
 Grabber.prototype.cleanup = function() {
     Pointers.removePointer(this.mouseRayEntities);
     Picks.removePick(this.mouseRayOverlays);
+    if (this.grabID) {
+        MyAvatar.releaseGrab(this.grabID);
+        this.grabID = null;
+    }
 };
 
 var grabber = new Grabber();
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js
index 8b7264eeb1..0bec77aa41 100644
--- a/scripts/system/html/js/entityProperties.js
+++ b/scripts/system/html/js/entityProperties.js
@@ -184,6 +184,20 @@ const GROUPS = [
         id: "zone",
         addToGroup: "base",
         properties: [
+            {
+                label: "Shape Type",
+                type: "dropdown",
+                options: { "box": "Box", "sphere": "Sphere", "ellipsoid": "Ellipsoid", 
+                           "cylinder-y": "Cylinder", "compound": "Use Compound Shape URL" },
+                propertyID: "zoneShapeType",
+                propertyName: "shapeType", // actual entity property name
+            },
+            {
+                label: "Compound Shape URL",
+                type: "string",
+                propertyID: "zoneCompoundShapeURL",
+                propertyName: "compoundShapeURL", // actual entity property name
+            },
             {
                 label: "Flying Allowed",
                 type: "bool",
@@ -1345,24 +1359,6 @@ const GROUPS = [
             },
         ]
     },
-    {
-        id: "zone_shape",
-        label: "ZONE SHAPE",
-        properties: [
-            {
-                label: "Shape Type",
-                type: "dropdown",
-                options: { "box": "Box", "sphere": "Sphere", "ellipsoid": "Ellipsoid", 
-                           "cylinder-y": "Cylinder", "compound": "Use Compound Shape URL" },
-                propertyID: "shapeType",
-            },
-            {
-                label: "Compound Shape URL",
-                type: "string",
-                propertyID: "compoundShapeURL",
-            },
-        ]
-    },
     {
         id: "physics",
         label: "PHYSICS",
@@ -1454,7 +1450,7 @@ const GROUPS_PER_TYPE = {
   None: [ 'base', 'spatial', 'behavior', 'collision', 'physics' ],
   Shape: [ 'base', 'shape', 'spatial', 'behavior', 'collision', 'physics' ],
   Text: [ 'base', 'text', 'spatial', 'behavior', 'collision', 'physics' ],
-  Zone: [ 'base', 'zone', 'spatial', 'behavior', 'zone_shape', 'physics' ],
+  Zone: [ 'base', 'zone', 'spatial', 'behavior', 'physics' ],
   Model: [ 'base', 'model', 'spatial', 'behavior', 'collision', 'physics' ],
   Image: [ 'base', 'image', 'spatial', 'behavior', 'collision', 'physics' ],
   Web: [ 'base', 'web', 'spatial', 'behavior', 'collision', 'physics' ],
@@ -3317,7 +3313,8 @@ function loaded() {
                             }
                         }
 
-                        let doSelectElement = lastEntityID === '"' + selectedEntityProperties.id + '"';
+                        let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"';
+                        let doSelectElement = !hasSelectedEntityChanged;
 
                         // the event bridge and json parsing handle our avatar id string differently.
                         lastEntityID = '"' + selectedEntityProperties.id + '"';
@@ -3437,7 +3434,7 @@ function loaded() {
                                     property.elColorPicker.style.backgroundColor = "rgb(" + propertyValue.red + "," + 
                                                                                      propertyValue.green + "," + 
                                                                                      propertyValue.blue + ")";
-                                    if ($(property.elColorPicker).attr('active') === 'true') {
+                                    if (hasSelectedEntityChanged && $(property.elColorPicker).attr('active') === 'true') {
                                         // Set the color picker inactive before setting the color,
                                         // otherwise an update will be sent directly after setting it here.
                                         $(property.elColorPicker).attr('active', 'false');
diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js
index 221af07474..385ed954b0 100644
--- a/scripts/system/libraries/controllerDispatcherUtils.js
+++ b/scripts/system/libraries/controllerDispatcherUtils.js
@@ -33,6 +33,7 @@
    getGrabbableData:true,
    isAnothersAvatarEntity:true,
    isAnothersChildEntity:true,
+   entityIsEquippable:true,
    entityIsGrabbable:true,
    entityIsDistanceGrabbable:true,
    getControllerJointIndexCacheTime:true,
@@ -46,7 +47,7 @@
    makeLaserLockInfo:true,
    entityHasActions:true,
    ensureDynamic:true,
-   findGroupParent:true,
+   findGrabbableGroupParent:true,
    BUMPER_ON_VALUE:true,
    getEntityParents:true,
    findHandChildEntities:true,
@@ -58,7 +59,6 @@
    NEAR_GRAB_DISTANCE: true,
    distanceBetweenPointAndEntityBoundingBox:true,
    entityIsEquipped:true,
-   entityIsFarGrabbedByOther:true,
    highlightTargetEntity:true,
    clearHighlightedEntities:true,
    unhighlightTargetEntity:true,
@@ -323,6 +323,18 @@ isAnothersChildEntity = function (iaceProps) {
     return false;
 };
 
+
+entityIsEquippable = function (eieProps) {
+    var grabbable = getGrabbableData(eieProps).grabbable;
+    if (!grabbable ||
+        isAnothersAvatarEntity(eieProps) ||
+        isAnothersChildEntity(eieProps) ||
+        FORBIDDEN_GRAB_TYPES.indexOf(eieProps.type) >= 0) {
+        return false;
+    }
+    return true;
+};
+
 entityIsGrabbable = function (eigProps) {
     var grabbable = getGrabbableData(eigProps).grabbable;
     if (!grabbable ||
@@ -439,7 +451,7 @@ ensureDynamic = function (entityID) {
     }
 };
 
-findGroupParent = function (controllerData, targetProps) {
+findGrabbableGroupParent = function (controllerData, targetProps) {
     while (targetProps.grab.grabDelegateToParent &&
            targetProps.parentID &&
            targetProps.parentID !== Uuid.NULL &&
@@ -448,6 +460,9 @@ findGroupParent = function (controllerData, targetProps) {
         if (!parentProps) {
             break;
         }
+        if (!entityIsGrabbable(parentProps)) {
+            break;
+        }
         parentProps.id = targetProps.parentID;
         targetProps = parentProps;
         controllerData.nearbyEntityPropertiesByID[targetProps.id] = targetProps;
@@ -561,27 +576,6 @@ entityIsEquipped = function(entityID) {
     return equippedInRightHand || equippedInLeftHand;
 };
 
-entityIsFarGrabbedByOther = function(entityID) {
-    // by convention, a far grab sets the tag of its action to be far-grab-*owner-session-id*.
-    var actionIDs = Entities.getActionIDs(entityID);
-    var myFarGrabTag = "far-grab-" + MyAvatar.sessionUUID;
-    for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) {
-        var actionID = actionIDs[actionIndex];
-        var actionArguments = Entities.getActionArguments(entityID, actionID);
-        var tag = actionArguments.tag;
-        if (tag == myFarGrabTag) {
-            // we see a far-grab-*uuid* shaped tag, but it's our tag, so that's okay.
-            continue;
-        }
-        if (tag.slice(0, 9) == "far-grab-") {
-            // we see a far-grab-*uuid* shaped tag and it's not ours, so someone else is grabbing it.
-            return true;
-        }
-    }
-    return false;
-};
-
-
 worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) {
     // get world matrix for intersection point
     var intersectionMat = new Xform({ x: 0, y: 0, z:0, w: 1 }, pos);
@@ -614,12 +608,13 @@ if (typeof module !== 'undefined') {
         unhighlightTargetEntity: unhighlightTargetEntity,
         clearHighlightedEntities: clearHighlightedEntities,
         makeRunningValues: makeRunningValues,
-        findGroupParent: findGroupParent,
+        findGrabbableGroupParent: findGrabbableGroupParent,
         LEFT_HAND: LEFT_HAND,
         RIGHT_HAND: RIGHT_HAND,
         BUMPER_ON_VALUE: BUMPER_ON_VALUE,
         TEAR_AWAY_DISTANCE: TEAR_AWAY_DISTANCE,
         propsArePhysical: propsArePhysical,
+        entityIsEquippable: entityIsEquippable,
         entityIsGrabbable: entityIsGrabbable,
         NEAR_GRAB_RADIUS: NEAR_GRAB_RADIUS,
         projectOntoOverlayXYPlane: projectOntoOverlayXYPlane,
diff --git a/tools/dissectors/3-hf-avatar.lua b/tools/dissectors/3-hf-avatar.lua
index f4172b01cb..bc449770f5 100644
--- a/tools/dissectors/3-hf-avatar.lua
+++ b/tools/dissectors/3-hf-avatar.lua
@@ -379,6 +379,9 @@ function decode_avatar_data_packet(buf)
     i = i + num_validity_bytes
     result["valid_translations"] = "Valid Translations: " .. string.format("(%d/%d) {", #indices, num_joints) .. table.concat(indices, ", ") .. "}"
 
+    -- TODO: skip maxTranslationDimension
+    i = i + 4
+
     -- TODO: skip translations for now
     i = i + #indices * 6
 
diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md
index 23105a0e02..3a664a12e9 100644
--- a/tools/nitpick/README.md
+++ b/tools/nitpick/README.md
@@ -13,36 +13,39 @@ Nitpick has 5 functions, separated into separate tabs:
 1. Web interface
 
 ## Build (for developers)
-Nitpick is built as part of the High Fidelity build.
+Nitpick is built as part of the High Fidelity build.  
+XXXX refers to the version number - replace as necessary. For example, replace with 3.2.11
 ### Creating installers
 #### Windows
+1.  Perform Release build
 1.  Verify that 7Zip is installed.
 1.  cd to the `build\tools\nitpick\Release` directory
 1.  Delete any existing installers (named nitpick-installer-###.exe)
 1.  Select all, right-click and select 7-Zip->Add to archive...
 1.  Set Archive format to 7z
 1.  Check "Create SFX archive
-1.  Enter installer name (i.e. `nitpick-installer-v1.2.exe`)
+1.  Enter installer name (i.e. `nitpick-installer-vXXXX.exe`)
 1.  Click "OK"
-1.  Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe: aws s3 cp nitpick-installer-v1.2.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.2.exe
+1.  Copy created installer to https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe: aws s3 cp nitpick-installer-vXXXX.exe s3://hifi-qa/nitpick/Mac/nitpick-installer-vXXXX.exe
 #### Mac
 These steps assume the hifi repository has been cloned to `~/hifi`.
 1.  (first time) Install brew
     In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)`
 1.  (First time) install create-dmg:
     In a terminal: `brew install create-dmg`
+1.  Perform Release build
 1.  In a terminal: cd to the `build/tools/nitpick/Release` folder
 1.  Copy the quazip dynamic library (note final period):
     In a terminal: `cp ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib .`
 1.  Change the loader instruction to find the dynamic library locally
     In a terminal: `install_name_tool -change ~/hifi/build/ext/Xcode/quazip/project/lib/libquazip5.1.dylib libquazip5.1.dylib nitpick`
 1.  Delete any existing disk images. In a terminal: `rm *.dmg`
-1.  Create installer (note final period).In a terminal: `create-dmg --volname nitpick-installer-v1.2 nitpick-installer-v1.2.dmg .`  
+1.  Create installer (note final period).In a terminal: `create-dmg --volname nitpick-installer-vXXXX nitpick-installer-vXXXX.dmg .`  
     Make sure to wait for completion.
-1.  Copy created installer to AWS: `~/Library/Python/3.7/bin/aws s3 cp nitpick-installer-v1.2.dmg s3://hifi-qa/nitpick/Mac/nitpick-installer-v1.2.dmg`
+1.  Copy created installer to AWS: `~/Library/Python/3.7/bin/aws s3 cp nitpick-installer-vXXXX.dmg s3://hifi-qa/nitpick/Mac/nitpick-installer-vXXXX.dmg`
 ### Installation
 #### Windows
-1.  (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe)
+1.  (First time) download and install vc_redist.x64.exe (available at https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe)
 1.  (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/)
     1. After installation - create an environment variable called PYTHON_PATH and set it to the folder containing the Python executable.
 1.  (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/
@@ -52,7 +55,7 @@ These steps assume the hifi repository has been cloned to `~/hifi`.
     1.  Leave region name and ouput format as default [None]
     1.  Install the latest release of Boto3 via pip:  `pip install boto3`
 
-1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-v1.2.exe>)
+1. Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Windows/nitpick-installer-vXXXX.exe>)
 1. Double click on the installer and install to a convenient location  
 ![](./setup_7z.PNG)
 
@@ -76,14 +79,14 @@ In a terminal: `python3 get-pip.py --user`
     1.  Enter the secret key
     1.  Leave region name and ouput format as default [None]
     1.  Install the latest release of Boto3 via pip:  pip3 install boto3
-1.  Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Mac/nitpick-installer-v1.2.dmg>).
+1.  Download the installer by browsing to [here](<https://hifi-qa.s3.amazonaws.com/nitpick/Mac/nitpick-installer-vXXXX.dmg>).
 1.  Double-click on the downloaded image to mount it
 1. Create a folder for the nitpick files (e.g. ~/nitpick)
    If this folder exists then delete all it's contents.
 1. Copy the downloaded files to the folder  
    In a terminal:  
      `cd ~/nitpick`  
-     `cp -r /Volumes/nitpick-installer-v1.2/* .`
+     `cp -r /Volumes/nitpick-installer-vXXXX/* .`
 
 1. __To run nitpick, cd to the folder that you copied to and run `./nitpick`__  
 # Usage