Merge branch 'master' into M21222

This commit is contained in:
David Rowe 2019-03-09 07:57:53 +13:00
commit 4779a6b156
362 changed files with 14173 additions and 3145 deletions

10
.gitignore vendored
View file

@ -24,6 +24,7 @@ android/**/bin
android/**/src/main/res/values/libs.xml
android/**/src/main/assets
android/**/gradle*
*.class
# VSCode
# List taken from Github Global Ignores master@435c4d92
@ -98,12 +99,9 @@ tools/jsdoc/package-lock.json
# Python compile artifacts
**/__pycache__
# ignore unneeded unity project files for avatar exporter
tools/unity-avatar-exporter/Library
tools/unity-avatar-exporter/Logs
tools/unity-avatar-exporter/Packages
tools/unity-avatar-exporter/ProjectSettings
tools/unity-avatar-exporter/Temp
# ignore local unity project files for avatar exporter
tools/unity-avatar-exporter
server-console/package-lock.json
vcpkg/
/tools/nitpick/compiledResources

65
BUILD_QUEST.md Normal file
View file

@ -0,0 +1,65 @@
Please read the [general build guide](BUILD.md) for information on building other platform. Only Quest specific instructions are found in this file.
# Dependencies
Building is currently supported on OSX, Windows and Linux platforms, but developers intending to do work on the library dependencies are strongly urged to use 64 bit Linux as a build platform
You will need the following tools to build Android targets.
* [Android Studio](https://developer.android.com/studio/index.html)
### Android Studio
Download the Android Studio installer and run it. Once installed, at the welcome screen, click configure in the lower right corner and select SDK manager
From the SDK Platforms tab, select API levels 24 and 26.
From the SDK Tools tab select the following
* Android SDK Build-Tools
* GPU Debugging Tools
* CMake (even if you have a separate CMake installation)
* LLDB
* Android SDK Platform-Tools
* Android SDK Tools
* NDK (even if you have the NDK installed separately)
Make sure the NDK installed version is 18 (or higher)
# Environment
Setting up the environment for android builds requires some additional steps
#### Set up machine specific Gradle properties
Create a `gradle.properties` file in $HOME/.gradle. Edit the file to contain the following
HIFI_ANDROID_PRECOMPILED=<your_home_directory>/Android/hifi_externals
HIFI_ANDROID_KEYSTORE=<key_store_directory>/<keystore_name>.jks
HIFI_ANDROID_KEYSTORE_PASSWORD=<password>
HIFI_ANDROID_KEY_ALIAS=<key_alias>
HIFI_ANDROID_KEY_PASSWORD=<key_password>
Note, do not use `$HOME` for the path. It must be a fully qualified path name.
### Setup the repository
Clone the repository
`git clone https://github.com/highfidelity/hifi.git`
Enter the repository `android` directory
`cd hifi/android`
# Building & Running
* Open Android Studio
* Choose _Open Existing Android Studio Project_
* Navigate to the `hifi` repository and choose the `android` folder and select _OK_
* Open Gradle.settings and comment out any projects not necessary
* From _File_ menu select _Sync with File System_ to resync Gradle settings
* From the _Build_ menu select _Make Project_
* From
* Once the build completes, from the _Run_ menu select _Run App_

View file

@ -52,6 +52,7 @@ else()
set(MOBILE 0)
endif()
set(HIFI_USE_OPTIMIZED_IK OFF)
set(BUILD_CLIENT_OPTION ON)
set(BUILD_SERVER_OPTION ON)
set(BUILD_TESTS_OPTION OFF)
@ -100,6 +101,13 @@ if (ANDROID)
add_definitions(-DCUSTOM_DISPLAY_PLUGINS)
set(PLATFORM_PLUGIN_LIBRARIES oculusMobile oculusMobilePlugin)
endif()
# Allow client code to use preprocessor macros to distinguish between quest and non-quest builds
if (${HIFI_ANDROID_APP} STREQUAL "questInterface")
add_definitions(-DANDROID_APP_QUEST_INTERFACE)
elseif(${HIFI_ANDROID_APP} STREQUAL "interface")
add_definitions(-DANDROID_APP_INTERFACE)
endif()
else ()
set(PLATFORM_QT_COMPONENTS WebEngine Xml)
endif ()
@ -108,7 +116,7 @@ if (USE_GLES AND (NOT ANDROID))
set(DISABLE_QML_OPTION ON)
endif()
option(HIFI_USE_OPTIMIZED_IK "USE OPTIMIZED IK" ${HIFI_USE_OPTIMIZED_IK_OPTION})
option(BUILD_CLIENT "Build client components" ${BUILD_CLIENT_OPTION})
option(BUILD_SERVER "Build server components" ${BUILD_SERVER_OPTION})
option(BUILD_TESTS "Build tests" ${BUILD_TESTS_OPTION})
@ -139,6 +147,7 @@ foreach(PLATFORM_QT_COMPONENT ${PLATFORM_QT_COMPONENTS})
list(APPEND PLATFORM_QT_LIBRARIES "Qt5::${PLATFORM_QT_COMPONENT}")
endforeach()
MESSAGE(STATUS "USE OPTIMIZED IK: " ${HIFI_USE_OPTIMIZED_IK})
MESSAGE(STATUS "Build server: " ${BUILD_SERVER})
MESSAGE(STATUS "Build client: " ${BUILD_CLIENT})
MESSAGE(STATUS "Build tests: " ${BUILD_TESTS})
@ -184,6 +193,10 @@ find_package( Threads )
add_definitions(-DGLM_FORCE_RADIANS)
add_definitions(-DGLM_ENABLE_EXPERIMENTAL)
add_definitions(-DGLM_FORCE_CTOR_INIT)
if (HIFI_USE_OPTIMIZED_IK)
MESSAGE(STATUS "SET THE USE IK DEFINITION ")
add_definitions(-DHIFI_USE_OPTIMIZED_IK)
endif()
set(HIFI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries")
set(EXTERNAL_PROJECT_PREFIX "project")

View file

@ -36,11 +36,6 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 26
//buildToolsVersion '27.0.3'
def appVersionCode = Integer.valueOf(VERSION_CODE ?: 1)
def appVersionName = RELEASE_NUMBER ?: "1.0"
defaultConfig {
applicationId "io.highfidelity.hifiinterface"
minSdkVersion 24
@ -66,10 +61,10 @@ android {
}
signingConfigs {
release {
storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : null
storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : ''
keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : ''
keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : ''
storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : file('../keystore.jks')
storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : 'password'
keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : 'key0'
keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : 'password'
}
}
}
@ -90,10 +85,7 @@ android {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig project.hasProperty("HIFI_ANDROID_KEYSTORE") &&
project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") &&
project.hasProperty("HIFI_ANDROID_KEY_ALIAS") &&
project.hasProperty("HIFI_ANDROID_KEY_PASSWORD")? signingConfigs.release : null
signingConfig signingConfigs.release
buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\""
buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\""
buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\""

View file

@ -81,6 +81,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
private boolean nativeEnterBackgroundCallEnqueued = false;
private SlidingDrawer mWebSlidingDrawer;
private boolean mStartInDomain;
private boolean isLoading;
// private GvrApi gvrApi;
// Opaque native pointer to the Application C++ object.
// This object is owned by the InterfaceActivity instance and passed to the native methods.
@ -94,7 +95,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
@Override
public void onCreate(Bundle savedInstanceState) {
super.isLoading = true;
isLoading = true;
Intent intent = getIntent();
if (intent.hasExtra(DOMAIN_URL) && !TextUtils.isEmpty(intent.getStringExtra(DOMAIN_URL))) {
intent.putExtra("applicationArguments", "--url " + intent.getStringExtra(DOMAIN_URL));
@ -145,7 +146,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
@Override
protected void onPause() {
super.onPause();
if (super.isLoading) {
if (isLoading) {
nativeEnterBackgroundCallEnqueued = true;
} else {
nativeEnterBackground();
@ -172,7 +173,6 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
super.onResume();
nativeEnterForeground();
surfacesWorkaround();
keepInterfaceRunning = false;
registerReceiver(headsetStateReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
//gvrApi.resumeTracking();
}
@ -382,7 +382,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
}
public void onAppLoadedComplete() {
super.isLoading = false;
isLoading = false;
if (nativeEnterBackgroundCallEnqueued) {
nativeEnterBackground();
}
@ -413,7 +413,6 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW
@Override
public void onExpand() {
keepInterfaceRunning = true;
}
@Override

BIN
android/apps/keystore.jks Normal file

Binary file not shown.

View file

@ -1,7 +1,7 @@
set(TARGET_NAME questFramePlayer)
setup_hifi_library(AndroidExtras)
link_hifi_libraries(shared ktx shaders gpu gl oculusMobile ${PLATFORM_GL_BACKEND})
target_include_directories(${TARGET_NAME} PRIVATE ${HIFI_ANDROID_PRECOMPILED}/ovr/VrApi/Include)
target_link_libraries(${TARGET_NAME} android log m)
target_opengl()
target_oculus_mobile()

View file

@ -19,24 +19,6 @@
android:name="org.qtproject.qt5.android.bindings.QtApplication"
tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon">
<meta-data android:name="com.samsung.android.vr.application.mode" android:value="vr_only"/>
<activity
android:name=".QuestQtActivity"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
android:label="@string/app_name"
android:screenOrientation="landscape"
android:excludeFromRecents="false"
android:alwaysRetainTaskState="true"
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
>
<!-- JNI nonsense -->
<meta-data android:name="android.app.lib_name" android:value="questFramePlayer"/>
<!-- Qt nonsense -->
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
<meta-data android:name="android.app.load_local_libs" android:value="plugins/platforms/android/libqtforandroid.so:plugins/bearer/libqandroidbearer.so:lib/libQt5QuickParticles.so"/>
</activity>
<activity
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
@ -50,6 +32,13 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- JNI nonsense -->
<meta-data android:name="android.app.lib_name" android:value="questFramePlayer"/>
<!-- Qt nonsense -->
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
<meta-data android:name="android.app.load_local_libs" android:value="plugins/platforms/android/libqtforandroid.so:plugins/bearer/libqandroidbearer.so:lib/libQt5QuickParticles.so"/>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,30 @@
//
// Created by Bradley Austin Davis on 2019/02/15
// Copyright 2013-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 "AndroidHelper.h"
#include <QtCore/QDebug>
#include <QtGui/QGuiApplication>
AndroidHelper::AndroidHelper() {
}
AndroidHelper::~AndroidHelper() {
}
void AndroidHelper::notifyLoadComplete() {
emit qtAppLoadComplete();
}
void AndroidHelper::notifyEnterForeground() {
emit enterForeground();
}
void AndroidHelper::notifyEnterBackground() {
emit enterBackground();
}

View file

@ -0,0 +1,43 @@
//
// Created by Bradley Austin Davis on 2019/02/15
// Copyright 2013-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_Android_Helper_h
#define hifi_Android_Helper_h
#include <QtCore/QObject>
#include <QtCore/QMap>
#include <QtCore/QUrl>
#include <QtCore/QEventLoop>
class AndroidHelper : public QObject {
Q_OBJECT
public:
AndroidHelper(AndroidHelper const&) = delete;
void operator=(AndroidHelper const&) = delete;
static AndroidHelper& instance() {
static AndroidHelper instance;
return instance;
}
void notifyLoadComplete();
void notifyEnterForeground();
void notifyEnterBackground();
signals:
void qtAppLoadComplete();
void enterForeground();
void enterBackground();
private:
AndroidHelper();
~AndroidHelper();
};
#endif

View file

@ -11,15 +11,11 @@
#include <QtWidgets/QFileDialog>
PlayerWindow::PlayerWindow() {
installEventFilter(this);
setFlags(Qt::MSWindowsOwnDC | Qt::Window | Qt::Dialog | Qt::WindowMinMaxButtonsHint | Qt::WindowTitleHint);
setFlags(Qt::Window);
setSurfaceType(QSurface::OpenGLSurface);
create();
showFullScreen();
// Ensure the window is visible and the GL context is valid
QCoreApplication::processEvents();
_renderThread.initialize(this);
}
PlayerWindow::~PlayerWindow() {
_renderThread.initialize();
}

View file

@ -8,22 +8,13 @@
#pragma once
#include <QtGui/QWindow>
#include <QtCore/QSettings>
#include <gpu/Forward.h>
#include "RenderThread.h"
// Create a simple OpenGL window that renders text in various ways
class PlayerWindow : public QWindow {
public:
PlayerWindow();
virtual ~PlayerWindow();
protected:
//bool eventFilter(QObject* obj, QEvent* event) override;
//void keyPressEvent(QKeyEvent* event) override;
virtual ~PlayerWindow() {}
private:
QSettings _settings;
RenderThread _renderThread;
};

View file

@ -16,6 +16,7 @@
#include <QtCore/QFileInfo>
#include <QtGui/QWindow>
#include <QtGui/QImageReader>
#include <QtAndroidExtras/QAndroidJniObject>
#include <gl/QOpenGLContextWrapper.h>
#include <gpu/FrameIO.h>
@ -29,9 +30,7 @@
#include <VrApi.h>
#include <VrApi_Input.h>
static JNIEnv* _env { nullptr };
static JavaVM* _vm { nullptr };
static jobject _activity { nullptr };
#include "AndroidHelper.h"
struct HandController{
ovrInputTrackedRemoteCapabilities caps {};
@ -48,21 +47,43 @@ struct HandController{
};
std::vector<HandController> devices;
QAndroidJniObject __interfaceActivity;
extern "C" {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void *) {
__android_log_write(ANDROID_LOG_WARN, "QQQ", __FUNCTION__);
return JNI_VERSION_1_6;
}
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnCreate(JNIEnv *env, jobject obj) {
__android_log_print(ANDROID_LOG_INFO, "QQQ", __FUNCTION__);
__interfaceActivity = QAndroidJniObject(obj);
QObject::connect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, []() {
__interfaceActivity.callMethod<void>("onAppLoadedComplete", "()V");
QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, nullptr, nullptr);
});
}
JNIEXPORT void JNICALL Java_io_highfidelity_frameplayer_QuestQtActivity_nativeOnCreate(JNIEnv* env, jobject obj) {
env->GetJavaVM(&_vm);
_activity = env->NewGlobalRef(obj);
JNIEXPORT void
Java_io_highfidelity_oculus_OculusMobileActivity_questOnAppAfterLoad(JNIEnv *env, jobject obj) {
AndroidHelper::instance().moveToThread(qApp->thread());
}
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnPause(JNIEnv *env, jobject obj) {
AndroidHelper::instance().notifyEnterBackground();
}
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnResume(JNIEnv *env, jobject obj) {
AndroidHelper::instance().notifyEnterForeground();
}
}
static const char* FRAME_FILE = "assets:/frames/20190121_1220.json";
static void textureLoader(const std::string& filename, const gpu::TexturePointer& texture, uint16_t layer) {
@ -84,11 +105,10 @@ void RenderThread::move(const glm::vec3& v) {
_correction = glm::inverse(glm::translate(mat4(), v)) * _correction;
}
void RenderThread::initialize(QWindow* window) {
void RenderThread::initialize() {
std::unique_lock<std::mutex> lock(_frameLock);
setObjectName("RenderThread");
Parent::initialize();
_window = window;
_thread->setObjectName("RenderThread");
}
@ -96,14 +116,7 @@ void RenderThread::setup() {
// Wait until the context has been moved to this thread
{ std::unique_lock<std::mutex> lock(_frameLock); }
ovr::VrHandler::initVr();
__android_log_write(ANDROID_LOG_WARN, "QQQ", "Launching oculus activity");
_vm->AttachCurrentThread(&_env, nullptr);
jclass cls = _env->GetObjectClass(_activity);
jmethodID mid = _env->GetMethodID(cls, "launchOculusActivity", "()V");
_env->CallVoidMethod(_activity, mid);
__android_log_write(ANDROID_LOG_WARN, "QQQ", "Launching oculus activity done");
ovr::VrHandler::setHandler(this);
makeCurrent();
@ -169,7 +182,6 @@ void RenderThread::handleInput() {
const auto &remote = controller.state;
if (remote.Joystick.x != 0.0f || remote.Joystick.y != 0.0f) {
glm::vec3 translation;
float rotation = 0.0f;
if (caps.ControllerCapabilities & ovrControllerCaps_LeftHand) {
translation = glm::vec3{0.0f, -remote.Joystick.y, 0.0f};
} else {

View file

@ -20,11 +20,9 @@
class RenderThread : public GenericThread, ovr::VrHandler {
using Parent = GenericThread;
public:
QWindow* _window{ nullptr };
std::mutex _mutex;
gpu::ContextPointer _gpuContext; // initialized during window creation
std::shared_ptr<gpu::Backend> _backend;
std::atomic<size_t> _presentCount{ 0 };
std::mutex _frameLock;
std::queue<gpu::FramePointer> _pendingFrames;
gpu::FramePointer _activeFrame;
@ -39,6 +37,6 @@ public:
void handleInput();
void submitFrame(const gpu::FramePointer& frame);
void initialize(QWindow* window);
void initialize();
void renderFrame();
};

View file

@ -11,30 +11,33 @@
#include <QtGui/QGuiApplication>
#include <QtCore/QTimer>
#include <QtCore/QFileInfo>
#include <QtAndroidExtras/QAndroidJniObject>
#include <Trace.h>
#include "PlayerWindow.h"
#include "AndroidHelper.h"
void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) {
if (!message.isEmpty()) {
const char * local=message.toStdString().c_str();
const char* local = message.toStdString().c_str();
switch (type) {
case QtDebugMsg:
__android_log_write(ANDROID_LOG_DEBUG,"Interface",local);
__android_log_write(ANDROID_LOG_DEBUG, "Interface", local);
break;
case QtInfoMsg:
__android_log_write(ANDROID_LOG_INFO,"Interface",local);
__android_log_write(ANDROID_LOG_INFO, "Interface", local);
break;
case QtWarningMsg:
__android_log_write(ANDROID_LOG_WARN,"Interface",local);
__android_log_write(ANDROID_LOG_WARN, "Interface", local);
break;
case QtCriticalMsg:
__android_log_write(ANDROID_LOG_ERROR,"Interface",local);
__android_log_write(ANDROID_LOG_ERROR, "Interface", local);
break;
case QtFatalMsg:
default:
__android_log_write(ANDROID_LOG_FATAL,"Interface",local);
__android_log_write(ANDROID_LOG_FATAL, "Interface", local);
abort();
}
}
@ -46,11 +49,13 @@ int main(int argc, char** argv) {
auto oldMessageHandler = qInstallMessageHandler(messageHandler);
DependencyManager::set<tracing::Tracer>();
PlayerWindow window;
__android_log_write(ANDROID_LOG_FATAL,"QQQ","Exec");
QTimer::singleShot(10, []{
__android_log_write(ANDROID_LOG_WARN, "QQQ", "notifyLoadComplete");
AndroidHelper::instance().notifyLoadComplete();
});
__android_log_write(ANDROID_LOG_WARN, "QQQ", "Exec");
app.exec();
__android_log_write(ANDROID_LOG_FATAL,"QQQ","Exec done");
__android_log_write(ANDROID_LOG_WARN, "QQQ", "Exec done");
qInstallMessageHandler(oldMessageHandler);
return 0;
}

View file

@ -1,53 +0,0 @@
//
// Created by Bradley Austin Davis on 2018/11/20
// Copyright 2013-2018 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
//
package io.highfidelity.frameplayer;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import org.qtproject.qt5.android.bindings.QtActivity;
import io.highfidelity.oculus.OculusMobileActivity;
public class QuestQtActivity extends QtActivity {
private native void nativeOnCreate();
private boolean launchedQuestMode = false;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.w("QQQ_Qt", "QuestQtActivity::onCreate");
super.onCreate(savedInstanceState);
nativeOnCreate();
}
@Override
public void onDestroy() {
Log.w("QQQ_Qt", "QuestQtActivity::onDestroy");
super.onDestroy();
}
public void launchOculusActivity() {
Log.w("QQQ_Qt", "QuestQtActivity::launchOculusActivity");
runOnUiThread(()->{
keepInterfaceRunning = true;
launchedQuestMode = true;
moveTaskToBack(true);
startActivity(new Intent(this, QuestRenderActivity.class));
});
}
@Override
public void onResume() {
super.onResume();
if (launchedQuestMode) {
moveTaskToBack(true);
}
}
}

View file

@ -1,14 +1,6 @@
package io.highfidelity.frameplayer;
import android.content.Intent;
import android.os.Bundle;
import io.highfidelity.oculus.OculusMobileActivity;
public class QuestRenderActivity extends OculusMobileActivity {
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
startActivity(new Intent(this, QuestQtActivity.class));
}
}

View file

@ -0,0 +1,16 @@
set(TARGET_NAME questInterface)
setup_hifi_library()
link_hifi_libraries(
shared task networking qml
image fbx hfm render-utils physics entities octree
oculusMobile oculusMobilePlugin
gl gpu ${PLATFORM_GL_BACKEND}
)
target_opengl()
target_bullet()
target_oculus_mobile()
add_subdirectory("${CMAKE_SOURCE_DIR}/interface" "libraries/interface")
include_directories("${CMAKE_SOURCE_DIR}/interface/src")
add_subdirectory("${CMAKE_SOURCE_DIR}/plugins/hifiCodec" "libraries/hifiCodecPlugin")
target_link_libraries(questInterface android log m interface)

View file

@ -0,0 +1,142 @@
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
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) }
}
}
android {
compileSdkVersion 28
defaultConfig {
applicationId "io.highfidelity.questInterface"
minSdkVersion 24
targetSdkVersion 28
versionCode appVersionCode
versionName appVersionName
ndk { abiFilters 'arm64-v8a' }
externalNativeBuild {
cmake {
arguments '-DHIFI_ANDROID=1',
'-DHIFI_ANDROID_APP=questInterface',
'-DANDROID_TOOLCHAIN=clang',
'-DANDROID_STL=c++_shared',
'-DCMAKE_VERBOSE_MAKEFILE=ON',
'-DRELEASE_NUMBER=' + RELEASE_NUMBER,
'-DRELEASE_TYPE=' + RELEASE_TYPE,
'-DSTABLE_BUILD=' + STABLE_BUILD,
'-DDISABLE_QML=OFF',
'-DDISABLE_KTX_CACHE=OFF',
'-DUSE_BREAKPAD=OFF'
targets = ['questInterface']
}
}
signingConfigs {
release {
storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : file('../keystore.jks')
storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : 'password'
keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : 'key0'
keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : 'password'
v2SigningEnabled false
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
debug {
buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\""
buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\""
buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\""
buildConfigField "String", "OAUTH_CLIENT_SECRET", "\"" + (System.getenv("OAUTH_CLIENT_SECRET") ? System.getenv("OAUTH_CLIENT_SECRET") : '') + "\""
buildConfigField "String", "OAUTH_REDIRECT_URI", "\"" + (System.getenv("OAUTH_REDIRECT_URI") ? System.getenv("OAUTH_REDIRECT_URI") : '') + "\""
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\""
buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\""
buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\""
buildConfigField "String", "OAUTH_CLIENT_SECRET", "\"" + (System.getenv("OAUTH_CLIENT_SECRET") ? System.getenv("OAUTH_CLIENT_SECRET") : '') + "\""
buildConfigField "String", "OAUTH_REDIRECT_URI", "\"" + (System.getenv("OAUTH_REDIRECT_URI") ? System.getenv("OAUTH_REDIRECT_URI") : '') + "\""
}
}
externalNativeBuild {
cmake {
path '../../../CMakeLists.txt'
}
}
applicationVariants.all { variant ->
// Our asset contents depend on items produced in the CMake build
// so our merge has to depend on the external native build
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()
runDumpSymsTask.dependsOn(task)
variant.assemble.dependsOn(uploadDumpSymsTask)
variant.mergeResources.dependsOn(renameHifiACTask)
}
}
variant.mergeAssets.doLast {
def assetList = new LinkedList<String>()
def youngestLastModified = 0
// Copy the compiled resources generated by the external native build
copy {
from new File(projectDir, "../../../interface/compiledResources")
into outputDir
duplicatesStrategy DuplicatesStrategy.INCLUDE
eachFile { details ->
youngestLastModified = Math.max(youngestLastModified, details.lastModified)
assetList.add(details.path)
}
}
// Copy the scripts directory
copy {
from new File(projectDir, "../../../scripts")
into new File(outputDir, "scripts")
duplicatesStrategy DuplicatesStrategy.INCLUDE
eachFile { details->
youngestLastModified = Math.max(youngestLastModified, details.lastModified)
assetList.add("scripts/" + details.path)
}
}
// Write a list of files to be unpacked to the cache folder
new File(outputDir, 'cache_assets.txt').withWriter { out ->
out.println(Long.toString(youngestLastModified))
assetList.each { file -> out.println(file) }
}
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: '../../libraries/qt/libs')
implementation project(':oculus')
implementation project(':qt')
}

View file

@ -0,0 +1,25 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\SDK/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.highfidelity.questInterface"
android:installLocation="auto">
<uses-feature android:glEsVersion="0x00030002" android:required="true" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-feature android:name="android.hardware.sensor.accelerometer" android:required="true"/>
<uses-feature android:name="android.hardware.sensor.gyroscope" android:required="true"/>
<uses-feature android:name="android.software.vr.mode" android:required="true"/>
<uses-feature android:name="android.hardware.vr.high_performance" android:required="true"/>
<application
android:name="org.qtproject.qt5.android.bindings.QtApplication"
android:label="@string/app_name"
android:allowBackup="true">
<meta-data android:name="com.samsung.android.vr.application.mode" android:value="vr_only"/>
<activity
android:name=".PermissionsChecker"
android:launchMode="singleTask"
android:screenOrientation="landscape"
android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.INFO" />
</intent-filter>
</activity>
<activity
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name=".InterfaceActivity"
android:label="@string/app_name"
android:launchMode="singleTask">
<meta-data android:name="android.app.lib_name" android:value="questInterface"/>
<meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
<meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
<meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
<meta-data android:name="android.app.load_local_libs" android:value="plugins/platforms/android/libqtforandroid.so:plugins/bearer/libqandroidbearer.so:lib/libQt5QuickParticles.so"/>
<meta-data android:name="android.app.background_running" android:value="false"/>
<meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/>
<meta-data android:name="android.app.extract_android_style" android:value="full"/>
</activity>
</application>
</manifest>

View file

@ -0,0 +1,110 @@
#include <functional>
#include <QtCore/QBuffer>
#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QThread>
#include <QtCore/QStringList>
#include <QtCore/QStandardPaths>
#include <QtCore/QTextStream>
#include <QtCore/QObject>
#include <QtAndroidExtras/QAndroidJniObject>
#include <QtAndroidExtras/QtAndroid>
#include <android/log.h>
#include <android/asset_manager.h>
#include <android/asset_manager_jni.h>
#include <shared/Storage.h>
#include <AddressManager.h>
#include <AndroidHelper.h>
#include <udt/PacketHeaders.h>
#include <OVR_Platform.h>
#include <OVR_Functions_Voip.h>
void initOculusPlatform(JNIEnv* env, jobject obj) {
static std::once_flag once;
std::call_once(once, [&]{
// static const char* appID = "2343652845669354";
// if (ovr_PlatformInitializeAndroid(appID, obj, env) != ovrPlatformInitialize_Success) {
// __android_log_write(ANDROID_LOG_WARN, "QQQ", "Failed to init platform SDK");
// return;
// }
// ovr_Voip_SetSystemVoipSuppressed(true);
});
}
void getClassName(JNIEnv *env, jobject obj){
jclass cls = env->GetObjectClass(obj);
jmethodID mid = env->GetMethodID(cls,"getClass", "()Ljava/lang/Class;");
jobject clsObj = env->CallObjectMethod(obj, mid);
cls= env->GetObjectClass(clsObj);
mid= env->GetMethodID(cls, "getName", "()Ljava/lang/String;");
jstring strObj = (jstring) env->CallObjectMethod(clsObj, mid);
const char* str = env->GetStringUTFChars(strObj, NULL);
__android_log_print(ANDROID_LOG_ERROR,__FUNCTION__, "Native Class call: %s",str);
env->ReleaseStringUTFChars(strObj, str);
}
extern "C" {
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_nativeInitOculusPlatform(JNIEnv *env, jobject obj){
initOculusPlatform(env, obj);
}
QAndroidJniObject __interfaceActivity;
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnCreate(JNIEnv *env, jobject obj) {
__android_log_print(ANDROID_LOG_INFO, "QQQ", __FUNCTION__);
initOculusPlatform(env, obj);
getClassName(env, obj);
__interfaceActivity = QAndroidJniObject(obj);
QObject::connect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, []() {
__interfaceActivity.callMethod<void>("onAppLoadedComplete", "()V");
QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete,
nullptr,
nullptr);
});
}
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_questNativeAwayMode(JNIEnv *env, jobject obj) {
AndroidHelper::instance().toggleAwayMode();
}
JNIEXPORT void Java_io_highfidelity_oculus_OculusMobileActivity_questOnAppAfterLoad(JNIEnv* env, jobject obj) {
AndroidHelper::instance().moveToThread(qApp->thread());
}
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnPause(JNIEnv *env, jobject obj) {
AndroidHelper::instance().notifyEnterBackground();
}
JNIEXPORT void JNICALL
Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnResume(JNIEnv *env, jobject obj) {
AndroidHelper::instance().notifyEnterForeground();
}
JNIEXPORT void JNICALL
Java_io_highfidelity_questInterface_receiver_HeadsetStateReceiver_notifyHeadsetOn(JNIEnv *env,
jobject instance,
jboolean pluggedIn) {
AndroidHelper::instance().notifyHeadsetOn(pluggedIn);
}
}

View file

@ -0,0 +1,14 @@
package io.highfidelity.questInterface;
import android.os.Bundle;
import io.highfidelity.oculus.OculusMobileActivity;
import io.highfidelity.utils.HifiUtils;
public class InterfaceActivity extends OculusMobileActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
HifiUtils.upackAssets(getAssets(), getCacheDir().getAbsolutePath());
super.onCreate(savedInstanceState);
}
}

View file

@ -0,0 +1,84 @@
package io.highfidelity.questInterface;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.text.TextUtils;
import io.highfidelity.oculus.OculusMobileActivity;
import io.highfidelity.utils.HifiUtils;
public class PermissionsChecker extends Activity {
private static final int REQUEST_PERMISSIONS = 20;
private static final String TAG = PermissionsChecker.class.getName();
private static final String[] REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.CAMERA
};
private static final String EXTRA_ARGS = "args";
private String mArgs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mArgs =(getIntent().getStringExtra(EXTRA_ARGS));
if(!TextUtils.isEmpty(mArgs)) {
System.out.println("Application launched with following args: " + mArgs);
}
requestAppPermissions(REQUIRED_PERMISSIONS,REQUEST_PERMISSIONS);
}
public void requestAppPermissions(final String[] requestedPermissions,
final int requestCode) {
int permissionCheck = PackageManager.PERMISSION_GRANTED;
boolean shouldShowRequestPermissionRationale = false;
for (String permission : requestedPermissions) {
permissionCheck = permissionCheck + checkSelfPermission(permission);
shouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale || shouldShowRequestPermissionRationale(permission);
}
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
System.out.println("Permission was not granted. Ask for permissions");
if (shouldShowRequestPermissionRationale) {
requestPermissions(requestedPermissions, requestCode);
} else {
requestPermissions(requestedPermissions, requestCode);
}
} else {
System.out.println("Launching the other activity..");
launchActivityWithPermissions();
}
}
private void launchActivityWithPermissions() {
Intent intent= new Intent(this, InterfaceActivity.class);
if(!TextUtils.isEmpty(mArgs)) {
intent.putExtra("applicationArguments", mArgs);
}
startActivity(intent);
finish();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
int permissionCheck = PackageManager.PERMISSION_GRANTED;
for (int permission : grantResults) {
permissionCheck = permissionCheck + permission;
}
if ((grantResults.length > 0) && permissionCheck == PackageManager.PERMISSION_GRANTED) {
launchActivityWithPermissions();
} else if (grantResults.length > 0) {
System.out.println("User has deliberately denied Permissions. Launching anyways");
launchActivityWithPermissions();
}
}
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--suppress AndroidUnknownAttribute -->
<vector xmlns:api24="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="192"
android:viewportHeight="192"
android:width="192dp"
android:height="192dp">
<path
android:pathData="M189.5 96.5A93.5 93.5 0 0 1 96 190 93.5 93.5 0 0 1 2.5 96.5 93.5 93.5 0 0 1 96 3 93.5 93.5 0 0 1 189.5 96.5Z"
android:fillColor="#333333" />
<path
android:pathData="M96.2 173.1c-10.3 0 -20.4 -2.1 -29.8 -6 -9.2 -3.8 -17.3 -9.4 -24.3 -16.4 -7 -7 -12.6 -15.2 -16.4 -24.3 -4.1 -9.6 -6.2 -19.6 -6.2 -30 0 -10.3 2.1 -20.4 6 -29.8 3.8 -9.2 9.4 -17.3 16.4 -24.3 7 -7 15.2 -12.6 24.3 -16.4 9.5 -4 19.5 -6 29.8 -6 10.3 0 20.4 2.1 29.8 6 9.2 3.8 17.3 9.4 24.3 16.4 7 7 12.6 15.2 16.4 24.3 4 9.5 6 19.5 6 29.8 0 10.3 -2.1 20.4 -6 29.8 -3.8 9.2 -9.4 17.3 -16.4 24.3 -7 7 -15.2 12.6 -24.3 16.4 -9.2 4.1 -19.3 6.2 -29.6 6.2zm0 -145.3c-37.8 0 -68.6 30.8 -68.6 68.6 0 37.8 30.8 68.6 68.6 68.6 37.8 0 68.6 -30.8 68.6 -68.6 0 -37.8 -30.8 -68.6 -68.6 -68.6z"
android:fillColor="#00b4f0" />
<path
android:pathData="M119.6 129l0 -53.8c3.4 -1.1 5.8 -4.3 5.8 -8 0 -4.6 -3.8 -8.4 -8.4 -8.4 -4.6 0 -8.4 3.8 -8.4 8.4 0 3.6 2.2 6.6 5.4 7.9l0 25L79 83.8 79 64c3.4 -1.1 5.8 -4.3 5.8 -8 0 -4.6 -3.8 -8.4 -8.4 -8.4 -4.6 0 -8.4 3.8 -8.4 8.4 0 3.6 2.2 6.6 5.4 7.9l0 54.1c-3.1 1.2 -5.4 4.3 -5.4 7.9 0 4.6 3.8 8.4 8.4 8.4 4.6 0 8.4 -3.8 8.4 -8.4 0 -3.7 -2.4 -6.9 -5.8 -8l0 -27.3 35 16.3 0 22.2c-3.1 1.2 -5.4 4.3 -5.4 7.9 0 4.6 3.8 8.4 8.4 8.4 4.6 0 8.4 -3.8 8.4 -8.4 0 -3.8 -2.4 -6.9 -5.8 -8z"
android:fillColor="#00b4f0" />
</vector>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name" translatable="false">Interface</string>
</resources>

View file

@ -42,6 +42,8 @@ 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' : ''
appVersionCode = Integer.valueOf(VERSION_CODE ?: 1)
appVersionName = RELEASE_NUMBER ?: "1.0"
}
def appDir = new File(projectDir, 'apps/interface')

View file

@ -1,11 +1,35 @@
#!/usr/bin/env bash
set -xeuo pipefail
ANDROID_BUILD_TYPE=release
ANDROID_BUILD_TARGET=assembleRelease
if [[ "$RELEASE_TYPE" == "PR" ]]; then
ANDROID_APK_SUFFIX=PR${RELEASE_NUMBER}-${SHA7}.apk ;
elif [[ "${STABLE_BUILD}" == "1" ]]; then
ANDROID_APK_SUFFIX=${RELEASE_NUMBER}.apk ;
else
ANDROID_APK_SUFFIX=${RELEASE_NUMBER}-${SHA7}.apk ;
fi
# Interface build
ANDROID_APP=interface
ANDROID_OUTPUT_DIR=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_TYPE}
ANDROID_OUTPUT_FILE=${ANDROID_APP}-${ANDROID_BUILD_TYPE}.apk
ANDROID_APK_NAME=HighFidelity-Beta-${ANDROID_APK_SUFFIX}
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET}
cp ${ANDROID_OUTPUT_DIR}/${ANDROID_OUTPUT_FILE} ./${ANDROID_APK_NAME}
# Quest Interface build
ANDROID_APP=questInterface
ANDROID_OUTPUT_DIR=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_TYPE}
ANDROID_OUTPUT_FILE=${ANDROID_APP}-${ANDROID_BUILD_TYPE}.apk
ANDROID_APK_NAME=HighFidelity-Quest-Beta-${ANDROID_APK_SUFFIX}
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET} || true
cp ${ANDROID_OUTPUT_DIR}/${ANDROID_OUTPUT_FILE} ./${ANDROID_APK_NAME} || true
# 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_BUILT_APK_NAME}
# 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}

View file

@ -9,6 +9,11 @@ docker build --build-arg BUILD_UID=`id -u` -t "${DOCKER_IMAGE_NAME}" -f docker/D
# So make sure we use VERSION_CODE consistently
test -z "$VERSION_CODE" && export VERSION_CODE=$VERSION
# PR builds don't populate STABLE_BUILD, but the release builds do, and the build
# bash script requires it, so we need to populate it if it's not present
test -z "$STABLE_BUILD" && export STABLE_BUILD=0
# FIXME figure out which of these actually need to be forwarded and which can be eliminated
docker run \
--rm \
--security-opt seccomp:unconfined \
@ -27,6 +32,8 @@ docker run \
-e OAUTH_CLIENT_SECRET \
-e OAUTH_CLIENT_ID \
-e OAUTH_REDIRECT_URI \
-e SHA7 \
-e STABLE_BUILD \
-e VERSION_CODE \
"${DOCKER_IMAGE_NAME}" \
sh -c "./build_android.sh"

View file

@ -73,7 +73,7 @@ RUN mkdir "$HIFI_BASE" && \
RUN git clone https://github.com/jherico/hifi.git && \
cd ~/hifi && \
git checkout feature/quest_frame_player
git checkout quest/build
WORKDIR /home/jenkins/hifi

View file

@ -1 +1,2 @@
org.gradle.jvmargs=-Xms2g -Xmx4g
android.debug.obsoleteApi=true

View file

@ -1,6 +1,6 @@
#Sat Dec 01 08:32:47 PST 2018
#Wed Dec 19 13:46:46 PST 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip

View file

@ -15,3 +15,7 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation project(path: ':qt')
}

View file

@ -7,62 +7,72 @@
//
package io.highfidelity.oculus;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import org.qtproject.qt5.android.bindings.QtActivity;
import io.highfidelity.utils.HifiUtils;
/**
* Contains a native surface and forwards the activity lifecycle and surface lifecycle
* events to the OculusMobileDisplayPlugin
*/
public class OculusMobileActivity extends Activity implements SurfaceHolder.Callback {
public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Callback {
private static final String TAG = OculusMobileActivity.class.getSimpleName();
static { System.loadLibrary("oculusMobile"); }
private native void nativeOnCreate();
private native static void nativeOnResume();
private native static void nativeOnPause();
private native static void nativeOnDestroy();
private native static void nativeOnSurfaceChanged(Surface s);
private native void questNativeOnCreate();
private native void questNativeOnPause();
private native void questNativeOnResume();
private native void questOnAppAfterLoad();
private native void questNativeAwayMode();
private SurfaceView mView;
private SurfaceHolder mSurfaceHolder;
public static void launch(Activity activity) {
if (activity != null) {
activity.runOnUiThread(()->{
activity.startActivity(new Intent(activity, OculusMobileActivity.class));
});
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
Log.w(TAG, "QQQ onCreate");
if(getIntent().hasExtra("applicationArguments")){
super.APPLICATION_PARAMETERS=getIntent().getStringExtra("applicationArguments");
}
super.onCreate(savedInstanceState);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
Log.w(TAG, "QQQ onCreate");
// Create a native surface for VR rendering (Qt GL surfaces are not suitable
// because of the lack of fine control over the surface callbacks)
// Forward the create message to the JNI code
mView = new SurfaceView(this);
setContentView(mView);
mView.getHolder().addCallback(this);
// Forward the create message to the JNI code
nativeOnCreate();
questNativeOnCreate();
}
public void onAppLoadedComplete() {
Log.w(TAG, "QQQ Load Completed");
runOnUiThread(() -> {
setContentView(mView);
questOnAppAfterLoad();
});
}
@Override
protected void onDestroy() {
Log.w(TAG, "QQQ onDestroy");
if (mSurfaceHolder != null) {
nativeOnSurfaceChanged(null);
}
nativeOnDestroy();
isPausing=false;
super.onStop();
nativeOnSurfaceChanged(null);
Log.w(TAG, "QQQ onDestroy -- SUPER onDestroy");
super.onDestroy();
}
@ -70,33 +80,56 @@ public class OculusMobileActivity extends Activity implements SurfaceHolder.Call
protected void onResume() {
Log.w(TAG, "QQQ onResume");
super.onResume();
//Reconnect the global reference back to handler
nativeOnCreate();
questNativeOnResume();
nativeOnResume();
isPausing=false;
}
@Override
protected void onPause() {
Log.w(TAG, "QQQ onPause");
nativeOnPause();
super.onPause();
questNativeOnPause();
nativeOnPause();
isPausing=true;
}
@Override
protected void onStop(){
super.onStop();
Log.w(TAG, "QQQ_ Onstop called");
questNativeAwayMode();
}
@Override
protected void onRestart() {
super.onRestart();
Log.w(TAG, "QQQ_ onRestart called");
questOnAppAfterLoad();
questNativeAwayMode();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.w(TAG, "QQQ surfaceCreated");
Log.w(TAG, "QQQ_ surfaceCreated");
nativeOnSurfaceChanged(holder.getSurface());
mSurfaceHolder = holder;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.w(TAG, "QQQ surfaceChanged");
Log.w(TAG, "QQQ_ surfaceChanged");
nativeOnSurfaceChanged(holder.getSurface());
mSurfaceHolder = holder;
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.w(TAG, "QQQ surfaceDestroyed");
Log.w(TAG, "QQQ_ surfaceDestroyed");
nativeOnSurfaceChanged(null);
mSurfaceHolder = null;
}

View file

@ -70,9 +70,7 @@ public class QtActivity extends Activity {
public final String QT_ANDROID_DEFAULT_THEME = QT_ANDROID_THEMES[0]; // sets the default theme.
private QtActivityLoader m_loader = new QtActivityLoader(this);
public boolean isLoading;
public boolean keepInterfaceRunning;
public boolean isPausing=false;
public QtActivity() {
}
@ -229,10 +227,13 @@ public class QtActivity extends Activity {
//---------------------------------------------------------------------------
protected void onCreateHook(Bundle savedInstanceState) {
m_loader.APPLICATION_PARAMETERS = APPLICATION_PARAMETERS;
m_loader.ENVIRONMENT_VARIABLES = ENVIRONMENT_VARIABLES;
m_loader.QT_ANDROID_THEMES = QT_ANDROID_THEMES;
m_loader.QT_ANDROID_DEFAULT_THEME = QT_ANDROID_DEFAULT_THEME;
m_loader.onCreate(savedInstanceState);
}
@ -364,7 +365,10 @@ public class QtActivity extends Activity {
@Override
protected void onDestroy() {
super.onDestroy();
QtApplication.invokeDelegate();
QtNative.terminateQt();
QtNative.setActivity(null,null);
System.exit(0);
}
//---------------------------------------------------------------------------
@ -506,9 +510,9 @@ public class QtActivity extends Activity {
super.onPause();
// GC: this trick allow us to show a splash activity until Qt app finishes
// loading
if (!isLoading && !keepInterfaceRunning) {
QtApplication.invokeDelegate();
}
//QtApplication.invokeDelegate();
//TODO(Amer): looking into why this messes up pause.
}
//---------------------------------------------------------------------------
@ -647,13 +651,13 @@ public class QtActivity extends Activity {
@Override
protected void onStop() {
super.onStop();
if (!keepInterfaceRunning) {
if(!isPausing){
QtApplication.invokeDelegate();
}
QtNative.terminateQt();
QtNative.setActivity(null,null);
}
//---------------------------------------------------------------------------
@Override

View file

@ -12,15 +12,26 @@ project(':qt').projectDir = new File(settingsDir, 'libraries/qt')
// Applications
//
include ':interface'
project(':interface').projectDir = new File(settingsDir, 'apps/interface')
if (!getSettings().hasProperty("SUPPRESS_INTERFACE")) {
include ':interface'
project(':interface').projectDir = new File(settingsDir, 'apps/interface')
}
if (!getSettings().hasProperty("SUPPRESS_QUEST_INTERFACE")) {
include ':questInterface'
project(':questInterface').projectDir = new File(settingsDir, 'apps/questInterface')
}
//
// Test projects
//
include ':framePlayer'
project(':framePlayer').projectDir = new File(settingsDir, 'apps/framePlayer')
if (!getSettings().hasProperty("SUPPRESS_FRAME_PLAYER")) {
include ':framePlayer'
project(':framePlayer').projectDir = new File(settingsDir, 'apps/framePlayer')
}
include ':questFramePlayer'
project(':questFramePlayer').projectDir = new File(settingsDir, 'apps/questFramePlayer')
if (!getSettings().hasProperty("SUPPRESS_QUEST_FRAME_PLAYER")) {
include ':questFramePlayer'
project(':questFramePlayer').projectDir = new File(settingsDir, 'apps/questFramePlayer')
}

View file

@ -64,10 +64,6 @@ bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) {
return _pool._queue.try_pop(node);
}
#ifdef AUDIO_SINGLE_THREADED
static AudioMixerSlave slave;
#endif
void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) {
_function = &AudioMixerSlave::processPackets;
_configure = [](AudioMixerSlave& slave) {};
@ -87,19 +83,9 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) {
_begin = begin;
_end = end;
#ifdef AUDIO_SINGLE_THREADED
_configure(slave);
std::for_each(begin, end, [&](const SharedNodePointer& node) {
_function(slave, node);
});
#else
// fill the queue
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
#if defined(__clang__) && defined(Q_OS_LINUX)
_queue.push(node);
#else
_queue.emplace(node);
#endif
});
{
@ -119,17 +105,12 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) {
}
assert(_queue.empty());
#endif
}
void AudioMixerSlavePool::each(std::function<void(AudioMixerSlave& slave)> functor) {
#ifdef AUDIO_SINGLE_THREADED
functor(slave);
#else
for (auto& slave : _slaves) {
functor(*slave.get());
}
#endif
}
void AudioMixerSlavePool::setNumThreads(int numThreads) {
@ -155,9 +136,6 @@ void AudioMixerSlavePool::setNumThreads(int numThreads) {
void AudioMixerSlavePool::resize(int numThreads) {
assert(_numThreads == (int)_slaves.size());
#ifdef AUDIO_SINGLE_THREADED
qDebug("%s: running single threaded", __FUNCTION__, numThreads);
#else
qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads);
Lock lock(_mutex);
@ -205,5 +183,4 @@ void AudioMixerSlavePool::resize(int numThreads) {
_numThreads = _numStarted = _numFinished = numThreads;
assert(_numThreads == (int)_slaves.size());
#endif
}

View file

@ -23,6 +23,7 @@
#include <QtCore/QRegularExpression>
#include <QtCore/QTimer>
#include <QtCore/QThread>
#include <QtCore/QJsonDocument>
#include <AABox.h>
#include <AvatarLogging.h>
@ -32,6 +33,8 @@
#include <SharedUtil.h>
#include <UUID.h>
#include <TryLocker.h>
#include "../AssignmentDynamicFactory.h"
#include "../entities/AssignmentParentFinder.h"
const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer";
@ -55,6 +58,9 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
ThreadedAssignment(message),
_slavePool(&_slaveSharedData)
{
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
DependencyManager::set<AssignmentDynamicFactory>();
// make sure we hear about node kills so we can tell the other nodes
connect(DependencyManager::get<NodeList>().data(), &NodeList::nodeKilled, this, &AvatarMixer::handleAvatarKilled);
@ -69,6 +75,8 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
this, "handleOctreePacket");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity,
@ -240,6 +248,10 @@ void AvatarMixer::start() {
int lockWait, nodeTransform, functor;
{
_entityViewer.queryOctree();
}
// Allow nodes to process any pending/queued packets across our worker threads
{
auto start = usecTimestampNow();
@ -252,6 +264,10 @@ void AvatarMixer::start() {
}, &lockWait, &nodeTransform, &functor);
auto end = usecTimestampNow();
_processQueuedAvatarDataPacketsElapsedTime += (end - start);
_broadcastAvatarDataLockWait += lockWait;
_broadcastAvatarDataNodeTransform += nodeTransform;
_broadcastAvatarDataNodeFunctor += functor;
}
// process pending display names... this doesn't currently run on multiple threads, because it
@ -269,6 +285,10 @@ void AvatarMixer::start() {
}, &lockWait, &nodeTransform, &functor);
auto end = usecTimestampNow();
_displayNameManagementElapsedTime += (end - start);
_broadcastAvatarDataLockWait += lockWait;
_broadcastAvatarDataNodeTransform += nodeTransform;
_broadcastAvatarDataNodeFunctor += functor;
}
// this is where we need to put the real work...
@ -691,8 +711,11 @@ void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage
}
void AvatarMixer::sendStatsPacket() {
auto start = usecTimestampNow();
if (!_numTightLoopFrames) {
return;
}
auto start = usecTimestampNow();
QJsonObject statsObject;
@ -775,6 +798,7 @@ void AvatarMixer::sendStatsPacket() {
slavesAggregatObject["sent_4_averageDataBytes"] = TIGHT_LOOP_STAT(aggregateStats.numDataBytesSent);
slavesAggregatObject["sent_5_averageTraitsBytes"] = TIGHT_LOOP_STAT(aggregateStats.numTraitsBytesSent);
slavesAggregatObject["sent_6_averageIdentityBytes"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityBytesSent);
slavesAggregatObject["sent_7_averageHeroAvatars"] = TIGHT_LOOP_STAT(aggregateStats.numHeroesIncluded);
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
@ -882,13 +906,15 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node
void AvatarMixer::domainSettingsRequestComplete() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->addSetOfNodeTypesToNodeInterestSet({
NodeType::Agent, NodeType::EntityScriptServer,
NodeType::Agent, NodeType::EntityScriptServer, NodeType::EntityServer,
NodeType::UpstreamAvatarMixer, NodeType::DownstreamAvatarMixer
});
// parse the settings to pull out the values we need
parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject());
setupEntityQuery();
// start our tight loop...
start();
}
@ -939,6 +965,14 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
qCDebug(avatars) << "Avatar mixer will automatically determine number of threads to use. Using:" << _slavePool.numThreads() << "threads.";
}
{
const QString CONNECTION_RATE = "connection_rate";
auto nodeList = DependencyManager::get<NodeList>();
auto defaultConnectionRate = nodeList->getMaxConnectionRate();
int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate);
nodeList->setMaxConnectionRate(connectionRate);
}
const QString AVATARS_SETTINGS_KEY = "avatars";
static const QString MIN_HEIGHT_OPTION = "min_avatar_height";
@ -976,3 +1010,58 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
qCDebug(avatars) << "Avatars other than" << _slaveSharedData.skeletonURLWhitelist << "will be replaced by" << (_slaveSharedData.skeletonReplacementURL.isEmpty() ? "default" : _slaveSharedData.skeletonReplacementURL.toString());
}
}
void AvatarMixer::setupEntityQuery() {
_entityViewer.init();
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
_slaveSharedData.entityTree = _entityViewer.getTree();
// ES query: {"avatarPriority": true, "type": "Zone"}
QJsonObject priorityZoneQuery;
priorityZoneQuery["avatarPriority"] = true;
priorityZoneQuery["type"] = "Zone";
_entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery);
}
void AvatarMixer::handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
PacketType packetType = message->getType();
switch (packetType) {
case PacketType::OctreeStats:
{ // Ignore stats, but may have a different Entity packet appended.
OctreeHeadlessViewer::parseOctreeStats(message, senderNode);
const auto piggyBackedSizeWithHeader = message->getBytesLeftToRead();
if (piggyBackedSizeWithHeader > 0) {
// pull out the piggybacked packet and create a new QSharedPointer<NLPacket> for it
auto buffer = std::unique_ptr<char[]>(new char[piggyBackedSizeWithHeader]);
memcpy(buffer.get(), message->getRawMessage() + message->getPosition(), piggyBackedSizeWithHeader);
auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggyBackedSizeWithHeader, message->getSenderSockAddr());
auto newMessage = QSharedPointer<ReceivedMessage>::create(*newPacket);
handleOctreePacket(newMessage, senderNode);
}
break;
}
case PacketType::EntityData:
_entityViewer.processDatagram(*message, senderNode);
break;
case PacketType::EntityErase:
_entityViewer.processEraseMessage(*message, senderNode);
break;
default:
qCDebug(avatars) << "Unexpected packet type:" << packetType;
break;
}
}
void AvatarMixer::aboutToFinish() {
DependencyManager::destroy<AssignmentDynamicFactory>();
DependencyManager::destroy<AssignmentParentFinder>();
ThreadedAssignment::aboutToFinish();
}

View file

@ -20,6 +20,7 @@
#include <PortableHighResolutionClock.h>
#include <ThreadedAssignment.h>
#include "../entities/EntityTreeHeadlessViewer.h"
#include "AvatarMixerClientData.h"
#include "AvatarMixerSlavePool.h"
@ -29,6 +30,7 @@ class AvatarMixer : public ThreadedAssignment {
Q_OBJECT
public:
AvatarMixer(ReceivedMessage& message);
virtual void aboutToFinish() override;
static bool shouldReplicateTo(const Node& from, const Node& to) {
return to.getType() == NodeType::DownstreamAvatarMixer &&
@ -57,6 +59,7 @@ private slots:
void handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message);
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void start();
private:
@ -71,8 +74,13 @@ private:
void optionallyReplicatePacket(ReceivedMessage& message, const Node& node);
void setupEntityQuery();
p_high_resolution_clock::time_point _lastFrameTimestamp;
// Attach to entity tree for avatar-priority zone info.
EntityTreeHeadlessViewer _entityViewer;
// FIXME - new throttling - use these values somehow
float _trailingMixRatio { 0.0f };
float _throttlingRatio { 0.0f };

View file

@ -16,6 +16,10 @@
#include <DependencyManager.h>
#include <NodeList.h>
#include <EntityTree.h>
#include <ZoneEntityItem.h>
#include "AvatarLogging.h"
#include "AvatarMixerSlave.h"
@ -62,7 +66,7 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
switch (packet->getType()) {
case PacketType::AvatarData:
parseData(*packet);
parseData(*packet, slaveSharedData);
break;
case PacketType::SetAvatarTraits:
processSetTraitsMessage(*packet, slaveSharedData, *node);
@ -80,7 +84,42 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
return packetsProcessed;
}
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
namespace {
using std::static_pointer_cast;
// Operator to find if a point is within an avatar-priority (hero) Zone Entity.
struct FindPriorityZone {
glm::vec3 position;
bool isInPriorityZone { false };
float zoneVolume { std::numeric_limits<float>::max() };
static bool operation(const OctreeElementPointer& element, void* extraData) {
auto findPriorityZone = static_cast<FindPriorityZone*>(extraData);
if (element->getAACube().contains(findPriorityZone->position)) {
const EntityTreeElementPointer entityTreeElement = static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->forEachEntity([&findPriorityZone](EntityItemPointer item) {
if (item->getType() == EntityTypes::Zone
&& item->contains(findPriorityZone->position)) {
auto zoneItem = static_pointer_cast<ZoneEntityItem>(item);
if (zoneItem->getAvatarPriority() != COMPONENT_MODE_INHERIT) {
float volume = zoneItem->getVolumeEstimate();
if (volume < findPriorityZone->zoneVolume) { // Smaller volume wins
findPriorityZone->isInPriorityZone = zoneItem->getAvatarPriority() == COMPONENT_MODE_ENABLED;
findPriorityZone->zoneVolume = volume;
}
}
}
});
return true; // Keep recursing
} else { // Position isn't within this subspace, so end recursion.
return false;
}
}
};
} // Close anonymous namespace.
int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveSharedData& slaveSharedData) {
// pull the sequence number from the data first
uint16_t sequenceNumber;
@ -90,9 +129,33 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) {
incrementNumOutOfOrderSends();
}
_lastReceivedSequenceNumber = sequenceNumber;
glm::vec3 oldPosition = getPosition();
// compute the offset to the data payload
return _avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()));
if (!_avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()))) {
return false;
}
auto newPosition = getPosition();
if (newPosition != oldPosition) {
//#define AVATAR_HERO_TEST_HACK
#ifdef AVATAR_HERO_TEST_HACK
{
const static QString heroKey { "HERO" };
_avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey));
}
#else
EntityTree& entityTree = *slaveSharedData.entityTree;
FindPriorityZone findPriorityZone { newPosition, false } ;
entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone);
_avatar->setHasPriority(findPriorityZone.isInPriorityZone);
//if (findPriorityZone.isInPriorityZone) {
// qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone";
//}
#endif
}
return true;
}
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,

View file

@ -21,7 +21,7 @@
#include <QtCore/QJsonObject>
#include <QtCore/QUrl>
#include <AvatarData.h>
#include "MixerAvatar.h"
#include <AssociatedTraitValues.h>
#include <NodeData.h>
#include <NumericalConstants.h>
@ -45,11 +45,12 @@ public:
using HRCTime = p_high_resolution_clock::time_point;
using PerNodeTraitVersions = std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions>;
int parseData(ReceivedMessage& message) override;
AvatarData& getAvatar() { return *_avatar; }
const AvatarData& getAvatar() const { return *_avatar; }
const AvatarData* getConstAvatarData() const { return _avatar.get(); }
AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; }
using NodeData::parseData; // Avoid clang warning about hiding.
int parseData(ReceivedMessage& message, const SlaveSharedData& SlaveSharedData);
MixerAvatar& getAvatar() { return *_avatar; }
const MixerAvatar& getAvatar() const { return *_avatar; }
const MixerAvatar* getConstAvatarData() const { return _avatar.get(); }
MixerAvatarSharedPointer getAvatarSharedPointer() const { return _avatar; }
uint16_t getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const;
void setLastBroadcastSequenceNumber(NLPacket::LocalID nodeID, uint16_t sequenceNumber)
@ -163,7 +164,7 @@ private:
};
PacketQueue _packetQueue;
AvatarSharedPointer _avatar { new AvatarData() };
MixerAvatarSharedPointer _avatar { new MixerAvatar() };
uint16_t _lastReceivedSequenceNumber { 0 };
std::unordered_map<NLPacket::LocalID, uint16_t> _lastBroadcastSequenceNumbers;

View file

@ -281,7 +281,34 @@ AABox computeBubbleBox(const AvatarData& avatar, float bubbleExpansionFactor) {
return box;
}
namespace {
class SortableAvatar : public PrioritySortUtil::Sortable {
public:
SortableAvatar() = delete;
SortableAvatar(const MixerAvatar* avatar, const Node* avatarNode, uint64_t lastEncodeTime)
: _avatar(avatar), _node(avatarNode), _lastEncodeTime(lastEncodeTime) {
}
glm::vec3 getPosition() const override { return _avatar->getClientGlobalPosition(); }
float getRadius() const override {
glm::vec3 nodeBoxScale = _avatar->getGlobalBoundingBox().getScale();
return 0.5f * glm::max(nodeBoxScale.x, glm::max(nodeBoxScale.y, nodeBoxScale.z));
}
uint64_t getTimestamp() const override {
return _lastEncodeTime;
}
const Node* getNode() const { return _node; }
const MixerAvatar* getAvatar() const { return _avatar; }
private:
const MixerAvatar* _avatar;
const Node* _node;
uint64_t _lastEncodeTime;
};
} // Close anonymous namespace.
void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) {
const float AVATAR_HERO_FRACTION { 0.4f };
const Node* destinationNode = node.data();
auto nodeList = DependencyManager::get<NodeList>();
@ -293,29 +320,30 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
_stats.nodesBroadcastedTo++;
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(destinationNode->getLinkedData());
AvatarMixerClientData* destinationNodeData = reinterpret_cast<AvatarMixerClientData*>(destinationNode->getLinkedData());
nodeData->resetInViewStats();
destinationNodeData->resetInViewStats();
const AvatarData& avatar = nodeData->getAvatar();
glm::vec3 myPosition = avatar.getClientGlobalPosition();
const AvatarData& avatar = destinationNodeData->getAvatar();
glm::vec3 destinationPosition = avatar.getClientGlobalPosition();
// reset the internal state for correct random number distribution
distribution.reset();
// Estimate number to sort on number sent last frame (with min. of 20).
const int numToSendEst = std::max(int(nodeData->getNumAvatarsSentLastFrame() * 2.5f), 20);
const int numToSendEst = std::max(int(destinationNodeData->getNumAvatarsSentLastFrame() * 2.5f), 20);
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
destinationNodeData->resetNumAvatarsSentLastFrame();
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
int identityBytesSent = 0;
int traitBytesSent = 0;
// max number of avatarBytes per frame
int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND);
// max number of avatarBytes per frame (13 900, typical)
const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND);
const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical
// keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0;
@ -325,8 +353,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// When this is true, the AvatarMixer will send Avatar data to a client
// about avatars they've ignored or that are out of view
bool PALIsOpen = nodeData->getRequestsDomainListData();
bool PALWasOpen = nodeData->getPrevRequestsDomainListData();
bool PALIsOpen = destinationNodeData->getRequestsDomainListData();
bool PALWasOpen = destinationNodeData->getPrevRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = PALIsOpen && destinationNode->getCanKick();
@ -337,36 +365,23 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// compute node bounding box
const float MY_AVATAR_BUBBLE_EXPANSION_FACTOR = 4.0f; // magic number determined emperically
AABox nodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR);
class SortableAvatar: public PrioritySortUtil::Sortable {
public:
SortableAvatar() = delete;
SortableAvatar(const AvatarData* avatar, const Node* avatarNode, uint64_t lastEncodeTime)
: _avatar(avatar), _node(avatarNode), _lastEncodeTime(lastEncodeTime) {}
glm::vec3 getPosition() const override { return _avatar->getClientGlobalPosition(); }
float getRadius() const override {
glm::vec3 nodeBoxScale = _avatar->getGlobalBoundingBox().getScale();
return 0.5f * glm::max(nodeBoxScale.x, glm::max(nodeBoxScale.y, nodeBoxScale.z));
}
uint64_t getTimestamp() const override {
return _lastEncodeTime;
}
const Node* getNode() const { return _node; }
private:
const AvatarData* _avatar;
const Node* _node;
uint64_t _lastEncodeTime;
};
AABox destinationNodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR);
// prepare to sort
const auto& cameraViews = nodeData->getViewFrustums();
PrioritySortUtil::PriorityQueue<SortableAvatar> sortedAvatars(cameraViews,
AvatarData::_avatarSortCoefficientSize,
AvatarData::_avatarSortCoefficientCenter,
AvatarData::_avatarSortCoefficientAge);
sortedAvatars.reserve(_end - _begin);
const auto& cameraViews = destinationNodeData->getViewFrustums();
using AvatarPriorityQueue = PrioritySortUtil::PriorityQueue<SortableAvatar>;
// Keep two independent queues, one for heroes and one for the riff-raff.
enum PriorityVariants { kHero, kNonhero };
AvatarPriorityQueue avatarPriorityQueues[2] =
{
{cameraViews, AvatarData::_avatarSortCoefficientSize,
AvatarData::_avatarSortCoefficientCenter, AvatarData::_avatarSortCoefficientAge},
{cameraViews, AvatarData::_avatarSortCoefficientSize,
AvatarData::_avatarSortCoefficientCenter, AvatarData::_avatarSortCoefficientAge}
};
avatarPriorityQueues[kNonhero].reserve(_end - _begin);
for (auto listedNode = _begin; listedNode != _end; ++listedNode) {
Node* otherNodeRaw = (*listedNode).data();
@ -376,47 +391,47 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
continue;
}
auto avatarNode = otherNodeRaw;
auto sourceAvatarNode = otherNodeRaw;
bool shouldIgnore = false;
bool sendAvatar = true; // We will consider this source avatar for sending.
// We ignore other nodes for a couple of reasons:
// 1) ignore bubbles and ignore specific node
// 2) the node hasn't really updated it's frame data recently, this can
// happen if for example the avatar is connected on a desktop and sending
// updates at ~30hz. So every 3 frames we skip a frame.
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
assert(sourceAvatarNode); // we can't have gotten here without the avatarData being a valid key in the map
const AvatarMixerClientData* avatarClientNodeData = reinterpret_cast<const AvatarMixerClientData*>(avatarNode->getLinkedData());
assert(avatarClientNodeData); // we can't have gotten here without avatarNode having valid data
const AvatarMixerClientData* sourceAvatarNodeData = reinterpret_cast<const AvatarMixerClientData*>(sourceAvatarNode->getLinkedData());
assert(sourceAvatarNodeData); // we can't have gotten here without sourceAvatarNode having valid data
quint64 startIgnoreCalculation = usecTimestampNow();
// make sure we have data for this avatar, that it isn't the same node,
// and isn't an avatar that the viewing node has ignored
// or that has ignored the viewing node
if ((destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen)
|| (avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) {
shouldIgnore = true;
if ((destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) && !PALIsOpen)
|| (sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) {
sendAvatar = false;
} else {
// Check to see if the space bubble is enabled
// Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored
if (nodeData->isIgnoreRadiusEnabled() || (avatarClientNodeData->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
if (destinationNodeData->isIgnoreRadiusEnabled() || (sourceAvatarNodeData->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
// Perform the collision check between the two bounding boxes
AABox otherNodeBox = avatarClientNodeData->getAvatar().getDefaultBubbleBox();
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(destinationNode, avatarNode);
shouldIgnore = !getsAnyIgnored;
AABox sourceNodeBox = sourceAvatarNodeData->getAvatar().getDefaultBubbleBox();
if (destinationNodeBox.touches(sourceNodeBox)) {
destinationNodeData->ignoreOther(destinationNode, sourceAvatarNode);
sendAvatar = getsAnyIgnored;
}
}
// Not close enough to ignore
if (!shouldIgnore) {
nodeData->removeFromRadiusIgnoringSet(avatarNode->getUUID());
if (sendAvatar) {
destinationNodeData->removeFromRadiusIgnoringSet(sourceAvatarNode->getUUID());
}
}
if (!shouldIgnore) {
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getLocalID());
AvatarDataSequenceNumber lastSeqFromSender = avatarClientNodeData->getLastReceivedSequenceNumber();
if (sendAvatar) {
AvatarDataSequenceNumber lastSeqToReceiver = destinationNodeData->getLastBroadcastSequenceNumber(sourceAvatarNode->getLocalID());
AvatarDataSequenceNumber lastSeqFromSender = sourceAvatarNodeData->getLastReceivedSequenceNumber();
// FIXME - This code does appear to be working. But it seems brittle.
// It supports determining if the frame of data for this "other"
@ -430,26 +445,28 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// or that somehow we haven't sent
if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) {
++numAvatarsHeldBack;
shouldIgnore = true;
sendAvatar = false;
} else if (lastSeqFromSender == 0) {
// We have have not yet recieved any data about this avatar. Ignore it for now
// We have have not yet received any data about this avatar. Ignore it for now
// This is important for Agent scripts that are not avatar
// so that they don't appear to be an avatar at the origin
shouldIgnore = true;
sendAvatar = false;
} else if (lastSeqFromSender - lastSeqToReceiver > 1) {
// this is a skip - we still send the packet but capture the presence of the skip so we see it happening
++numAvatarsWithSkippedFrames;
}
}
quint64 endIgnoreCalculation = usecTimestampNow();
_stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation);
if (!shouldIgnore) {
if (sendAvatar) {
// sort this one for later
const AvatarData* avatarNodeData = avatarClientNodeData->getConstAvatarData();
auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNode->getLocalID());
const MixerAvatar* avatarNodeData = sourceAvatarNodeData->getConstAvatarData();
auto lastEncodeTime = destinationNodeData->getLastOtherAvatarEncodeTime(sourceAvatarNode->getLocalID());
sortedAvatars.push(SortableAvatar(avatarNodeData, avatarNode, lastEncodeTime));
avatarPriorityQueues[avatarNodeData->getHasPriority() ? kHero : kNonhero].push(
SortableAvatar(avatarNodeData, sourceAvatarNode, lastEncodeTime));
}
// If Avatar A's PAL WAS open but is no longer open, AND
@ -459,135 +476,153 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
// will be sent when it doesn't need to be (but where it _should_ be OK to send).
// However, it's less heavy-handed than using `shouldIgnore`.
if (PALWasOpen && !PALIsOpen &&
(destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) ||
avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) {
(destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) ||
sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) {
// ...send a Kill Packet to Node A, instructing Node A to kill Avatar B,
// then have Node A cleanup the killed Node B.
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
packet->write(avatarNode->getUUID().toRfc4122());
packet->write(sourceAvatarNode->getUUID().toRfc4122());
packet->writePrimitive(KillAvatarReason::AvatarIgnored);
nodeList->sendPacket(std::move(packet), *destinationNode);
nodeData->cleanupKilledNode(avatarNode->getUUID(), avatarNode->getLocalID());
destinationNodeData->cleanupKilledNode(sourceAvatarNode->getUUID(), sourceAvatarNode->getLocalID());
}
nodeData->setPrevRequestsDomainListData(PALIsOpen);
destinationNodeData->setPrevRequestsDomainListData(PALIsOpen);
}
// loop through our sorted avatars and allocate our bandwidth to them accordingly
int remainingAvatars = (int)sortedAvatars.size();
int remainingAvatars = (int)avatarPriorityQueues[kHero].size() + (int)avatarPriorityQueues[kNonhero].size();
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
const int avatarPacketCapacity = avatarPacket->getPayloadCapacity();
int avatarSpaceAvailable = avatarPacketCapacity;
int numPacketsSent = 0;
int numAvatarsSent = 0;
auto identityPacketList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst);
for (const auto& sortedAvatar : sortedAvatarVector) {
const Node* otherNode = sortedAvatar.getNode();
auto lastEncodeForOther = sortedAvatar.getTimestamp();
// Loop over two priorities - hero avatars then everyone else:
for (PriorityVariants currentVariant = kHero; currentVariant <= kNonhero; ++((int&)currentVariant)) {
const auto& sortedAvatarVector = avatarPriorityQueues[currentVariant].getSortedVector(numToSendEst);
for (const auto& sortedAvatar : sortedAvatarVector) {
const Node* sourceNode = sortedAvatar.getNode();
auto lastEncodeForOther = sortedAvatar.getTimestamp();
assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map
assert(sourceNode); // we can't have gotten here without the avatarData being a valid key in the map
AvatarData::AvatarDataDetail detail = AvatarData::NoData;
AvatarData::AvatarDataDetail detail = AvatarData::NoData;
// NOTE: Here's where we determine if we are over budget and drop remaining avatars,
// or send minimal avatar data in uncommon case of PALIsOpen.
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes;
bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame;
if (overBudget) {
if (PALIsOpen) {
_stats.overBudgetAvatars++;
detail = AvatarData::PALMinimum;
} else {
_stats.overBudgetAvatars += remainingAvatars;
break;
// NOTE: Here's where we determine if we are over budget and drop remaining avatars,
// or send minimal avatar data in uncommon case of PALIsOpen.
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes;
bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame;
if (overBudget) {
if (PALIsOpen) {
_stats.overBudgetAvatars++;
detail = AvatarData::PALMinimum;
} else {
_stats.overBudgetAvatars += remainingAvatars;
break;
}
}
bool overHeroBudget = currentVariant == kHero && numAvatarDataBytes > maxHeroBytesPerFrame;
if (overHeroBudget) {
break; // No more heroes (this frame).
}
auto startAvatarDataPacking = chrono::high_resolution_clock::now();
const AvatarMixerClientData* sourceNodeData = reinterpret_cast<const AvatarMixerClientData*>(sourceNode->getLinkedData());
const MixerAvatar* sourceAvatar = sourceNodeData->getConstAvatarData();
// Typically all out-of-view avatars but such avatars' priorities will rise with time:
bool isLowerPriority = currentVariant != kHero && sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; // XXX: hero handling?
if (isLowerPriority) {
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData;
destinationNodeData->incrementAvatarOutOfView();
} else if (!overBudget) {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData;
destinationNodeData->incrementAvatarInView();
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (sourceAvatar->hasProcessedFirstIdentity()
&& destinationNodeData->getLastBroadcastTime(sourceNode->getLocalID()) <= sourceNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(*identityPacketList, sourceNodeData, *destinationNode);
// remember the last time we sent identity details about this other node to the receiver
destinationNodeData->setLastBroadcastTime(sourceNode->getLocalID(), usecTimestampNow());
}
}
QVector<JointData>& lastSentJointsForOther = destinationNodeData->getLastOtherAvatarSentJoints(sourceNode->getLocalID());
const bool distanceAdjust = true;
const bool dropFaceTracking = false;
AvatarDataPacket::SendStatus sendStatus;
sendStatus.sendUUID = true;
do {
auto startSerialize = chrono::high_resolution_clock::now();
QByteArray bytes = sourceAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
sendStatus, dropFaceTracking, distanceAdjust, destinationPosition,
&lastSentJointsForOther, avatarSpaceAvailable);
auto endSerialize = chrono::high_resolution_clock::now();
_stats.toByteArrayElapsedTime +=
(quint64)chrono::duration_cast<chrono::microseconds>(endSerialize - startSerialize).count();
avatarPacket->write(bytes);
avatarSpaceAvailable -= bytes.size();
numAvatarDataBytes += bytes.size();
if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) {
// Weren't able to fit everything.
nodeList->sendPacket(std::move(avatarPacket), *destinationNode);
++numPacketsSent;
avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
avatarSpaceAvailable = avatarPacketCapacity;
}
} while (!sendStatus);
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
if (sourceAvatar->getHasPriority()) {
_stats.numHeroesIncluded++;
}
// increment the number of avatars sent to this receiver
destinationNodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
destinationNodeData->setLastBroadcastSequenceNumber(sourceNode->getLocalID(),
sourceNodeData->getLastReceivedSequenceNumber());
destinationNodeData->setLastOtherAvatarEncodeTime(sourceNode->getLocalID(), usecTimestampNow());
}
auto endAvatarDataPacking = chrono::high_resolution_clock::now();
_stats.avatarDataPackingElapsedTime +=
(quint64)chrono::duration_cast<chrono::microseconds>(endAvatarDataPacking - startAvatarDataPacking).count();
if (!overBudget) {
// use helper to add any changed traits to our packet list
traitBytesSent += addChangedTraitsToBulkPacket(destinationNodeData, sourceNodeData, *traitsPacketList);
}
numAvatarsSent++;
remainingAvatars--;
}
if (currentVariant == kHero) { // Dump any remaining heroes into the commoners.
for (auto avIter = sortedAvatarVector.begin() + numAvatarsSent; avIter < sortedAvatarVector.end(); ++avIter) {
avatarPriorityQueues[kNonhero].push(*avIter);
}
}
auto startAvatarDataPacking = chrono::high_resolution_clock::now();
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
// Typically all out-of-view avatars but such avatars' priorities will rise with time:
bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD;
if (isLowerPriority) {
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView();
} else if (!overBudget) {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData;
nodeData->incrementAvatarInView();
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (otherAvatar->hasProcessedFirstIdentity()
&& nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(*identityPacketList, otherNodeData, *destinationNode);
// remember the last time we sent identity details about this other node to the receiver
nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow());
}
}
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getLocalID());
const bool distanceAdjust = true;
const bool dropFaceTracking = false;
AvatarDataPacket::SendStatus sendStatus;
sendStatus.sendUUID = true;
do {
auto startSerialize = chrono::high_resolution_clock::now();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
sendStatus, dropFaceTracking, distanceAdjust, myPosition,
&lastSentJointsForOther, avatarSpaceAvailable);
auto endSerialize = chrono::high_resolution_clock::now();
_stats.toByteArrayElapsedTime +=
(quint64)chrono::duration_cast<chrono::microseconds>(endSerialize - startSerialize).count();
avatarPacket->write(bytes);
avatarSpaceAvailable -= bytes.size();
numAvatarDataBytes += bytes.size();
if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) {
// Weren't able to fit everything.
nodeList->sendPacket(std::move(avatarPacket), *destinationNode);
++numPacketsSent;
avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
avatarSpaceAvailable = avatarPacketCapacity;
}
} while (!sendStatus);
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
// increment the number of avatars sent to this receiver
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getLocalID(),
otherNodeData->getLastReceivedSequenceNumber());
nodeData->setLastOtherAvatarEncodeTime(otherNode->getLocalID(), usecTimestampNow());
}
auto endAvatarDataPacking = chrono::high_resolution_clock::now();
_stats.avatarDataPackingElapsedTime +=
(quint64) chrono::duration_cast<chrono::microseconds>(endAvatarDataPacking - startAvatarDataPacking).count();
if (!overBudget) {
// use helper to add any changed traits to our packet list
traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList);
}
remainingAvatars--;
}
if (nodeData->getNumAvatarsSentLastFrame() > numToSendEst) {
qCWarning(avatars) << "More avatars sent than upper estimate" << nodeData->getNumAvatarsSentLastFrame()
if (destinationNodeData->getNumAvatarsSentLastFrame() > numToSendEst) {
qCWarning(avatars) << "More avatars sent than upper estimate" << destinationNodeData->getNumAvatarsSentLastFrame()
<< " / " << numToSendEst;
}
@ -618,12 +653,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
}
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent);
destinationNodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent);
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
destinationNodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
destinationNodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
quint64 endPacketSending = usecTimestampNow();
_stats.packetSendingElapsedTime += (endPacketSending - startPacketSending);

View file

@ -32,6 +32,7 @@ public:
int numIdentityPacketsSent { 0 };
int numOthersIncluded { 0 };
int overBudgetAvatars { 0 };
int numHeroesIncluded { 0 };
quint64 ignoreCalculationElapsedTime { 0 };
quint64 avatarDataPackingElapsedTime { 0 };
@ -57,6 +58,7 @@ public:
numIdentityPacketsSent = 0;
numOthersIncluded = 0;
overBudgetAvatars = 0;
numHeroesIncluded = 0;
ignoreCalculationElapsedTime = 0;
avatarDataPackingElapsedTime = 0;
@ -80,6 +82,7 @@ public:
numIdentityPacketsSent += rhs.numIdentityPacketsSent;
numOthersIncluded += rhs.numOthersIncluded;
overBudgetAvatars += rhs.overBudgetAvatars;
numHeroesIncluded += rhs.numHeroesIncluded;
ignoreCalculationElapsedTime += rhs.ignoreCalculationElapsedTime;
avatarDataPackingElapsedTime += rhs.avatarDataPackingElapsedTime;
@ -90,9 +93,13 @@ public:
}
};
class EntityTree;
using EntityTreePointer = std::shared_ptr<EntityTree>;
struct SlaveSharedData {
QStringList skeletonURLWhitelist;
QUrl skeletonReplacementURL;
EntityTreePointer entityTree;
};
class AvatarMixerSlave {

View file

@ -63,10 +63,6 @@ bool AvatarMixerSlaveThread::try_pop(SharedNodePointer& node) {
return _pool._queue.try_pop(node);
}
#ifdef AVATAR_SINGLE_THREADED
static AvatarMixerSlave slave;
#endif
void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) {
_function = &AvatarMixerSlave::processIncomingPackets;
_configure = [=](AvatarMixerSlave& slave) {
@ -89,19 +85,9 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) {
_begin = begin;
_end = end;
#ifdef AUDIO_SINGLE_THREADED
_configure(slave);
std::for_each(begin, end, [&](const SharedNodePointer& node) {
_function(slave, node);
});
#else
// fill the queue
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
#if defined(__clang__) && defined(Q_OS_LINUX)
_queue.push(node);
#else
_queue.emplace(node);
#endif
});
{
@ -121,18 +107,13 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) {
}
assert(_queue.empty());
#endif
}
void AvatarMixerSlavePool::each(std::function<void(AvatarMixerSlave& slave)> functor) {
#ifdef AVATAR_SINGLE_THREADED
functor(slave);
#else
for (auto& slave : _slaves) {
functor(*slave.get());
}
#endif
}
void AvatarMixerSlavePool::setNumThreads(int numThreads) {
@ -158,9 +139,6 @@ void AvatarMixerSlavePool::setNumThreads(int numThreads) {
void AvatarMixerSlavePool::resize(int numThreads) {
assert(_numThreads == (int)_slaves.size());
#ifdef AVATAR_SINGLE_THREADED
qDebug("%s: running single threaded", __FUNCTION__, numThreads);
#else
qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads);
Lock lock(_mutex);
@ -208,5 +186,4 @@ void AvatarMixerSlavePool::resize(int numThreads) {
_numThreads = _numStarted = _numFinished = numThreads;
assert(_numThreads == (int)_slaves.size());
#endif
}

View file

@ -0,0 +1,31 @@
//
// MixerAvatar.h
// assignment-client/src/avatars
//
// Created by Simon Walton Feb 2019.
// 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
//
// Avatar class for use within the avatar mixer - encapsulates data required only for
// sorting priorities within the mixer.
#ifndef hifi_MixerAvatar_h
#define hifi_MixerAvatar_h
#include <AvatarData.h>
class MixerAvatar : public AvatarData {
public:
bool getHasPriority() const { return _hasPriority; }
void setHasPriority(bool hasPriority) { _hasPriority = hasPriority; }
private:
bool _hasPriority { false };
};
using MixerAvatarSharedPointer = std::shared_ptr<MixerAvatar>;
#endif // hifi_MixerAvatar_h

View file

@ -1203,7 +1203,8 @@ void OctreeServer::beginRunning() {
auto nodeList = DependencyManager::get<NodeList>();
// we need to ask the DS about agents so we can ping/reply with them
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer,
NodeType::AvatarMixer });
beforeRun(); // after payload has been processed

View file

@ -270,6 +270,16 @@ macro(AUTOSCRIBE_SHADER_LIBS)
set(AUTOSCRIBE_SHADERGEN_COMMANDS_FILE ${CMAKE_CURRENT_BINARY_DIR}/shadergen.txt)
file(WRITE ${AUTOSCRIBE_SHADERGEN_COMMANDS_FILE} "${AUTOSCRIBE_SHADERGEN_COMMANDS}")
if (HIFI_ANDROID)
if (
(${HIFI_ANDROID_APP} STREQUAL "questInterface") OR
(${HIFI_ANDROID_APP} STREQUAL "questFramePlayer") OR
(${HIFI_ANDROID_APP} STREQUAL "framePlayer")
)
set(EXTRA_SHADERGEN_ARGS --extensions EXT_clip_cull_distance)
endif()
endif()
# A custom python script which will generate all our shader artifacts
add_custom_command(
OUTPUT ${SCRIBED_SHADERS} ${SPIRV_SHADERS} ${REFLECTED_SHADERS}
@ -279,6 +289,7 @@ macro(AUTOSCRIBE_SHADER_LIBS)
--tools-dir ${VCPKG_TOOLS_DIR}
--build-dir ${CMAKE_CURRENT_BINARY_DIR}
--source-dir ${CMAKE_SOURCE_DIR}
${EXTRA_SHADERGEN_ARGS}
DEPENDS ${AUTOSCRIBE_SHADER_HEADERS} ${CMAKE_SOURCE_DIR}/tools/shadergen.py ${ALL_SCRIBE_SHADERS})
add_custom_target(shadergen DEPENDS ${SCRIBED_SHADERS} ${SPIRV_SHADERS} ${REFLECTED_SHADERS})

View file

@ -1302,6 +1302,14 @@
"placeholder": "1",
"default": "1",
"advanced": true
},
{
"name": "connection_rate",
"label": "Connection Rate",
"help": "Number of new agents that can connect to the mixer every second",
"placeholder": "50",
"default": "50",
"advanced": true
}
]
},

View file

@ -1243,12 +1243,11 @@ void DomainServer::broadcastNewNode(const SharedNodePointer& addedNode) {
limitedNodeList->eachMatchingNode(
[this, addedNode](const SharedNodePointer& node)->bool {
if (node->getLinkedData() && node->getActiveSocket() && node != addedNode) {
// is the added Node in this node's interest list?
return isInInterestSet(node, addedNode);
} else {
return false;
}
// is the added Node in this node's interest list?
return node->getLinkedData()
&& node->getActiveSocket()
&& node != addedNode
&& isInInterestSet(node, addedNode);
},
[this, &addNodePacket, connectionSecretIndex, addedNode, limitedNodeListWeak](const SharedNodePointer& node) {
// send off this packet to the node
@ -2548,7 +2547,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite
_pendingFileContent.seek(_pendingFileContent.size());
_pendingFileContent.write(dataChunk);
_pendingFileContent.close();
// Respond immediately - will timeout if we wait for restore.
connection->respond(HTTPConnection::StatusCode200);
if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") {

View file

@ -25,13 +25,13 @@
#include <AccountManager.h>
#include <Assignment.h>
#include <AvatarData.h>
#include <HifiConfigVariantMap.h>
#include <HTTPConnection.h>
#include <NLPacketList.h>
#include <NumericalConstants.h>
#include <SettingHandle.h>
#include <SettingHelpers.h>
#include <AvatarData.h> //for KillAvatarReason
#include <FingerprintUtils.h>
#include "DomainServerNodeData.h"
@ -870,14 +870,6 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
}
}
// if we are here, then we kicked them, so send the KillAvatar message
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
packet->write(nodeUUID.toRfc4122());
packet->writePrimitive(KillAvatarReason::NoReason);
// send to avatar mixer, it sends the kill to everyone else
limitedNodeList->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer);
if (newPermissions) {
qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
<< "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID());

View file

@ -53,9 +53,9 @@ ANDROID_PACKAGES = {
'includeLibs': ['libvrapi.so']
},
'oculusPlatform': {
'file': 'OVRPlatformSDK_v1.32.0.zip',
'versionId': 'jG9DB16zOGxSrmtZy4jcQnwO0TJUuaeL',
'checksum': 'ab5b203b3a39a56ab148d68fff769e05',
'file': 'OVRPlatformSDK_v1.34.0.zip',
'versionId': 'vbRUkkyzUAXfTGSEtuiUr_7.Fm5h5BZk',
'checksum': '16e4c5f39520f122bc49cb6d5bb88289',
'sharedLibFolder': 'Android/libs/arm64-v8a',
'includeLibs': ['libovrplatformloader.so']
},

View file

@ -1016,8 +1016,8 @@
"type": "clip",
"data": {
"url": "qrc:///avatar/animations/run_fwd.fbx",
"startFrame": 0.0,
"endFrame": 21.0,
"startFrame": 1.0,
"endFrame": 22.0,
"timeScale": 1.0,
"loopFlag": true
},

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,9 @@
"invert"
],
"to": "Actions.Pitch"
}
},
{ "from": "TouchscreenVirtualPad.RB", "to": "Standard.RB"}
]
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -425,11 +425,12 @@ FocusScope {
console.warn("Could not find top level window for " + item);
return;
}
/*
if (typeof Controller === "undefined") {
console.warn("Controller not yet available... can't center");
return;
}
*/
var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedHUDRect();
var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y,
@ -455,15 +456,17 @@ FocusScope {
console.warn("Could not find top level window for " + item);
return;
}
/*
if (typeof Controller === "undefined") {
console.warn("Controller not yet available... can't reposition targetWindow:" + targetWindow);
return;
}
*/
var oldRecommendedRect = recommendedRect;
var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height };
var newRecommendedRect = Controller.getRecommendedHUDRect();
var newRecommendedRect = { width: 1280, height: 720, x: 0, y: 0 };
if (typeof Controller !== "undefined") newRecommendedRect = Controller.getRecommendedHUDRect();
var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height };
repositionWindow(targetWindow, false, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions);
}
@ -480,7 +483,8 @@ FocusScope {
return;
}
var recommended = Controller.getRecommendedHUDRect();
var recommended = { width: 1280, height: 720, x: 0, y: 0 };
if (typeof Controller !== "undefined") recommended = Controller.getRecommendedHUDRect();
var maxX = recommended.x + recommended.width;
var maxY = recommended.y + recommended.height;
var newPosition = Qt.vector2d(targetWindow.x, targetWindow.y);

View file

@ -273,11 +273,7 @@ ModalWindow {
onTriggered: {
root.result = null;
root.canceled();
// FIXME we are leaking memory to avoid a crash
// root.destroy();
root.disableFade = true
visible = false;
root.destroy();
}
}
@ -299,11 +295,7 @@ ModalWindow {
}
root.result = JSON.stringify(result);
root.selected(root.result);
// FIXME we are leaking memory to avoid a crash
// root.destroy();
root.disableFade = true
visible = false;
root.destroy();
}
}
}

View file

@ -815,7 +815,7 @@ ModalWindow {
Action {
id: cancelAction
text: "Cancel"
onTriggered: { canceled(); root.shown = false; }
onTriggered: { canceled(); root.destroy(); }
}
}

View file

@ -168,11 +168,7 @@ ModalWindow {
shortcut: "Esc"
onTriggered: {
root.canceled();
// FIXME we are leaking memory to avoid a crash
// root.destroy();
root.disableFade = true
visible = false;
root.destroy();
}
}
@ -183,11 +179,7 @@ ModalWindow {
onTriggered: {
root.result = items ? comboBox.currentText : textResult.text
root.selected(root.result);
// FIXME we are leaking memory to avoid a crash
// root.destroy();
root.disableFade = true
visible = false;
root.destroy();
}
}
}

View file

@ -18,8 +18,8 @@ OriginalDesktop.Desktop {
hoverEnabled: true
propagateComposedEvents: true
scrollGestureEnabled: false // we don't need/want these
onEntered: ApplicationCompositor.reticleOverDesktop = true
onExited: ApplicationCompositor.reticleOverDesktop = false
onEntered: if (typeof ApplicationCompositor !== "undefined") ApplicationCompositor.reticleOverDesktop = true
onExited: if (typeof ApplicationCompositor !== "undefined") ApplicationCompositor.reticleOverDesktop = false
acceptedButtons: Qt.NoButton
}

View file

@ -213,6 +213,63 @@ Item {
popup.open();
}
HiFiGlyphs {
id: errorsGlyph
visible: !AvatarPackagerCore.currentAvatarProject || AvatarPackagerCore.currentAvatarProject.hasErrors
text: hifi.glyphs.alert
size: 315
color: "#EA4C5F"
anchors {
top: parent.top
topMargin: -30
horizontalCenter: parent.horizontalCenter
}
}
Image {
id: successGlyph
visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.hasErrors
anchors {
top: parent.top
topMargin: 52
horizontalCenter: parent.horizontalCenter
}
width: 149.6
height: 149
source: "../../../icons/checkmark-stroke.svg"
}
RalewayRegular {
id: doctorStatusMessage
states: [
State {
when: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.hasErrors
name: "noErrors"
PropertyChanges {
target: doctorStatusMessage
text: "Your avatar looks fine."
}
},
State {
when: !AvatarPackagerCore.currentAvatarProject || AvatarPackagerCore.currentAvatarProject.hasErrors
name: "errors"
PropertyChanges {
target: doctorStatusMessage
text: "Your avatar has a few issues."
}
}
]
color: 'white'
size: 20
anchors.left: parent.left
anchors.right: parent.right
anchors.top: errorsGlyph.bottom
wrapMode: Text.Wrap
}
RalewayRegular {
id: infoMessage
@ -240,7 +297,7 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.top: doctorStatusMessage.bottom
anchors.bottomMargin: 24
@ -249,6 +306,53 @@ Item {
text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users."
}
RalewayRegular {
id: notForSaleMessage
visible: root.hasSuccessfullyUploaded
color: 'white'
linkColor: '#00B4EF'
size: 20
anchors.left: parent.left
anchors.right: parent.right
anchors.top: infoMessage.bottom
anchors.topMargin: 10
anchors.bottomMargin: 24
wrapMode: Text.Wrap
text: "This item is not for sale yet, <a href='#'>learn more</a>."
onLinkActivated: {
Qt.openUrlExternally("https://docs.highfidelity.com/sell/add-item/upload-avatar.html");
}
}
RalewayRegular {
id: showErrorsLink
color: 'white'
linkColor: '#00B4EF'
visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors
anchors {
top: notForSaleMessage.bottom
topMargin: 16
horizontalCenter: parent.horizontalCenter
}
size: 28
text: "<a href='toggle'>View all errors</a>"
onLinkActivated: {
avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport;
}
}
HifiControls.Button {
id: openFolderButton

View file

@ -33,15 +33,19 @@ Rectangle {
property bool balanceReceived: false;
property bool availableUpdatesReceived: false;
property bool itemInfoReceived: false;
property bool dataReady: itemInfoReceived && ownershipStatusReceived && balanceReceived && availableUpdatesReceived;
property string baseItemName: "";
property string itemName;
property string itemId;
property string itemHref;
property string itemAuthor;
property int itemEdition: -1;
property bool hasSomethingToTradeIn: itemEdition > 0; // i.e., don't trade in your artist's proof
property bool isTradingIn: canUpdate && hasSomethingToTradeIn;
property bool isStocking: (availability === 'not for sale') && (creator === Account.username) && !updated_item_id;
property string certificateId;
property double balanceAfterPurchase;
property bool alreadyOwned: false;
property bool alreadyOwned: false; // Including proofs
property int itemPrice: -1;
property bool isCertified;
property string itemType: "unknown";
@ -55,7 +59,10 @@ Rectangle {
property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified();
property string referrer;
property bool isInstalled;
property bool isUpdating;
property bool canUpdate;
property string availability: "available";
property string updated_item_id: "";
property string creator: "";
property string baseAppURL;
property int currentUpdatesPage: 1;
// Style
@ -138,6 +145,7 @@ Rectangle {
}
onAvailableUpdatesResult: {
// Answers the updatable original item cert data still owned by this user that are EITHER instances of this marketplace id, or update to this marketplace id.
if (result.status !== 'success') {
console.log("Failed to get Available Updates", result.data.message);
} else {
@ -149,7 +157,7 @@ Rectangle {
if (root.itemEdition !== -1 && root.itemEdition !== parseInt(result.data.updates[i].edition_number)) {
continue;
}
root.isUpdating = true;
root.canUpdate = true;
root.baseItemName = result.data.updates[i].base_item_title;
// This CertID is the one corresponding to the base item CertID that the user already owns
root.certificateId = result.data.updates[i].certificate_id;
@ -160,7 +168,7 @@ Rectangle {
}
}
if (result.data.updates.length === 0 || root.isUpdating) {
if (result.data.updates.length === 0 || root.canUpdate) {
root.availableUpdatesReceived = true;
refreshBuyUI();
} else {
@ -172,7 +180,7 @@ Rectangle {
onUpdateItemResult: {
if (result.status !== 'success') {
failureErrorText.text = result.message;
failureErrorText.text = result.data ? (result.data.message || "Unknown Error") : JSON.stringify(result);
root.activeView = "checkoutFailure";
} else {
root.itemHref = result.data.download_url;
@ -260,13 +268,6 @@ Rectangle {
}
}
}
MouseArea {
enabled: titleBarContainer.usernameDropdownVisible;
anchors.fill: parent;
onClicked: {
titleBarContainer.usernameDropdownVisible = false;
}
}
//
// TITLE BAR END
//
@ -434,7 +435,7 @@ Rectangle {
anchors.top: parent.top;
anchors.left: itemPreviewImage.right;
anchors.leftMargin: 12;
anchors.right: itemPriceContainer.left;
anchors.right: parent.right;
anchors.rightMargin: 8;
height: 30;
// Style
@ -449,21 +450,22 @@ Rectangle {
Item {
id: itemPriceContainer;
// Anchors
anchors.top: parent.top;
anchors.right: parent.right;
anchors.top: itemNameText.bottom;
anchors.topMargin: 8;
anchors.left: itemNameText.left;
height: 30;
width: itemPriceTextLabel.width + itemPriceText.width + 20;
width: itemPriceText.width + 20;
// "HFC" balance label
// "HFC" label
HiFiGlyphs {
id: itemPriceTextLabel;
visible: !(root.isUpdating && root.itemEdition > 0) && (root.itemPrice > 0);
visible: !isTradingIn && (root.itemPrice > 0);
text: hifi.glyphs.hfc;
// Size
size: 30;
// Anchors
anchors.right: itemPriceText.left;
anchors.rightMargin: 4;
anchors.right: parent.right;
//anchors.rightMargin: 4;
anchors.top: parent.top;
anchors.topMargin: 0;
width: paintedWidth;
@ -473,13 +475,15 @@ Rectangle {
}
FiraSansSemiBold {
id: itemPriceText;
text: (root.isUpdating && root.itemEdition > 0) ? "FREE\nUPDATE" : ((root.itemPrice === -1) ? "--" : ((root.itemPrice > 0) ? root.itemPrice : "FREE"));
text: isTradingIn ? "FREE\nUPDATE" :
(isStocking ? "Free for creator" :
((root.itemPrice === -1) ? "--" : ((root.itemPrice > 0) ? root.itemPrice : "FREE")));
// Text size
size: (root.isUpdating && root.itemEdition > 0) ? 20 : 26;
size: isTradingIn ? 20 : 26;
// Anchors
anchors.top: parent.top;
anchors.right: parent.right;
anchors.rightMargin: 16;
anchors.left: itemPriceTextLabel.visible ? itemPriceTextLabel.right : parent.left;
anchors.leftMargin: 4;
width: paintedWidth;
height: paintedHeight;
// Style
@ -571,7 +575,7 @@ Rectangle {
// "View in Inventory" button
HifiControlsUit.Button {
id: viewInMyPurchasesButton;
visible: false;
visible: isCertified && dataReady && (isTradingIn ? hasSomethingToTradeIn : alreadyOwned);
color: hifi.buttons.blue;
colorScheme: hifi.colorSchemes.light;
anchors.top: buyTextContainer.visible ? buyTextContainer.bottom : checkoutActionButtonsContainer.top;
@ -579,9 +583,9 @@ Rectangle {
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: root.isUpdating ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN YOUR INVENTORY";
text: (canUpdate && !isTradingIn) ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN YOUR INVENTORY";
onClicked: {
if (root.isUpdating) {
if (root.canUpdate) {
sendToScript({method: 'checkout_goToPurchases', filterText: root.baseItemName});
} else {
sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName});
@ -592,8 +596,12 @@ Rectangle {
// "Buy" button
HifiControlsUit.Button {
id: buyButton;
visible: !((root.itemType === "avatar" || root.itemType === "app") && viewInMyPurchasesButton.visible)
enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived && availableUpdatesReceived) || (!root.isCertified) || root.isUpdating;
visible: isTradingIn || !alreadyOwned || isStocking || !(root.itemType === "avatar" || root.itemType === "app");
property bool checkBalance: dataReady && (root.availability === "available")
enabled: (checkBalance && (balanceAfterPurchase >= 0)) || !isCertified || isTradingIn || isStocking;
text: isTradingIn ? "Confirm Update" :
(enabled ? (viewInMyPurchasesButton.visible ? "Get It Again" : (dataReady ? "Get Item" : "--")) :
(checkBalance ? "Insufficient Funds" : availability))
color: viewInMyPurchasesButton.visible ? hifi.buttons.white : hifi.buttons.blue;
colorScheme: hifi.colorSchemes.light;
anchors.top: viewInMyPurchasesButton.visible ? viewInMyPurchasesButton.bottom :
@ -602,10 +610,8 @@ Rectangle {
height: 50;
anchors.left: parent.left;
anchors.right: parent.right;
text: (root.isUpdating && root.itemEdition > 0) ? "CONFIRM UPDATE" : (((root.isCertified) ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ?
((viewInMyPurchasesButton.visible && !root.isUpdating) ? "Get It Again" : "Confirm") : "--") : "Get Item"));
onClicked: {
if (root.isUpdating && root.itemEdition > 0) {
if (isTradingIn) {
// If we're updating an app, the existing app needs to be uninstalled.
// This call will fail/return `false` if the app isn't installed, but that's OK.
if (root.itemType === "app") {
@ -1063,7 +1069,11 @@ Rectangle {
buyButton.color = hifi.buttons.red;
root.shouldBuyWithControlledFailure = true;
} else {
buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item");
buyButton.text = (root.isCertified ?
(dataReady ?
(root.alreadyOwned ? "Buy Another" : "Buy") :
"--") :
"Get Item");
buyButton.color = hifi.buttons.blue;
root.shouldBuyWithControlledFailure = false;
}
@ -1091,6 +1101,9 @@ Rectangle {
root.itemPrice = result.data.cost;
root.itemAuthor = result.data.creator;
root.itemType = result.data.item_type || "unknown";
root.availability = result.data.availability;
root.updated_item_id = result.data.updated_item_id || ""
root.creator = result.data.creator;
if (root.itemType === "unknown") {
root.itemHref = result.data.review_url;
} else {
@ -1139,7 +1152,7 @@ Rectangle {
signal sendToScript(var message);
function canBuyAgain() {
return (root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "unknown");
return root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "unknown" || isStocking;
}
function handleContentSets() {
@ -1185,29 +1198,23 @@ Rectangle {
function refreshBuyUI() {
if (root.isCertified) {
if (root.ownershipStatusReceived && root.balanceReceived && root.availableUpdatesReceived) {
if (dataReady) {
buyText.text = "";
// If the user IS on the checkout page for the updated version of an owned item...
if (root.isUpdating) {
if (root.canUpdate) {
// If the user HAS already selected a specific edition to update...
if (root.itemEdition > 0) {
if (hasSomethingToTradeIn) {
buyText.text = "By pressing \"Confirm Update\", you agree to trade in your old item for the updated item that replaces it.";
buyTextContainer.color = "#FFFFFF";
buyTextContainer.border.color = "#FFFFFF";
// Else if the user HAS NOT selected a specific edition to update...
} else {
viewInMyPurchasesButton.visible = true;
handleBuyAgainLogic();
}
// If the user IS NOT on the checkout page for the updated verison of an owned item...
// (i.e. they are checking out an item "normally")
} else {
if (root.alreadyOwned) {
viewInMyPurchasesButton.visible = true;
}
handleBuyAgainLogic();
}
} else {

View file

@ -41,6 +41,7 @@ Rectangle {
property string searchScopeString: "Featured"
property bool isLoggedIn: false
property bool supports3DHTML: true
property bool pendingGetMarketplaceItemCall: false
anchors.fill: (typeof parent === undefined) ? undefined : parent
@ -100,7 +101,9 @@ Rectangle {
getMarketplaceItems();
}
onGetMarketplaceItemsResult: {
marketBrowseModel.handlePage(result.status !== "success" && result.message, result);
if (!pendingGetMarketplaceItemCall) {
marketBrowseModel.handlePage(result.status !== "success" && result.message, result);
}
}
onGetMarketplaceItemResult: {
@ -112,7 +115,7 @@ Rectangle {
marketplaceItem.image_url = result.data.thumbnail_url;
marketplaceItem.name = result.data.title;
marketplaceItem.likes = result.data.likes;
if(result.data.has_liked !== undefined) {
if (result.data.has_liked !== undefined) {
marketplaceItem.liked = result.data.has_liked;
}
marketplaceItem.creator = result.data.creator;
@ -121,11 +124,13 @@ Rectangle {
marketplaceItem.description = result.data.description;
marketplaceItem.attributions = result.data.attributions;
marketplaceItem.license = result.data.license;
marketplaceItem.available = result.data.availability === "available";
marketplaceItem.availability = result.data.availability;
marketplaceItem.updated_item_id = result.data.updated_item_id || "";
marketplaceItem.created_at = result.data.created_at;
marketplaceItemScrollView.contentHeight = marketplaceItemContent.height;
itemsList.visible = false;
marketplaceItemView.visible = true;
pendingGetMarketplaceItemCall = false;
}
}
}
@ -539,7 +544,7 @@ Rectangle {
creator: model.creator
category: model.primary_category
price: model.cost
available: model.availability === "available"
availability: model.availability
isLoggedIn: root.isLoggedIn;
onShowItem: {
@ -711,7 +716,7 @@ Rectangle {
topMargin: 10;
leftMargin: 15;
}
height: visible ? childrenRect.height : 0
height: visible ? 36 : 0
RalewayRegular {
id: sortText
@ -733,8 +738,9 @@ Rectangle {
top: parent.top
leftMargin: 20
}
width: root.isLoggedIn ? 342 : 262
height: 36
height: parent.height
radius: 4
border.width: 1
@ -978,7 +984,6 @@ Rectangle {
xhr.open("GET", url);
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
console.log(xhr.responseText);
licenseText.text = xhr.responseText;
licenseInfo.visible = true;
}
@ -1223,6 +1228,7 @@ Rectangle {
console.log("A message with method 'updateMarketplaceQMLItem' was sent without an itemId!");
return;
}
pendingGetMarketplaceItemCall = true;
marketplaceItem.edition = message.params.edition ? message.params.edition : -1;
MarketplaceScriptingInterface.getMarketplaceItem(message.params.itemId);
break;

View file

@ -2,7 +2,7 @@
// MarketplaceListItem.qml
// qml/hifi/commerce/marketplace
//
// MarketplaceListItem
// MarketplaceItem
//
// Created by Roxanne Skelly on 2019-01-22
// Copyright 2019 High Fidelity, Inc.
@ -33,16 +33,16 @@ Rectangle {
property string creator: ""
property var categories: []
property int price: 0
property string availability: "unknown"
property string updated_item_id: ""
property var attributions: []
property string description: ""
property string license: ""
property string posted: ""
property bool available: false
property string created_at: ""
property bool isLoggedIn: false;
property int edition: -1;
property bool supports3DHTML: false;
onCategoriesChanged: {
categoriesListModel.clear();
@ -52,13 +52,7 @@ Rectangle {
}
onDescriptionChanged: {
if(root.supports3DHTML) {
descriptionTextModel.clear();
descriptionTextModel.append({text: description});
} else {
descriptionText.text = description;
}
descriptionText.text = description;
}
onAttributionsChanged: {
@ -250,7 +244,6 @@ Rectangle {
function evalHeight() {
height = categoriesList.y - buyButton.y + categoriesList.height;
console.log("HEIGHT: " + height);
}
HifiControlsUit.Button {
@ -264,11 +257,17 @@ Rectangle {
}
height: 50
text: root.edition >= 0 ? "UPGRADE FOR FREE" : (root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)")
enabled: root.edition >= 0 || root.available
buttonGlyph: root.available ? (root.price ? hifi.glyphs.hfc : "") : ""
property bool isUpdate: root.edition >= 0 // Special case of updating from a specific older item
property bool isStocking: (creator === Account.username) && (availability === "not for sale") && !updated_item_id // Note: server will say "sold out" or "invalidated" before it says NFS
property bool isFreeSpecial: isStocking || isUpdate
enabled: isFreeSpecial || (availability === 'available')
buttonGlyph: (enabled && !isUpdate && (price > 0)) ? hifi.glyphs.hfc : ""
text: isUpdate ? "UPDATE FOR FREE" : (isStocking ? "FREE STOCK" : (enabled ? (price || "FREE") : availability))
color: hifi.buttons.blue
buttonGlyphSize: 24
fontSize: 24
onClicked: root.buy();
}

View file

@ -34,7 +34,7 @@ Rectangle {
property string creator: ""
property string category: ""
property int price: 0
property bool available: false
property string availability: "unknown"
property bool isLoggedIn: false;
signal buy()
@ -226,6 +226,7 @@ Rectangle {
top: parent.top
left: parent.left
leftMargin: 15
topMargin: 10
}
width: paintedWidth
@ -241,7 +242,7 @@ Rectangle {
anchors {
top: creatorLabel.top;
left: creatorLabel.right;
leftMargin: 15;
leftMargin: 10;
}
width: paintedWidth;
@ -273,7 +274,7 @@ Rectangle {
anchors {
top: categoryLabel.top
left: categoryLabel.right
leftMargin: 15
leftMargin: 10
}
width: paintedWidth
@ -298,11 +299,21 @@ Rectangle {
topMargin:10
bottomMargin: 10
}
width: 180
property bool isNFS: availability === "not for sale" // Note: server will say "sold out" or "invalidated" before it says NFS
property bool isMine: creator === Account.username
property bool isUpgrade: root.edition >= 0
property int costToMe: ((isMine && isNFS) || isUpgrade) ? 0 : price
property bool isAvailable: availability === "available"
text: isUpgrade ? "UPGRADE FOR FREE" : (isAvailable ? (costToMe || "FREE") : availability)
enabled: isAvailable
buttonGlyph: isAvailable ? (costToMe ? hifi.glyphs.hfc : "") : ""
text: root.price ? root.price : "FREE"
buttonGlyph: root.price ? hifi.glyphs.hfc : ""
color: hifi.buttons.blue;
buttonGlyphSize: 24
fontSize: 24
onClicked: root.buy();
}
}

View file

@ -380,7 +380,7 @@ Item {
if (updateButton.visible && uninstallButton.visible) {
item.itemButtonText = "";
item.glyphSize = 20;
} else {
} else if (item) {
item.itemButtonText = "Send to Trash";
item.glyphSize = 30;
}

View file

@ -40,10 +40,6 @@ Rectangle {
source: "images/wallet-bg.jpg";
}
Component.onDestruction: {
KeyboardScriptingInterface.raised = false;
}
Connections {
target: Commerce;

View file

@ -6,7 +6,7 @@ import QtQuick.Layouts 1.3
import TabletScriptingInterface 1.0
import "."
import stylesUit 1.0
import stylesUit 1.0 as HifiStylesUit
import "../audio" as HifiAudio
Item {
@ -49,44 +49,116 @@ Item {
}
Item {
width: 150
height: 50
id: rightContainer
width: clockItem.width > loginItem.width ? clockItem.width + clockAmPmTextMetrics.width :
loginItem.width + clockAmPmTextMetrics.width
height: parent.height
anchors.top: parent.top
anchors.topMargin: 15
anchors.right: parent.right
anchors.rightMargin: 30
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 20
anchors.bottom: parent.bottom
ColumnLayout {
anchors.fill: parent
function timeChanged() {
var date = new Date();
clockTime.text = date.toLocaleTimeString(Qt.locale("en_US"), "h:mm ap");
var regex = /[\sa-zA-z]+/;
clockTime.text = clockTime.text.replace(regex, "");
clockAmPm.text = date.toLocaleTimeString(Qt.locale("en_US"), "ap");
}
RalewaySemiBold {
text: Account.loggedIn ? qsTr("Log out") : qsTr("Log in")
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignRight
font.pixelSize: 20
color: "#afafaf"
Timer {
interval: 1000; running: true; repeat: true;
onTriggered: rightContainer.timeChanged();
}
Item {
id: clockAmPmItem
width: clockAmPmTextMetrics.width
height: clockAmPmTextMetrics.height
anchors.top: parent.top
anchors.right: parent.right
TextMetrics {
id: clockAmPmTextMetrics
text: clockAmPm.text
font: clockAmPm.font
}
RalewaySemiBold {
visible: Account.loggedIn
height: Account.loggedIn ? parent.height/2 - parent.spacing/2 : 0
text: Account.loggedIn ? "[" + tabletRoot.usernameShort + "]" : ""
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignRight
font.pixelSize: 20
Text {
anchors.left: parent.left
id: clockAmPm
anchors.right: parent.right
font.capitalization: Font.AllUppercase
font.pixelSize: 12
font.family: "Rawline"
color: "#afafaf"
}
}
MouseArea {
anchors.fill: parent
onClicked: {
if (!Account.loggedIn) {
DialogsManager.showLoginDialog()
} else {
Account.logOut()
Item {
id: clockItem
width: clockTimeTextMetrics.width
height: clockTimeTextMetrics.height
anchors {
top: parent.top
topMargin: -10
right: clockAmPmItem.left
rightMargin: 5
}
TextMetrics {
id: clockTimeTextMetrics
text: clockTime.text
font: clockTime.font
}
Text {
anchors.top: parent.top
anchors.right: parent.right
id: clockTime
font.bold: false
font.pixelSize: 36
font.family: "Rawline"
color: "#afafaf"
}
}
Item {
id: loginItem
width: loginTextMetrics.width
height: loginTextMetrics.height
anchors {
bottom: parent.bottom
bottomMargin: 10
right: clockAmPmItem.left
rightMargin: 5
}
Text {
id: loginText
anchors.right: parent.right
text: Account.loggedIn ? tabletRoot.usernameShort : qsTr("Log in")
horizontalAlignment: Text.AlignRight
Layout.alignment: Qt.AlignRight
font.pixelSize: 18
font.family: "Rawline"
color: "#afafaf"
}
TextMetrics {
id: loginTextMetrics
text: loginText.text
font: loginText.font
}
MouseArea {
anchors.fill: parent
onClicked: {
if (!Account.loggedIn) {
DialogsManager.showLoginDialog();
}
}
}
}
Component.onCompleted: {
rightContainer.timeChanged();
}
}
}

View file

@ -178,10 +178,10 @@ Rectangle {
function setUsername(newUsername) {
username = newUsername;
usernameShort = newUsername.substring(0, 8);
usernameShort = newUsername.substring(0, 14);
if (newUsername.length > 8) {
usernameShort = usernameShort + "..."
if (newUsername.length > 14) {
usernameShort = usernameShort + "..."
}
}

View file

@ -0,0 +1,20 @@
//
// Rawline.qml
//
// Created by Wayne Chen on 25 Feb 2019
// 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
//
import QtQuick 2.7
Text {
id: root
property real size: 32
font.pixelSize: size
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
font.family: "Rawline"
}

View file

@ -0,0 +1,20 @@
//
// Rawline.qml
//
// Created by Wayne Chen on 25 Feb 2019
// 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
//
import QtQuick 2.7
Text {
id: root
property real size: 32
font.pixelSize: size
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
font.family: "Rawline"
}

View file

@ -16,8 +16,8 @@
#include <QObject>
/**jsdoc
* The <code>HifiAbout</code> API provides information about the version of Interface that is currently running. It also
* provides the ability to open a Web page in an Interface browser window.
* The <code>HifiAbout</code> API provides information about the version of Interface that is currently running. It also
* has the functionality to open a web page in an Interface browser window.
*
* @namespace HifiAbout
*
@ -30,9 +30,9 @@
* @property {string} qtVersion - The Qt version used in Interface that is currently running. <em>Read-only.</em>
*
* @example <caption>Report build information for the version of Interface currently running.</caption>
* print("HiFi build date: " + HifiAbout.buildDate); // 11 Feb 2019
* print("HiFi version: " + HifiAbout.buildVersion); // 0.78.0
* print("Qt version: " + HifiAbout.qtVersion); // 5.10.1
* print("HiFi build date: " + HifiAbout.buildDate); // Returns the build date of the version of Interface currently running on your machine.
* print("HiFi version: " + HifiAbout.buildVersion); // Returns the build version of Interface currently running on your machine.
* print("Qt version: " + HifiAbout.qtVersion); // Returns the Qt version details of the version of Interface currently running on your machine.
*/
class AboutUtil : public QObject {
@ -52,9 +52,9 @@ public:
public slots:
/**jsdoc
* Display a Web page in an Interface browser window.
* Display a web page in an Interface browser window.
* @function HifiAbout.openUrl
* @param {string} url - The URL of the Web page to display.
* @param {string} url - The URL of the web page you want to view in Interface.
*/
void openUrl(const QString &url) const;
private:

View file

@ -45,6 +45,10 @@ void AndroidHelper::notifyBeforeEnterBackground() {
emit beforeEnterBackground();
}
void AndroidHelper::notifyToggleAwayMode() {
emit toggleAwayMode();
}
void AndroidHelper::notifyEnterBackground() {
emit enterBackground();
}

View file

@ -31,6 +31,7 @@ public:
void notifyEnterForeground();
void notifyBeforeEnterBackground();
void notifyEnterBackground();
void notifyToggleAwayMode();
void performHapticFeedback(int duration);
void processURL(const QString &url);
@ -55,7 +56,7 @@ signals:
void enterForeground();
void beforeEnterBackground();
void enterBackground();
void toggleAwayMode();
void hapticFeedbackRequested(int duration);
void handleSignupCompleted();

View file

@ -9,6 +9,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "Application.h"
#include <chrono>
@ -37,7 +38,6 @@
#include <QtNetwork/QLocalSocket>
#include <QtNetwork/QLocalServer>
#include <QtQml/QQmlContext>
#include <QtQml/QQmlEngine>
#include <QtQuick/QQuickWindow>
@ -51,6 +51,7 @@
#include <QProcessEnvironment>
#include <QTemporaryDir>
#include <gl/QOpenGLContextWrapper.h>
#include <gl/GLWindow.h>
#include <gl/GLHelpers.h>
@ -191,6 +192,9 @@
#include "scripting/WalletScriptingInterface.h"
#include "scripting/TTSScriptingInterface.h"
#include "scripting/KeyboardScriptingInterface.h"
#if defined(Q_OS_MAC) || defined(Q_OS_WIN)
#include "SpeechRecognizer.h"
#endif
@ -239,6 +243,7 @@
#include "webbrowser/WebBrowserSuggestionsEngine.h"
#include <DesktopPreviewProvider.h>
#include "AboutUtil.h"
#if defined(Q_OS_WIN)
@ -622,8 +627,6 @@ public:
switch (type) {
case NestableType::Entity:
return getEntityModelProvider(static_cast<EntityItemID>(uuid));
case NestableType::Overlay:
return nullptr;
case NestableType::Avatar:
return getAvatarModelProvider(uuid);
}
@ -1058,6 +1061,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged,
controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices);
EntityTree::setEntityClicksCapturedOperator([this] {
return _controllerScriptingInterface->areEntityClicksCaptured();
});
_entityClipboard->createRootElement();
#ifdef Q_OS_WIN
@ -1072,6 +1079,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf");
@ -1747,12 +1755,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
});
_applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float {
#if defined(Q_OS_ANDROID)
return 1;
return 1 ;
#else
return 0;
#endif
});
// Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings
userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice());
// if the _touchscreenDevice is not supported it will not be registered
@ -1850,6 +1859,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
this->installEventFilter(this);
#ifdef HAVE_DDE
auto ddeTracker = DependencyManager::get<DdeFaceTracker>();
ddeTracker->init();
@ -1911,46 +1922,34 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
}
}, Qt::QueuedConnection);
EntityTree::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
if (_aboutToQuit) {
return false;
}
// try to find the renderable
auto renderable = getEntities()->renderableForEntityId(entityID);
if (renderable) {
renderable->addMaterial(material, parentMaterialName);
}
// even if we don't find it, try to find the entity
auto entity = getEntities()->getEntity(entityID);
if (entity) {
entity->addMaterial(material, parentMaterialName);
return true;
}
return false;
});
EntityTree::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) {
EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) {
if (_aboutToQuit) {
return false;
}
// try to find the renderable
auto renderable = getEntities()->renderableForEntityId(entityID);
if (renderable) {
renderable->removeMaterial(material, parentMaterialName);
}
// even if we don't find it, try to find the entity
auto entity = getEntities()->getEntity(entityID);
if (entity) {
entity->removeMaterial(material, parentMaterialName);
return true;
}
return false;
});
EntityTree::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
auto avatarManager = DependencyManager::get<AvatarManager>();
auto avatar = avatarManager->getAvatarBySessionID(avatarID);
if (avatar) {
@ -1959,7 +1958,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
}
return false;
});
EntityTree::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) {
EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) {
auto avatarManager = DependencyManager::get<AvatarManager>();
auto avatar = avatarManager->getAvatarBySessionID(avatarID);
if (avatar) {
@ -2282,7 +2281,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// Setup the mouse ray pick and related operators
{
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true);
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true);
mouseRayPick->parentTransform = std::make_shared<MouseTransformNode>();
mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE);
auto mouseRayPickID = DependencyManager::get<PickManager>()->addPick(PickQuery::Ray, mouseRayPick);
@ -2308,31 +2307,31 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
DependencyManager::get<PickManager>()->setPrecisionPicking(rayPickID, value);
});
EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode) {
EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) {
if (billboardMode == BillboardMode::YAW) {
//rotate about vertical to face the camera
ViewFrustum frustum;
copyViewFrustum(frustum);
glm::vec3 dPosition = frustum.getPosition() - position;
glm::vec3 dPosition = frustumPos - position;
// If x and z are 0, atan(x, z) is undefined, so default to 0 degrees
float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z);
return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f));
} else if (billboardMode == BillboardMode::FULL) {
ViewFrustum frustum;
copyViewFrustum(frustum);
glm::vec3 cameraPos = frustum.getPosition();
// use the referencial from the avatar, y isn't always up
glm::vec3 avatarUP = DependencyManager::get<AvatarManager>()->getMyAvatar()->getWorldOrientation() * Vectors::UP;
// check to see if glm::lookAt will work / using glm::lookAt variable name
glm::highp_vec3 s(glm::cross(position - cameraPos, avatarUP));
glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP));
// make sure s is not NaN for any component
if (glm::length2(s) > 0.0f) {
return glm::conjugate(glm::toQuat(glm::lookAt(cameraPos, position, avatarUP)));
return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP)));
}
}
return rotation;
});
EntityItem::setPrimaryViewFrustumPositionOperator([this]() {
ViewFrustum viewFrustum;
copyViewFrustum(viewFrustum);
return viewFrustum.getPosition();
});
render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer<OffscreenQmlSurface>& webSurface, bool& cachedWebSurface) {
bool isTablet = url == TabletScriptingInterface::QML;
@ -2347,6 +2346,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
} else {
QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor);
}
auto surfaceContext = webSurface->getSurfaceContext();
surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get<KeyboardScriptingInterface>().data());
} else {
// FIXME: the tablet should use the OffscreenQmlSurfaceCache
webSurface = QSharedPointer<OffscreenQmlSurface>(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) {
@ -2418,6 +2419,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground);
connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground);
connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground);
connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode);
AndroidHelper::instance().notifyLoadComplete();
#endif
pauseUntilLoginDetermined();
@ -3099,7 +3101,7 @@ void Application::initializeUi() {
}
if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) {
_touchscreenVirtualPadDevice = std::dynamic_pointer_cast<TouchscreenVirtualPadDevice>(inputPlugin);
#if defined(Q_OS_ANDROID)
#if defined(ANDROID_APP_INTERFACE)
auto& virtualPadManager = VirtualPad::Manager::instance();
connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested,
this, [](int duration) {
@ -3282,6 +3284,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona
surfaceContext->setContextProperty("MyAvatar", DependencyManager::get<AvatarManager>()->getMyAvatar().get());
surfaceContext->setContextProperty("Entities", DependencyManager::get<EntityScriptingInterface>().data());
surfaceContext->setContextProperty("Snapshot", DependencyManager::get<Snapshot>().data());
surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get<KeyboardScriptingInterface>().data());
if (setAdditionalContextProperties) {
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
@ -3292,7 +3295,6 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona
surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance());
surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance());
surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get<KeyboardScriptingInterface>().data());
surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED
@ -3631,10 +3633,14 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
}
// Get controller availability
#ifdef ANDROID_APP_QUEST_INTERFACE
bool hasHandControllers = true;
#else
bool hasHandControllers = false;
if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) {
hasHandControllers = true;
}
#endif
// Check HMD use (may be technically available without being in use)
bool hasHMD = PluginUtils::isHMDAvailable();
@ -3664,8 +3670,8 @@ void Application::handleSandboxStatus(QNetworkReply* reply) {
// If this is a first run we short-circuit the address passed in
if (_firstRun.get()) {
#if !defined(Q_OS_ANDROID)
DependencyManager::get<AddressManager>()->goToEntry();
sentTo = SENT_TO_ENTRY;
DependencyManager::get<AddressManager>()->goToEntry();
sentTo = SENT_TO_ENTRY;
#endif
_firstRun.set(false);
@ -3776,9 +3782,12 @@ std::map<QString, QString> Application::prepareServerlessDomainContents(QUrl dom
tmpTree->reaverageOctreeElements();
tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0);
}
std::map<QString, QString> namedPaths = tmpTree->getNamedPaths();
return tmpTree->getNamedPaths();
// we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak
tmpTree->eraseAllOctreeElements(false);
return namedPaths;
}
void Application::loadServerlessDomain(QUrl domainURL) {
@ -4389,7 +4398,6 @@ void Application::mouseMoveEvent(QMouseEvent* event) {
if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() ||
getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) {
getEntities()->mouseMoveEvent(&mappedEvent);
getOverlays().mouseMoveEvent(&mappedEvent);
}
_controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts
@ -4423,14 +4431,8 @@ void Application::mousePressEvent(QMouseEvent* event) {
#endif
QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers());
std::pair<float, QUuid> entityResult;
if (!_controllerScriptingInterface->areEntityClicksCaptured()) {
entityResult = getEntities()->mousePressEvent(&mappedEvent);
}
std::pair<float, QUuid> overlayResult = getOverlays().mousePressEvent(&mappedEvent);
QUuid focusedEntity = entityResult.first < overlayResult.first ? entityResult.second : overlayResult.second;
setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(focusedEntity) ? focusedEntity : UNKNOWN_ENTITY_ID);
QUuid result = getEntities()->mousePressEvent(&mappedEvent);
setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID);
_controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts
@ -4469,11 +4471,7 @@ void Application::mouseDoublePressEvent(QMouseEvent* event) {
transformedPos,
event->screenPos(), event->button(),
event->buttons(), event->modifiers());
if (!_controllerScriptingInterface->areEntityClicksCaptured()) {
getEntities()->mouseDoublePressEvent(&mappedEvent);
}
getOverlays().mouseDoublePressEvent(&mappedEvent);
getEntities()->mouseDoublePressEvent(&mappedEvent);
// if one of our scripts have asked to capture this event, then stop processing it
if (_controllerScriptingInterface->isMouseCaptured()) {
@ -4498,7 +4496,6 @@ void Application::mouseReleaseEvent(QMouseEvent* event) {
event->buttons(), event->modifiers());
getEntities()->mouseReleaseEvent(&mappedEvent);
getOverlays().mouseReleaseEvent(&mappedEvent);
_controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts
@ -4982,6 +4979,15 @@ void Application::idle() {
}
}
{
if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) {
QUuid entityId = _keyboardFocusedEntity.get();
setKeyboardFocusEntity(UNKNOWN_ENTITY_ID);
_keyboardFocusWaitingOnRenderable = false;
setKeyboardFocusEntity(entityId);
}
}
{
PerformanceTimer perfTimer("pluginIdle");
PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()");
@ -5764,6 +5770,11 @@ void Application::reloadResourceCaches() {
DependencyManager::get<NodeList>()->reset(); // Force redownload of .fst models
DependencyManager::get<ScriptEngines>()->reloadAllScripts();
getOffscreenUI()->clearCache();
DependencyManager::get<Keyboard>()->createKeyboard();
getMyAvatar()->resetFullAvatarURL();
}
@ -5810,7 +5821,7 @@ void Application::setKeyboardFocusEntity(const QUuid& id) {
if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) {
if (id == _loginDialogID) {
emit loginDialogFocusEnabled();
} else {
} else if (!_keyboardFocusWaitingOnRenderable) {
// that's the only entity we want in focus;
return;
}
@ -5827,7 +5838,10 @@ void Application::setKeyboardFocusEntity(const QUuid& id) {
if (properties.getVisible()) {
auto entities = getEntities();
auto entityId = _keyboardFocusedEntity.get();
if (entities->wantsKeyboardFocus(entityId)) {
auto entityItemRenderable = entities->renderableForEntityId(entityId);
if (!entityItemRenderable) {
_keyboardFocusWaitingOnRenderable = true;
} else if (entityItemRenderable->wantsKeyboardFocus()) {
entities->setProxyWindow(entityId, _window->windowHandle());
if (_keyboardMouseDevice->isActive()) {
_keyboardMouseDevice->pluginFocusOutEvent();
@ -6931,7 +6945,7 @@ void Application::clearDomainOctreeDetails(bool clearAll) {
});
// reset the model renderer
clearAll ? getEntities()->clear() : getEntities()->clearNonLocalEntities();
clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities();
auto skyStage = DependencyManager::get<SceneScriptingInterface>()->getSkyStage();
@ -8251,6 +8265,7 @@ void Application::loadDomainConnectionDialog() {
}
void Application::toggleLogDialog() {
#ifndef ANDROID_APP_QUEST_INTERFACE
if (getLoginDialogPoppedUp()) {
return;
}
@ -8274,6 +8289,7 @@ void Application::toggleLogDialog() {
} else {
_logDialog->show();
}
#endif
}
void Application::recreateLogWindow(int keepOnTop) {
@ -9136,23 +9152,40 @@ void Application::beforeEnterBackground() {
clearDomainOctreeDetails();
}
void Application::enterBackground() {
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
"stop", Qt::BlockingQueuedConnection);
// Quest only supports one plugin which can't be deactivated currently
#if !defined(ANDROID_APP_QUEST_INTERFACE)
if (getActiveDisplayPlugin()->isActive()) {
getActiveDisplayPlugin()->deactivate();
}
#endif
}
void Application::enterForeground() {
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
"start", Qt::BlockingQueuedConnection);
// Quest only supports one plugin which can't be deactivated currently
#if !defined(ANDROID_APP_QUEST_INTERFACE)
if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) {
qWarning() << "Could not re-activate display plugin";
}
#endif
auto nodeList = DependencyManager::get<NodeList>();
nodeList->setSendDomainServerCheckInEnabled(true);
}
void Application::toggleAwayMode(){
QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier);
QCoreApplication::sendEvent (this, &event);
}
#endif
#include "Application.moc"

View file

@ -338,7 +338,8 @@ public:
void beforeEnterBackground();
void enterBackground();
void enterForeground();
#endif
void toggleAwayMode();
#endif
signals:
void svoImportRequested(const QString& url);
@ -732,6 +733,7 @@ private:
bool _failedToConnectToEntityServer { false };
bool _reticleClickPressed { false };
bool _keyboardFocusWaitingOnRenderable { false };
int _avatarAttachmentRequest = 0;

View file

@ -19,13 +19,19 @@
#include <SimpleMovingAverage.h>
#include <render/Args.h>
#ifdef Q_OS_ANDROID
const float LOD_DEFAULT_QUALITY_LEVEL = 0.75f; // default quality level setting is High (lower framerate)
const float LOD_DEFAULT_QUALITY_LEVEL = 0.2f; // default quality level setting is High (lower framerate)
#else
const float LOD_DEFAULT_QUALITY_LEVEL = 0.5f; // default quality level setting is Mid
#endif
const float LOD_MAX_LIKELY_DESKTOP_FPS = 60.0f; // this is essentially, V-synch fps
#ifdef Q_OS_ANDROID
const float LOD_MAX_LIKELY_HMD_FPS = 36.0f; // this is essentially, V-synch fps
#else
const float LOD_MAX_LIKELY_HMD_FPS = 90.0f; // this is essentially, V-synch fps
#endif
const float LOD_OFFSET_FPS = 5.0f; // offset of FPS to add for computing the target framerate
class AABox;

View file

@ -297,6 +297,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
avatar->setIsNewAvatar(false);
}
avatar->simulate(deltaTime, inView);
if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) {
_myAvatar->addAvatarHandsToFlow(avatar);
}
avatar->updateRenderItem(renderTransaction);
avatar->updateSpaceProxy(workloadTransaction);
avatar->setLastRenderUpdateTime(startTime);
@ -716,7 +719,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic
}
}
if (rayAvatarResult._intersect && pickAgainstMesh) {
if (avatar && rayAvatarResult._intersect && pickAgainstMesh) {
glm::vec3 localRayOrigin = avatar->worldToJointPoint(ray.origin, rayAvatarResult._intersectWithJoint);
glm::vec3 localRayPoint = avatar->worldToJointPoint(ray.origin + rayAvatarResult._distance * rayDirection, rayAvatarResult._intersectWithJoint);

View file

@ -254,11 +254,15 @@ void AvatarProject::openInInventory() const {
DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml");
DependencyManager::get<HMDScriptingInterface>()->openTablet();
tablet->getTabletRoot()->forceActiveFocus();
auto name = getProjectName();
// I'm not a fan of this, but it's the only current option.
auto name = getProjectName();
QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() {
tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } }));
});
QQuickItem* root = tablet->getTabletRoot();
if (root) {
root->forceActiveFocus();
}
}

View file

@ -77,7 +77,10 @@ public:
return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath()));
}
Q_INVOKABLE bool getHasErrors() const { return _hasErrors; }
Q_INVOKABLE void setHasErrors(bool hasErrors) { _hasErrors = hasErrors; }
Q_INVOKABLE void setHasErrors(bool hasErrors) {
_hasErrors = hasErrors;
emit hasErrorsChanged();
}
/**
* returns the AvatarProject or a nullptr on failure.

View file

@ -32,6 +32,17 @@ void GrabManager::simulateGrabs() {
bool success;
SpatiallyNestablePointer grabbedThing = SpatiallyNestable::findByID(grabbedThingID, success);
if (success && grabbedThing) {
auto entity = std::dynamic_pointer_cast<EntityItem>(grabbedThing);
if (entity) {
if (entity->getLocked()) {
continue; // even if someone else claims to be grabbing it, don't move a locked thing
}
const GrabPropertyGroup& grabProps = entity->getGrabProperties();
if (!grabProps.getGrabbable()) {
continue; // even if someone else claims to be grabbing it, don't move non-grabbable
}
}
glm::vec3 finalPosition = acc.finalizePosition();
glm::quat finalOrientation = acc.finalizeOrientation();
grabbedThing->setTransform(createMatFromQuatAndPos(finalOrientation, finalPosition));

117
interface/src/avatar/MyAvatar.cpp Executable file → Normal file
View file

@ -910,7 +910,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
recorder->recordFrame(FRAME_TYPE, toFrame(*this));
}
locationChanged();
locationChanged(true, false);
// if a entity-child of this avatar has moved outside of its queryAACube, update the cube and tell the entity server.
auto entityTreeRenderer = qApp->getEntities();
EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr;
@ -920,6 +920,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
zoneInteractionProperties = entityTreeRenderer->getZoneInteractionProperties();
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
forEachDescendant([&](SpatiallyNestablePointer object) {
locationChanged(true, false);
// 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
@ -932,7 +933,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
bool isPhysicsEnabled = qApp->isPhysicsEnabled();
bool zoneAllowsFlying = zoneInteractionProperties.first;
bool collisionlessAllowed = zoneInteractionProperties.second;
_characterController.setFlyingAllowed((zoneAllowsFlying && _enableFlying) || !isPhysicsEnabled);
_characterController.setZoneFlyingAllowed(zoneAllowsFlying || !isPhysicsEnabled);
_characterController.setComfortFlyingAllowed(_enableFlying);
_characterController.setCollisionlessAllowed(collisionlessAllowed);
}
@ -2323,23 +2325,25 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
std::shared_ptr<QMetaObject::Connection> skeletonConnection = std::make_shared<QMetaObject::Connection>();
*skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() {
if (skeletonModelChangeCount == _skeletonModelChangeCount) {
if (skeletonModelChangeCount == _skeletonModelChangeCount) {
if (_fullAvatarModelName.isEmpty()) {
// Store the FST file name into preferences
const auto& mapping = _skeletonModel->getGeometry()->getMapping();
if (mapping.value("name").isValid()) {
_fullAvatarModelName = mapping.value("name").toString();
}
}
if (_fullAvatarModelName.isEmpty()) {
// Store the FST file name into preferences
const auto& mapping = _skeletonModel->getGeometry()->getMapping();
if (mapping.value("name").isValid()) {
_fullAvatarModelName = mapping.value("name").toString();
}
}
initHeadBones();
_skeletonModel->setCauterizeBoneSet(_headBoneSet);
_fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl();
initAnimGraph();
_skeletonModelLoaded = true;
}
QObject::disconnect(*skeletonConnection);
initHeadBones();
_skeletonModel->setCauterizeBoneSet(_headBoneSet);
_fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl();
initAnimGraph();
initFlowFromFST();
_skeletonModelLoaded = true;
}
QObject::disconnect(*skeletonConnection);
});
saveAvatarUrl();
@ -2963,6 +2967,10 @@ void MyAvatar::initAnimGraph() {
graphUrl = _fstAnimGraphOverrideUrl;
} else {
graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation.json");
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation_withSplineIKNode.json");
#endif
}
emit animGraphUrlChanged(graphUrl);
@ -5339,6 +5347,81 @@ void MyAvatar::releaseGrab(const QUuid& grabID) {
}
}
void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr<Avatar>& otherAvatar) {
auto &flow = _skeletonModel->getRig().getFlow();
for (auto &handJointName : HAND_COLLISION_JOINTS) {
int jointIndex = otherAvatar->getJointIndex(handJointName);
if (jointIndex != -1) {
glm::vec3 position = otherAvatar->getJointPosition(jointIndex);
flow.setOthersCollision(otherAvatar->getID(), jointIndex, position);
}
}
}
void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) {
if (_skeletonModel->isLoaded()) {
_skeletonModel->getRig().initFlow(isActive);
auto &flow = _skeletonModel->getRig().getFlow();
auto &collisionSystem = flow.getCollisionSystem();
collisionSystem.setActive(isCollidable);
auto physicsGroups = physicsConfig.keys();
if (physicsGroups.size() > 0) {
for (auto &groupName : physicsGroups) {
auto settings = physicsConfig[groupName].toMap();
FlowPhysicsSettings physicsSettings;
if (settings.contains("active")) {
physicsSettings._active = settings["active"].toBool();
}
if (settings.contains("damping")) {
physicsSettings._damping = settings["damping"].toFloat();
}
if (settings.contains("delta")) {
physicsSettings._delta = settings["delta"].toFloat();
}
if (settings.contains("gravity")) {
physicsSettings._gravity = settings["gravity"].toFloat();
}
if (settings.contains("inertia")) {
physicsSettings._inertia = settings["inertia"].toFloat();
}
if (settings.contains("radius")) {
physicsSettings._radius = settings["radius"].toFloat();
}
if (settings.contains("stiffness")) {
physicsSettings._stiffness = settings["stiffness"].toFloat();
}
flow.setPhysicsSettingsForGroup(groupName, physicsSettings);
}
}
auto collisionJoints = collisionsConfig.keys();
if (collisionJoints.size() > 0) {
collisionSystem.resetCollisions();
for (auto &jointName : collisionJoints) {
int jointIndex = getJointIndex(jointName);
FlowCollisionSettings collisionsSettings;
auto settings = collisionsConfig[jointName].toMap();
collisionsSettings._entityID = getID();
if (settings.contains("radius")) {
collisionsSettings._radius = settings["radius"].toFloat();
}
if (settings.contains("offset")) {
collisionsSettings._offset = vec3FromVariant(settings["offset"]);
}
collisionSystem.addCollisionSphere(jointIndex, collisionsSettings);
}
}
}
}
void MyAvatar::initFlowFromFST() {
if (_skeletonModel->isLoaded()) {
auto &flowData = _skeletonModel->getHFMModel().flowData;
if (flowData.shouldInitFlow()) {
useFlow(true, flowData.shouldInitCollisions(), flowData._physicsConfig, flowData._collisionsConfig);
}
}
}
void MyAvatar::sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const {
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;

View file

@ -1544,6 +1544,20 @@ public:
* @comment Uses the base class's JSDoc.
*/
int sendAvatarDataPacket(bool sendAll = false) override;
void addAvatarHandsToFlow(const std::shared_ptr<Avatar>& otherAvatar);
/**jsdoc
* Init flow simulation on avatar.
* @function MyAvatar.useFlow
* @param {boolean} - Set to <code>true</code> to activate flow simulation.
* @param {boolean} - Set to <code>true</code> to activate collisions.
* @param {Object} physicsConfig - object with the customized physic parameters
* i.e. {"hair": {"active": true, "stiffness": 0.0, "radius": 0.04, "gravity": -0.035, "damping": 0.8, "inertia": 0.8, "delta": 0.35}}
* @param {Object} collisionsConfig - object with the customized collision parameters
* i.e. {"Spine2": {"type": "sphere", "radius": 0.14, "offset": {"x": 0.0, "y": 0.2, "z": 0.0}}}
*/
Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap());
public slots:
@ -2201,6 +2215,7 @@ private:
void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency);
void initHeadBones();
void initAnimGraph();
void initFlowFromFST();
// Avatar Preferences
QUrl _fullAvatarURLFromPreferences;

View file

@ -10,12 +10,14 @@
#include <avatars-renderer/Avatar.h>
#include <DebugDraw.h>
#include <CubicHermiteSpline.h>
#include "Application.h"
#include "InterfaceLogging.h"
#include "AnimUtil.h"
MySkeletonModel::MySkeletonModel(Avatar* owningAvatar, QObject* parent) : SkeletonModel(owningAvatar, parent) {
}
@ -33,6 +35,22 @@ Rig::CharacterControllerState convertCharacterControllerState(CharacterControlle
};
}
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
static glm::vec3 computeSpine2WithHeadHipsSpline(MyAvatar* myAvatar, AnimPose hipsIKTargetPose, AnimPose headIKTargetPose) {
// the the ik targets to compute the spline with
CubicHermiteSplineFunctorWithArcLength splineFinal(headIKTargetPose.rot(), headIKTargetPose.trans(), hipsIKTargetPose.rot(), hipsIKTargetPose.trans());
// measure the total arc length along the spline
float totalArcLength = splineFinal.arcLength(1.0f);
float tFinal = splineFinal.arcLengthInverse(myAvatar->getSpine2SplineRatio() * totalArcLength);
glm::vec3 spine2Translation = splineFinal(tFinal);
return spine2Translation + myAvatar->getSpine2SplineOffset();
}
#endif
static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) {
glm::mat4 worldToSensorMat = glm::inverse(myAvatar->getSensorToWorldMatrix());
@ -233,6 +251,12 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() &&
!(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) {
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
AnimPose headAvatarSpace(avatarHeadPose.getRotation(), avatarHeadPose.getTranslation());
AnimPose headRigSpace = avatarToRigPose * headAvatarSpace;
AnimPose hipsRigSpace = sensorToRigPose * sensorHips;
glm::vec3 spine2TargetTranslation = computeSpine2WithHeadHipsSpline(myAvatar, hipsRigSpace, headRigSpace);
#endif
const float SPINE2_ROTATION_FILTER = 0.5f;
AnimPose currentSpine2Pose;
AnimPose currentHeadPose;
@ -243,6 +267,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
if (spine2Exists && headExists && hipsExists) {
AnimPose rigSpaceYaw(myAvatar->getSpine2RotationRigSpace());
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
rigSpaceYaw.rot() = safeLerp(Quaternions::IDENTITY, rigSpaceYaw.rot(), 0.5f);
#endif
glm::vec3 u, v, w;
glm::vec3 fwd = rigSpaceYaw.rot() * glm::vec3(0.0f, 0.0f, 1.0f);
glm::vec3 up = currentHeadPose.trans() - currentHipsPose.trans();
@ -253,6 +280,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) {
}
generateBasisVectors(up, fwd, u, v, w);
AnimPose newSpinePose(glm::mat4(glm::vec4(w, 0.0f), glm::vec4(u, 0.0f), glm::vec4(v, 0.0f), glm::vec4(glm::vec3(0.0f, 0.0f, 0.0f), 1.0f)));
#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK)
currentSpine2Pose.trans() = spine2TargetTranslation;
#endif
currentSpine2Pose.rot() = safeLerp(currentSpine2Pose.rot(), newSpinePose.rot(), SPINE2_ROTATION_FILTER);
params.primaryControllerPoses[Rig::PrimaryControllerType_Spine2] = currentSpine2Pose;
params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated;

View file

@ -368,7 +368,6 @@ void OtherAvatar::simulate(float deltaTime, bool inView) {
PROFILE_RANGE(simulation, "grabs");
applyGrabChanges();
}
updateFadingStatus();
}
@ -504,6 +503,7 @@ void OtherAvatar::handleChangedAvatarEntityData() {
// then set the the original ID for the changes to take effect
// TODO: This is a horrible hack and once properties.constructFromBuffer no longer causes
// side effects...remove the following three lines
const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}");
entity->setParentID(NULL_ID);
entity->setParentID(oldParentID);

View file

@ -48,6 +48,7 @@ public:
void rebuildCollisionShape() override;
void setWorkloadRegion(uint8_t region);
uint8_t getWorkloadRegion() { return _workloadRegion; }
bool shouldBeInPhysicsSimulation() const;
bool needsPhysicsUpdate() const;

View file

@ -62,8 +62,8 @@ public:
* <tr><th>Value</th><th>Meaning</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td><code>0</code></td><td>Not logged in</td><td>The user isn't logged in.</td></tr>
* <tr><td><code>1</code></td><td>Not set up</td><td>The user's wallet isn't set up.</td></tr>
* <tr><td><code>0</code></td><td>Not logged in</td><td>The user is not logged in.</td></tr>
* <tr><td><code>1</code></td><td>Not set up</td><td>The user's wallet has not been set up.</td></tr>
* <tr><td><code>2</code></td><td>Pre-existing</td><td>There is a wallet present on the server but not one
* locally.</td></tr>
* <tr><td><code>3</code></td><td>Conflicting</td><td>There is a wallet present on the server plus one present locally,
@ -73,8 +73,8 @@ public:
* <tr><td><code>5</code></td><td>Ready</td><td>The wallet is ready for use.</td></tr>
* </tbody>
* </table>
* <p>Wallets used to be stored locally but now they're stored on the server, unless the computer once had a wallet stored
* locally in which case the wallet may be present in both places.</p>
* <p>Wallets used to be stored locally but now they're only stored on the server. A wallet is present in both places if
* your computer previously stored its information locally.</p>
* @typedef {number} WalletScriptingInterface.WalletStatus
*/
enum WalletStatus {

View file

@ -72,7 +72,7 @@ void SafeLanding::addTrackedEntity(const EntityItemID& entityID) {
Locker lock(_lock);
EntityItemPointer entity = _entityTree->findEntityByID(entityID);
if (entity && entity->getCreated() < _startTime) {
if (entity && !entity->isLocalEntity() && entity->getCreated() < _startTime) {
_trackedEntities.emplace(entityID, entity);
int trackedEntityCount = (int)_trackedEntities.size();
@ -81,7 +81,7 @@ void SafeLanding::addTrackedEntity(const EntityItemID& entityID) {
_maxTrackedEntityCount = trackedEntityCount;
_trackedEntityStabilityCount = 0;
}
qCDebug(interfaceapp) << "Safe Landing: Tracking entity " << entity->getItemName();
//qCDebug(interfaceapp) << "Safe Landing: Tracking entity " << entity->getItemName();
}
}
}

View file

@ -172,7 +172,10 @@ void LaserPointer::RenderState::update(const glm::vec3& origin, const glm::vec3&
properties.setVisible(true);
properties.setIgnorePickIntersection(doesPathIgnorePicks());
QVector<float> widths;
widths.append(getLineWidth() * parentScale);
float width = getLineWidth() * parentScale;
widths.append(width);
widths.append(width);
properties.setStrokeWidths(widths);
DependencyManager::get<EntityScriptingInterface>()->editEntity(getPathID(), properties);
}
}

View file

@ -71,6 +71,18 @@ PickFilter getPickFilter(unsigned int filter) {
unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) {
QVariantMap propMap = properties.toMap();
#if defined (Q_OS_ANDROID)
QString jointName { "" };
if (propMap["joint"].isValid()) {
QString jointName = propMap["joint"].toString();
const QString MOUSE_JOINT = "Mouse";
if (jointName == MOUSE_JOINT) {
return PointerEvent::INVALID_POINTER_ID;
}
}
#endif
bool enabled = false;
if (propMap["enabled"].isValid()) {
enabled = propMap["enabled"].toBool();

View file

@ -150,6 +150,17 @@ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties)
unsigned int PointerScriptingInterface::createLaserPointer(const QVariant& properties) const {
QVariantMap propertyMap = properties.toMap();
#if defined (Q_OS_ANDROID)
QString jointName { "" };
if (propertyMap["joint"].isValid()) {
QString jointName = propertyMap["joint"].toString();
const QString MOUSE_JOINT = "Mouse";
if (jointName == MOUSE_JOINT) {
return PointerEvent::INVALID_POINTER_ID;
}
}
#endif
bool faceAvatar = false;
if (propertyMap["faceAvatar"].isValid()) {
faceAvatar = propertyMap["faceAvatar"].toBool();

View file

@ -89,6 +89,8 @@ glm::vec3 RayPick::intersectRayWithEntityXYPlane(const QUuid& entityID, const gl
return intersectRayWithXYPlane(origin, direction, props.getPosition(), props.getRotation(), props.getRegistrationPoint());
}
glm::vec2 RayPick::projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized) {
glm::quat invRot = glm::inverse(rotation);
glm::vec3 localPos = invRot * (worldPos - position);
@ -102,6 +104,19 @@ glm::vec2 RayPick::projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3
return pos2D;
}
glm::vec2 RayPick::projectOntoXZPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized) {
glm::quat invRot = glm::inverse(rotation);
glm::vec3 localPos = invRot * (worldPos - position);
glm::vec3 normalizedPos = (localPos / dimensions) + registrationPoint;
glm::vec2 pos2D = glm::vec2(normalizedPos.x, (1.0f - normalizedPos.z));
if (unNormalized) {
pos2D *= glm::vec2(dimensions.x, dimensions.z);
}
return pos2D;
}
glm::vec2 RayPick::projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized) {
EntityPropertyFlags desiredProperties;
desiredProperties += PROP_POSITION;

View file

@ -85,9 +85,11 @@ public:
static glm::vec3 intersectRayWithEntityXYPlane(const QUuid& entityID, const glm::vec3& origin, const glm::vec3& direction);
static glm::vec2 projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized = true);
static glm::vec2 projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized);
static glm::vec2 projectOntoXZPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNoemalized);
private:
static glm::vec3 intersectRayWithXYPlane(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& point, const glm::quat& rotation, const glm::vec3& registration);
static glm::vec2 projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized);
};
#endif // hifi_RayPick_h

View file

@ -137,13 +137,14 @@ PickResultPointer StylusPick::getDefaultResult(const QVariantMap& pickVariant) c
}
PickResultPointer StylusPick::getEntityIntersection(const StylusTip& pick) {
std::vector<StylusPickResult> results;
auto entityTree = qApp->getEntities()->getTree();
StylusPickResult nearestTarget(pick.toVariantMap());
for (const auto& target : getIncludeItems()) {
if (target.isNull()) {
continue;
}
auto entity = qApp->getEntities()->getTree()->findEntityByEntityItemID(target);
auto entity = entityTree->findEntityByEntityItemID(target);
if (!entity) {
continue;
}
@ -154,31 +155,45 @@ PickResultPointer StylusPick::getEntityIntersection(const StylusTip& pick) {
const auto entityRotation = entity->getWorldOrientation();
const auto entityPosition = entity->getWorldPosition();
const auto entityType = entity->getType();
glm::vec3 normal;
glm::vec3 normal = entityRotation * Vectors::UNIT_Z;
// TODO: Use the xz projection method for Sphere and Quad.
if (entityType == EntityTypes::Gizmo) {
normal = entityRotation * Vectors::UNIT_Y;
} else {
normal = entityRotation * Vectors::UNIT_Z;
}
float distance = glm::dot(pick.position - entityPosition, normal);
glm::vec3 intersection = pick.position - (normal * distance);
if (distance < nearestTarget.distance) {
const auto entityDimensions = entity->getScaledDimensions();
const auto entityRegistrationPoint = entity->getRegistrationPoint();
glm::vec3 intersection = pick.position - (normal * distance);
glm::vec2 pos2D;
glm::vec2 pos2D = RayPick::projectOntoEntityXYPlane(target, intersection, false);
if (pos2D == glm::clamp(pos2D, glm::vec2(0), glm::vec2(1))) {
IntersectionType type = IntersectionType::ENTITY;
if (getFilter().doesPickLocalEntities()) {
EntityPropertyFlags desiredProperties;
desiredProperties += PROP_ENTITY_HOST_TYPE;
if (DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(target, desiredProperties).getEntityHostType() == entity::HostType::LOCAL) {
type = IntersectionType::LOCAL_ENTITY;
}
auto entityType = entity->getType();
if (entityType == EntityTypes::Gizmo) {
pos2D = RayPick::projectOntoXZPlane(intersection, entityPosition, entityRotation,
entityDimensions, entityRegistrationPoint, false);
} else {
pos2D = RayPick::projectOntoXYPlane(intersection, entityPosition, entityRotation,
entityDimensions, entityRegistrationPoint, false);
}
if (pos2D == glm::clamp(pos2D, glm::vec2(0), glm::vec2(1))) {
IntersectionType type = IntersectionType::ENTITY;
if (getFilter().doesPickLocalEntities()) {
if (entity->getEntityHostType() == entity::HostType::LOCAL) {
type = IntersectionType::LOCAL_ENTITY;
}
}
nearestTarget = StylusPickResult(type, target, distance, intersection, pick, normal);
}
results.push_back(StylusPickResult(type, target, distance, intersection, pick, normal));
}
}
StylusPickResult nearestTarget(pick.toVariantMap());
for (const auto& result : results) {
if (result.distance < nearestTarget.distance) {
nearestTarget = result;
}
}
return std::make_shared<StylusPickResult>(nearestTarget);
}

View file

@ -115,8 +115,8 @@ DownloadInfoResult::DownloadInfoResult() :
/**jsdoc
* Information on the assets currently being downloaded and pending download.
* @typedef {object} AccountServices.DownloadInfoResult
* @property {number[]} downloading - The percentage complete for each asset currently being downloaded.
* @property {number} pending - The number of assets waiting to be download.
* @property {number[]} downloading - The download percentage remaining of each asset currently downloading.
* @property {number} pending - The number of assets pending download.
*/
QScriptValue DownloadInfoResultToScriptValue(QScriptEngine* engine, const DownloadInfoResult& result) {
QScriptValue object = engine->newObject();

View file

@ -38,19 +38,19 @@ class AccountServicesScriptingInterface : public QObject {
Q_OBJECT
/**jsdoc
* The <code>AccountServices</code> API provides functions related to user connectivity, visibility, and asset download
* progress.
* The <code>AccountServices</code> API provides functions that give information on user connectivity, visibility, and
* asset download progress.
*
* @hifi-interface
* @hifi-client-entity
* @hifi-avatar
*
* @namespace AccountServices
* @property {string} username - The user name if the user is logged in, otherwise <code>"Unknown user"</code>.
* <em>Read-only.</em>
* @property {string} username - The user name of the user logged in. If there is no user logged in, it is
* <code>"Unknown user"</code>. <em>Read-only.</em>
* @property {boolean} loggedIn - <code>true</code> if the user is logged in, otherwise <code>false</code>.
* <em>Read-only.</em>
* @property {string} findableBy - The user's visibility to other people:<br />
* @property {string} findableBy - The user's visibility to other users:<br />
* <code>"none"</code> - user appears offline.<br />
* <code>"friends"</code> - user is visible only to friends.<br />
* <code>"connections"</code> - user is visible to friends and connections.<br />
@ -74,23 +74,23 @@ public:
public slots:
/**jsdoc
* Get information on the progress of downloading assets in the domain.
* Gets information on the download progress of assets in the domain.
* @function AccountServices.getDownloadInfo
* @returns {AccountServices.DownloadInfoResult} Information on the progress of assets download.
* @returns {AccountServices.DownloadInfoResult} Information on the download progress of assets.
*/
DownloadInfoResult getDownloadInfo();
/**jsdoc
* Cause a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal to be triggered with information on the
* current progress of the download of assets in the domain.
* Triggers a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal with information on the current
* download progress of the assets in the domain.
* @function AccountServices.updateDownloadInfo
*/
void updateDownloadInfo();
/**jsdoc
* Check whether the user is logged in.
* Checks whether the user is logged in.
* @function AccountServices.isLoggedIn
* @returns {boolean} <code>true</code> if the user is logged in, <code>false</code> otherwise.
* @returns {boolean} <code>true</code> if the user is logged in, <code>false</code> if not.
* @example <caption>Report whether you are logged in.</caption>
* var isLoggedIn = AccountServices.isLoggedIn();
* print("You are logged in: " + isLoggedIn); // true or false
@ -98,9 +98,9 @@ public slots:
bool isLoggedIn();
/**jsdoc
* Prompts the user to log in (the login dialog is displayed) if they're not already logged in.
* The function returns the login status of the user and prompts the user to log in (with a login dialog) if they're not already logged in.
* @function AccountServices.checkAndSignalForAccessToken
* @returns {boolean} <code>true</code> if the user is already logged in, <code>false</code> otherwise.
* @returns {boolean} <code>true</code> if the user is logged in, <code>false</code> if not.
*/
bool checkAndSignalForAccessToken();
@ -140,7 +140,7 @@ signals:
/**jsdoc
* Triggered when the username logged in with changes, i.e., when the user logs in or out.
* @function AccountServices.myUsernameChanged
* @param {string} username - The username logged in with if the user is logged in, otherwise <code>""</code>.
* @param {string} username - The user name of the user logged in. If there is no user logged in, it is <code>""</code>.
* @returns {Signal}
* @example <caption>Report when your username changes.</caption>
* AccountServices.myUsernameChanged.connect(function (username) {
@ -150,9 +150,9 @@ signals:
void myUsernameChanged(const QString& username);
/**jsdoc
* Triggered when the progress of the download of assets for the domain changes.
* Triggered when the download progress of the assets in the domain changes.
* @function AccountServices.downloadInfoChanged
* @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the progress of assets download.
* @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the download progress of assets.
* @returns {Signal}
*/
void downloadInfoChanged(DownloadInfoResult info);
@ -186,7 +186,7 @@ signals:
/**jsdoc
* Triggered when the login status of the user changes.
* @function AccountServices.loggedInChanged
* @param {boolean} loggedIn - <code>true</code> if the user is logged in, otherwise <code>false</code>.
* @param {boolean} loggedIn - <code>true</code> if the user is logged in, <code>false</code> if not.
* @returns {Signal}
* @example <caption>Report when your login status changes.</caption>
* AccountServices.loggedInChanged.connect(function(loggedIn) {

View file

@ -27,8 +27,9 @@ QString Audio::HMD { "VR" };
Setting::Handle<bool> enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true };
float Audio::loudnessToLevel(float loudness) {
float level = 6.02059991f * fastLog2f(loudness); // level in dBFS
level = (level + 48.0f) * (1/39.0f); // map [-48, -9] dBFS to [0, 1]
float level = loudness * (1/32768.0f); // level in [0, 1]
level = 6.02059991f * fastLog2f(level); // convert to dBFS
level = (level + 48.0f) * (1/42.0f); // map [-48, -6] dBFS to [0, 1]
return glm::clamp(level, 0.0f, 1.0f);
}

View file

@ -41,8 +41,8 @@ public:
*
* @property {WalletScriptingInterface.WalletStatus} walletStatus - The status of the user's wallet. <em>Read-only.</em>
* @property {boolean} limitedCommerce - <code>true</code> if Interface is running in limited commerce mode. In limited commerce
* mode, certain Interface functionality is disabled, e.g., users can't buy non-free items from the Marketplace. The Oculus
* Store version of Interface runs in limited commerce mode. <em>Read-only.</em>
* mode, certain Interface functionalities are disabled, e.g., users can't buy items that are not free from the Marketplace.
* The Oculus Store version of Interface runs in limited commerce mode. <em>Read-only.</em>
*/
class WalletScriptingInterface : public QObject, public Dependency {
Q_OBJECT
@ -55,16 +55,16 @@ public:
WalletScriptingInterface();
/**jsdoc
* Check and update the user's wallet status.
* Checks and updates the user's wallet status.
* @function WalletScriptingInterface.refreshWalletStatus
*/
Q_INVOKABLE void refreshWalletStatus();
/**jsdoc
* Get the current status of the user's wallet.
* Gets the current status of the user's wallet.
* @function WalletScriptingInterface.getWalletStatus
* @returns {WalletScriptingInterface.WalletStatus}
* @example <caption>Two ways to report your wallet status.</caption>
* @example <caption>Use two methods to report your wallet's status.</caption>
* print("Wallet status: " + WalletScriptingInterface.walletStatus); // Same value as next line.
* print("Wallet status: " + WalletScriptingInterface.getWalletStatus());
*/
@ -74,11 +74,11 @@ public:
* Check that a certified avatar entity is owned by the avatar whose entity it is. The result of the check is provided via
* the {@link WalletScriptingInterface.ownershipVerificationSuccess|ownershipVerificationSuccess} and
* {@link WalletScriptingInterface.ownershipVerificationFailed|ownershipVerificationFailed} signals.<br />
* <strong>Warning:</strong> Neither of these signals fire if the entity is not an avatar entity or it's not a certified
* entity.
* <strong>Warning:</strong> Neither of these signals are triggered if the entity is not an avatar entity or is not
* certified.
* @function WalletScriptingInterface.proveAvatarEntityOwnershipVerification
* @param {Uuid} entityID - The ID of the avatar entity to check.
* @example <caption>Check ownership of all nearby certified avatar entities.</caption>
* @param {Uuid} entityID - The avatar entity's ID.
* @example <caption>Check the ownership of all nearby certified avatar entities.</caption>
* // Set up response handling.
* function ownershipSuccess(entityID) {
* print("Ownership test succeeded for: " + entityID);
@ -118,7 +118,7 @@ public:
signals:
/**jsdoc
* Triggered when the status of the user's wallet changes.
* Triggered when the user's wallet status changes.
* @function WalletScriptingInterface.walletStatusChanged
* @returns {Signal}
* @example <caption>Report when your wallet status changes, e.g., when you log in and out.</caption>
@ -136,7 +136,7 @@ signals:
void limitedCommerceChanged();
/**jsdoc
* Triggered when the user rezzes a certified entity but the user's wallet is not ready and so the certified location of the
* Triggered when the user rezzes a certified entity but the user's wallet is not ready. So the certified location of the
* entity cannot be updated in the metaverse.
* @function WalletScriptingInterface.walletNotSetup
* @returns {Signal}

Some files were not shown because too many files have changed in this diff Show more