diff --git a/CMakeLists.txt b/CMakeLists.txt index d1c69e4f6b..9e95974383 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,8 @@ if (NOT ANDROID) set_target_properties(ice-server PROPERTIES FOLDER "Apps") add_subdirectory(interface) set_target_properties(interface PROPERTIES FOLDER "Apps") + add_subdirectory(stack-manager) + set_target_properties(stack-manager PROPERTIES FOLDER "Apps") add_subdirectory(tests) add_subdirectory(plugins) add_subdirectory(tools) diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 830164eb60..58f200b0fe 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -10,5 +10,4 @@ link_hifi_libraries( ) include_application_version() - -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 5cdceb06ae..de9d7e2453 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -51,6 +52,14 @@ Agent::Agent(NLPacket& packet) : { DependencyManager::get()->setPacketSender(&_entityEditSender); + auto assetClient = DependencyManager::set(); + + QThread* assetThread = new QThread; + assetThread->setObjectName("Asset Thread"); + assetClient->moveToThread(assetThread); + connect(assetThread, &QThread::started, assetClient.data(), &AssetClient::init); + assetThread->start(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -133,7 +142,7 @@ void Agent::run() { messagesThread->start(); nodeList->addSetOfNodeTypesToNodeInterestSet({ - NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer + NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::EntityServer, NodeType::MessagesMixer, NodeType::AssetServer }); } @@ -448,4 +457,10 @@ void Agent::aboutToFinish() { // our entity tree is going to go away so tell that to the EntityScriptingInterface DependencyManager::get()->setEntityTree(NULL); + + // cleanup the AssetClient thread + QThread* assetThread = DependencyManager::get()->thread(); + DependencyManager::destroy(); + assetThread->quit(); + assetThread->wait(); } diff --git a/cmake/externals/quazip/CMakeLists.txt b/cmake/externals/quazip/CMakeLists.txt new file mode 100644 index 0000000000..db0e218835 --- /dev/null +++ b/cmake/externals/quazip/CMakeLists.txt @@ -0,0 +1,43 @@ +set(EXTERNAL_NAME quazip) +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) +cmake_policy(SET CMP0046 OLD) + +include(ExternalProject) +ExternalProject_Add( + ${EXTERNAL_NAME} + URL http://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.6.2.zip + URL_MD5 514851970f1a14d815bdc3ad6267af4d + BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -DCMAKE_PREFIX_PATH=$ENV{QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=/lib -DZLIB_ROOT=${ZLIB_ROOT} + LOG_DOWNLOAD 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 +) + +add_dependencies(quazip zlib) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES + FOLDER "hidden/externals" + INSTALL_NAME_DIR ${INSTALL_DIR}/lib + BUILD_WITH_INSTALL_RPATH True) + +ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIR ${INSTALL_DIR}/include CACHE PATH "List of QuaZip include directories") +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIR} CACHE PATH "List of QuaZip include directories") +set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/lib CACHE FILEPATH "Location of QuaZip DLL") + +if (APPLE) + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.1.0.0.dylib CACHE FILEPATH "Location of QuaZip release library") +elseif (WIN32) + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/quazip.lib CACHE FILEPATH "Location of QuaZip release library") +else () + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip.so CACHE FILEPATH "Location of QuaZip release library") +endif () + +include(SelectLibraryConfigurations) +select_library_configurations(${EXTERNAL_NAME_UPPER}) + +# Force selected libraries into the cache +set(${EXTERNAL_NAME_UPPER}_LIBRARY ${${EXTERNAL_NAME_UPPER}_LIBRARY} CACHE FILEPATH "Location of QuaZip libraries") +set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries") diff --git a/cmake/externals/zlib/CMakeLists.txt b/cmake/externals/zlib/CMakeLists.txt index 0d279cc469..06b6b564ba 100644 --- a/cmake/externals/zlib/CMakeLists.txt +++ b/cmake/externals/zlib/CMakeLists.txt @@ -4,19 +4,20 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) include(ExternalProject) ExternalProject_Add( -${EXTERNAL_NAME} -URL http://zlib.net/zlib128.zip -CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= -BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build -LOG_DOWNLOAD 1 -LOG_CONFIGURE 1 -LOG_BUILD 1 + ${EXTERNAL_NAME} + URL http://zlib.net/zlib128.zip + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= + BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build + LOG_DOWNLOAD 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 ) # Hide this external target (for ide users) set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) +set(${EXTERNAL_NAME_UPPER}_ROOT ${INSTALL_DIR} CACHE PATH "Path for Zlib install root") set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIR ${INSTALL_DIR}/include CACHE PATH "List of zlib include directories") set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${${EXTERNAL_NAME_UPPER}_INCLUDE_DIR} CACHE PATH "List of zlib include directories") set(${EXTERNAL_NAME_UPPER}_DLL_PATH ${INSTALL_DIR}/bin CACHE FILEPATH "Location of ZLib DLL") diff --git a/cmake/macros/CopyDllsBesideWindowsExecutable.cmake b/cmake/macros/CopyDllsBesideWindowsExecutable.cmake index 69fd20a57b..9330515a62 100644 --- a/cmake/macros/CopyDllsBesideWindowsExecutable.cmake +++ b/cmake/macros/CopyDllsBesideWindowsExecutable.cmake @@ -9,7 +9,7 @@ # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html # -macro(COPY_DLLS_BESIDE_WINDOWS_EXECUTABLE) +macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT) if (WIN32) configure_file( @@ -18,11 +18,7 @@ macro(COPY_DLLS_BESIDE_WINDOWS_EXECUTABLE) @ONLY ) - if (APPLE) - set(PLUGIN_PATH "interface.app/Contents/MacOS/plugins") - else() - set(PLUGIN_PATH "plugins") - endif() + set(PLUGIN_PATH "plugins") # add a post-build command to copy DLLs beside the executable add_custom_command( @@ -46,5 +42,18 @@ macro(COPY_DLLS_BESIDE_WINDOWS_EXECUTABLE) POST_BUILD COMMAND CMD /C "SET PATH=%PATH%;${QT_DIR}/bin && ${WINDEPLOYQT_COMMAND} $<$,$,$>:--release> $" ) + elseif (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) + find_program(MACDEPLOYQT_COMMAND macdeployqt PATHS ${QT_DIR}/bin NO_DEFAULT_PATH) + + if (NOT MACDEPLOYQT_COMMAND) + message(FATAL_ERROR "Could not find macdeployqt at ${QT_DIR}/bin. macdeployqt is required.") + endif () + + # add a post-build command to call macdeployqt to copy Qt plugins + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND ${MACDEPLOYQT_COMMAND} ${CMAKE_CURRENT_BINARY_DIR}/\${CONFIGURATION}/${TARGET_NAME}.app -verbose 0 + ) endif () -endmacro() \ No newline at end of file +endmacro() diff --git a/cmake/macros/SetupHifiProject.cmake b/cmake/macros/SetupHifiProject.cmake index 166a3fd4b9..79ca0122c9 100644 --- a/cmake/macros/SetupHifiProject.cmake +++ b/cmake/macros/SetupHifiProject.cmake @@ -22,8 +22,11 @@ macro(SETUP_HIFI_PROJECT) endif () endforeach() - # add the executable, include additional optional sources - add_executable(${TARGET_NAME} ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) + if (DEFINED BUILD_BUNDLE AND BUILD_BUNDLE AND APPLE) + add_executable(${TARGET_NAME} MACOSX_BUNDLE ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) + else () + add_executable(${TARGET_NAME} ${TARGET_SRCS} ${AUTOMTC_SRC} ${AUTOSCRIBE_SHADER_LIB_SRC}) + endif() set(${TARGET_NAME}_DEPENDENCY_QT_MODULES ${ARGN}) list(APPEND ${TARGET_NAME}_DEPENDENCY_QT_MODULES Core) diff --git a/cmake/macros/TargetQuazip.cmake b/cmake/macros/TargetQuazip.cmake new file mode 100644 index 0000000000..0536a1a9d8 --- /dev/null +++ b/cmake/macros/TargetQuazip.cmake @@ -0,0 +1,16 @@ +# +# Copyright 2015 High Fidelity, Inc. +# Created by Leonardo Murillo on 2015/11/20 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_QUAZIP) + add_dependency_external_projects(quazip) + find_package(QuaZip REQUIRED) + target_include_directories(${TARGET_NAME} PUBLIC ${QUAZIP_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) + if (WIN32) + add_paths_to_fixup_libs(${QUAZIP_DLL_PATH}) + endif () +endmacro() diff --git a/cmake/modules/FindQuaZip.cmake b/cmake/modules/FindQuaZip.cmake new file mode 100644 index 0000000000..110f239c68 --- /dev/null +++ b/cmake/modules/FindQuaZip.cmake @@ -0,0 +1,29 @@ +# +# FindQuaZip.h +# StackManagerQt/cmake/modules +# +# Created by Mohammed Nafees. +# Copyright (c) 2014 High Fidelity. All rights reserved. +# + +# QUAZIP_FOUND - QuaZip library was found +# QUAZIP_INCLUDE_DIR - Path to QuaZip include dir +# QUAZIP_INCLUDE_DIRS - Path to QuaZip and zlib include dir (combined from QUAZIP_INCLUDE_DIR + ZLIB_INCLUDE_DIR) +# QUAZIP_LIBRARIES - List of QuaZip libraries +# QUAZIP_ZLIB_INCLUDE_DIR - The include dir of zlib headers + +include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("quazip") + +if (WIN32) + find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) +elseif (APPLE) + find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES include/quazip HINTS ${QUAZIP_SEARCH_DIRS}) +else () + find_path(QUAZIP_INCLUDE_DIRS quazip.h PATH_SUFFIXES quazip HINTS ${QUAZIP_SEARCH_DIRS}) +endif () + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(QUAZIP DEFAULT_MSG QUAZIP_INCLUDE_DIRS) + +mark_as_advanced(QUAZIP_INCLUDE_DIRS QUAZIP_SEARCH_DIRS) diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index ce683df698..1f9280a899 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -37,4 +37,4 @@ if (UNIX) endif (UNIX) include_application_version() -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/examples/entityScripts/recordingEntityScript.js b/examples/entityScripts/recordingEntityScript.js index e423de9afd..0694ff431e 100644 --- a/examples/entityScripts/recordingEntityScript.js +++ b/examples/entityScripts/recordingEntityScript.js @@ -16,24 +16,39 @@ var _this; var isAvatarRecording = false; - var channel = "groupRecordingChannel"; - var startMessage = "RECONDING STARTED"; - var stopMessage = "RECONDING ENDED"; + var MASTER_TO_CLIENTS_CHANNEL = "startStopChannel"; + var CLIENTS_TO_MASTER_CHANNEL = "resultsChannel"; + var START_MESSAGE = "recordingStarted"; + var STOP_MESSAGE = "recordingEnded"; + var PARTICIPATING_MESSAGE = "participatingToRecording"; + var RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-active.svg"; + var NOT_RECORDING_ICON_URL = "http://cdn.highfidelity.com/alan/production/icons/ICO_rec-inactive.svg"; + var ICON_WIDTH = 60; + var ICON_HEIGHT = 60; + var overlay = null; + function recordingEntity() { _this = this; return; - } + }; function receivingMessage(channel, message, senderID) { - print("message received on channel:" + channel + ", message:" + message + ", senderID:" + senderID); - if(message === startMessage) { - _this.startRecording(); - } else if(message === stopMessage) { - _this.stopRecording(); + if (channel === MASTER_TO_CLIENTS_CHANNEL) { + print("CLIENT received message:" + message); + if (message === START_MESSAGE) { + _this.startRecording(); + } else if (message === STOP_MESSAGE) { + _this.stopRecording(); + } } }; + function getClipUrl(url) { + Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, url); //send back the url to the master + print("clip uploaded and url sent to master"); + }; + recordingEntity.prototype = { preload: function (entityID) { @@ -50,21 +65,32 @@ enterEntity: function (entityID) { print("entering in the recording area"); - Messages.subscribe(channel); - + Messages.subscribe(MASTER_TO_CLIENTS_CHANNEL); + overlay = Overlays.addOverlay("image", { + imageURL: NOT_RECORDING_ICON_URL, + width: ICON_HEIGHT, + height: ICON_WIDTH, + x: 275, + y: 0, + visible: true + }); }, leaveEntity: function (entityID) { print("leaving the recording area"); _this.stopRecording(); - Messages.unsubscribe(channel); + Messages.unsubscribe(MASTER_TO_CLIENTS_CHANNEL); + Overlays.deleteOverlay(overlay); + overlay = null; }, startRecording: function (entityID) { if (!isAvatarRecording) { print("RECORDING STARTED"); + Messages.sendMessage(CLIENTS_TO_MASTER_CHANNEL, PARTICIPATING_MESSAGE); //tell to master that I'm participating Recording.startRecording(); isAvatarRecording = true; + Overlays.editOverlay(overlay, {imageURL: RECORDING_ICON_URL}); } }, @@ -73,18 +99,20 @@ print("RECORDING ENDED"); Recording.stopRecording(); isAvatarRecording = false; - recordingFile = Window.save("Save recording to file", "./groupRecording", "Recordings (*.hfr)"); - if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { - Recording.saveRecording(recordingFile); - } + Recording.saveRecordingToAsset(getClipUrl); //save the clip to the asset and link a callback to get its url + Overlays.editOverlay(overlay, {imageURL: NOT_RECORDING_ICON_URL}); } }, unload: function (entityID) { print("RECORDING ENTITY UNLOAD"); _this.stopRecording(); - Messages.unsubscribe(channel); + Messages.unsubscribe(MASTER_TO_CLIENTS_CHANNEL); Messages.messageReceived.disconnect(receivingMessage); + if(overlay !== null){ + Overlays.deleteOverlay(overlay); + overlay = null; + } } } diff --git a/examples/entityScripts/recordingMaster.js b/examples/entityScripts/recordingMaster.js index 27f84f44c2..51149991c2 100644 --- a/examples/entityScripts/recordingMaster.js +++ b/examples/entityScripts/recordingMaster.js @@ -22,11 +22,24 @@ var TOOL_ICON_URL = HIFI_PUBLIC_BUCKET + "images/tools/"; var ALPHA_ON = 1.0; var ALPHA_OFF = 0.7; var COLOR_TOOL_BAR = { red: 0, green: 0, blue: 0 }; +var MASTER_TO_CLIENTS_CHANNEL = "startStopChannel"; +var CLIENTS_TO_MASTER_CHANNEL = "resultsChannel"; +var START_MESSAGE = "recordingStarted"; +var STOP_MESSAGE = "recordingEnded"; +var PARTICIPATING_MESSAGE = "participatingToRecording"; +var TIMEOUT = 20; var toolBar = null; var recordIcon; var isRecording = false; -var channel = "groupRecordingChannel"; +var performanceJSON = { "avatarClips" : [] }; +var responsesExpected = 0; +var waitingForPerformanceFile = true; +var totalWaitingTime = 0; +var extension = "txt"; + + +Messages.subscribe(CLIENTS_TO_MASTER_CHANNEL); setupToolBar(); function setupToolBar() { @@ -49,28 +62,82 @@ function setupToolBar() { visible: true, }, true, isRecording); } +toolBar.selectTool(recordIcon, !isRecording); function mousePressEvent(event) { clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); if (recordIcon === toolBar.clicked(clickedOverlay, false)) { if (!isRecording) { print("I'm the master. I want to start recording"); - var message = "RECONDING STARTED"; - Messages.sendMessage(channel, message); + Messages.sendMessage(MASTER_TO_CLIENTS_CHANNEL, START_MESSAGE); isRecording = true; } else { print("I want to stop recording"); - var message = "RECONDING ENDED"; - Messages.sendMessage(channel, message); + waitingForPerformanceFile = true; + Script.update.connect(update); + Messages.sendMessage(MASTER_TO_CLIENTS_CHANNEL, STOP_MESSAGE); isRecording = false; } + toolBar.selectTool(recordIcon, !isRecording); + } +} + +function masterReceivingMessage(channel, message, senderID) { + if (channel === CLIENTS_TO_MASTER_CHANNEL) { + print("MASTER received message:" + message ); + if (message === PARTICIPATING_MESSAGE) { + //increment the counter of all the participants + responsesExpected++; + } else if (waitingForPerformanceFile) { + //I get an atp url from one participant + performanceJSON.avatarClips[performanceJSON.avatarClips.length] = message; + } } } +function update(deltaTime) { + if (waitingForPerformanceFile) { + totalWaitingTime += deltaTime; + if (totalWaitingTime > TIMEOUT || performanceJSON.avatarClips.length === responsesExpected) { + if (performanceJSON.avatarClips.length !== 0) { + print("UPLOADING PERFORMANCE FILE"); + //I can upload the performance file on the asset + Assets.uploadData(JSON.stringify(performanceJSON), extension, uploadFinished); + } else { + print("PERFORMANCE FILE EMPTY"); + } + //clean things after upload performance file to asset + waitingForPerformanceFile = false; + responsesExpected = 0; + totalWaitingTime = 0; + Script.update.disconnect(update); + performanceJSON = { "avatarClips" : [] }; + } + } +} + +function uploadFinished(url){ + //need to print somehow the url here this way the master can copy the url + print("PERFORMANCE FILE URL: " + url); + Assets.downloadData(url, function (data) { + printPerformanceJSON(JSON.parse(data)); + }); +} + +function printPerformanceJSON(obj) { + print("some info:"); + print("downloaded performance file from asset and examinating its content..."); + var avatarClips = obj.avatarClips; + avatarClips.forEach(function(param) { + print("clip url obtained: " + param); + }); +} + function cleanup() { toolBar.cleanup(); - Messages.unsubscribe(channel); + Messages.unsubscribe(CLIENTS_TO_MASTER_CHANNEL); } Script.scriptEnding.connect(cleanup); Controller.mousePressEvent.connect(mousePressEvent); +Messages.messageReceived.connect(masterReceivingMessage); \ No newline at end of file diff --git a/examples/painting/whiteboard/whiteboardEntityScript.js b/examples/painting/whiteboard/whiteboardEntityScript.js index d2b2c386ef..61d7291e11 100644 --- a/examples/painting/whiteboard/whiteboardEntityScript.js +++ b/examples/painting/whiteboard/whiteboardEntityScript.js @@ -20,7 +20,7 @@ var _this; var RIGHT_HAND = 1; var LEFT_HAND = 0; - var MIN_POINT_DISTANCE = 0.01 ; + var MIN_POINT_DISTANCE = 0.01; var MAX_POINT_DISTANCE = 0.5; var MAX_POINTS_PER_LINE = 40; var MAX_DISTANCE = 5; @@ -29,6 +29,11 @@ var MIN_STROKE_WIDTH = 0.0005; var MAX_STROKE_WIDTH = 0.03; + var TRIGGER_CONTROLS = [ + Controller.Standard.LT, + Controller.Standard.RT, + ]; + Whiteboard = function() { _this = this; }; @@ -51,11 +56,9 @@ if (this.hand === RIGHT_HAND) { this.getHandPosition = MyAvatar.getRightPalmPosition; this.getHandRotation = MyAvatar.getRightPalmRotation; - this.triggerAction = Controller.findAction("RIGHT_HAND_CLICK"); } else if (this.hand === LEFT_HAND) { this.getHandPosition = MyAvatar.getLeftPalmPosition; this.getHandRotation = MyAvatar.getLeftPalmRotation; - this.triggerAction = Controller.findAction("LEFT_HAND_CLICK"); } Overlays.editOverlay(this.laserPointer, { visible: true @@ -76,7 +79,7 @@ if (this.intersection.intersects) { var distance = Vec3.distance(handPosition, this.intersection.intersection); if (distance < MAX_DISTANCE) { - this.triggerValue = Controller.getActionValue(this.triggerAction); + this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[this.hand]); this.currentStrokeWidth = map(this.triggerValue, 0, 1, MIN_STROKE_WIDTH, MAX_STROKE_WIDTH); var displayPoint = this.intersection.intersection; displayPoint = Vec3.sum(displayPoint, Vec3.multiply(this.normal, 0.01)); @@ -184,7 +187,7 @@ }, stopFarTrigger: function() { - if(this.hand !== this.whichHand) { + if (this.hand !== this.whichHand) { return; } this.stopPainting(); @@ -209,7 +212,7 @@ entities.forEach(function(entity) { var props = Entities.getEntityProperties(entity, ["name, userData"]); var name = props.name; - if(!props.userData) { + if (!props.userData) { return; } var whiteboardID = JSON.parse(props.userData).whiteboard; diff --git a/ice-server/CMakeLists.txt b/ice-server/CMakeLists.txt index b056d79efd..cfec3c966c 100644 --- a/ice-server/CMakeLists.txt +++ b/ice-server/CMakeLists.txt @@ -5,5 +5,4 @@ setup_hifi_project(Network) # link the shared hifi libraries link_hifi_libraries(embedded-webserver networking shared) - -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 98a9dad909..3357b57858 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -201,4 +201,4 @@ else (APPLE) endif() endif (APPLE) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7143ece7c8..c71e78a533 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -679,7 +679,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : })); userInputMapper->registerDevice(_applicationStateDevice); - + // Setup the keyboardMouseDevice and the user input mapper with the default bindings userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); @@ -749,7 +749,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _oldHandRightClick[0] = false; _oldHandLeftClick[1] = false; _oldHandRightClick[1] = false; - + auto applicationUpdater = DependencyManager::get(); connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); applicationUpdater->checkForUpdate(); @@ -768,7 +768,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : // If the user clicks an an entity, we will check that it's an unlocked web entity, and if so, set the focus to it auto entityScriptingInterface = DependencyManager::get(); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity, + connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity, [this, entityScriptingInterface](const EntityItemID& entityItemID, const MouseEvent& event) { if (_keyboardFocusedItem != entityItemID) { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; @@ -817,7 +817,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : }); // If the user clicks somewhere where there is NO entity at all, we will release focus - connect(getEntities(), &EntityTreeRenderer::mousePressOffEntity, + connect(getEntities(), &EntityTreeRenderer::mousePressOffEntity, [=](const RayToEntityIntersectionResult& entityItemID, const QMouseEvent* event, unsigned int deviceId) { _keyboardFocusedItem = UNKNOWN_ENTITY_ID; if (_keyboardFocusHighlight) { @@ -826,17 +826,17 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : }); connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); - + qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); } void Application::aboutToQuit() { emit beforeAboutToQuit(); - + getActiveDisplayPlugin()->deactivate(); - + _aboutToQuit = true; - + cleanupBeforeQuit(); } @@ -860,16 +860,16 @@ void Application::cleanupBeforeQuit() { _keyboardFocusHighlight = nullptr; _entities.clear(); // this will allow entity scripts to properly shutdown - + auto nodeList = DependencyManager::get(); - + // send the domain a disconnect packet, force stoppage of domain-server check-ins nodeList->getDomainHandler().disconnect(); nodeList->setIsShuttingDown(true); - + // tell the packet receiver we're shutting down, so it can drop packets nodeList->getPacketReceiver().setShouldDropPackets(true); - + _entities.shutdown(); // tell the entities system we're shutting down, so it will stop running scripts ScriptEngine::stopAllScripts(this); // stop all currently running global scripts @@ -947,7 +947,7 @@ Application::~Application() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - + // cleanup the AssetClient thread QThread* assetThread = DependencyManager::get()->thread(); DependencyManager::destroy(); @@ -955,14 +955,14 @@ Application::~Application() { assetThread->wait(); QThread* nodeThread = DependencyManager::get()->thread(); - + // remove the NodeList from the DependencyManager DependencyManager::destroy(); // ask the node thread to quit and wait until it is done nodeThread->quit(); nodeThread->wait(); - + Leapmotion::destroy(); RealSense::destroy(); @@ -1058,7 +1058,7 @@ void Application::initializeUi() { resizeGL(); } }); - + // This will set up the input plugins UI _activeInputPlugins.clear(); foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { @@ -1100,8 +1100,8 @@ void Application::paintGL() { return; } - // Some plugins process message events, potentially leading to - // re-entering a paint event. don't allow further processing if this + // Some plugins process message events, potentially leading to + // re-entering a paint event. don't allow further processing if this // happens if (_inPaint) { return; @@ -1137,17 +1137,17 @@ void Application::paintGL() { if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { PerformanceTimer perfTimer("Mirror"); auto primaryFbo = DependencyManager::get()->getPrimaryFramebufferDepthColor(); - + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; renderRearViewMirror(&renderArgs, _mirrorViewRect); renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - + { float ratio = ((float)QApplication::desktop()->windowHandle()->devicePixelRatio() * getRenderResolutionScale()); // Flip the src and destination rect horizontally to do the mirror auto mirrorRect = glm::ivec4(0, 0, _mirrorViewRect.width() * ratio, _mirrorViewRect.height() * ratio); auto mirrorRectDest = glm::ivec4(mirrorRect.z, mirrorRect.y, mirrorRect.x, mirrorRect.w); - + auto selfieFbo = DependencyManager::get()->getSelfieFramebuffer(); gpu::doInBatch(renderArgs._context, [=](gpu::Batch& batch) { batch.setFramebuffer(selfieFbo); @@ -1169,9 +1169,9 @@ void Application::paintGL() { { PerformanceTimer perfTimer("CameraUpdates"); - + auto myAvatar = getMyAvatar(); - + myAvatar->startCapture(); if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); @@ -1208,26 +1208,26 @@ void Application::paintGL() { * (myAvatar->getScale() * myAvatar->getBoomLength() * glm::vec3(0.0f, 0.0f, 1.0f))); } else { _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getOrientation() + + myAvatar->getOrientation() * (myAvatar->getScale() * myAvatar->getBoomLength() * glm::vec3(0.0f, 0.0f, 1.0f))); } } } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { if (isHMDMode()) { glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setRotation(myAvatar->getWorldAlignedOrientation() + _myCamera.setRotation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)) * hmdRotation); glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getScale(), 0) + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getScale(), 0) + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))) * hmdOffset); } else { - _myCamera.setRotation(myAvatar->getWorldAlignedOrientation() + _myCamera.setRotation(myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getScale(), 0) + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getScale(), 0) + (myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror); } @@ -1246,7 +1246,7 @@ void Application::paintGL() { } } } - // Update camera position + // Update camera position if (!isHMDMode()) { _myCamera.update(1.0f / _fps); } @@ -1264,12 +1264,12 @@ void Application::paintGL() { if (displayPlugin->isStereo()) { // Stereo modes will typically have a larger projection matrix overall, // so we ask for the 'mono' projection matrix, which for stereo and HMD - // plugins will imply the combined projection for both eyes. + // plugins will imply the combined projection for both eyes. // // This is properly implemented for the Oculus plugins, but for OpenVR - // and Stereo displays I'm not sure how to get / calculate it, so we're - // just relying on the left FOV in each case and hoping that the - // overall culling margin of error doesn't cause popping in the + // and Stereo displays I'm not sure how to get / calculate it, so we're + // just relying on the left FOV in each case and hoping that the + // overall culling margin of error doesn't cause popping in the // right eye. There are FIXMEs in the relevant plugins _myCamera.setProjection(displayPlugin->getProjection(Mono, _myCamera.getProjection())); renderArgs._context->enableStereo(true); @@ -1279,11 +1279,11 @@ void Application::paintGL() { auto hmdInterface = DependencyManager::get(); float IPDScale = hmdInterface->getIPDScale(); // FIXME we probably don't need to set the projection matrix every frame, - // only when the display plugin changes (or in non-HMD modes when the user + // only when the display plugin changes (or in non-HMD modes when the user // changes the FOV manually, which right now I don't think they can. for_each_eye([&](Eye eye) { - // For providing the stereo eye views, the HMD head pose has already been - // applied to the avatar, so we need to get the difference between the head + // For providing the stereo eye views, the HMD head pose has already been + // applied to the avatar, so we need to get the difference between the head // pose applied to the avatar and the per eye pose, and use THAT as // the per-eye stereo matrix adjustment. mat4 eyeToHead = displayPlugin->getEyeToHeadTransform(eye); @@ -1293,10 +1293,10 @@ void Application::paintGL() { mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * IPDScale); eyeOffsets[eye] = eyeOffsetTransform; - // Tell the plugin what pose we're using to render. In this case we're just using the - // unmodified head pose because the only plugin that cares (the Oculus plugin) uses it - // for rotational timewarp. If we move to support positonal timewarp, we need to - // ensure this contains the full pose composed with the eye offsets. + // Tell the plugin what pose we're using to render. In this case we're just using the + // unmodified head pose because the only plugin that cares (the Oculus plugin) uses it + // for rotational timewarp. If we move to support positonal timewarp, we need to + // ensure this contains the full pose composed with the eye offsets. mat4 headPose = displayPlugin->getHeadPose(); displayPlugin->setEyeRenderPose(eye, headPose); @@ -1343,7 +1343,7 @@ void Application::paintGL() { PerformanceTimer perfTimer("pluginOutput"); auto primaryFbo = framebufferCache->getPrimaryFramebuffer(); GLuint finalTexture = gpu::GLBackend::getTextureID(primaryFbo->getRenderBuffer(0)); - // Ensure the rendering context commands are completed when rendering + // Ensure the rendering context commands are completed when rendering GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); // Ensure the sync object is flushed to the driver thread before releasing the context // CRITICAL for the mac driver apparently. @@ -1394,7 +1394,7 @@ void Application::audioMuteToggled() { } void Application::faceTrackerMuteToggled() { - + QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); Q_CHECK_PTR(muteAction); bool isMuted = getSelectedFaceTracker()->isMuted(); @@ -1427,7 +1427,7 @@ void Application::resizeGL() { if (nullptr == _displayPlugin) { return; } - + auto displayPlugin = getActiveDisplayPlugin(); // Set the desired FBO texture size. If it hasn't changed, this does nothing. // Otherwise, it must rebuild the FBOs @@ -1437,14 +1437,14 @@ void Application::resizeGL() { _renderResolution = renderSize; DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); } - + // FIXME the aspect ratio for stereo displays is incorrect based on this. float aspectRatio = displayPlugin->getRecommendedAspectRatio(); _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); // Possible change in aspect ratio loadViewFrustum(_myCamera, _viewFrustum); - + auto offscreenUi = DependencyManager::get(); auto uiSize = displayPlugin->getRecommendedUiSize(); // Bit of a hack since there's no device pixel ratio change event I can find. @@ -1613,7 +1613,7 @@ void Application::keyPressEvent(QKeyEvent* event) { if (isMeta) { auto offscreenUi = DependencyManager::get(); offscreenUi->load("Browser.qml"); - } + } break; case Qt::Key_X: @@ -2109,7 +2109,7 @@ void Application::wheelEvent(QWheelEvent* event) { if (_controllerScriptingInterface->isWheelCaptured()) { return; } - + if (Menu::getInstance()->isOptionChecked(KeyboardMouseDevice::NAME)) { _keyboardMouseDevice->wheelEvent(event); } @@ -2227,7 +2227,7 @@ void Application::idle(uint64_t now) { _idleLoopStdev.reset(); } } - + _overlayConductor.update(secondsSinceLastUpdate); // check for any requested background downloads. @@ -2481,7 +2481,7 @@ void Application::init() { DependencyManager::get()->loadSettings(addressLookupString); qCDebug(interfaceapp) << "Loaded settings"; - + Leapmotion::init(); RealSense::init(); @@ -2539,7 +2539,7 @@ void Application::setAvatarUpdateThreading(bool isThreaded) { if (_avatarUpdate && (_avatarUpdate->isThreaded() == isThreaded)) { return; } - + auto myAvatar = getMyAvatar(); if (_avatarUpdate) { _avatarUpdate->terminate(); // Must be before we shutdown anim graph. @@ -2750,7 +2750,7 @@ void Application::updateDialogs(float deltaTime) { if(audioStatsDialog) { audioStatsDialog->update(); } - + // Update bandwidth dialog, if any BandwidthDialog* bandwidthDialog = dialogsManager->getBandwidthDialog(); if (bandwidthDialog) { @@ -2828,14 +2828,8 @@ void Application::update(float deltaTime) { myAvatar->setDriveKeys(TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); myAvatar->setDriveKeys(TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); if (deltaTime > FLT_EPSILON) { - // For rotations what we really want are meausures of "angles per second" (in order to prevent - // fps-dependent spin rates) so we need to scale the units of the controller contribution. - // (TODO?: maybe we should similarly scale ALL action state info, or change the expected behavior - // controllers to provide a delta_per_second value rather than a raw delta.) - const float EXPECTED_FRAME_RATE = 60.0f; - float timeFactor = EXPECTED_FRAME_RATE * deltaTime; - myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH) / timeFactor); - myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW) / timeFactor); + myAvatar->setDriveKeys(PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKeys(YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); myAvatar->setDriveKeys(STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); } } @@ -2892,7 +2886,7 @@ void Application::update(float deltaTime) { _entities.getTree()->withWriteLock([&] { _physicsEngine->stepSimulation(); }); - + if (_physicsEngine->hasOutgoingChanges()) { _entities.getTree()->withWriteLock([&] { _entitySimulation.handleOutgoingChanges(_physicsEngine->getOutgoingChanges(), _physicsEngine->getSessionID()); @@ -3021,10 +3015,10 @@ int Application::sendNackPackets() { foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { nackPacketList->writePrimitive(missingNumber); } - + if (nackPacketList->getNumPackets()) { packetsSent += nackPacketList->getNumPackets(); - + // send the packet list nodeList->sendPacketList(std::move(nackPacketList), *node); } @@ -3632,7 +3626,7 @@ void Application::renderRearViewMirror(RenderArgs* renderArgs, const QRect& regi float fov = MIRROR_FIELD_OF_VIEW; auto myAvatar = getMyAvatar(); - + // bool eyeRelativeCamera = false; if (billboard) { fov = BILLBOARD_FIELD_OF_VIEW; // degees @@ -3840,7 +3834,7 @@ void Application::nodeKilled(SharedNodePointer node) { Menu::getInstance()->getActionForOption(MenuOption::UploadAsset)->setEnabled(false); } } - + void Application::trackIncomingOctreePacket(NLPacket& packet, SharedNodePointer sendingNode, bool wasStatsPacket) { // Attempt to identify the sender from its address. @@ -3865,7 +3859,7 @@ int Application::processOctreeStats(NLPacket& packet, SharedNodePointer sendingN int statsMessageLength = 0; const QUuid& nodeUUID = sendingNode->getUUID(); - + // now that we know the node ID, let's add these stats to the stats for that node... _octreeServerSceneStats.withWriteLock([&] { OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; @@ -4066,7 +4060,7 @@ bool Application::acceptURL(const QString& urlString, bool defaultUpload) { Qt::AutoConnection, Q_ARG(const QString&, urlString)); return true; } - + QUrl url(urlString); QHashIterator i(_acceptedExtensions); QString lowerPath = url.path().toLower(); @@ -4077,7 +4071,7 @@ bool Application::acceptURL(const QString& urlString, bool defaultUpload) { return (this->*method)(urlString); } } - + return defaultUpload && askToUploadAsset(urlString); } @@ -4159,10 +4153,10 @@ bool Application::askToUploadAsset(const QString& filename) { QString("You don't have upload rights on that domain.\n\n")); return false; } - + QUrl url { filename }; if (auto upload = DependencyManager::get()->createUpload(url.toLocalFile())) { - + QMessageBox messageBox; messageBox.setWindowTitle("Asset upload"); messageBox.setText("You are about to upload the following file to the asset server:\n" + @@ -4170,19 +4164,19 @@ bool Application::askToUploadAsset(const QString& filename) { messageBox.setInformativeText("Do you want to continue?"); messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); messageBox.setDefaultButton(QMessageBox::Ok); - + // Option to drop model in world for models if (filename.endsWith(FBX_EXTENSION) || filename.endsWith(OBJ_EXTENSION)) { auto checkBox = new QCheckBox(&messageBox); checkBox->setText("Add to scene"); messageBox.setCheckBox(checkBox); } - + if (messageBox.exec() != QMessageBox::Ok) { upload->deleteLater(); return false; } - + // connect to the finished signal so we know when the AssetUpload is done if (messageBox.checkBox() && (messageBox.checkBox()->checkState() == Qt::Checked)) { // Custom behavior for models @@ -4192,12 +4186,12 @@ bool Application::askToUploadAsset(const QString& filename) { &AssetUploadDialogFactory::getInstance(), &AssetUploadDialogFactory::handleUploadFinished); } - + // start the upload now upload->start(); return true; } - + // display a message box with the error QMessageBox::warning(_window, "Failed Upload", QString("Failed to upload %1.\n\n").arg(filename)); return false; @@ -4205,20 +4199,20 @@ bool Application::askToUploadAsset(const QString& filename) { void Application::modelUploadFinished(AssetUpload* upload, const QString& hash) { auto filename = QFileInfo(upload->getFilename()).fileName(); - + if ((upload->getError() == AssetUpload::NoError) && (filename.endsWith(FBX_EXTENSION) || filename.endsWith(OBJ_EXTENSION))) { - + auto entities = DependencyManager::get(); - + EntityItemProperties properties; properties.setType(EntityTypes::Model); properties.setModelURL(QString("%1:%2.%3").arg(URL_SCHEME_ATP).arg(hash).arg(upload->getExtension())); properties.setPosition(_myCamera.getPosition() + _myCamera.getOrientation() * Vectors::FRONT * 2.0f); properties.setName(QUrl(upload->getFilename()).fileName()); - + entities->addEntity(properties); - + upload->deleteLater(); } else { AssetUploadDialogFactory::getInstance().handleUploadFinished(upload, hash); @@ -4499,7 +4493,7 @@ void Application::takeSnapshot() { _snapshotShareDialog = new SnapshotShareDialog(fileName, _glWidget); } _snapshotShareDialog->show(); - + } float Application::getRenderResolutionScale() const { @@ -4744,8 +4738,8 @@ void Application::updateDisplayMode() { return; } - // Some plugins *cough* Oculus *cough* process message events from inside their - // display function, and we don't want to change the display plugin underneath + // Some plugins *cough* Oculus *cough* process message events from inside their + // display function, and we don't want to change the display plugin underneath // the paintGL call, so we need to guard against that if (_inPaint) { qDebug() << "Deferring plugin switch until out of painting"; @@ -4779,14 +4773,14 @@ void Application::updateDisplayMode() { oldDisplayPlugin = _displayPlugin; _displayPlugin = newDisplayPlugin; - + // If the displayPlugin is a screen based HMD, then it will want the HMDTools displayed // Direct Mode HMDs (like windows Oculus) will be isHmd() but will have a screen of -1 bool newPluginWantsHMDTools = newDisplayPlugin ? (newDisplayPlugin->isHmd() && (newDisplayPlugin->getHmdScreen() >= 0)) : false; - bool oldPluginWantedHMDTools = oldDisplayPlugin ? + bool oldPluginWantedHMDTools = oldDisplayPlugin ? (oldDisplayPlugin->isHmd() && (oldDisplayPlugin->getHmdScreen() >= 0)) : false; - + // Only show the hmd tools after the correct plugin has // been activated so that it's UI is setup correctly if (newPluginWantsHMDTools) { @@ -4796,7 +4790,7 @@ void Application::updateDisplayMode() { if (oldDisplayPlugin) { oldDisplayPlugin->deactivate(); _offscreenContext->makeCurrent(); - + // if the old plugin was HMD and the new plugin is not HMD, then hide our hmdtools if (oldPluginWantedHMDTools && !newPluginWantsHMDTools) { DependencyManager::get()->hmdTools(false); @@ -4929,7 +4923,7 @@ void Application::setPalmData(Hand* hand, const controller::Pose& pose, float de rawVelocity = glm::vec3(0.0f); } palm.setRawVelocity(rawVelocity); // meters/sec - + // Angular Velocity of Palm glm::quat deltaRotation = rotation * glm::inverse(palm.getRawRotation()); glm::vec3 angularVelocity(0.0f); @@ -5009,7 +5003,7 @@ void Application::emulateMouse(Hand* hand, float click, float shift, HandData::H pos.setY(canvasSize.y / 2.0f + cursorRange * yAngle); } - + //If we are off screen then we should stop processing, and if a trigger or bumper is pressed, //we should unpress them. if (pos.x() == INT_MAX) { diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 3c249a6685..99152bfb6c 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -40,7 +40,6 @@ #include "Menu.h" #include "ModelReferential.h" #include "Physics.h" -#include "Recorder.h" #include "Util.h" #include "world.h" #include "InterfaceLogging.h" diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 543c079d7a..e9aa064160 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -167,7 +167,7 @@ protected: QVector _attachmentModels; QVector _attachmentsToRemove; QVector _unusedAttachments; - float _bodyYawDelta; + float _bodyYawDelta; // degrees/sec // These position histories and derivatives are in the world-frame. // The derivatives are the MEASURED results of all external and internal forces diff --git a/interface/src/avatar/AvatarActionHold.cpp b/interface/src/avatar/AvatarActionHold.cpp index 8e13fa8385..dd96b3a3d9 100644 --- a/interface/src/avatar/AvatarActionHold.cpp +++ b/interface/src/avatar/AvatarActionHold.cpp @@ -327,4 +327,6 @@ void AvatarActionHold::deserialize(QByteArray serializedArguments) { _active = true; }); + + forceBodyNonStatic(); } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index ed004efcee..8306e10890 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -154,7 +154,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { QVector::iterator fadingIterator = _avatarFades.begin(); const float SHRINK_RATE = 0.9f; - const float MIN_FADE_SCALE = 0.001f; + const float MIN_FADE_SCALE = MIN_AVATAR_SCALE; render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; @@ -162,7 +162,7 @@ void AvatarManager::simulateAvatarFades(float deltaTime) { auto avatar = std::static_pointer_cast(*fadingIterator); avatar->startUpdate(); avatar->setTargetScale(avatar->getScale() * SHRINK_RATE, true); - if (avatar->getTargetScale() < MIN_FADE_SCALE) { + if (avatar->getTargetScale() <= MIN_FADE_SCALE) { avatar->removeFromScene(*fadingIterator, scene, pendingChanges); fadingIterator = _avatarFades.erase(fadingIterator); } else { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 3ee5f8b202..ccb69f1ff1 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -48,7 +48,6 @@ #include "ModelReferential.h" #include "MyAvatar.h" #include "Physics.h" -#include "Recorder.h" #include "Util.h" #include "InterfaceLogging.h" #include "DebugDraw.h" @@ -180,10 +179,22 @@ MyAvatar::MyAvatar(RigPointer rig) : setPosition(dummyAvatar.getPosition()); setOrientation(dummyAvatar.getOrientation()); - // FIXME attachments - // FIXME joints - // FIXME head lean - // FIXME head orientation + if (!dummyAvatar.getAttachmentData().isEmpty()) { + setAttachmentData(dummyAvatar.getAttachmentData()); + } + + auto headData = dummyAvatar.getHeadData(); + if (headData && _headData) { + // blendshapes + if (!headData->getBlendshapeCoefficients().isEmpty()) { + _headData->setBlendshapeCoefficients(headData->getBlendshapeCoefficients()); + } + // head lean + _headData->setLeanForward(headData->getLeanForward()); + _headData->setLeanSideways(headData->getLeanSideways()); + // head orientation + _headData->setLookAtPosition(headData->getLookAtPosition()); + } }); } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 997a0ad9ae..80a9dc9d53 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -893,23 +893,21 @@ void Rig::updateNeckJoint(int index, const HeadParameters& params) { void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) { // AJT: TODO: fix eye tracking! /* - { - if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { - auto& state = _jointStates[index]; - auto& parentState = _jointStates[state.getParentIndex()]; + if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { + auto& state = _jointStates[index]; + auto& parentState = _jointStates[state.getParentIndex()]; - // NOTE: at the moment we do the math in the world-frame, hence the inverse transform is more complex than usual. - glm::mat4 inverse = glm::inverse(glm::mat4_cast(modelRotation) * parentState.getTransform() * - glm::translate(state.getDefaultTranslationInConstrainedFrame()) * - state.getPreTransform() * glm::mat4_cast(state.getPreRotation() * state.getDefaultRotation())); - glm::vec3 front = glm::vec3(inverse * glm::vec4(worldHeadOrientation * IDENTITY_FRONT, 0.0f)); - glm::vec3 lookAtDelta = lookAtSpot - modelTranslation; - glm::vec3 lookAt = glm::vec3(inverse * glm::vec4(lookAtDelta + glm::length(lookAtDelta) * saccade, 1.0f)); - glm::quat between = rotationBetween(front, lookAt); - const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; - state.setRotationInConstrainedFrame(glm::angleAxis(glm::clamp(glm::angle(between), -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) * - state.getDefaultRotation(), DEFAULT_PRIORITY); - } + // NOTE: at the moment we do the math in the world-frame, hence the inverse transform is more complex than usual. + glm::mat4 inverse = glm::inverse(glm::mat4_cast(modelRotation) * parentState.getTransform() * + glm::translate(state.getUnscaledDefaultTranslation()) * + state.getPreTransform() * glm::mat4_cast(state.getPreRotation() * state.getDefaultRotation())); + glm::vec3 front = glm::vec3(inverse * glm::vec4(worldHeadOrientation * IDENTITY_FRONT, 0.0f)); + glm::vec3 lookAtDelta = lookAtSpot - modelTranslation; + glm::vec3 lookAt = glm::vec3(inverse * glm::vec4(lookAtDelta + glm::length(lookAtDelta) * saccade, 1.0f)); + glm::quat between = rotationBetween(front, lookAt); + const float MAX_ANGLE = 30.0f * RADIANS_PER_DEGREE; + state.setRotationInConstrainedFrame(glm::angleAxis(glm::clamp(glm::angle(between), -MAX_ANGLE, MAX_ANGLE), glm::axis(between)) * + state.getDefaultRotation(), DEFAULT_PRIORITY); } */ } diff --git a/libraries/audio/src/AudioConstants.h b/libraries/audio/src/AudioConstants.h index e252a5354d..785060d364 100644 --- a/libraries/audio/src/AudioConstants.h +++ b/libraries/audio/src/AudioConstants.h @@ -35,4 +35,4 @@ namespace AudioConstants { } -#endif // hifi_AudioConstants_h \ No newline at end of file +#endif // hifi_AudioConstants_h diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 0574f712bc..0f588b5013 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1297,8 +1297,51 @@ void AvatarData::updateJointMappings() { } } -AttachmentData::AttachmentData() : - scale(1.0f) { +static const QString JSON_ATTACHMENT_URL = QStringLiteral("modelUrl"); +static const QString JSON_ATTACHMENT_JOINT_NAME = QStringLiteral("jointName"); +static const QString JSON_ATTACHMENT_TRANSFORM = QStringLiteral("transform"); + +QJsonObject AttachmentData::toJson() const { + QJsonObject result; + if (modelURL.isValid() && !modelURL.isEmpty()) { + result[JSON_ATTACHMENT_URL] = modelURL.toString(); + } + if (!jointName.isEmpty()) { + result[JSON_ATTACHMENT_JOINT_NAME] = jointName; + } + // FIXME the transform constructor that takes rot/scale/translation + // doesn't return the correct value for isIdentity() + Transform transform; + transform.setRotation(rotation); + transform.setScale(scale); + transform.setTranslation(translation); + if (!transform.isIdentity()) { + result[JSON_ATTACHMENT_TRANSFORM] = Transform::toJson(transform); + } + return result; +} + +void AttachmentData::fromJson(const QJsonObject& json) { + if (json.contains(JSON_ATTACHMENT_URL)) { + const QString modelURLTemp = json[JSON_ATTACHMENT_URL].toString(); + if (modelURLTemp != modelURL.toString()) { + modelURL = modelURLTemp; + } + } + + if (json.contains(JSON_ATTACHMENT_JOINT_NAME)) { + const QString jointNameTemp = json[JSON_ATTACHMENT_JOINT_NAME].toString(); + if (jointNameTemp != jointName) { + jointName = jointNameTemp; + } + } + + if (json.contains(JSON_ATTACHMENT_TRANSFORM)) { + Transform transform = Transform::fromJson(json[JSON_ATTACHMENT_TRANSFORM]); + translation = transform.getTranslation(); + rotation = transform.getRotation(); + scale = transform.getScale().x; + } } bool AttachmentData::operator==(const AttachmentData& other) const { @@ -1399,15 +1442,11 @@ static const QString JSON_AVATAR_BASIS = QStringLiteral("basisTransform"); static const QString JSON_AVATAR_RELATIVE = QStringLiteral("relativeTransform"); static const QString JSON_AVATAR_JOINT_ARRAY = QStringLiteral("jointArray"); static const QString JSON_AVATAR_HEAD = QStringLiteral("head"); -static const QString JSON_AVATAR_HEAD_ROTATION = QStringLiteral("rotation"); -static const QString JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS = QStringLiteral("blendShapes"); -static const QString JSON_AVATAR_HEAD_LEAN_FORWARD = QStringLiteral("leanForward"); -static const QString JSON_AVATAR_HEAD_LEAN_SIDEWAYS = QStringLiteral("leanSideways"); -static const QString JSON_AVATAR_HEAD_LOOKAT = QStringLiteral("lookAt"); static const QString JSON_AVATAR_HEAD_MODEL = QStringLiteral("headModel"); static const QString JSON_AVATAR_BODY_MODEL = QStringLiteral("bodyModel"); static const QString JSON_AVATAR_DISPLAY_NAME = QStringLiteral("displayName"); static const QString JSON_AVATAR_ATTACHEMENTS = QStringLiteral("attachments"); +static const QString JSON_AVATAR_SCALE = QStringLiteral("scale"); QJsonValue toJsonValue(const JointData& joint) { QJsonArray result; @@ -1428,93 +1467,84 @@ JointData jointDataFromJsonValue(const QJsonValue& json) { return result; } -// Every frame will store both a basis for the recording and a relative transform -// This allows the application to decide whether playback should be relative to an avatar's -// transform at the start of playback, or relative to the transform of the recorded -// avatar -QByteArray AvatarData::toFrame(const AvatarData& avatar) { +QJsonObject AvatarData::toJson() const { QJsonObject root; - if (!avatar.getFaceModelURL().isEmpty()) { - root[JSON_AVATAR_HEAD_MODEL] = avatar.getFaceModelURL().toString(); + if (!getFaceModelURL().isEmpty()) { + root[JSON_AVATAR_HEAD_MODEL] = getFaceModelURL().toString(); } - if (!avatar.getSkeletonModelURL().isEmpty()) { - root[JSON_AVATAR_BODY_MODEL] = avatar.getSkeletonModelURL().toString(); + if (!getSkeletonModelURL().isEmpty()) { + root[JSON_AVATAR_BODY_MODEL] = getSkeletonModelURL().toString(); } - if (!avatar.getDisplayName().isEmpty()) { - root[JSON_AVATAR_DISPLAY_NAME] = avatar.getDisplayName(); + if (!getDisplayName().isEmpty()) { + root[JSON_AVATAR_DISPLAY_NAME] = getDisplayName(); } - if (!avatar.getAttachmentData().isEmpty()) { - // FIXME serialize attachment data + if (!getAttachmentData().isEmpty()) { + QJsonArray attachmentsJson; + for (auto attachment : getAttachmentData()) { + attachmentsJson.push_back(attachment.toJson()); + } + root[JSON_AVATAR_ATTACHEMENTS] = attachmentsJson; } - auto recordingBasis = avatar.getRecordingBasis(); + auto recordingBasis = getRecordingBasis(); if (recordingBasis) { root[JSON_AVATAR_BASIS] = Transform::toJson(*recordingBasis); // Find the relative transform - auto relativeTransform = recordingBasis->relativeTransform(avatar.getTransform()); - root[JSON_AVATAR_RELATIVE] = Transform::toJson(relativeTransform); + auto relativeTransform = recordingBasis->relativeTransform(getTransform()); + if (!relativeTransform.isIdentity()) { + root[JSON_AVATAR_RELATIVE] = Transform::toJson(relativeTransform); + } } else { - root[JSON_AVATAR_RELATIVE] = Transform::toJson(avatar.getTransform()); + root[JSON_AVATAR_RELATIVE] = Transform::toJson(getTransform()); + } + + auto scale = getTargetScale(); + if (scale != 1.0f) { + root[JSON_AVATAR_SCALE] = scale; } // Skeleton pose QJsonArray jointArray; - for (const auto& joint : avatar.getRawJointData()) { + for (const auto& joint : getRawJointData()) { jointArray.push_back(toJsonValue(joint)); } root[JSON_AVATAR_JOINT_ARRAY] = jointArray; - const HeadData* head = avatar.getHeadData(); + const HeadData* head = getHeadData(); if (head) { - QJsonObject headJson; - QJsonArray blendshapeCoefficients; - for (const auto& blendshapeCoefficient : head->getBlendshapeCoefficients()) { - blendshapeCoefficients.push_back(blendshapeCoefficient); + auto headJson = head->toJson(); + if (!headJson.isEmpty()) { + root[JSON_AVATAR_HEAD] = headJson; } - headJson[JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS] = blendshapeCoefficients; - headJson[JSON_AVATAR_HEAD_ROTATION] = toJsonValue(head->getRawOrientation()); - headJson[JSON_AVATAR_HEAD_LEAN_FORWARD] = QJsonValue(head->getLeanForward()); - headJson[JSON_AVATAR_HEAD_LEAN_SIDEWAYS] = QJsonValue(head->getLeanSideways()); - vec3 relativeLookAt = glm::inverse(avatar.getOrientation()) * - (head->getLookAtPosition() - avatar.getPosition()); - headJson[JSON_AVATAR_HEAD_LOOKAT] = toJsonValue(relativeLookAt); - root[JSON_AVATAR_HEAD] = headJson; } - - return QJsonDocument(root).toBinaryData(); + return root; } -void AvatarData::fromFrame(const QByteArray& frameData, AvatarData& result) { - QJsonDocument doc = QJsonDocument::fromBinaryData(frameData); -#ifdef WANT_JSON_DEBUG - qDebug() << doc.toJson(QJsonDocument::JsonFormat::Indented); -#endif - QJsonObject root = doc.object(); - - if (root.contains(JSON_AVATAR_HEAD_MODEL)) { - auto faceModelURL = root[JSON_AVATAR_HEAD_MODEL].toString(); - if (faceModelURL != result.getFaceModelURL().toString()) { +void AvatarData::fromJson(const QJsonObject& json) { + if (json.contains(JSON_AVATAR_HEAD_MODEL)) { + auto faceModelURL = json[JSON_AVATAR_HEAD_MODEL].toString(); + if (faceModelURL != getFaceModelURL().toString()) { QUrl faceModel(faceModelURL); if (faceModel.isValid()) { - result.setFaceModelURL(faceModel); + setFaceModelURL(faceModel); } } } - if (root.contains(JSON_AVATAR_BODY_MODEL)) { - auto bodyModelURL = root[JSON_AVATAR_BODY_MODEL].toString(); - if (bodyModelURL != result.getSkeletonModelURL().toString()) { - result.setSkeletonModelURL(bodyModelURL); + if (json.contains(JSON_AVATAR_BODY_MODEL)) { + auto bodyModelURL = json[JSON_AVATAR_BODY_MODEL].toString(); + if (bodyModelURL != getSkeletonModelURL().toString()) { + setSkeletonModelURL(bodyModelURL); } } - if (root.contains(JSON_AVATAR_DISPLAY_NAME)) { - auto newDisplayName = root[JSON_AVATAR_DISPLAY_NAME].toString(); - if (newDisplayName != result.getDisplayName()) { - result.setDisplayName(newDisplayName); + if (json.contains(JSON_AVATAR_DISPLAY_NAME)) { + auto newDisplayName = json[JSON_AVATAR_DISPLAY_NAME].toString(); + if (newDisplayName != getDisplayName()) { + setDisplayName(newDisplayName); } - } + } - if (root.contains(JSON_AVATAR_RELATIVE)) { + if (json.contains(JSON_AVATAR_RELATIVE)) { // During playback you can either have the recording basis set to the avatar current state // meaning that all playback is relative to this avatars starting position, or // the basis can be loaded from the recording, meaning the playback is relative to the @@ -1522,70 +1552,83 @@ void AvatarData::fromFrame(const QByteArray& frameData, AvatarData& result) { // The first is more useful for playing back recordings on your own avatar, while // the latter is more useful for playing back other avatars within your scene. - auto currentBasis = result.getRecordingBasis(); + auto currentBasis = getRecordingBasis(); if (!currentBasis) { - currentBasis = std::make_shared(Transform::fromJson(root[JSON_AVATAR_BASIS])); + currentBasis = std::make_shared(Transform::fromJson(json[JSON_AVATAR_BASIS])); } - auto relativeTransform = Transform::fromJson(root[JSON_AVATAR_RELATIVE]); + auto relativeTransform = Transform::fromJson(json[JSON_AVATAR_RELATIVE]); auto worldTransform = currentBasis->worldTransform(relativeTransform); - result.setPosition(worldTransform.getTranslation()); - result.setOrientation(worldTransform.getRotation()); - - // TODO: find a way to record/playback the Scale of the avatar - //result.setTargetScale(worldTransform.getScale().x); + setPosition(worldTransform.getTranslation()); + setOrientation(worldTransform.getRotation()); } + if (json.contains(JSON_AVATAR_SCALE)) { + setTargetScale((float)json[JSON_AVATAR_SCALE].toDouble()); + } - if (root.contains(JSON_AVATAR_ATTACHEMENTS)) { - // FIXME de-serialize attachment data + if (json.contains(JSON_AVATAR_ATTACHEMENTS) && json[JSON_AVATAR_ATTACHEMENTS].isArray()) { + QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHEMENTS].toArray(); + QVector attachments; + for (auto attachmentJson : attachmentsJson) { + AttachmentData attachment; + attachment.fromJson(attachmentJson.toObject()); + attachments.push_back(attachment); + } + setAttachmentData(attachments); } // Joint rotations are relative to the avatar, so they require no basis correction - if (root.contains(JSON_AVATAR_JOINT_ARRAY)) { + if (json.contains(JSON_AVATAR_JOINT_ARRAY)) { QVector jointArray; - QJsonArray jointArrayJson = root[JSON_AVATAR_JOINT_ARRAY].toArray(); + QJsonArray jointArrayJson = json[JSON_AVATAR_JOINT_ARRAY].toArray(); jointArray.reserve(jointArrayJson.size()); int i = 0; for (const auto& jointJson : jointArrayJson) { auto joint = jointDataFromJsonValue(jointJson); jointArray.push_back(joint); - result.setJointData(i, joint.rotation, joint.translation); - result._jointData[i].rotationSet = true; // Have to do that to broadcast the avatar new pose + setJointData(i, joint.rotation, joint.translation); + _jointData[i].rotationSet = true; // Have to do that to broadcast the avatar new pose i++; } - result.setRawJointData(jointArray); + setRawJointData(jointArray); } -#if 0 // Most head data is relative to the avatar, and needs no basis correction, // but the lookat vector does need correction - HeadData* head = result._headData; - if (head && root.contains(JSON_AVATAR_HEAD)) { - QJsonObject headJson = root[JSON_AVATAR_HEAD].toObject(); - if (headJson.contains(JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS)) { - QVector blendshapeCoefficients; - QJsonArray blendshapeCoefficientsJson = headJson[JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS].toArray(); - for (const auto& blendshapeCoefficient : blendshapeCoefficientsJson) { - blendshapeCoefficients.push_back((float)blendshapeCoefficient.toDouble()); - } - head->setBlendshapeCoefficients(blendshapeCoefficients); - } - if (headJson.contains(JSON_AVATAR_HEAD_ROTATION)) { - head->setOrientation(quatFromJsonValue(headJson[JSON_AVATAR_HEAD_ROTATION])); - } - if (headJson.contains(JSON_AVATAR_HEAD_LEAN_FORWARD)) { - head->setLeanForward((float)headJson[JSON_AVATAR_HEAD_LEAN_FORWARD].toDouble()); - } - if (headJson.contains(JSON_AVATAR_HEAD_LEAN_SIDEWAYS)) { - head->setLeanSideways((float)headJson[JSON_AVATAR_HEAD_LEAN_SIDEWAYS].toDouble()); - } - if (headJson.contains(JSON_AVATAR_HEAD_LOOKAT)) { - auto relativeLookAt = vec3FromJsonValue(headJson[JSON_AVATAR_HEAD_LOOKAT]); - if (glm::length2(relativeLookAt) > 0.01) { - head->setLookAtPosition((result.getOrientation() * relativeLookAt) + result.getPosition()); - } + if (json.contains(JSON_AVATAR_HEAD)) { + if (!_headData) { + _headData = new HeadData(this); } + _headData->fromJson(json[JSON_AVATAR_HEAD].toObject()); + } +} + +// Every frame will store both a basis for the recording and a relative transform +// This allows the application to decide whether playback should be relative to an avatar's +// transform at the start of playback, or relative to the transform of the recorded +// avatar +QByteArray AvatarData::toFrame(const AvatarData& avatar) { + QJsonObject root = avatar.toJson(); +#ifdef WANT_JSON_DEBUG + { + QJsonObject obj = root; + obj.remove(JSON_AVATAR_JOINT_ARRAY); + qDebug().noquote() << QJsonDocument(obj).toJson(QJsonDocument::JsonFormat::Indented); } #endif + return QJsonDocument(root).toBinaryData(); +} + + +void AvatarData::fromFrame(const QByteArray& frameData, AvatarData& result) { + QJsonDocument doc = QJsonDocument::fromBinaryData(frameData); +#ifdef WANT_JSON_DEBUG + { + QJsonObject obj = doc.object(); + obj.remove(JSON_AVATAR_JOINT_ARRAY); + qDebug().noquote() << QJsonDocument(obj).toJson(QJsonDocument::JsonFormat::Indented); + } +#endif + result.fromJson(doc.object()); } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 528ee93219..bc71bc52cd 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -342,6 +342,8 @@ public: void clearRecordingBasis(); TransformPointer getRecordingBasis() const; void setRecordingBasis(TransformPointer recordingBasis = TransformPointer()); + QJsonObject toJson() const; + void fromJson(const QJsonObject& json); public slots: void sendAvatarDataPacket(); @@ -441,13 +443,14 @@ public: QString jointName; glm::vec3 translation; glm::quat rotation; - float scale; - - AttachmentData(); + float scale { 1.0f }; bool isValid() const { return modelURL.isValid(); } bool operator==(const AttachmentData& other) const; + + QJsonObject toJson() const; + void fromJson(const QJsonObject& json); }; QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment); diff --git a/libraries/avatars/src/HeadData.cpp b/libraries/avatars/src/HeadData.cpp index e971b184c8..1d664aa3ff 100644 --- a/libraries/avatars/src/HeadData.cpp +++ b/libraries/avatars/src/HeadData.cpp @@ -9,13 +9,18 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include +#include "HeadData.h" + +#include + +#include +#include #include #include +#include #include "AvatarData.h" -#include "HeadData.h" /// The names of the blendshapes expected by Faceshift, terminated with an empty string. extern const char* FACESHIFT_BLENDSHAPES[]; @@ -58,6 +63,7 @@ glm::quat HeadData::getOrientation() const { return _owningAvatar->getOrientation() * getRawOrientation(); } + void HeadData::setOrientation(const glm::quat& orientation) { // rotate body about vertical axis glm::quat bodyOrientation = _owningAvatar->getOrientation(); @@ -72,19 +78,24 @@ void HeadData::setOrientation(const glm::quat& orientation) { _baseRoll = eulers.z; } -void HeadData::setBlendshape(QString name, float val) { - static bool hasInitializedLookupMap = false; +//Lazily construct a lookup map from the blendshapes +static const QMap& getBlendshapesLookupMap() { + static std::once_flag once; static QMap blendshapeLookupMap; - //Lazily construct a lookup map from the blendshapes - if (!hasInitializedLookupMap) { + std::call_once(once, [&] { for (int i = 0; i < NUM_FACESHIFT_BLENDSHAPES; i++) { - blendshapeLookupMap[FACESHIFT_BLENDSHAPES[i]] = i; + blendshapeLookupMap[FACESHIFT_BLENDSHAPES[i]] = i; } - hasInitializedLookupMap = true; - } + }); + return blendshapeLookupMap; +} + + +void HeadData::setBlendshape(QString name, float val) { + const auto& blendshapeLookupMap = getBlendshapesLookupMap(); //Check to see if the named blendshape exists, and then set its value if it does - QMap::iterator it = blendshapeLookupMap.find(name); + auto it = blendshapeLookupMap.find(name); if (it != blendshapeLookupMap.end()) { if (_blendshapeCoefficients.size() <= it.value()) { _blendshapeCoefficients.resize(it.value() + 1); @@ -92,3 +103,85 @@ void HeadData::setBlendshape(QString name, float val) { _blendshapeCoefficients[it.value()] = val; } } + +static const QString JSON_AVATAR_HEAD_ROTATION = QStringLiteral("rotation"); +static const QString JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS = QStringLiteral("blendShapes"); +static const QString JSON_AVATAR_HEAD_LEAN_FORWARD = QStringLiteral("leanForward"); +static const QString JSON_AVATAR_HEAD_LEAN_SIDEWAYS = QStringLiteral("leanSideways"); +static const QString JSON_AVATAR_HEAD_LOOKAT = QStringLiteral("lookAt"); + +QJsonObject HeadData::toJson() const { + QJsonObject headJson; + const auto& blendshapeLookupMap = getBlendshapesLookupMap(); + QJsonObject blendshapesJson; + for (auto name : blendshapeLookupMap.keys()) { + auto index = blendshapeLookupMap[name]; + if (index >= _blendshapeCoefficients.size()) { + continue; + } + auto value = _blendshapeCoefficients[index]; + if (value == 0.0f) { + continue; + } + blendshapesJson[name] = value; + } + if (!blendshapesJson.isEmpty()) { + headJson[JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS] = blendshapesJson; + } + if (getRawOrientation() != quat()) { + headJson[JSON_AVATAR_HEAD_ROTATION] = toJsonValue(getRawOrientation()); + } + if (getLeanForward() != 0.0f) { + headJson[JSON_AVATAR_HEAD_LEAN_FORWARD] = getLeanForward(); + } + if (getLeanSideways() != 0.0f) { + headJson[JSON_AVATAR_HEAD_LEAN_SIDEWAYS] = getLeanSideways(); + } + auto lookat = getLookAtPosition(); + if (lookat != vec3()) { + vec3 relativeLookAt = glm::inverse(_owningAvatar->getOrientation()) * + (getLookAtPosition() - _owningAvatar->getPosition()); + headJson[JSON_AVATAR_HEAD_LOOKAT] = toJsonValue(relativeLookAt); + } + return headJson; +} + +void HeadData::fromJson(const QJsonObject& json) { + if (json.contains(JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS)) { + auto jsonValue = json[JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS]; + if (jsonValue.isArray()) { + QVector blendshapeCoefficients; + QJsonArray blendshapeCoefficientsJson = jsonValue.toArray(); + for (const auto& blendshapeCoefficient : blendshapeCoefficientsJson) { + blendshapeCoefficients.push_back((float)blendshapeCoefficient.toDouble()); + setBlendshapeCoefficients(blendshapeCoefficients); + } + } else if (jsonValue.isObject()) { + QJsonObject blendshapeCoefficientsJson = jsonValue.toObject(); + for (const QString& name : blendshapeCoefficientsJson.keys()) { + float value = (float)blendshapeCoefficientsJson[name].toDouble(); + setBlendshape(name, value); + } + } else { + qWarning() << "Unable to deserialize head json: " << jsonValue; + } + } + + if (json.contains(JSON_AVATAR_HEAD_ROTATION)) { + setOrientation(quatFromJsonValue(json[JSON_AVATAR_HEAD_ROTATION])); + } + if (json.contains(JSON_AVATAR_HEAD_LEAN_FORWARD)) { + setLeanForward((float)json[JSON_AVATAR_HEAD_LEAN_FORWARD].toDouble()); + } + if (json.contains(JSON_AVATAR_HEAD_LEAN_SIDEWAYS)) { + setLeanSideways((float)json[JSON_AVATAR_HEAD_LEAN_SIDEWAYS].toDouble()); + } + + if (json.contains(JSON_AVATAR_HEAD_LOOKAT)) { + auto relativeLookAt = vec3FromJsonValue(json[JSON_AVATAR_HEAD_LOOKAT]); + if (glm::length2(relativeLookAt) > 0.01f) { + setLookAtPosition((_owningAvatar->getOrientation() * relativeLookAt) + _owningAvatar->getPosition()); + } + } +} + diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index 38503f6e1e..dac266f4a2 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -28,6 +28,7 @@ const float MIN_HEAD_ROLL = -50.0f; const float MAX_HEAD_ROLL = 50.0f; class AvatarData; +class QJsonObject; class HeadData { public: @@ -83,6 +84,9 @@ public: friend class AvatarData; + QJsonObject toJson() const; + void fromJson(const QJsonObject& json); + protected: // degrees float _baseYaw; diff --git a/libraries/avatars/src/Player.cpp b/libraries/avatars/src/Player.cpp deleted file mode 100644 index 31efb4cd9c..0000000000 --- a/libraries/avatars/src/Player.cpp +++ /dev/null @@ -1,443 +0,0 @@ -// -// Player.cpp -// -// -// Created by Clement on 9/17/14. -// Copyright 2014 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 -// - - -#if 0 -#include -#include -#include -#include - -#include "AvatarData.h" -#include "AvatarLogging.h" -#include "Player.h" - -static const int INVALID_FRAME = -1; - -Player::Player(AvatarData* avatar) : - _avatar(avatar), - _recording(new Recording()), - _currentFrame(INVALID_FRAME), - _frameInterpolationFactor(0.0f), - _pausedFrame(INVALID_FRAME), - _timerOffset(0), - _audioOffset(0), - _audioThread(NULL), - _playFromCurrentPosition(true), - _loop(false), - _useAttachments(true), - _useDisplayName(true), - _useHeadURL(true), - _useSkeletonURL(true) -{ - _timer.invalidate(); -} - -bool Player::isPlaying() const { - return _timer.isValid(); -} - -bool Player::isPaused() const { - return (_pausedFrame != INVALID_FRAME); -} - -qint64 Player::elapsed() const { - if (isPlaying()) { - return _timerOffset + _timer.elapsed(); - } else if (isPaused()) { - return _timerOffset; - } else { - return 0; - } -} - -void Player::startPlaying() { - if (!_recording || _recording->getFrameNumber() <= 1) { - return; - } - - if (!isPaused()) { - _currentContext.globalTimestamp = usecTimestampNow(); - _currentContext.domain = DependencyManager::get()->getDomainHandler().getHostname(); - _currentContext.position = _avatar->getPosition(); - _currentContext.orientation = _avatar->getOrientation(); - _currentContext.scale = _avatar->getTargetScale(); - _currentContext.headModel = _avatar->getFaceModelURL().toString(); - _currentContext.skeletonModel = _avatar->getSkeletonModelURL().toString(); - _currentContext.displayName = _avatar->getDisplayName(); - _currentContext.attachments = _avatar->getAttachmentData(); - - _currentContext.orientationInv = glm::inverse(_currentContext.orientation); - - RecordingContext& context = _recording->getContext(); - if (_useAttachments) { - _avatar->setAttachmentData(context.attachments); - } - if (_useDisplayName) { - _avatar->setDisplayName(context.displayName); - } - if (_useHeadURL) { - _avatar->setFaceModelURL(context.headModel); - } - if (_useSkeletonURL) { - _avatar->setSkeletonModelURL(context.skeletonModel); - } - - bool wantDebug = false; - if (wantDebug) { - qCDebug(avatars) << "Player::startPlaying(): Recording Context"; - qCDebug(avatars) << "Domain:" << _currentContext.domain; - qCDebug(avatars) << "Position:" << _currentContext.position; - qCDebug(avatars) << "Orientation:" << _currentContext.orientation; - qCDebug(avatars) << "Scale:" << _currentContext.scale; - qCDebug(avatars) << "Head URL:" << _currentContext.headModel; - qCDebug(avatars) << "Skeleton URL:" << _currentContext.skeletonModel; - qCDebug(avatars) << "Display Name:" << _currentContext.displayName; - qCDebug(avatars) << "Num Attachments:" << _currentContext.attachments.size(); - - for (int i = 0; i < _currentContext.attachments.size(); ++i) { - qCDebug(avatars) << "Model URL:" << _currentContext.attachments[i].modelURL; - qCDebug(avatars) << "Joint Name:" << _currentContext.attachments[i].jointName; - qCDebug(avatars) << "Translation:" << _currentContext.attachments[i].translation; - qCDebug(avatars) << "Rotation:" << _currentContext.attachments[i].rotation; - qCDebug(avatars) << "Scale:" << _currentContext.attachments[i].scale; - } - } - - // Fake faceshift connection - _avatar->setForceFaceTrackerConnected(true); - - qCDebug(avatars) << "Recorder::startPlaying()"; - setupAudioThread(); - _currentFrame = 0; - _timerOffset = 0; - _timer.start(); - } else { - qCDebug(avatars) << "Recorder::startPlaying(): Unpause"; - setupAudioThread(); - _timer.start(); - - setCurrentFrame(_pausedFrame); - _pausedFrame = INVALID_FRAME; - } -} - -void Player::stopPlaying() { - if (!isPlaying()) { - return; - } - _pausedFrame = INVALID_FRAME; - _timer.invalidate(); - cleanupAudioThread(); - _avatar->clearJointsData(); - - // Turn off fake face tracker connection - _avatar->setForceFaceTrackerConnected(false); - - if (_useAttachments) { - _avatar->setAttachmentData(_currentContext.attachments); - } - if (_useDisplayName) { - _avatar->setDisplayName(_currentContext.displayName); - } - if (_useHeadURL) { - _avatar->setFaceModelURL(_currentContext.headModel); - } - if (_useSkeletonURL) { - _avatar->setSkeletonModelURL(_currentContext.skeletonModel); - } - - qCDebug(avatars) << "Recorder::stopPlaying()"; -} - -void Player::pausePlayer() { - _timerOffset = elapsed(); - _timer.invalidate(); - cleanupAudioThread(); - - _pausedFrame = _currentFrame; - qCDebug(avatars) << "Recorder::pausePlayer()"; -} - -void Player::setupAudioThread() { - _audioThread = new QThread(); - _audioThread->setObjectName("Player Audio Thread"); - _options.position = _avatar->getPosition(); - _options.orientation = _avatar->getOrientation(); - _options.stereo = _recording->numberAudioChannel() == 2; - - _injector.reset(new AudioInjector(_recording->getAudioData(), _options), &QObject::deleteLater); - _injector->moveToThread(_audioThread); - _audioThread->start(); - QMetaObject::invokeMethod(_injector.data(), "injectAudio", Qt::QueuedConnection); -} - -void Player::cleanupAudioThread() { - _injector->stop(); - QObject::connect(_injector.data(), &AudioInjector::finished, - _injector.data(), &AudioInjector::deleteLater); - QObject::connect(_injector.data(), &AudioInjector::destroyed, - _audioThread, &QThread::quit); - QObject::connect(_audioThread, &QThread::finished, - _audioThread, &QThread::deleteLater); - _injector.clear(); - _audioThread = NULL; -} - -void Player::loopRecording() { - cleanupAudioThread(); - setupAudioThread(); - _currentFrame = 0; - _timerOffset = 0; - _timer.restart(); -} - -void Player::loadFromFile(const QString& file) { - if (_recording) { - _recording->clear(); - } else { - _recording = QSharedPointer(); - } - readRecordingFromFile(_recording, file); - - _pausedFrame = INVALID_FRAME; -} - -void Player::loadRecording(RecordingPointer recording) { - _recording = recording; - _pausedFrame = INVALID_FRAME; -} - -void Player::play() { - computeCurrentFrame(); - if (_currentFrame < 0 || (_currentFrame >= _recording->getFrameNumber() - 2)) { // -2 because of interpolation - if (_loop) { - loopRecording(); - } else { - stopPlaying(); - } - return; - } - - const RecordingContext* context = &_recording->getContext(); - if (_playFromCurrentPosition) { - context = &_currentContext; - } - const RecordingFrame& currentFrame = _recording->getFrame(_currentFrame); - const RecordingFrame& nextFrame = _recording->getFrame(_currentFrame + 1); - - glm::vec3 translation = glm::mix(currentFrame.getTranslation(), - nextFrame.getTranslation(), - _frameInterpolationFactor); - _avatar->setPosition(context->position + context->orientation * translation); - - glm::quat rotation = safeMix(currentFrame.getRotation(), - nextFrame.getRotation(), - _frameInterpolationFactor); - _avatar->setOrientation(context->orientation * rotation); - - float scale = glm::mix(currentFrame.getScale(), - nextFrame.getScale(), - _frameInterpolationFactor); - _avatar->setTargetScale(context->scale * scale); - - // Joint array playback - // FIXME: THis is still using a deprecated path to assign the joint orientation since setting the full RawJointData array doesn't - // work for Avatar. We need to fix this working with the animation team - const auto& prevJointArray = currentFrame.getJointArray(); - const auto& nextJointArray = currentFrame.getJointArray(); - QVector jointArray(prevJointArray.size()); - QVector jointRotations(prevJointArray.size()); // FIXME: remove once the setRawJointData is fixed - QVector jointTranslations(prevJointArray.size()); // FIXME: remove once the setRawJointData is fixed - - for (int i = 0; i < jointArray.size(); i++) { - const auto& prevJoint = prevJointArray[i]; - const auto& nextJoint = nextJointArray[i]; - auto& joint = jointArray[i]; - - // Rotation - joint.rotationSet = prevJoint.rotationSet || nextJoint.rotationSet; - if (joint.rotationSet) { - joint.rotation = safeMix(prevJoint.rotation, nextJoint.rotation, _frameInterpolationFactor); - jointRotations[i] = joint.rotation; // FIXME: remove once the setRawJointData is fixed - } - - joint.translationSet = prevJoint.translationSet || nextJoint.translationSet; - if (joint.translationSet) { - joint.translation = glm::mix(prevJoint.translation, nextJoint.translation, _frameInterpolationFactor); - jointTranslations[i] = joint.translation; // FIXME: remove once the setRawJointData is fixed - } - } - - // _avatar->setRawJointData(jointArray); // FIXME: Enable once the setRawJointData is fixed - _avatar->setJointRotations(jointRotations); // FIXME: remove once the setRawJointData is fixed - // _avatar->setJointTranslations(jointTranslations); // FIXME: remove once the setRawJointData is fixed - - HeadData* head = const_cast(_avatar->getHeadData()); - if (head) { - // Make sure fake face tracker connection doesn't get turned off - _avatar->setForceFaceTrackerConnected(true); - - QVector blendCoef(currentFrame.getBlendshapeCoefficients().size()); - for (int i = 0; i < currentFrame.getBlendshapeCoefficients().size(); ++i) { - blendCoef[i] = glm::mix(currentFrame.getBlendshapeCoefficients()[i], - nextFrame.getBlendshapeCoefficients()[i], - _frameInterpolationFactor); - } - head->setBlendshapeCoefficients(blendCoef); - - float leanSideways = glm::mix(currentFrame.getLeanSideways(), - nextFrame.getLeanSideways(), - _frameInterpolationFactor); - head->setLeanSideways(leanSideways); - - float leanForward = glm::mix(currentFrame.getLeanForward(), - nextFrame.getLeanForward(), - _frameInterpolationFactor); - head->setLeanForward(leanForward); - - glm::quat headRotation = safeMix(currentFrame.getHeadRotation(), - nextFrame.getHeadRotation(), - _frameInterpolationFactor); - glm::vec3 eulers = glm::degrees(safeEulerAngles(headRotation)); - head->setFinalPitch(eulers.x); - head->setFinalYaw(eulers.y); - head->setFinalRoll(eulers.z); - - - glm::vec3 lookAt = glm::mix(currentFrame.getLookAtPosition(), - nextFrame.getLookAtPosition(), - _frameInterpolationFactor); - head->setLookAtPosition(context->position + context->orientation * lookAt); - } else { - qCDebug(avatars) << "WARNING: Player couldn't find head data."; - } - - _options.position = _avatar->getPosition(); - _options.orientation = _avatar->getOrientation(); - _injector->setOptions(_options); -} - -void Player::setCurrentFrame(int currentFrame) { - if (_recording && currentFrame >= _recording->getFrameNumber()) { - stopPlaying(); - return; - } - - _currentFrame = currentFrame; - _timerOffset = _recording->getFrameTimestamp(_currentFrame); - - if (isPlaying()) { - _timer.start(); - setAudioInjectorPosition(); - } else { - _pausedFrame = _currentFrame; - } -} - -void Player::setCurrentTime(int currentTime) { - if (currentTime >= _recording->getLength()) { - stopPlaying(); - return; - } - - // Find correct frame - int lowestBound = 0; - int highestBound = _recording->getFrameNumber() - 1; - while (lowestBound + 1 != highestBound) { - assert(lowestBound < highestBound); - - int bestGuess = lowestBound + - (highestBound - lowestBound) * - (float)(currentTime - _recording->getFrameTimestamp(lowestBound)) / - (float)(_recording->getFrameTimestamp(highestBound) - _recording->getFrameTimestamp(lowestBound)); - - if (_recording->getFrameTimestamp(bestGuess) <= currentTime) { - if (currentTime < _recording->getFrameTimestamp(bestGuess + 1)) { - lowestBound = bestGuess; - highestBound = bestGuess + 1; - } else { - lowestBound = bestGuess + 1; - } - } else { - if (_recording->getFrameTimestamp(bestGuess - 1) <= currentTime) { - lowestBound = bestGuess - 1; - highestBound = bestGuess; - } else { - highestBound = bestGuess - 1; - } - } - } - - setCurrentFrame(lowestBound); -} - -void Player::setVolume(float volume) { - _options.volume = volume; - if (_injector) { - _injector->setOptions(_options); - } - qCDebug(avatars) << "New volume: " << volume; -} - -void Player::setAudioOffset(int audioOffset) { - _audioOffset = audioOffset; -} - -void Player::setAudioInjectorPosition() { - int MSEC_PER_SEC = 1000; - int FRAME_SIZE = sizeof(AudioConstants::AudioSample) * _recording->numberAudioChannel(); - int currentAudioFrame = elapsed() * FRAME_SIZE * (AudioConstants::SAMPLE_RATE / MSEC_PER_SEC); - _injector->setCurrentSendOffset(currentAudioFrame); -} - -void Player::setPlayFromCurrentLocation(bool playFromCurrentLocation) { - _playFromCurrentPosition = playFromCurrentLocation; -} - -bool Player::computeCurrentFrame() { - if (!isPlaying()) { - _currentFrame = INVALID_FRAME; - return false; - } - if (_currentFrame < 0) { - _currentFrame = 0; - } - - qint64 elapsed = glm::clamp(Player::elapsed() - _audioOffset, (qint64)0, (qint64)_recording->getLength()); - while (_currentFrame < _recording->getFrameNumber() && - _recording->getFrameTimestamp(_currentFrame) < elapsed) { - ++_currentFrame; - } - - while(_currentFrame > 0 && - _recording->getFrameTimestamp(_currentFrame) > elapsed) { - --_currentFrame; - } - - if (_currentFrame == _recording->getFrameNumber() - 1) { - --_currentFrame; - _frameInterpolationFactor = 1.0f; - } else { - qint64 currentTimestamps = _recording->getFrameTimestamp(_currentFrame); - qint64 nextTimestamps = _recording->getFrameTimestamp(_currentFrame + 1); - _frameInterpolationFactor = (float)(elapsed - currentTimestamps) / - (float)(nextTimestamps - currentTimestamps); - } - - if (_frameInterpolationFactor < 0.0f || _frameInterpolationFactor > 1.0f) { - _frameInterpolationFactor = 0.0f; - qCDebug(avatars) << "Invalid frame interpolation value: overriding"; - } - return true; -} - -#endif diff --git a/libraries/avatars/src/Player.h b/libraries/avatars/src/Player.h deleted file mode 100644 index 558ff309e6..0000000000 --- a/libraries/avatars/src/Player.h +++ /dev/null @@ -1,94 +0,0 @@ -// -// Player.h -// -// -// Created by Clement on 9/17/14. -// Copyright 2014 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_Player_h -#define hifi_Player_h - -#include - -#if 0 -#include - -#include - -#include "Recording.h" - -class AvatarData; -class Player; - -typedef QSharedPointer PlayerPointer; -typedef QWeakPointer WeakPlayerPointer; - -/// Plays back a recording -class Player { -public: - Player(AvatarData* avatar); - - bool isPlaying() const; - bool isPaused() const; - qint64 elapsed() const; - - RecordingPointer getRecording() const { return _recording; } - int getCurrentFrame() const { return _currentFrame; } - -public slots: - void startPlaying(); - void stopPlaying(); - void pausePlayer(); - void loadFromFile(const QString& file); - void loadRecording(RecordingPointer recording); - void play(); - - void setCurrentFrame(int currentFrame); - void setCurrentTime(int currentTime); - - void setVolume(float volume); - void setAudioOffset(int audioOffset); - - void setPlayFromCurrentLocation(bool playFromCurrentPosition); - void setLoop(bool loop) { _loop = loop; } - void useAttachements(bool useAttachments) { _useAttachments = useAttachments; } - void useDisplayName(bool useDisplayName) { _useDisplayName = useDisplayName; } - void useHeadModel(bool useHeadURL) { _useHeadURL = useHeadURL; } - void useSkeletonModel(bool useSkeletonURL) { _useSkeletonURL = useSkeletonURL; } - -private: - void setupAudioThread(); - void cleanupAudioThread(); - void loopRecording(); - void setAudioInjectorPosition(); - bool computeCurrentFrame(); - - AvatarData* _avatar; - RecordingPointer _recording; - int _currentFrame; - float _frameInterpolationFactor; - int _pausedFrame; - - QElapsedTimer _timer; - int _timerOffset; - int _audioOffset; - - QThread* _audioThread; - QSharedPointer _injector; - AudioInjectorOptions _options; - - RecordingContext _currentContext; - bool _playFromCurrentPosition; - bool _loop; - bool _useAttachments; - bool _useDisplayName; - bool _useHeadURL; - bool _useSkeletonURL; -}; -#endif - -#endif // hifi_Player_h diff --git a/libraries/avatars/src/Recorder.cpp b/libraries/avatars/src/Recorder.cpp deleted file mode 100644 index 343302d472..0000000000 --- a/libraries/avatars/src/Recorder.cpp +++ /dev/null @@ -1,147 +0,0 @@ -// -// Recorder.cpp -// -// -// Created by Clement on 8/7/14. -// Copyright 2014 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 -// - - -#if 0 -#include -#include -#include - -#include "AvatarData.h" -#include "AvatarLogging.h" -#include "Recorder.h" - -Recorder::Recorder(AvatarData* avatar) : - _recording(new Recording()), - _avatar(avatar) -{ - _timer.invalidate(); -} - -bool Recorder::isRecording() const { - return _timer.isValid(); -} - -qint64 Recorder::elapsed() const { - if (isRecording()) { - return _timer.elapsed(); - } else { - return 0; - } -} - -void Recorder::startRecording() { - qCDebug(avatars) << "Recorder::startRecording()"; - _recording->clear(); - - RecordingContext& context = _recording->getContext(); - context.globalTimestamp = usecTimestampNow(); - context.domain = DependencyManager::get()->getDomainHandler().getHostname(); - context.position = _avatar->getPosition(); - context.orientation = _avatar->getOrientation(); - context.scale = _avatar->getTargetScale(); - context.headModel = _avatar->getFaceModelURL().toString(); - context.skeletonModel = _avatar->getSkeletonModelURL().toString(); - context.displayName = _avatar->getDisplayName(); - context.attachments = _avatar->getAttachmentData(); - - context.orientationInv = glm::inverse(context.orientation); - - bool wantDebug = false; - if (wantDebug) { - qCDebug(avatars) << "Recorder::startRecording(): Recording Context"; - qCDebug(avatars) << "Global timestamp:" << context.globalTimestamp; - qCDebug(avatars) << "Domain:" << context.domain; - qCDebug(avatars) << "Position:" << context.position; - qCDebug(avatars) << "Orientation:" << context.orientation; - qCDebug(avatars) << "Scale:" << context.scale; - qCDebug(avatars) << "Head URL:" << context.headModel; - qCDebug(avatars) << "Skeleton URL:" << context.skeletonModel; - qCDebug(avatars) << "Display Name:" << context.displayName; - qCDebug(avatars) << "Num Attachments:" << context.attachments.size(); - - for (int i = 0; i < context.attachments.size(); ++i) { - qCDebug(avatars) << "Model URL:" << context.attachments[i].modelURL; - qCDebug(avatars) << "Joint Name:" << context.attachments[i].jointName; - qCDebug(avatars) << "Translation:" << context.attachments[i].translation; - qCDebug(avatars) << "Rotation:" << context.attachments[i].rotation; - qCDebug(avatars) << "Scale:" << context.attachments[i].scale; - } - } - - _timer.start(); - record(); -} - -void Recorder::stopRecording() { - qCDebug(avatars) << "Recorder::stopRecording()"; - _timer.invalidate(); - - qCDebug(avatars).nospace() << "Recorded " << _recording->getFrameNumber() << " during " << _recording->getLength() << " msec (" << _recording->getFrameNumber() / (_recording->getLength() / 1000.0f) << " fps)"; -} - -void Recorder::saveToFile(const QString& file) { - if (_recording->isEmpty()) { - qCDebug(avatars) << "Cannot save recording to file, recording is empty."; - } - - writeRecordingToFile(_recording, file); -} - -void Recorder::record() { - if (isRecording()) { - const RecordingContext& context = _recording->getContext(); - RecordingFrame frame; - frame.setBlendshapeCoefficients(_avatar->getHeadData()->getBlendshapeCoefficients()); - - // Capture the full skeleton joint data - auto& jointData = _avatar->getRawJointData(); - frame.setJointArray(jointData); - - frame.setTranslation(context.orientationInv * (_avatar->getPosition() - context.position)); - frame.setRotation(context.orientationInv * _avatar->getOrientation()); - frame.setScale(_avatar->getTargetScale() / context.scale); - - const HeadData* head = _avatar->getHeadData(); - if (head) { - glm::vec3 rotationDegrees = glm::vec3(head->getFinalPitch(), - head->getFinalYaw(), - head->getFinalRoll()); - frame.setHeadRotation(glm::quat(glm::radians(rotationDegrees))); - frame.setLeanForward(head->getLeanForward()); - frame.setLeanSideways(head->getLeanSideways()); - glm::vec3 relativeLookAt = context.orientationInv * - (head->getLookAtPosition() - context.position); - frame.setLookAtPosition(relativeLookAt); - } - - bool wantDebug = false; - if (wantDebug) { - qCDebug(avatars) << "Recording frame #" << _recording->getFrameNumber(); - qCDebug(avatars) << "Blendshapes:" << frame.getBlendshapeCoefficients().size(); - qCDebug(avatars) << "JointArray:" << frame.getJointArray().size(); - qCDebug(avatars) << "Translation:" << frame.getTranslation(); - qCDebug(avatars) << "Rotation:" << frame.getRotation(); - qCDebug(avatars) << "Scale:" << frame.getScale(); - qCDebug(avatars) << "Head rotation:" << frame.getHeadRotation(); - qCDebug(avatars) << "Lean Forward:" << frame.getLeanForward(); - qCDebug(avatars) << "Lean Sideways:" << frame.getLeanSideways(); - qCDebug(avatars) << "LookAtPosition:" << frame.getLookAtPosition(); - } - - _recording->addFrame(_timer.elapsed(), frame); - } -} - -void Recorder::recordAudio(const QByteArray& audioByteArray) { - _recording->addAudioPacket(audioByteArray); -} -#endif diff --git a/libraries/avatars/src/Recorder.h b/libraries/avatars/src/Recorder.h deleted file mode 100644 index 15bffcec8b..0000000000 --- a/libraries/avatars/src/Recorder.h +++ /dev/null @@ -1,57 +0,0 @@ -// -// Recorder.h -// libraries/avatars/src -// -// Created by Clement on 8/7/14. -// Copyright 2014 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_Recorder_h -#define hifi_Recorder_h - -#include - -#if 0 -#include "Recording.h" - -template -class QSharedPointer; - -class AttachmentData; -class AvatarData; -class Recorder; -class Recording; - -typedef QSharedPointer RecorderPointer; -typedef QWeakPointer WeakRecorderPointer; - -/// Records a recording -class Recorder : public QObject { - Q_OBJECT -public: - Recorder(AvatarData* avatar); - - bool isRecording() const; - qint64 elapsed() const; - - RecordingPointer getRecording() const { return _recording; } - -public slots: - void startRecording(); - void stopRecording(); - void saveToFile(const QString& file); - void record(); - void recordAudio(const QByteArray& audioArray); - -private: - QElapsedTimer _timer; - RecordingPointer _recording; - - AvatarData* _avatar; -}; -#endif - -#endif // hifi_Recorder_h diff --git a/libraries/avatars/src/Recording.cpp b/libraries/avatars/src/Recording.cpp deleted file mode 100644 index 884ed495be..0000000000 --- a/libraries/avatars/src/Recording.cpp +++ /dev/null @@ -1,663 +0,0 @@ -// -// Recording.cpp -// -// -// Created by Clement on 9/17/14. -// Copyright 2014 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 -// - -#if 0 -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "AvatarData.h" -#include "AvatarLogging.h" -#include "Recording.h" - -// HFR file format magic number (Inspired by PNG) -// (decimal) 17 72 70 82 13 10 26 10 -// (hexadecimal) 11 48 46 52 0d 0a 1a 0a -// (ASCII C notation) \021 H F R \r \n \032 \n -static const int MAGIC_NUMBER_SIZE = 8; -static const char MAGIC_NUMBER[MAGIC_NUMBER_SIZE] = {17, 72, 70, 82, 13, 10, 26, 10}; -// Version (Major, Minor) -static const QPair VERSION(0, 2); - -int SCALE_RADIX = 10; -int BLENDSHAPE_RADIX = 15; -int LEAN_RADIX = 7; - -void RecordingFrame::setBlendshapeCoefficients(QVector blendshapeCoefficients) { - _blendshapeCoefficients = blendshapeCoefficients; -} - -int Recording::getLength() const { - if (_timestamps.isEmpty()) { - return 0; - } - return _timestamps.last(); -} - -qint32 Recording::getFrameTimestamp(int i) const { - if (i >= _timestamps.size()) { - return getLength(); - } - if (i < 0) { - return 0; - } - return _timestamps[i]; -} - -const RecordingFrame& Recording::getFrame(int i) const { - assert(i < _timestamps.size()); - return _frames[i]; -} - - -int Recording::numberAudioChannel() const { - // Check for stereo audio - float MSEC_PER_SEC = 1000.0f; - float channelLength = ((float)getLength() / MSEC_PER_SEC) * AudioConstants::SAMPLE_RATE * - sizeof(AudioConstants::AudioSample); - return glm::round((float)getAudioData().size() / channelLength); -} - -void Recording::addFrame(int timestamp, RecordingFrame &frame) { - _timestamps << timestamp; - _frames << frame; -} - -void Recording::clear() { - _timestamps.clear(); - _frames.clear(); - _audioData.clear(); -} - -void writeVec3(QDataStream& stream, const glm::vec3& value) { - unsigned char buffer[sizeof(value)]; - memcpy(buffer, &value, sizeof(value)); - stream.writeRawData(reinterpret_cast(buffer), sizeof(value)); -} - -bool readVec3(QDataStream& stream, glm::vec3& value) { - unsigned char buffer[sizeof(value)]; - stream.readRawData(reinterpret_cast(buffer), sizeof(value)); - memcpy(&value, buffer, sizeof(value)); - return true; -} - -void writeQuat(QDataStream& stream, const glm::quat& value) { - unsigned char buffer[256]; - int writtenToBuffer = packOrientationQuatToBytes(buffer, value); - stream.writeRawData(reinterpret_cast(buffer), writtenToBuffer); -} - -bool readQuat(QDataStream& stream, glm::quat& value) { - int quatByteSize = 4 * 2; // 4 floats * 2 bytes - unsigned char buffer[256]; - stream.readRawData(reinterpret_cast(buffer), quatByteSize); - int readFromBuffer = unpackOrientationQuatFromBytes(buffer, value); - if (readFromBuffer != quatByteSize) { - return false; - } - return true; -} - -bool readFloat(QDataStream& stream, float& value, int radix) { - int floatByteSize = 2; // 1 floats * 2 bytes - int16_t buffer[256]; - stream.readRawData(reinterpret_cast(buffer), floatByteSize); - int readFromBuffer = unpackFloatScalarFromSignedTwoByteFixed(buffer, &value, radix); - if (readFromBuffer != floatByteSize) { - return false; - } - return true; -} - -void writeRecordingToFile(RecordingPointer recording, const QString& filename) { - if (!recording || recording->getFrameNumber() < 1) { - qCDebug(avatars) << "Can't save empty recording"; - return; - } - - QElapsedTimer timer; - QFile file(filename); - if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)){ - qCDebug(avatars) << "Couldn't open " << filename; - return; - } - timer.start(); - qCDebug(avatars) << "Writing recording to " << filename << "."; - - QDataStream fileStream(&file); - - // HEADER - file.write(MAGIC_NUMBER, MAGIC_NUMBER_SIZE); // Magic number - fileStream << VERSION; // File format version - const qint64 dataOffsetPos = file.pos(); - fileStream << (quint16)0; // Save two empty bytes for the data offset - const qint64 dataLengthPos = file.pos(); - fileStream << (quint32)0; // Save four empty bytes for the data offset - const quint64 crc16Pos = file.pos(); - fileStream << (quint16)0; // Save two empty bytes for the CRC-16 - - - // METADATA - // TODO - - - - // Write data offset - quint16 dataOffset = file.pos(); - file.seek(dataOffsetPos); - fileStream << dataOffset; - file.seek(dataOffset); - - // CONTEXT - RecordingContext& context = recording->getContext(); - // Global Timestamp - fileStream << context.globalTimestamp; - // Domain - fileStream << context.domain; - // Position - writeVec3(fileStream, context.position); - // Orientation - writeQuat(fileStream, context.orientation); - // Scale - fileStream << context.scale; - // Head model - fileStream << context.headModel; - // Skeleton model - fileStream << context.skeletonModel; - // Display name - fileStream << context.displayName; - // Attachements - fileStream << (quint8)context.attachments.size(); - foreach (AttachmentData data, context.attachments) { - // Model - fileStream << data.modelURL.toString(); - // Joint name - fileStream << data.jointName; - // Position - writeVec3(fileStream, data.translation); - // Orientation - writeQuat(fileStream, data.rotation); - // Scale - fileStream << data.scale; - } - - // RECORDING - fileStream << recording->_timestamps; - - QBitArray mask; - quint32 numBlendshapes = 0; - quint32 numJoints = 0; - - for (int i = 0; i < recording->_timestamps.size(); ++i) { - mask.fill(false); - int maskIndex = 0; - QByteArray buffer; - QDataStream stream(&buffer, QIODevice::WriteOnly); - RecordingFrame& previousFrame = recording->_frames[(i != 0) ? i - 1 : i]; - RecordingFrame& frame = recording->_frames[i]; - - // Blendshape Coefficients - if (i == 0) { - numBlendshapes = frame.getBlendshapeCoefficients().size(); - stream << numBlendshapes; - mask.resize(mask.size() + numBlendshapes); - } - for (quint32 j = 0; j < numBlendshapes; ++j) { - if (i == 0 || - frame._blendshapeCoefficients[j] != previousFrame._blendshapeCoefficients[j]) { - stream << frame.getBlendshapeCoefficients()[j]; - mask.setBit(maskIndex); - } - ++maskIndex; - } - - const auto& jointArray = frame.getJointArray(); - if (i == 0) { - numJoints = jointArray.size(); - stream << numJoints; - // 2 fields per joints - mask.resize(mask.size() + numJoints * 2); - } - for (quint32 j = 0; j < numJoints; j++) { - const auto& joint = jointArray[j]; - if (true) { //(joint.rotationSet) { - writeQuat(stream, joint.rotation); - mask.setBit(maskIndex); - } - maskIndex++; - if (joint.translationSet) { - writeVec3(stream, joint.translation); - mask.setBit(maskIndex); - } - maskIndex++; - } - - // Translation - if (i == 0) { - mask.resize(mask.size() + 1); - } - if (i == 0 || frame._translation != previousFrame._translation) { - writeVec3(stream, frame._translation); - mask.setBit(maskIndex); - } - maskIndex++; - - // Rotation - if (i == 0) { - mask.resize(mask.size() + 1); - } - if (i == 0 || frame._rotation != previousFrame._rotation) { - writeQuat(stream, frame._rotation); - mask.setBit(maskIndex); - } - maskIndex++; - - // Scale - if (i == 0) { - mask.resize(mask.size() + 1); - } - if (i == 0 || frame._scale != previousFrame._scale) { - stream << frame._scale; - mask.setBit(maskIndex); - } - maskIndex++; - - // Head Rotation - if (i == 0) { - mask.resize(mask.size() + 1); - } - if (i == 0 || frame._headRotation != previousFrame._headRotation) { - writeQuat(stream, frame._headRotation); - mask.setBit(maskIndex); - } - maskIndex++; - - // Lean Sideways - if (i == 0) { - mask.resize(mask.size() + 1); - } - if (i == 0 || frame._leanSideways != previousFrame._leanSideways) { - stream << frame._leanSideways; - mask.setBit(maskIndex); - } - maskIndex++; - - // Lean Forward - if (i == 0) { - mask.resize(mask.size() + 1); - } - if (i == 0 || frame._leanForward != previousFrame._leanForward) { - stream << frame._leanForward; - mask.setBit(maskIndex); - } - maskIndex++; - - // LookAt Position - if (i == 0) { - mask.resize(mask.size() + 1); - } - if (i == 0 || frame._lookAtPosition != previousFrame._lookAtPosition) { - writeVec3(stream, frame._lookAtPosition); - mask.setBit(maskIndex); - } - maskIndex++; - - fileStream << mask; - fileStream << buffer; - } - - fileStream << recording->getAudioData(); - - qint64 writingTime = timer.restart(); - // Write data length and CRC-16 - quint32 dataLength = file.pos() - dataOffset; - file.seek(dataOffset); // Go to beginning of data for checksum - quint16 crc16 = qChecksum(file.readAll().constData(), dataLength); - - file.seek(dataLengthPos); - fileStream << dataLength; - file.seek(crc16Pos); - fileStream << crc16; - file.seek(dataOffset + dataLength); - - bool wantDebug = true; - if (wantDebug) { - qCDebug(avatars) << "[DEBUG] WRITE recording"; - qCDebug(avatars) << "Header:"; - qCDebug(avatars) << "File Format version:" << VERSION; - qCDebug(avatars) << "Data length:" << dataLength; - qCDebug(avatars) << "Data offset:" << dataOffset; - qCDebug(avatars) << "CRC-16:" << crc16; - - qCDebug(avatars) << "Context block:"; - qCDebug(avatars) << "Global timestamp:" << context.globalTimestamp; - qCDebug(avatars) << "Domain:" << context.domain; - qCDebug(avatars) << "Position:" << context.position; - qCDebug(avatars) << "Orientation:" << context.orientation; - qCDebug(avatars) << "Scale:" << context.scale; - qCDebug(avatars) << "Head Model:" << context.headModel; - qCDebug(avatars) << "Skeleton Model:" << context.skeletonModel; - qCDebug(avatars) << "Display Name:" << context.displayName; - qCDebug(avatars) << "Num Attachments:" << context.attachments.size(); - for (int i = 0; i < context.attachments.size(); ++i) { - qCDebug(avatars) << "Model URL:" << context.attachments[i].modelURL; - qCDebug(avatars) << "Joint Name:" << context.attachments[i].jointName; - qCDebug(avatars) << "Translation:" << context.attachments[i].translation; - qCDebug(avatars) << "Rotation:" << context.attachments[i].rotation; - qCDebug(avatars) << "Scale:" << context.attachments[i].scale; - } - - qCDebug(avatars) << "Recording:"; - qCDebug(avatars) << "Total frames:" << recording->getFrameNumber(); - qCDebug(avatars) << "Audio array:" << recording->getAudioData().size(); - } - - qint64 checksumTime = timer.elapsed(); - qCDebug(avatars) << "Wrote" << file.size() << "bytes in" << writingTime + checksumTime << "ms. (" << checksumTime << "ms for checksum)"; -} - -RecordingPointer readRecordingFromFile(RecordingPointer recording, const QString& filename) { - QByteArray byteArray; - QUrl url(filename); - QElapsedTimer timer; - timer.start(); // timer used for debug informations (download/parsing time) - - // Aquire the data and place it in byteArray - // Return if data unavailable - if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "ftp") { - // Download file if necessary - qCDebug(avatars) << "Downloading recording at" << url; - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - QEventLoop loop; - QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); // wait for file - if (reply->error() != QNetworkReply::NoError) { - qCDebug(avatars) << "Error while downloading recording: " << reply->error(); - reply->deleteLater(); - return recording; - } - byteArray = reply->readAll(); - reply->deleteLater(); - // print debug + restart timer - qCDebug(avatars) << "Downloaded " << byteArray.size() << " bytes in " << timer.restart() << " ms."; - } else { - // If local file, just read it. - qCDebug(avatars) << "Reading recording from " << filename << "."; - QFile file(filename); - if (!file.open(QIODevice::ReadOnly)){ - qCDebug(avatars) << "Could not open local file: " << url; - return recording; - } - byteArray = file.readAll(); - file.close(); - } - - if (!filename.endsWith(".hfr") && !filename.endsWith(".HFR")) { - qCDebug(avatars) << "File extension not recognized"; - } - - // Reset the recording passed in the arguments - if (!recording) { - recording = QSharedPointer::create(); - } - - QDataStream fileStream(byteArray); - - // HEADER - QByteArray magicNumber(MAGIC_NUMBER, MAGIC_NUMBER_SIZE); - if (!byteArray.startsWith(magicNumber)) { - qCDebug(avatars) << "ERROR: This is not a .HFR file. (Magic Number incorrect)"; - return recording; - } - fileStream.skipRawData(MAGIC_NUMBER_SIZE); - - QPair version; - fileStream >> version; // File format version - if (version != VERSION && version != QPair(0,1)) { - qCDebug(avatars) << "ERROR: This file format version is not supported."; - return recording; - } - - quint16 dataOffset = 0; - fileStream >> dataOffset; - quint32 dataLength = 0; - fileStream >> dataLength; - quint16 crc16 = 0; - fileStream >> crc16; - - - // Check checksum - quint16 computedCRC16 = qChecksum(byteArray.constData() + dataOffset, dataLength); - if (computedCRC16 != crc16) { - qCDebug(avatars) << "Checksum does not match. Bailling!"; - recording.clear(); - return recording; - } - - // METADATA - // TODO - - - - // CONTEXT - RecordingContext& context = recording->getContext(); - // Global Timestamp - fileStream >> context.globalTimestamp; - // Domain - fileStream >> context.domain; - // Position - if (!readVec3(fileStream, context.position)) { - qCDebug(avatars) << "Couldn't read file correctly. (Invalid vec3)"; - recording.clear(); - return recording; - } - // Orientation - if (!readQuat(fileStream, context.orientation)) { - qCDebug(avatars) << "Couldn't read file correctly. (Invalid quat)"; - recording.clear(); - return recording; - } - - // Scale - if (version == QPair(0,1)) { - readFloat(fileStream, context.scale, SCALE_RADIX); - } else { - fileStream >> context.scale; - } - // Head model - fileStream >> context.headModel; - // Skeleton model - fileStream >> context.skeletonModel; - // Display Name - fileStream >> context.displayName; - - // Attachements - quint8 numAttachments = 0; - fileStream >> numAttachments; - for (int i = 0; i < numAttachments; ++i) { - AttachmentData data; - // Model - QString modelURL; - fileStream >> modelURL; - data.modelURL = modelURL; - // Joint name - fileStream >> data.jointName; - // Translation - if (!readVec3(fileStream, data.translation)) { - qCDebug(avatars) << "Couldn't read attachment correctly. (Invalid vec3)"; - continue; - } - // Rotation - if (!readQuat(fileStream, data.rotation)) { - qCDebug(avatars) << "Couldn't read attachment correctly. (Invalid quat)"; - continue; - } - - // Scale - if (version == QPair(0,1)) { - readFloat(fileStream, data.scale, SCALE_RADIX); - } else { - fileStream >> data.scale; - } - context.attachments << data; - } - - quint32 numBlendshapes = 0; - quint32 numJoints = 0; - // RECORDING - fileStream >> recording->_timestamps; - - for (int i = 0; i < recording->_timestamps.size(); ++i) { - QBitArray mask; - QByteArray buffer; - QDataStream stream(&buffer, QIODevice::ReadOnly); - RecordingFrame frame; - RecordingFrame& previousFrame = (i == 0) ? frame : recording->_frames.last(); - - fileStream >> mask; - fileStream >> buffer; - int maskIndex = 0; - - // Blendshape Coefficients - if (i == 0) { - stream >> numBlendshapes; - } - frame._blendshapeCoefficients.resize(numBlendshapes); - for (quint32 j = 0; j < numBlendshapes; ++j) { - if (!mask[maskIndex++]) { - frame._blendshapeCoefficients[j] = previousFrame._blendshapeCoefficients[j]; - } else if (version == QPair(0,1)) { - readFloat(stream, frame._blendshapeCoefficients[j], BLENDSHAPE_RADIX); - } else { - stream >> frame._blendshapeCoefficients[j]; - } - } - // Joint Array - if (i == 0) { - stream >> numJoints; - } - - frame._jointArray.resize(numJoints); - for (quint32 j = 0; j < numJoints; ++j) { - auto& joint = frame._jointArray[2]; - - if (mask[maskIndex++] && readQuat(stream, joint.rotation)) { - joint.rotationSet = true; - } else { - joint.rotationSet = false; - } - - if (mask[maskIndex++] || readVec3(stream, joint.translation)) { - joint.translationSet = true; - } else { - joint.translationSet = false; - } - } - - if (!mask[maskIndex++] || !readVec3(stream, frame._translation)) { - frame._translation = previousFrame._translation; - } - - if (!mask[maskIndex++] || !readQuat(stream, frame._rotation)) { - frame._rotation = previousFrame._rotation; - } - - if (!mask[maskIndex++]) { - frame._scale = previousFrame._scale; - } else if (version == QPair(0,1)) { - readFloat(stream, frame._scale, SCALE_RADIX); - } else { - stream >> frame._scale; - } - - if (!mask[maskIndex++] || !readQuat(stream, frame._headRotation)) { - frame._headRotation = previousFrame._headRotation; - } - - if (!mask[maskIndex++]) { - frame._leanSideways = previousFrame._leanSideways; - } else if (version == QPair(0,1)) { - readFloat(stream, frame._leanSideways, LEAN_RADIX); - } else { - stream >> frame._leanSideways; - } - - if (!mask[maskIndex++]) { - frame._leanForward = previousFrame._leanForward; - } else if (version == QPair(0,1)) { - readFloat(stream, frame._leanForward, LEAN_RADIX); - } else { - stream >> frame._leanForward; - } - - if (!mask[maskIndex++] || !readVec3(stream, frame._lookAtPosition)) { - frame._lookAtPosition = previousFrame._lookAtPosition; - } - - recording->_frames << frame; - } - - QByteArray audioArray; - fileStream >> audioArray; - recording->addAudioPacket(audioArray); - - bool wantDebug = true; - if (wantDebug) { - qCDebug(avatars) << "[DEBUG] READ recording"; - qCDebug(avatars) << "Header:"; - qCDebug(avatars) << "File Format version:" << VERSION; - qCDebug(avatars) << "Data length:" << dataLength; - qCDebug(avatars) << "Data offset:" << dataOffset; - qCDebug(avatars) << "CRC-16:" << crc16; - - qCDebug(avatars) << "Context block:"; - qCDebug(avatars) << "Global timestamp:" << context.globalTimestamp; - qCDebug(avatars) << "Domain:" << context.domain; - qCDebug(avatars) << "Position:" << context.position; - qCDebug(avatars) << "Orientation:" << context.orientation; - qCDebug(avatars) << "Scale:" << context.scale; - qCDebug(avatars) << "Head Model:" << context.headModel; - qCDebug(avatars) << "Skeleton Model:" << context.skeletonModel; - qCDebug(avatars) << "Display Name:" << context.displayName; - qCDebug(avatars) << "Num Attachments:" << numAttachments; - for (int i = 0; i < numAttachments; ++i) { - qCDebug(avatars) << "Model URL:" << context.attachments[i].modelURL; - qCDebug(avatars) << "Joint Name:" << context.attachments[i].jointName; - qCDebug(avatars) << "Translation:" << context.attachments[i].translation; - qCDebug(avatars) << "Rotation:" << context.attachments[i].rotation; - qCDebug(avatars) << "Scale:" << context.attachments[i].scale; - } - - qCDebug(avatars) << "Recording:"; - qCDebug(avatars) << "Total frames:" << recording->getFrameNumber(); - qCDebug(avatars) << "Audio array:" << recording->getAudioData().size(); - - } - - qCDebug(avatars) << "Read " << byteArray.size() << " bytes in " << timer.elapsed() << " ms."; - return recording; -} - -#endif diff --git a/libraries/avatars/src/Recording.h b/libraries/avatars/src/Recording.h deleted file mode 100644 index a5829b1e2f..0000000000 --- a/libraries/avatars/src/Recording.h +++ /dev/null @@ -1,131 +0,0 @@ -// -// Recording.h -// -// -// Created by Clement on 9/17/14. -// Copyright 2014 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_Recording_h -#define hifi_Recording_h - -#if 0 - -#include -#include - -#include -#include - -template -class QSharedPointer; - -class AttachmentData; -class Recording; -class RecordingFrame; -class Sound; -class JointData; - -typedef QSharedPointer RecordingPointer; - -/// Stores recordings static data -class RecordingContext { -public: - quint64 globalTimestamp; - QString domain; - glm::vec3 position; - glm::quat orientation; - float scale; - QString headModel; - QString skeletonModel; - QString displayName; - QVector attachments; - - // This avoids recomputation every frame while recording. - glm::quat orientationInv; -}; - -/// Stores a recording -class Recording { -public: - bool isEmpty() const { return _timestamps.isEmpty(); } - int getLength() const; // in ms - - RecordingContext& getContext() { return _context; } - int getFrameNumber() const { return _frames.size(); } - qint32 getFrameTimestamp(int i) const; - const RecordingFrame& getFrame(int i) const; - const QByteArray& getAudioData() const { return _audioData; } - int numberAudioChannel() const; - -protected: - void addFrame(int timestamp, RecordingFrame& frame); - void addAudioPacket(const QByteArray& byteArray) { _audioData.append(byteArray); } - void clear(); - -private: - RecordingContext _context; - QVector _timestamps; - QVector _frames; - - QByteArray _audioData; - - friend class Recorder; - friend class Player; - friend void writeRecordingToFile(RecordingPointer recording, const QString& file); - friend RecordingPointer readRecordingFromFile(RecordingPointer recording, const QString& file); - friend RecordingPointer readRecordingFromRecFile(RecordingPointer recording, const QString& filename, - const QByteArray& byteArray); -}; - -/// Stores the different values associated to one recording frame -class RecordingFrame { -public: - QVector getBlendshapeCoefficients() const { return _blendshapeCoefficients; } - QVector getJointArray() const { return _jointArray; } - glm::vec3 getTranslation() const { return _translation; } - glm::quat getRotation() const { return _rotation; } - float getScale() const { return _scale; } - glm::quat getHeadRotation() const { return _headRotation; } - float getLeanSideways() const { return _leanSideways; } - float getLeanForward() const { return _leanForward; } - glm::vec3 getLookAtPosition() const { return _lookAtPosition; } - -protected: - void setBlendshapeCoefficients(QVector blendshapeCoefficients); - void setJointArray(const QVector& jointArray) { _jointArray = jointArray; } - void setTranslation(const glm::vec3& translation) { _translation = translation; } - void setRotation(const glm::quat& rotation) { _rotation = rotation; } - void setScale(float scale) { _scale = scale; } - void setHeadRotation(glm::quat headRotation) { _headRotation = headRotation; } - void setLeanSideways(float leanSideways) { _leanSideways = leanSideways; } - void setLeanForward(float leanForward) { _leanForward = leanForward; } - void setLookAtPosition(const glm::vec3& lookAtPosition) { _lookAtPosition = lookAtPosition; } - -private: - QVector _blendshapeCoefficients; - QVector _jointArray; - - glm::vec3 _translation; - glm::quat _rotation; - float _scale; - glm::quat _headRotation; - float _leanSideways; - float _leanForward; - glm::vec3 _lookAtPosition; - - friend class Recorder; - friend void writeRecordingToFile(RecordingPointer recording, const QString& file); - friend RecordingPointer readRecordingFromFile(RecordingPointer recording, const QString& file); - friend RecordingPointer readRecordingFromRecFile(RecordingPointer recording, const QString& filename, - const QByteArray& byteArray); -}; - -void writeRecordingToFile(RecordingPointer recording, const QString& filename); -RecordingPointer readRecordingFromFile(RecordingPointer recording, const QString& filename); -RecordingPointer readRecordingFromRecFile(RecordingPointer recording, const QString& filename, const QByteArray& byteArray); -#endif -#endif // hifi_Recording_h diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 1f5777a0e4..12e976d2bd 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -122,7 +122,7 @@ void EntityTreeRenderer::init() { } void EntityTreeRenderer::shutdown() { - _entitiesScriptEngine->disconnect(); // disconnect all slots/signals from the script engine + _entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential _shuttingDown = true; } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index f1be8611e1..782458894d 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -219,9 +219,11 @@ void RenderableModelEntityItem::render(RenderArgs* args) { if (hasModel()) { if (_model) { - if (getModelURL() != _model->getURL().toString()) { - qDebug() << "Updating model URL: " << getModelURL(); - _model->setURL(getModelURL()); + // check if the URL has changed + auto& currentURL = getParsedModelURL(); + if (currentURL != _model->getURL()) { + qDebug().noquote() << "Updating model URL: " << currentURL.toDisplayString(); + _model->setURL(currentURL); } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 75bc26a560..992b6a1bdd 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -622,7 +622,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef auto nodeList = DependencyManager::get(); const QUuid& myNodeID = nodeList->getSessionUUID(); bool weOwnSimulation = _simulationOwner.matchesValidID(myNodeID); - + if (args.bitstreamVersion >= VERSION_ENTITIES_HAVE_SIMULATION_OWNER_AND_ACTIONS_OVER_WIRE) { // pack SimulationOwner and terse update properties near each other @@ -799,17 +799,11 @@ void EntityItem::setDensity(float density) { _density = glm::max(glm::min(density, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); } -const float ACTIVATION_RELATIVE_DENSITY_DELTA = 0.01f; // 1 percent - void EntityItem::updateDensity(float density) { float clampedDensity = glm::max(glm::min(density, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); if (_density != clampedDensity) { _density = clampedDensity; - - if (fabsf(_density - clampedDensity) / _density > ACTIVATION_RELATIVE_DENSITY_DELTA) { - // the density has changed enough that we should update the physics simulation - _dirtyFlags |= Simulation::DIRTY_MASS; - } + _dirtyFlags |= Simulation::DIRTY_MASS; } } @@ -822,11 +816,16 @@ void EntityItem::setMass(float mass) { // compute new density const float MIN_VOLUME = 1.0e-6f; // 0.001mm^3 + float newDensity = 1.0f; if (volume < 1.0e-6f) { // avoid divide by zero - _density = glm::min(mass / MIN_VOLUME, ENTITY_ITEM_MAX_DENSITY); + newDensity = glm::min(mass / MIN_VOLUME, ENTITY_ITEM_MAX_DENSITY); } else { - _density = glm::max(glm::min(mass / volume, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); + newDensity = glm::max(glm::min(mass / volume, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); + } + if (_density != newDensity) { + _density = newDensity; + _dirtyFlags |= Simulation::DIRTY_MASS; } } @@ -884,12 +883,12 @@ void EntityItem::simulateKinematicMotion(float timeElapsed, bool setFlags) { #ifdef WANT_DEBUG qCDebug(entities) << "EntityItem::simulateKinematicMotion timeElapsed" << timeElapsed; #endif - + const float MIN_TIME_SKIP = 0.0f; const float MAX_TIME_SKIP = 1.0f; // in seconds - + timeElapsed = glm::clamp(timeElapsed, MIN_TIME_SKIP, MAX_TIME_SKIP); - + if (hasActions()) { return; } @@ -1312,24 +1311,16 @@ void EntityItem::updatePosition(const glm::vec3& value) { if (shouldSuppressLocationEdits()) { return; } - auto delta = glm::distance(getPosition(), value); - if (delta > IGNORE_POSITION_DELTA) { - _dirtyFlags |= Simulation::DIRTY_POSITION; + if (getPosition() != value) { setPosition(value); - if (delta > ACTIVATION_POSITION_DELTA) { - _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - } + _dirtyFlags |= Simulation::DIRTY_POSITION; } } void EntityItem::updateDimensions(const glm::vec3& value) { - auto delta = glm::distance(getDimensions(), value); - if (delta > IGNORE_DIMENSIONS_DELTA) { + if (getDimensions() != value) { setDimensions(value); - if (delta > ACTIVATION_DIMENSIONS_DELTA) { - // rebuilding the shape will always activate - _dirtyFlags |= (Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); - } + _dirtyFlags |= (Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); } } @@ -1339,14 +1330,7 @@ void EntityItem::updateRotation(const glm::quat& rotation) { } if (getRotation() != rotation) { setRotation(rotation); - - auto alignmentDot = glm::abs(glm::dot(getRotation(), rotation)); - if (alignmentDot < IGNORE_ALIGNMENT_DOT) { - _dirtyFlags |= Simulation::DIRTY_ROTATION; - } - if (alignmentDot < ACTIVATION_ALIGNMENT_DOT) { - _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - } + _dirtyFlags |= Simulation::DIRTY_ROTATION; } } @@ -1367,11 +1351,8 @@ void EntityItem::updateMass(float mass) { newDensity = glm::max(glm::min(mass / volume, ENTITY_ITEM_MAX_DENSITY), ENTITY_ITEM_MIN_DENSITY); } - float oldDensity = _density; - _density = newDensity; - - if (fabsf(_density - oldDensity) / _density > ACTIVATION_RELATIVE_DENSITY_DELTA) { - // the density has changed enough that we should update the physics simulation + if (_density != newDensity) { + _density = newDensity; _dirtyFlags |= Simulation::DIRTY_MASS; } } @@ -1380,38 +1361,29 @@ void EntityItem::updateVelocity(const glm::vec3& value) { if (shouldSuppressLocationEdits()) { return; } - auto delta = glm::distance(_velocity, value); - if (delta > IGNORE_LINEAR_VELOCITY_DELTA) { - _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; + if (_velocity != value) { const float MIN_LINEAR_SPEED = 0.001f; if (glm::length(value) < MIN_LINEAR_SPEED) { _velocity = ENTITY_ITEM_ZERO_VEC3; } else { _velocity = value; - // only activate when setting non-zero velocity - if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { - _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - } } + _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; } } void EntityItem::updateDamping(float value) { auto clampedDamping = glm::clamp(value, 0.0f, 1.0f); - if (fabsf(_damping - clampedDamping) > IGNORE_DAMPING_DELTA) { + if (_damping != clampedDamping) { _damping = clampedDamping; _dirtyFlags |= Simulation::DIRTY_MATERIAL; } } void EntityItem::updateGravity(const glm::vec3& value) { - auto delta = glm::distance(_gravity, value); - if (delta > IGNORE_GRAVITY_DELTA) { + if (_gravity != value) { _gravity = value; _dirtyFlags |= Simulation::DIRTY_LINEAR_VELOCITY; - if (delta > ACTIVATION_GRAVITY_DELTA) { - _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - } } } @@ -1419,25 +1391,20 @@ void EntityItem::updateAngularVelocity(const glm::vec3& value) { if (shouldSuppressLocationEdits()) { return; } - auto delta = glm::distance(_angularVelocity, value); - if (delta > IGNORE_ANGULAR_VELOCITY_DELTA) { - _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; + if (_angularVelocity != value) { const float MIN_ANGULAR_SPEED = 0.0002f; if (glm::length(value) < MIN_ANGULAR_SPEED) { _angularVelocity = ENTITY_ITEM_ZERO_VEC3; } else { _angularVelocity = value; - // only activate when setting non-zero velocity - if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { - _dirtyFlags |= Simulation::DIRTY_PHYSICS_ACTIVATION; - } } + _dirtyFlags |= Simulation::DIRTY_ANGULAR_VELOCITY; } } void EntityItem::updateAngularDamping(float value) { auto clampedDamping = glm::clamp(value, 0.0f, 1.0f); - if (fabsf(_angularDamping - clampedDamping) > IGNORE_DAMPING_DELTA) { + if (_angularDamping != clampedDamping) { _angularDamping = clampedDamping; _dirtyFlags |= Simulation::DIRTY_MATERIAL; } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 5ceccef4b1..d80de4d427 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -48,6 +48,7 @@ namespace render { class PendingChanges; } +/* // these thesholds determine what updates will be ignored (client and server) const float IGNORE_POSITION_DELTA = 0.0001f; const float IGNORE_DIMENSIONS_DELTA = 0.0005f; @@ -64,6 +65,7 @@ const float ACTIVATION_ALIGNMENT_DOT = 0.99990f; const float ACTIVATION_LINEAR_VELOCITY_DELTA = 0.01f; const float ACTIVATION_GRAVITY_DELTA = 0.1f; const float ACTIVATION_ANGULAR_VELOCITY_DELTA = 0.03f; +*/ #define DONT_ALLOW_INSTANTIATION virtual void pureVirtualFunctionPlaceHolder() = 0; #define ALLOW_INSTANTIATION virtual void pureVirtualFunctionPlaceHolder() { }; @@ -398,6 +400,7 @@ public: void getAllTerseUpdateProperties(EntityItemProperties& properties) const; void flagForOwnership() { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_OWNERSHIP; } + void flagForMotionStateChange() { _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; } bool addAction(EntitySimulation* simulation, EntityActionPointer action); bool updateAction(EntitySimulation* simulation, const QUuid& actionID, const QVariantMap& arguments); diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index e8ffcab3e7..fb41ac4b77 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -63,6 +63,7 @@ public: static const QString DEFAULT_MODEL_URL; const QString& getModelURL() const { return _modelURL; } + const QUrl& getParsedModelURL() const { return _parsedModelURL; } static const QString DEFAULT_COMPOUND_SHAPE_URL; const QString& getCompoundShapeURL() const { return _compoundShapeURL; } @@ -75,7 +76,7 @@ public: } // model related properties - void setModelURL(const QString& url) { _modelURL = url; } + void setModelURL(const QString& url) { _modelURL = url; _parsedModelURL = QUrl(url); } virtual void setCompoundShapeURL(const QString& url); @@ -134,6 +135,7 @@ protected: rgbColor _color; QString _modelURL; + QUrl _parsedModelURL; QString _compoundShapeURL; AnimationPropertyGroup _animationProperties; diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp index 8ac019ff56..83d91b32d3 100644 --- a/libraries/networking/src/AssetClient.cpp +++ b/libraries/networking/src/AssetClient.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include "AssetRequest.h" @@ -374,16 +375,21 @@ void AssetScriptingInterface::uploadData(QString data, QString extension, QScrip return; } - QObject::connect(upload, &AssetUpload::finished, this, [callback, extension](AssetUpload* upload, const QString& hash) mutable { + QObject::connect(upload, &AssetUpload::finished, this, [this, callback, extension](AssetUpload* upload, const QString& hash) mutable { if (callback.isFunction()) { QString url = "atp://" + hash + "." + extension; QScriptValueList args { url }; - callback.call(QScriptValue(), args); + callback.call(_engine->currentContext()->thisObject(), args); } }); upload->start(); } +AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) : + _engine(engine) +{ +} + void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) { const QString ATP_SCHEME { "atp://" }; @@ -410,14 +416,14 @@ void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callb _pendingRequests << assetRequest; - connect(assetRequest, &AssetRequest::finished, [this, callback](AssetRequest* request) mutable { + connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable { Q_ASSERT(request->getState() == AssetRequest::Finished); if (request->getError() == AssetRequest::Error::NoError) { if (callback.isFunction()) { QString data = QString::fromUtf8(request->getData()); QScriptValueList args { data }; - callback.call(QScriptValue(), args); + callback.call(_engine->currentContext()->thisObject(), args); } } diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h index f1bb210614..0616317eec 100644 --- a/libraries/networking/src/AssetClient.h +++ b/libraries/networking/src/AssetClient.h @@ -74,10 +74,13 @@ private: class AssetScriptingInterface : public QObject { Q_OBJECT public: + AssetScriptingInterface(QScriptEngine* engine); + Q_INVOKABLE void uploadData(QString data, QString extension, QScriptValue callback); Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete); protected: QSet _pendingRequests; + QScriptEngine* _engine; }; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 181ae7060e..34439e57ce 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -95,7 +95,7 @@ void EntityMotionState::updateServerPhysicsVariables(const QUuid& sessionID) { } // virtual -bool EntityMotionState::handleEasyChanges(uint32_t flags, PhysicsEngine* engine) { +bool EntityMotionState::handleEasyChanges(uint32_t& flags, PhysicsEngine* engine) { assert(entityTreeIsLocked()); updateServerPhysicsVariables(engine->getSessionID()); ObjectMotionState::handleEasyChanges(flags, engine); @@ -120,7 +120,7 @@ bool EntityMotionState::handleEasyChanges(uint32_t flags, PhysicsEngine* engine) } if (flags & Simulation::DIRTY_SIMULATOR_OWNERSHIP) { // (DIRTY_SIMULATOR_OWNERSHIP really means "we should bid for ownership with SCRIPT priority") - // we're manipulating this object directly via script, so we artificially + // we're manipulating this object directly via script, so we artificially // manipulate the logic to trigger an immediate bid for ownership setOutgoingPriority(SCRIPT_EDIT_SIMULATION_PRIORITY); } @@ -133,7 +133,7 @@ bool EntityMotionState::handleEasyChanges(uint32_t flags, PhysicsEngine* engine) // virtual -bool EntityMotionState::handleHardAndEasyChanges(uint32_t flags, PhysicsEngine* engine) { +bool EntityMotionState::handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) { updateServerPhysicsVariables(engine->getSessionID()); return ObjectMotionState::handleHardAndEasyChanges(flags, engine); } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 188e7096b9..c666f87221 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -29,8 +29,8 @@ public: virtual ~EntityMotionState(); void updateServerPhysicsVariables(const QUuid& sessionID); - virtual bool handleEasyChanges(uint32_t flags, PhysicsEngine* engine); - virtual bool handleHardAndEasyChanges(uint32_t flags, PhysicsEngine* engine); + virtual bool handleEasyChanges(uint32_t& flags, PhysicsEngine* engine); + virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine); /// \return MOTION_TYPE_DYNAMIC or MOTION_TYPE_STATIC based on params set in EntityItem virtual MotionType computeObjectMotionType() const; diff --git a/libraries/physics/src/ObjectAction.cpp b/libraries/physics/src/ObjectAction.cpp index 3188283f68..17b565ba21 100644 --- a/libraries/physics/src/ObjectAction.cpp +++ b/libraries/physics/src/ObjectAction.cpp @@ -246,6 +246,18 @@ void ObjectAction::activateBody() { } } +void ObjectAction::forceBodyNonStatic() { + auto ownerEntity = _ownerEntity.lock(); + if (!ownerEntity) { + return; + } + void* physicsInfo = ownerEntity->getPhysicsInfo(); + ObjectMotionState* motionState = static_cast(physicsInfo); + if (motionState && motionState->getMotionType() == MOTION_TYPE_STATIC) { + ownerEntity->flagForMotionStateChange(); + } +} + bool ObjectAction::lifetimeIsOver() { if (_expires == 0) { return false; diff --git a/libraries/physics/src/ObjectAction.h b/libraries/physics/src/ObjectAction.h index afb6745e9c..e44036eadc 100644 --- a/libraries/physics/src/ObjectAction.h +++ b/libraries/physics/src/ObjectAction.h @@ -63,6 +63,7 @@ protected: virtual glm::vec3 getAngularVelocity(); virtual void setAngularVelocity(glm::vec3 angularVelocity); virtual void activateBody(); + virtual void forceBodyNonStatic(); EntityItemWeakPointer _ownerEntity; QString _tag; diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp index be0edafff5..c39f47eaf8 100644 --- a/libraries/physics/src/ObjectMotionState.cpp +++ b/libraries/physics/src/ObjectMotionState.cpp @@ -17,6 +17,14 @@ #include "PhysicsHelpers.h" #include "PhysicsLogging.h" +// these thresholds determine what updates (object-->body) will activate the physical object +const float ACTIVATION_POSITION_DELTA = 0.005f; +const float ACTIVATION_ALIGNMENT_DOT = 0.99990f; +const float ACTIVATION_LINEAR_VELOCITY_DELTA = 0.01f; +const float ACTIVATION_GRAVITY_DELTA = 0.1f; +const float ACTIVATION_ANGULAR_VELOCITY_DELTA = 0.03f; + + // origin of physics simulation in world-frame glm::vec3 _worldOffset(0.0f); @@ -128,28 +136,65 @@ void ObjectMotionState::setRigidBody(btRigidBody* body) { } } -bool ObjectMotionState::handleEasyChanges(uint32_t flags, PhysicsEngine* engine) { +bool ObjectMotionState::handleEasyChanges(uint32_t& flags, PhysicsEngine* engine) { if (flags & Simulation::DIRTY_POSITION) { - btTransform worldTrans; - if (flags & Simulation::DIRTY_ROTATION) { - worldTrans.setRotation(glmToBullet(getObjectRotation())); - } else { - worldTrans = _body->getWorldTransform(); + btTransform worldTrans = _body->getWorldTransform(); + btVector3 newPosition = glmToBullet(getObjectPosition()); + float delta = (newPosition - worldTrans.getOrigin()).length(); + if (delta > ACTIVATION_POSITION_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } + worldTrans.setOrigin(newPosition); + + if (flags & Simulation::DIRTY_ROTATION) { + btQuaternion newRotation = glmToBullet(getObjectRotation()); + float alignmentDot = fabsf(worldTrans.getRotation().dot(newRotation)); + if (alignmentDot < ACTIVATION_ALIGNMENT_DOT) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } + worldTrans.setRotation(newRotation); } - worldTrans.setOrigin(glmToBullet(getObjectPosition())); _body->setWorldTransform(worldTrans); } else if (flags & Simulation::DIRTY_ROTATION) { btTransform worldTrans = _body->getWorldTransform(); - worldTrans.setRotation(glmToBullet(getObjectRotation())); + btQuaternion newRotation = glmToBullet(getObjectRotation()); + float alignmentDot = fabsf(worldTrans.getRotation().dot(newRotation)); + if (alignmentDot < ACTIVATION_ALIGNMENT_DOT) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } + worldTrans.setRotation(newRotation); _body->setWorldTransform(worldTrans); } if (flags & Simulation::DIRTY_LINEAR_VELOCITY) { - _body->setLinearVelocity(glmToBullet(getObjectLinearVelocity())); - _body->setGravity(glmToBullet(getObjectGravity())); + btVector3 newLinearVelocity = glmToBullet(getObjectLinearVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newLinearVelocity - _body->getLinearVelocity()).length(); + if (delta > ACTIVATION_LINEAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } + } + _body->setLinearVelocity(newLinearVelocity); + + btVector3 newGravity = glmToBullet(getObjectGravity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newGravity - _body->getGravity()).length(); + if (delta > ACTIVATION_GRAVITY_DELTA || + (delta > 0.0f && _body->getGravity().length2() == 0.0f)) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } + } + _body->setGravity(newGravity); } if (flags & Simulation::DIRTY_ANGULAR_VELOCITY) { - _body->setAngularVelocity(glmToBullet(getObjectAngularVelocity())); + btVector3 newAngularVelocity = glmToBullet(getObjectAngularVelocity()); + if (!(flags & Simulation::DIRTY_PHYSICS_ACTIVATION)) { + float delta = (newAngularVelocity - _body->getAngularVelocity()).length(); + if (delta > ACTIVATION_ANGULAR_VELOCITY_DELTA) { + flags |= Simulation::DIRTY_PHYSICS_ACTIVATION; + } + } + _body->setAngularVelocity(newAngularVelocity); } if (flags & Simulation::DIRTY_MATERIAL) { @@ -163,7 +208,7 @@ bool ObjectMotionState::handleEasyChanges(uint32_t flags, PhysicsEngine* engine) return true; } -bool ObjectMotionState::handleHardAndEasyChanges(uint32_t flags, PhysicsEngine* engine) { +bool ObjectMotionState::handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) { if (flags & Simulation::DIRTY_SHAPE) { // make sure the new shape is valid if (!isReadyToComputeShape()) { @@ -195,8 +240,8 @@ bool ObjectMotionState::handleHardAndEasyChanges(uint32_t flags, PhysicsEngine* if (flags & EASY_DIRTY_PHYSICS_FLAGS) { handleEasyChanges(flags, engine); } - // it is possible that there are no HARD flags at this point (if DIRTY_SHAPE was removed) - // so we check again befoe we reinsert: + // it is possible there are no HARD flags at this point (if DIRTY_SHAPE was removed) + // so we check again before we reinsert: if (flags & HARD_DIRTY_PHYSICS_FLAGS) { engine->reinsertObject(this); } diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index 992bdd11d7..7d5c727d6d 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -80,8 +80,8 @@ public: ObjectMotionState(btCollisionShape* shape); ~ObjectMotionState(); - virtual bool handleEasyChanges(uint32_t flags, PhysicsEngine* engine); - virtual bool handleHardAndEasyChanges(uint32_t flags, PhysicsEngine* engine); + virtual bool handleEasyChanges(uint32_t& flags, PhysicsEngine* engine); + virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine); void updateBodyMaterialProperties(); void updateBodyVelocities(); diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 665ae9706d..10e285186c 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -244,14 +244,14 @@ void PhysicsEngine::stepSimulation() { float timeStep = btMin(dt, MAX_TIMESTEP); if (_myAvatarController) { - // ADEBUG TODO: move this stuff outside and in front of stepSimulation, because + // TODO: move this stuff outside and in front of stepSimulation, because // the updateShapeIfNecessary() call needs info from MyAvatar and should // be done on the main thread during the pre-simulation stuff if (_myAvatarController->needsRemoval()) { _myAvatarController->setDynamicsWorld(nullptr); // We must remove any existing contacts for the avatar so that any new contacts will have - // valid data. MyAvatar's RigidBody is the ONLY one in the simulation that does not yet + // valid data. MyAvatar's RigidBody is the ONLY one in the simulation that does not yet // have a MotionState so we pass nullptr to removeContacts(). removeContacts(nullptr); } diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 6f82e22b8d..25abf2e09e 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -56,10 +56,12 @@ bool RecordingScriptingInterface::loadRecording(const QString& url) { using namespace recording; auto loader = ClipCache::instance().getClipLoader(url); - QEventLoop loop; - QObject::connect(loader.data(), &Resource::loaded, &loop, &QEventLoop::quit); - QObject::connect(loader.data(), &Resource::failed, &loop, &QEventLoop::quit); - loop.exec(); + if (!loader->isLoaded()) { + QEventLoop loop; + QObject::connect(loader.data(), &Resource::loaded, &loop, &QEventLoop::quit); + QObject::connect(loader.data(), &Resource::failed, &loop, &QEventLoop::quit); + loop.exec(); + } if (!loader->isLoaded()) { qWarning() << "Clip failed to load from " << url; diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f313cd7d9e..995a92bf83 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -124,14 +124,9 @@ static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNameString, bool wantSignals) : _scriptContents(scriptContents), - _isFinished(false), - _isRunning(false), - _isInitialized(false), _timerFunctionMap(), _wantSignals(wantSignals), _fileNameString(fileNameString), - _isUserLoaded(false), - _isReloading(false), _arrayBufferClass(new ArrayBufferClass(this)) { _allScriptsMutex.lock(); @@ -140,6 +135,8 @@ ScriptEngine::ScriptEngine(const QString& scriptContents, const QString& fileNam } ScriptEngine::~ScriptEngine() { + qCDebug(scriptengine) << "Script Engine shutting down (destructor) for script:" << getFilename(); + // If we're not already in the middle of stopping all scripts, then we should remove ourselves // from the list of running scripts. We don't do this if we're in the process of stopping all scripts // because that method removes scripts from its list as it iterates them @@ -150,11 +147,21 @@ ScriptEngine::~ScriptEngine() { } } +void ScriptEngine::disconnectNonEssentialSignals() { + disconnect(); + connect(this, &ScriptEngine::doneRunning, thread(), &QThread::quit); +} + void ScriptEngine::runInThread() { + _isThreaded = true; QThread* workerThread = new QThread(); // thread is not owned, so we need to manage the delete QString scriptEngineName = QString("Script Thread:") + getFilename(); workerThread->setObjectName(scriptEngineName); + // NOTE: If you connect any essential signals for proper shutdown or cleanup of + // the script engine, make sure to add code to "reconnect" them to the + // disconnectNonEssentialSignals() method + // when the worker thread is started, call our engine's run.. connect(workerThread, &QThread::started, this, &ScriptEngine::run); @@ -176,12 +183,13 @@ void ScriptEngine::runInThread() { QSet ScriptEngine::_allKnownScriptEngines; QMutex ScriptEngine::_allScriptsMutex; bool ScriptEngine::_stoppingAllScripts = false; -bool ScriptEngine::_doneRunningThisScript = false; void ScriptEngine::stopAllScripts(QObject* application) { _allScriptsMutex.lock(); _stoppingAllScripts = true; + qCDebug(scriptengine) << "Stopping all scripts.... currently known scripts:" << _allKnownScriptEngines.size(); + QMutableSetIterator i(_allKnownScriptEngines); while (i.hasNext()) { ScriptEngine* scriptEngine = i.next(); @@ -219,7 +227,9 @@ void ScriptEngine::stopAllScripts(QObject* application) { // We need to wait for the engine to be done running before we proceed, because we don't // want any of the scripts final "scriptEnding()" or pending "update()" methods from accessing // any application state after we leave this stopAllScripts() method + qCDebug(scriptengine) << "waiting on script:" << scriptName; scriptEngine->waitTillDoneRunning(); + qCDebug(scriptengine) << "done waiting on script:" << scriptName; // If the script is stopped, we can remove it from our set i.remove(); @@ -227,21 +237,19 @@ void ScriptEngine::stopAllScripts(QObject* application) { } _stoppingAllScripts = false; _allScriptsMutex.unlock(); + qCDebug(scriptengine) << "DONE Stopping all scripts...."; } void ScriptEngine::waitTillDoneRunning() { // If the script never started running or finished running before we got here, we don't need to wait for it - if (_isRunning) { - - _doneRunningThisScript = false; // NOTE: this is static, we serialize our waiting for scripts to finish + if (_isRunning && _isThreaded) { // NOTE: waitTillDoneRunning() will be called on the main Application thread, inside of stopAllScripts() // we want the application thread to continue to process events, because the scripts will likely need to // marshall messages across to the main thread. For example if they access Settings or Meny in any of their // shutdown code. - while (!_doneRunningThisScript) { - + while (thread()->isRunning()) { // process events for the main application thread, allowing invokeMethod calls to pass between threads QCoreApplication::processEvents(); } @@ -752,8 +760,6 @@ void ScriptEngine::run() { emit runningStateChanged(); emit doneRunning(); } - - _doneRunningThisScript = true; } // NOTE: This is private because it must be called on the same thread that created the timers, which is why @@ -1168,7 +1174,7 @@ void ScriptEngine::refreshFileScript(const EntityItemID& entityID) { QString filePath = QUrl(details.scriptText).toLocalFile(); auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch(); if (lastModified > details.lastModified) { - qDebug() << "Reloading modified script " << details.scriptText; + qCDebug(scriptengine) << "Reloading modified script " << details.scriptText; QFile file(filePath); file.open(QIODevice::ReadOnly); diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index 1412ba7aaf..fed3384d0b 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -128,6 +128,7 @@ public: bool isFinished() const { return _isFinished; } // used by Application and ScriptWidget bool isRunning() const { return _isRunning; } // used by ScriptWidget + void disconnectNonEssentialSignals(); static void stopAllScripts(QObject* application); // used by Application on shutdown //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -165,15 +166,16 @@ signals: protected: QString _scriptContents; QString _parentURL; - bool _isFinished; - bool _isRunning; - int _evaluatesPending = 0; - bool _isInitialized; + bool _isFinished { false }; + bool _isRunning { false }; + int _evaluatesPending { 0 }; + bool _isInitialized { false }; QHash _timerFunctionMap; QSet _includedURLs; - bool _wantSignals = true; + bool _wantSignals { true }; QHash _entityScripts; -private: + bool _isThreaded { false }; + void init(); QString getFilename() const; void waitTillDoneRunning(); @@ -191,12 +193,12 @@ private: Quat _quatLibrary; Vec3 _vec3Library; ScriptUUID _uuidLibrary; - bool _isUserLoaded; - bool _isReloading; + bool _isUserLoaded { false }; + bool _isReloading { false }; ArrayBufferClass* _arrayBufferClass; - AssetScriptingInterface _assetScriptingInterface; + AssetScriptingInterface _assetScriptingInterface{ this }; QHash _registeredHandlers; void forwardHandlerCall(const EntityItemID& entityID, const QString& eventName, QScriptValueList eventHanderArgs); @@ -205,8 +207,6 @@ private: static QSet _allKnownScriptEngines; static QMutex _allScriptsMutex; static bool _stoppingAllScripts; - static bool _doneRunningThisScript; - }; #endif // hifi_ScriptEngine_h diff --git a/libraries/shared/src/Transform.cpp b/libraries/shared/src/Transform.cpp index d0aef8a9be..c51b3dae4b 100644 --- a/libraries/shared/src/Transform.cpp +++ b/libraries/shared/src/Transform.cpp @@ -128,17 +128,25 @@ QJsonObject Transform::toJson(const Transform& transform) { } QJsonObject result; - auto json = toJsonValue(transform.getTranslation()); - if (!json.isNull()) { - result[JSON_TRANSLATION] = json; + if (transform.getTranslation() != vec3()) { + auto json = toJsonValue(transform.getTranslation()); + if (!json.isNull()) { + result[JSON_TRANSLATION] = json; + } } - json = toJsonValue(transform.getRotation()); - if (!json.isNull()) { - result[JSON_ROTATION] = json; + + if (transform.getRotation() != quat()) { + auto json = toJsonValue(transform.getRotation()); + if (!json.isNull()) { + result[JSON_ROTATION] = json; + } } - json = toJsonValue(transform.getScale()); - if (!json.isNull()) { - result[JSON_SCALE] = json; + + if (transform.getScale() != vec3(1.0f)) { + auto json = toJsonValue(transform.getScale()); + if (!json.isNull()) { + result[JSON_SCALE] = json; + } } return result; } diff --git a/stack-manager/CMakeLists.txt b/stack-manager/CMakeLists.txt new file mode 100644 index 0000000000..e70fefc6e0 --- /dev/null +++ b/stack-manager/CMakeLists.txt @@ -0,0 +1,48 @@ +set(TARGET_NAME "stack-manager") +set(BUILD_BUNDLE YES) +setup_hifi_project(Widgets Gui Svg Core Network WebKitWidgets) + +if (WIN32) + target_zlib() +endif () +target_quazip() + +set_target_properties( + ${TARGET_NAME} PROPERTIES + EXCLUDE_FROM_ALL TRUE +) + +if (DEFINED ENV{JOB_ID}) + set(PR_BUILD "false") + set(BUILD_SEQ $ENV{JOB_ID}) + set(BASE_URL "http://s3.amazonaws.com/hifi-public") +else () + set(BUILD_SEQ "dev") + if (DEFINED ENV{PR_NUMBER}) + set(PR_BUILD "true") + set(BASE_URL "http://s3.amazonaws.com/hifi-public/pr-builds/$ENV{PR_NUMBER}") + else () + set(PR_BUILD "false") + set(BASE_URL "http://s3.amazonaws.com/hifi-public") + endif () +endif () + +configure_file(src/StackManagerVersion.h.in "${PROJECT_BINARY_DIR}/includes/StackManagerVersion.h") +include_directories( + ${PROJECT_BINARY_DIR}/includes + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/ui + ${QUAZIP_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} +) + +if (APPLE) + set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) + set(MACOSX_BUNDLE_BUNDLE_NAME "Stack Manager") + set(MACOSX_BUNDLE_GUI_IDENTIFIER io.highfidelity.StackManager) + set(MACOSX_BUNDLE_ICON_FILE icon.icns) + set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + set(SM_SRCS ${SM_SRCS} "${CMAKE_CURRENT_SOURCE_DIR}/assets/icon.icns") +endif () + +package_libraries_for_deployment() \ No newline at end of file diff --git a/stack-manager/assets/assignment-run.svg b/stack-manager/assets/assignment-run.svg new file mode 100644 index 0000000000..4005d58fbf --- /dev/null +++ b/stack-manager/assets/assignment-run.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/stack-manager/assets/assignment-stop.svg b/stack-manager/assets/assignment-stop.svg new file mode 100644 index 0000000000..ecc1b190c4 --- /dev/null +++ b/stack-manager/assets/assignment-stop.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + diff --git a/stack-manager/assets/icon.icns b/stack-manager/assets/icon.icns new file mode 100644 index 0000000000..711c7f6380 Binary files /dev/null and b/stack-manager/assets/icon.icns differ diff --git a/stack-manager/assets/icon.ico b/stack-manager/assets/icon.ico new file mode 100644 index 0000000000..0c0c023bdb Binary files /dev/null and b/stack-manager/assets/icon.ico differ diff --git a/stack-manager/assets/icon.png b/stack-manager/assets/icon.png new file mode 100644 index 0000000000..3889ed729e Binary files /dev/null and b/stack-manager/assets/icon.png differ diff --git a/stack-manager/assets/logo-larger.png b/stack-manager/assets/logo-larger.png new file mode 100644 index 0000000000..7872e44b5a Binary files /dev/null and b/stack-manager/assets/logo-larger.png differ diff --git a/stack-manager/assets/server-start.svg b/stack-manager/assets/server-start.svg new file mode 100644 index 0000000000..e95d2f3a25 --- /dev/null +++ b/stack-manager/assets/server-start.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stack-manager/assets/server-stop.svg b/stack-manager/assets/server-stop.svg new file mode 100644 index 0000000000..13bf8f3067 --- /dev/null +++ b/stack-manager/assets/server-stop.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stack-manager/content-sets/content-sets.html b/stack-manager/content-sets/content-sets.html new file mode 100644 index 0000000000..05c5f4d6f7 --- /dev/null +++ b/stack-manager/content-sets/content-sets.html @@ -0,0 +1,64 @@ + + + + High Fidelity Stack Manager Content Sets + + + + + + + + + + + + + + +
+
+
+
Click on the name of one of the content sets below to replace your local content with that set.
+
Note that the content set you choose may change the index path ('/') in your domain-server settings.
+
+
+
+ +
+
+ + + + diff --git a/stack-manager/content-sets/content-sets.json b/stack-manager/content-sets/content-sets.json new file mode 100644 index 0000000000..250c40adba --- /dev/null +++ b/stack-manager/content-sets/content-sets.json @@ -0,0 +1,27 @@ +{ + "floating-island": { + "name": "Floating Island", + "description": "Start your galactic empire with this floating island and small oasis. Build it up and share it with your friends.", + "path": "/1064.2,75.6,915.1/0.0000127922,0.71653,0.0000684642,0.697556" + }, + "low-poly-floating-island": { + "name": "Low-poly Floating Island", + "description": "Impressionism with polygons. If you want your virtual island to be nothing but a beautiful painting, this is the aesthetic for you.", + "path": "/8216.88,580.568,8264.03/-0.000192036,-0.838296,-0.000124955,0.545216" + }, + "mid-century-modern-living-room": { + "name": "Mid-century Modern Living Room", + "description": "Timeless, mid-century modern beauty. Notice the classic Eames Recliner and the beautiful built-in shelving.", + "path": "/8206.22,22.8716,8210.47/1.61213e-06,0.814919,1.44589e-06,0.579575" + }, + "bar" : { + "name": "The Bar", + "description": "A sexy club scene to plan your parties and live shows.", + "path": "/1048.52,9.5386,1005.7/-0.0000565125,-0.395713,-0.000131155,0.918374" + }, + "space": { + "name": "Space", + "description": "Vast, empty, nothingness. A completely clean slate for you to start building anything you desire.", + "path": "/1000,100,100" + } +} diff --git a/stack-manager/src/AppDelegate.cpp b/stack-manager/src/AppDelegate.cpp new file mode 100644 index 0000000000..ea9310a2d8 --- /dev/null +++ b/stack-manager/src/AppDelegate.cpp @@ -0,0 +1,776 @@ +// +// AppDelegate.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 06/27/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include + +#include "AppDelegate.h" +#include "BackgroundProcess.h" +#include "GlobalData.h" +#include "DownloadManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const QString HIGH_FIDELITY_API_URL = "https://metaverse.highfidelity.com/api/v1"; + +const QString CHECK_BUILDS_URL = "https://highfidelity.com/builds.xml"; + +// Use a custom User-Agent to avoid ModSecurity filtering, e.g. by hosting providers. +const QByteArray HIGH_FIDELITY_USER_AGENT = "Mozilla/5.0 (HighFidelity)"; + +const int VERSION_CHECK_INTERVAL_MS = 86400000; // a day + +const int WAIT_FOR_CHILD_MSECS = 5000; + +void signalHandler(int param) { + AppDelegate* app = AppDelegate::getInstance(); + + app->quit(); +} + +static QTextStream* outStream = NULL; + +void myMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { + Q_UNUSED(context); + + QString dateTime = QDateTime::currentDateTime().toString("dd/MM/yyyy hh:mm:ss"); + QString txt = QString("[%1] ").arg(dateTime); + + //in this function, you can write the message to any stream! + switch (type) { + case QtDebugMsg: + fprintf(stdout, "Debug: %s\n", qPrintable(msg)); + txt += msg; + break; + case QtWarningMsg: + fprintf(stdout, "Warning: %s\n", qPrintable(msg)); + txt += msg; + break; + case QtCriticalMsg: + fprintf(stdout, "Critical: %s\n", qPrintable(msg)); + txt += msg; + break; + case QtFatalMsg: + fprintf(stdout, "Fatal: %s\n", qPrintable(msg)); + txt += msg; + } + + if (outStream) { + *outStream << txt << endl; + } +} + +AppDelegate::AppDelegate(int argc, char* argv[]) : + QApplication(argc, argv), + _qtReady(false), + _dsReady(false), + _dsResourcesReady(false), + _acReady(false), + _domainServerProcess(NULL), + _acMonitorProcess(NULL), + _domainServerName("localhost") +{ + // be a signal handler for SIGTERM so we can stop child processes if we get it + signal(SIGTERM, signalHandler); + + // look for command-line options + parseCommandLine(); + + setApplicationName("Stack Manager"); + setOrganizationName("High Fidelity"); + setOrganizationDomain("io.highfidelity.StackManager"); + + QFile* logFile = new QFile("last_run_log", this); + if (!logFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) { + qDebug() << "Failed to open log file. Will not be able to write STDOUT/STDERR to file."; + } else { + outStream = new QTextStream(logFile); + } + + + qInstallMessageHandler(myMessageHandler); + _domainServerProcess = new BackgroundProcess(GlobalData::getInstance().getDomainServerExecutablePath(), this); + _acMonitorProcess = new BackgroundProcess(GlobalData::getInstance().getAssignmentClientExecutablePath(), this); + + _manager = new QNetworkAccessManager(this); + + _window = new MainWindow(); + + createExecutablePath(); + downloadLatestExecutablesAndRequirements(); + + _checkVersionTimer.setInterval(0); + connect(&_checkVersionTimer, SIGNAL(timeout()), this, SLOT(checkVersion())); + _checkVersionTimer.start(); + + connect(this, &QApplication::aboutToQuit, this, &AppDelegate::stopStack); +} + +AppDelegate::~AppDelegate() { + QHash::iterator it = _scriptProcesses.begin(); + + qDebug() << "Stopping scripted assignment-client processes prior to quit."; + while (it != _scriptProcesses.end()) { + BackgroundProcess* backgroundProcess = it.value(); + + // remove from the script processes hash + it = _scriptProcesses.erase(it); + + // make sure the process is dead + backgroundProcess->terminate(); + backgroundProcess->waitForFinished(); + backgroundProcess->deleteLater(); + } + + qDebug() << "Stopping domain-server process prior to quit."; + _domainServerProcess->terminate(); + _domainServerProcess->waitForFinished(); + + qDebug() << "Stopping assignment-client process prior to quit."; + _acMonitorProcess->terminate(); + _acMonitorProcess->waitForFinished(); + + _domainServerProcess->deleteLater(); + _acMonitorProcess->deleteLater(); + + _window->deleteLater(); + + delete outStream; + outStream = NULL; +} + +void AppDelegate::parseCommandLine() { + QCommandLineParser parser; + parser.setApplicationDescription("High Fidelity Stack Manager"); + parser.addHelpOption(); + + const QCommandLineOption helpOption = parser.addHelpOption(); + + const QCommandLineOption hifiBuildDirectoryOption("b", "Path to build of hifi", "build-directory"); + parser.addOption(hifiBuildDirectoryOption); + + if (!parser.parse(QCoreApplication::arguments())) { + qCritical() << parser.errorText() << endl; + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(helpOption)) { + parser.showHelp(); + Q_UNREACHABLE(); + } + + if (parser.isSet(hifiBuildDirectoryOption)) { + const QString hifiBuildDirectory = parser.value(hifiBuildDirectoryOption); + qDebug() << "hifiBuildDirectory=" << hifiBuildDirectory << "\n"; + GlobalData::getInstance().setHifiBuildDirectory(hifiBuildDirectory); + } +} + +void AppDelegate::toggleStack(bool start) { + toggleDomainServer(start); + toggleAssignmentClientMonitor(start); + toggleScriptedAssignmentClients(start); + emit stackStateChanged(start); +} + +void AppDelegate::toggleDomainServer(bool start) { + + if (start) { + _domainServerProcess->start(QStringList()); + + _window->getLogsWidget()->addTab(_domainServerProcess->getLogViewer(), "Domain Server"); + + if (_domainServerID.isEmpty()) { + // after giving the domain server some time to set up, ask for its ID + QTimer::singleShot(1000, this, SLOT(requestDomainServerID())); + } + } else { + _domainServerProcess->terminate(); + _domainServerProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + _domainServerProcess->kill(); + } +} + +void AppDelegate::toggleAssignmentClientMonitor(bool start) { + if (start) { + _acMonitorProcess->start(QStringList() << "--min" << "5"); + _window->getLogsWidget()->addTab(_acMonitorProcess->getLogViewer(), "Assignment Clients"); + } else { + _acMonitorProcess->terminate(); + _acMonitorProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + _acMonitorProcess->kill(); + } +} + +void AppDelegate::toggleScriptedAssignmentClients(bool start) { + foreach(BackgroundProcess* scriptProcess, _scriptProcesses) { + if (start) { + scriptProcess->start(scriptProcess->getLastArgList()); + } else { + scriptProcess->terminate(); + scriptProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + scriptProcess->kill(); + } + } +} + +int AppDelegate::startScriptedAssignment(const QUuid& scriptID, const QString& pool) { + + BackgroundProcess* scriptProcess = _scriptProcesses.value(scriptID); + + if (!scriptProcess) { + QStringList argList = QStringList() << "-t" << "2"; + if (!pool.isEmpty()) { + argList << "--pool" << pool; + } + + scriptProcess = new BackgroundProcess(GlobalData::getInstance().getAssignmentClientExecutablePath(), + this); + + scriptProcess->start(argList); + + qint64 processID = scriptProcess->processId(); + _scriptProcesses.insert(scriptID, scriptProcess); + + _window->getLogsWidget()->addTab(scriptProcess->getLogViewer(), "Scripted Assignment " + + QString::number(processID)); + } else { + scriptProcess->QProcess::start(); + } + + return scriptProcess->processId(); +} + +void AppDelegate::stopScriptedAssignment(BackgroundProcess* backgroundProcess) { + _window->getLogsWidget()->removeTab(_window->getLogsWidget()->indexOf(backgroundProcess->getLogViewer())); + backgroundProcess->terminate(); + backgroundProcess->waitForFinished(WAIT_FOR_CHILD_MSECS); + backgroundProcess->kill(); +} + +void AppDelegate::stopScriptedAssignment(const QUuid& scriptID) { + BackgroundProcess* processValue = _scriptProcesses.take(scriptID); + if (processValue) { + stopScriptedAssignment(processValue); + } +} + + +void AppDelegate::requestDomainServerID() { + // ask the domain-server for its ID so we can update the accessible name + emit domainAddressChanged(); + QUrl domainIDURL = GlobalData::getInstance().getDomainServerBaseUrl() + "/id"; + + qDebug() << "Requesting domain server ID from" << domainIDURL.toString(); + + QNetworkReply* idReply = _manager->get(QNetworkRequest(domainIDURL)); + + connect(idReply, &QNetworkReply::finished, this, &AppDelegate::handleDomainIDReply); +} + +const QString AppDelegate::getServerAddress() const { + return "hifi://" + _domainServerName; +} + +void AppDelegate::handleDomainIDReply() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + _domainServerID = QString(reply->readAll()); + + if (!_domainServerID.isEmpty()) { + + if (!QUuid(_domainServerID).isNull()) { + qDebug() << "The domain server ID is" << _domainServerID; + qDebug() << "Asking High Fidelity API for associated domain name."; + + // fire off a request to high fidelity API to see if this domain exists with them + QUrl domainGetURL = HIGH_FIDELITY_API_URL + "/domains/" + _domainServerID; + QNetworkReply* domainGetReply = _manager->get(QNetworkRequest(domainGetURL)); + connect(domainGetReply, &QNetworkReply::finished, this, &AppDelegate::handleDomainGetReply); + } else { + emit domainServerIDMissing(); + } + } + } else { + qDebug() << "Error getting domain ID from domain-server - " + << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() + << reply->errorString(); + } +} + +void AppDelegate::handleDomainGetReply() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + QJsonDocument responseDocument = QJsonDocument::fromJson(reply->readAll()); + + QJsonObject domainObject = responseDocument.object()["domain"].toObject(); + + const QString DOMAIN_NAME_KEY = "name"; + const QString DOMAIN_OWNER_PLACES_KEY = "owner_places"; + + if (domainObject.contains(DOMAIN_NAME_KEY)) { + _domainServerName = domainObject[DOMAIN_NAME_KEY].toString(); + } else if (domainObject.contains(DOMAIN_OWNER_PLACES_KEY)) { + QJsonArray ownerPlaces = domainObject[DOMAIN_OWNER_PLACES_KEY].toArray(); + if (ownerPlaces.size() > 0) { + _domainServerName = ownerPlaces[0].toObject()[DOMAIN_NAME_KEY].toString(); + } + } + + qDebug() << "This domain server's name is" << _domainServerName << "- updating address link."; + + emit domainAddressChanged(); + } +} + +void AppDelegate::changeDomainServerIndexPath(const QString& newPath) { + if (!newPath.isEmpty()) { + QString pathsJSON = "{\"paths\": { \"/\": { \"viewpoint\": \"%1\" }}}"; + + QNetworkRequest settingsRequest(GlobalData::getInstance().getDomainServerBaseUrl() + "/settings.json"); + settingsRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QNetworkReply* settingsReply = _manager->post(settingsRequest, pathsJSON.arg(newPath).toLocal8Bit()); + connect(settingsReply, &QNetworkReply::finished, this, &AppDelegate::handleChangeIndexPathResponse); + } +} + +void AppDelegate::handleChangeIndexPathResponse() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + + qDebug() << "Successfully changed index path in domain-server."; + emit indexPathChangeResponse(true); + } else { + qDebug() << "Error changing domain-server index path-" << reply->errorString(); + emit indexPathChangeResponse(false); + } +} + +void AppDelegate::downloadContentSet(const QUrl& contentSetURL) { + // make sure this link was an svo + if (contentSetURL.path().endsWith(".svo")) { + // setup a request for this content set + QNetworkRequest contentRequest(contentSetURL); + QNetworkReply* contentReply = _manager->get(contentRequest); + connect(contentReply, &QNetworkReply::finished, this, &AppDelegate::handleContentSetDownloadFinished); + } +} + +void AppDelegate::handleContentSetDownloadFinished() { + QNetworkReply* reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::NoError + && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + + QString modelFilename = GlobalData::getInstance().getClientsResourcesPath() + "models.svo"; + + // write the model file + QFile modelFile(modelFilename); + modelFile.open(QIODevice::WriteOnly); + + // stop the base assignment clients before we try to write the new content + toggleAssignmentClientMonitor(false); + + if (modelFile.write(reply->readAll()) == -1) { + qDebug() << "Error writing content set to" << modelFilename; + modelFile.close(); + toggleAssignmentClientMonitor(true); + } else { + qDebug() << "Wrote new content set to" << modelFilename; + modelFile.close(); + + // restart the assignment-client + toggleAssignmentClientMonitor(true); + + emit contentSetDownloadResponse(true); + + // did we have a path in the query? + // if so when we need to set the DS index path to that path + QUrlQuery svoQuery(reply->url().query()); + changeDomainServerIndexPath(svoQuery.queryItemValue("path")); + + emit domainAddressChanged(); + + return; + } + } + + // if we failed we need to emit our signal with a fail + emit contentSetDownloadResponse(false); + emit domainAddressChanged(); +} + +void AppDelegate::onFileSuccessfullyInstalled(const QUrl& url) { + if (url == GlobalData::getInstance().getRequirementsURL()) { + _qtReady = true; + } else if (url == GlobalData::getInstance().getAssignmentClientURL()) { + _acReady = true; + } else if (url == GlobalData::getInstance().getDomainServerURL()) { + _dsReady = true; + } else if (url == GlobalData::getInstance().getDomainServerResourcesURL()) { + _dsResourcesReady = true; + } + + if (_qtReady && _acReady && _dsReady && _dsResourcesReady) { + _window->setRequirementsLastChecked(QDateTime::currentDateTime().toString()); + _window->show(); + toggleStack(true); + } +} + +void AppDelegate::createExecutablePath() { + QDir launchDir(GlobalData::getInstance().getClientsLaunchPath()); + QDir resourcesDir(GlobalData::getInstance().getClientsResourcesPath()); + QDir logsDir(GlobalData::getInstance().getLogsPath()); + if (!launchDir.exists()) { + if (QDir().mkpath(launchDir.absolutePath())) { + qDebug() << "Successfully created directory: " + << launchDir.absolutePath(); + } else { + qCritical() << "Failed to create directory: " + << launchDir.absolutePath(); + } + } + if (!resourcesDir.exists()) { + if (QDir().mkpath(resourcesDir.absolutePath())) { + qDebug() << "Successfully created directory: " + << resourcesDir.absolutePath(); + } else { + qCritical() << "Failed to create directory: " + << resourcesDir.absolutePath(); + } + } + if (!logsDir.exists()) { + if (QDir().mkpath(logsDir.absolutePath())) { + qDebug() << "Successfully created directory: " + << logsDir.absolutePath(); + } else { + qCritical() << "Failed to create directory: " + << logsDir.absolutePath(); + } + } +} + +void AppDelegate::downloadLatestExecutablesAndRequirements() { + // Check if Qt is already installed + if (GlobalData::getInstance().getPlatform() == "mac") { + if (QDir(GlobalData::getInstance().getClientsLaunchPath() + "QtCore.framework").exists()) { + _qtReady = true; + } + } else if (GlobalData::getInstance().getPlatform() == "win") { + if (QFileInfo(GlobalData::getInstance().getClientsLaunchPath() + "Qt5Core.dll").exists()) { + _qtReady = true; + } + } else { // linux + if (QFileInfo(GlobalData::getInstance().getClientsLaunchPath() + "libQt5Core.so.5").exists()) { + _qtReady = true; + } + } + + + QFile reqZipFile(GlobalData::getInstance().getRequirementsZipPath()); + QByteArray reqZipData; + if (reqZipFile.open(QIODevice::ReadOnly)) { + reqZipData = reqZipFile.readAll(); + reqZipFile.close(); + } + QFile resZipFile(GlobalData::getInstance().getDomainServerResourcesZipPath()); + QByteArray resZipData; + if (resZipFile.open(QIODevice::ReadOnly)) { + resZipData = resZipFile.readAll(); + resZipFile.close(); + } + + QDir resourcesDir(GlobalData::getInstance().getClientsResourcesPath()); + if (!(resourcesDir.entryInfoList(QDir::AllEntries).size() < 3)) { + _dsResourcesReady = true; + } + + // if the user has set hifiBuildDirectory, don't attempt to download the domain-server or assignement-client + if (GlobalData::getInstance().isGetHifiBuildDirectorySet()) { + _dsReady = true; + _acReady = true; + } else { + QByteArray dsData; + QFile dsFile(GlobalData::getInstance().getDomainServerExecutablePath()); + if (dsFile.open(QIODevice::ReadOnly)) { + dsData = dsFile.readAll(); + dsFile.close(); + } + QByteArray acData; + QFile acFile(GlobalData::getInstance().getAssignmentClientExecutablePath()); + if (acFile.open(QIODevice::ReadOnly)) { + acData = acFile.readAll(); + acFile.close(); + } + + QNetworkRequest acReq(QUrl(GlobalData::getInstance().getAssignmentClientMD5URL())); + QNetworkReply* acReply = _manager->get(acReq); + QEventLoop acLoop; + connect(acReply, SIGNAL(finished()), &acLoop, SLOT(quit())); + acLoop.exec(); + QByteArray acMd5Data = acReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows-generated + // binary data of the MD5 hash + QTextStream stream(acMd5Data); + stream >> acMd5Data; + } + + // fix for Mac and Linux network accessibility + if (acMd5Data.size() == 0) { + // network is not accessible + qDebug() << "Could not connect to the internet."; + _window->show(); + return; + } + + qDebug() << "AC MD5: " << acMd5Data; + if (acMd5Data.toLower() == QCryptographicHash::hash(acData, QCryptographicHash::Md5).toHex()) { + _acReady = true; + } + + + QNetworkRequest dsReq(QUrl(GlobalData::getInstance().getDomainServerMD5URL())); + QNetworkReply* dsReply = _manager->get(dsReq); + QEventLoop dsLoop; + connect(dsReply, SIGNAL(finished()), &dsLoop, SLOT(quit())); + dsLoop.exec(); + QByteArray dsMd5Data = dsReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows generated + // binary data of the MD5 hash + QTextStream stream(dsMd5Data); + stream >> dsMd5Data; + } + qDebug() << "DS MD5: " << dsMd5Data; + if (dsMd5Data.toLower() == QCryptographicHash::hash(dsData, QCryptographicHash::Md5).toHex()) { + _dsReady = true; + } + } + + if (_qtReady) { + // check MD5 of requirements.zip only if Qt is found + QNetworkRequest reqZipReq(QUrl(GlobalData::getInstance().getRequirementsMD5URL())); + QNetworkReply* reqZipReply = _manager->get(reqZipReq); + QEventLoop reqZipLoop; + connect(reqZipReply, SIGNAL(finished()), &reqZipLoop, SLOT(quit())); + reqZipLoop.exec(); + QByteArray reqZipMd5Data = reqZipReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows generated + // binary data of the MD5 hash + QTextStream stream(reqZipMd5Data); + stream >> reqZipMd5Data; + } + qDebug() << "Requirements ZIP MD5: " << reqZipMd5Data; + if (reqZipMd5Data.toLower() != QCryptographicHash::hash(reqZipData, QCryptographicHash::Md5).toHex()) { + _qtReady = false; + } + } + + if (_dsResourcesReady) { + // check MD5 of resources.zip only if Domain Server + // resources are installed + QNetworkRequest resZipReq(QUrl(GlobalData::getInstance().getDomainServerResourcesMD5URL())); + QNetworkReply* resZipReply = _manager->get(resZipReq); + QEventLoop resZipLoop; + connect(resZipReply, SIGNAL(finished()), &resZipLoop, SLOT(quit())); + resZipLoop.exec(); + QByteArray resZipMd5Data = resZipReply->readAll().trimmed(); + if (GlobalData::getInstance().getPlatform() == "win") { + // fix for reading the MD5 hash from Windows generated + // binary data of the MD5 hash + QTextStream stream(resZipMd5Data); + stream >> resZipMd5Data; + } + qDebug() << "Domain Server Resources ZIP MD5: " << resZipMd5Data; + if (resZipMd5Data.toLower() != QCryptographicHash::hash(resZipData, QCryptographicHash::Md5).toHex()) { + _dsResourcesReady = false; + } + } + + DownloadManager* downloadManager = 0; + if (!_qtReady || !_acReady || !_dsReady || !_dsResourcesReady) { + // initialise DownloadManager + downloadManager = new DownloadManager(_manager); + downloadManager->setWindowModality(Qt::ApplicationModal); + connect(downloadManager, SIGNAL(fileSuccessfullyInstalled(QUrl)), + SLOT(onFileSuccessfullyInstalled(QUrl))); + downloadManager->show(); + } else { + _window->setRequirementsLastChecked(QDateTime::currentDateTime().toString()); + _window->show(); + toggleStack(true); + } + + if (!_qtReady) { + downloadManager->downloadFile(GlobalData::getInstance().getRequirementsURL()); + } + + if (!_acReady) { + downloadManager->downloadFile(GlobalData::getInstance().getAssignmentClientURL()); + } + + if (!_dsReady) { + downloadManager->downloadFile(GlobalData::getInstance().getDomainServerURL()); + } + + if (!_dsResourcesReady) { + downloadManager->downloadFile(GlobalData::getInstance().getDomainServerResourcesURL()); + } +} + +void AppDelegate::checkVersion() { + QNetworkRequest latestVersionRequest((QUrl(CHECK_BUILDS_URL))); + latestVersionRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + latestVersionRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); + QNetworkReply* reply = _manager->get(latestVersionRequest); + connect(reply, &QNetworkReply::finished, this, &AppDelegate::parseVersionXml); + + _checkVersionTimer.setInterval(VERSION_CHECK_INTERVAL_MS); + _checkVersionTimer.start(); +} + +struct VersionInformation { + QString version; + QUrl downloadUrl; + QString timeStamp; + QString releaseNotes; +}; + +void AppDelegate::parseVersionXml() { + +#ifdef Q_OS_WIN32 + QString operatingSystem("windows"); +#endif + +#ifdef Q_OS_MAC + QString operatingSystem("mac"); +#endif + +#ifdef Q_OS_LINUX + QString operatingSystem("ubuntu"); +#endif + + QNetworkReply* sender = qobject_cast(QObject::sender()); + QXmlStreamReader xml(sender); + + QHash projectVersions; + + while (!xml.atEnd() && !xml.hasError()) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "project") { + QString projectName = ""; + foreach(const QXmlStreamAttribute &attr, xml.attributes()) { + if (attr.name().toString() == "name") { + projectName = attr.value().toString(); + break; + } + } + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "project")) { + if (projectName != "") { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "platform") { + QString platformName = ""; + foreach(const QXmlStreamAttribute &attr, xml.attributes()) { + if (attr.name().toString() == "name") { + platformName = attr.value().toString(); + break; + } + } + int latestVersion = 0; + VersionInformation latestVersionInformation; + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "platform")) { + if (platformName == operatingSystem) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "build") { + VersionInformation buildVersionInformation; + while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name().toString() == "build")) { + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "version") { + xml.readNext(); + buildVersionInformation.version = xml.text().toString(); + } + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "url") { + xml.readNext(); + buildVersionInformation.downloadUrl = QUrl(xml.text().toString()); + } + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "timestamp") { + xml.readNext(); + buildVersionInformation.timeStamp = xml.text().toString(); + } + if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name().toString() == "note") { + xml.readNext(); + if (buildVersionInformation.releaseNotes != "") { + buildVersionInformation.releaseNotes += "\n"; + } + buildVersionInformation.releaseNotes += xml.text().toString(); + } + xml.readNext(); + } + if (latestVersion < buildVersionInformation.version.toInt()) { + latestVersionInformation = buildVersionInformation; + latestVersion = buildVersionInformation.version.toInt(); + } + } + } + xml.readNext(); + } + if (latestVersion>0) { + projectVersions[projectName] = latestVersionInformation; + } + } + } + xml.readNext(); + } + } + xml.readNext(); + } + +#ifdef WANT_DEBUG + qDebug() << "parsed projects for OS" << operatingSystem; + QHashIterator projectVersion(projectVersions); + while (projectVersion.hasNext()) { + projectVersion.next(); + qDebug() << "project:" << projectVersion.key(); + qDebug() << "version:" << projectVersion.value().version; + qDebug() << "downloadUrl:" << projectVersion.value().downloadUrl.toString(); + qDebug() << "timeStamp:" << projectVersion.value().timeStamp; + qDebug() << "releaseNotes:" << projectVersion.value().releaseNotes; + } +#endif + + if (projectVersions.contains("stackmanager")) { + VersionInformation latestVersion = projectVersions["stackmanager"]; + if (QCoreApplication::applicationVersion() != latestVersion.version && QCoreApplication::applicationVersion() != "dev") { + _window->setUpdateNotification("There is an update available. Please download and install version " + latestVersion.version + "."); + _window->update(); + } + } + + sender->deleteLater(); +} diff --git a/stack-manager/src/AppDelegate.h b/stack-manager/src/AppDelegate.h new file mode 100644 index 0000000000..1d4728b7ce --- /dev/null +++ b/stack-manager/src/AppDelegate.h @@ -0,0 +1,89 @@ +// +// AppDelegate.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 06/27/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_AppDelegate_h +#define hifi_AppDelegate_h + + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "MainWindow.h" + +class BackgroundProcess; + +class AppDelegate : public QApplication +{ + Q_OBJECT +public: + static AppDelegate* getInstance() { return static_cast(QCoreApplication::instance()); } + + AppDelegate(int argc, char* argv[]); + ~AppDelegate(); + + void toggleStack(bool start); + void toggleDomainServer(bool start); + void toggleAssignmentClientMonitor(bool start); + void toggleScriptedAssignmentClients(bool start); + + int startScriptedAssignment(const QUuid& scriptID, const QString& pool = QString()); + void stopScriptedAssignment(BackgroundProcess* backgroundProcess); + void stopScriptedAssignment(const QUuid& scriptID); + + void stopStack() { toggleStack(false); } + + const QString getServerAddress() const; +public slots: + void downloadContentSet(const QUrl& contentSetURL); +signals: + void domainServerIDMissing(); + void domainAddressChanged(); + void contentSetDownloadResponse(bool wasSuccessful); + void indexPathChangeResponse(bool wasSuccessful); + void stackStateChanged(bool isOn); +private slots: + void onFileSuccessfullyInstalled(const QUrl& url); + void requestDomainServerID(); + void handleDomainIDReply(); + void handleDomainGetReply(); + void handleChangeIndexPathResponse(); + void handleContentSetDownloadFinished(); + void checkVersion(); + void parseVersionXml(); + +private: + void parseCommandLine(); + void createExecutablePath(); + void downloadLatestExecutablesAndRequirements(); + + void changeDomainServerIndexPath(const QString& newPath); + + QNetworkAccessManager* _manager; + bool _qtReady; + bool _dsReady; + bool _dsResourcesReady; + bool _acReady; + BackgroundProcess* _domainServerProcess; + BackgroundProcess* _acMonitorProcess; + QHash _scriptProcesses; + + QString _domainServerID; + QString _domainServerName; + + QTimer _checkVersionTimer; + + MainWindow* _window; +}; + +#endif diff --git a/stack-manager/src/BackgroundProcess.cpp b/stack-manager/src/BackgroundProcess.cpp new file mode 100644 index 0000000000..e5d0452a83 --- /dev/null +++ b/stack-manager/src/BackgroundProcess.cpp @@ -0,0 +1,125 @@ +// +// BackgroundProcess.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/03/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "BackgroundProcess.h" +#include "GlobalData.h" + +#include +#include +#include +#include +#include +#include +#include + +const int LOG_CHECK_INTERVAL_MS = 500; + +const QString DATETIME_FORMAT = "yyyy-MM-dd_hh.mm.ss"; +const QString LOGS_DIRECTORY = "/Logs/"; + +BackgroundProcess::BackgroundProcess(const QString& program, QObject *parent) : + QProcess(parent), + _program(program), + _stdoutFilePos(0), + _stderrFilePos(0) +{ + _logViewer = new LogViewer; + + connect(this, SIGNAL(started()), SLOT(processStarted())); + connect(this, SIGNAL(error(QProcess::ProcessError)), SLOT(processError())); + + _logFilePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + _logFilePath.append(LOGS_DIRECTORY); + QDir logDir(_logFilePath); + if (!logDir.exists(_logFilePath)) { + logDir.mkpath(_logFilePath); + } + + _logTimer.setInterval(LOG_CHECK_INTERVAL_MS); + _logTimer.setSingleShot(false); + connect(&_logTimer, SIGNAL(timeout()), this, SLOT(receivedStandardError())); + connect(&_logTimer, SIGNAL(timeout()), this, SLOT(receivedStandardOutput())); + connect(this, SIGNAL(started()), &_logTimer, SLOT(start())); + + setWorkingDirectory(GlobalData::getInstance().getClientsLaunchPath()); +} + +void BackgroundProcess::start(const QStringList& arguments) { + QDateTime now = QDateTime::currentDateTime(); + QString nowString = now.toString(DATETIME_FORMAT); + QFileInfo programFile(_program); + QString baseFilename = _logFilePath + programFile.completeBaseName(); + _stdoutFilename = QString("%1_stdout_%2.txt").arg(baseFilename, nowString); + _stderrFilename = QString("%1_stderr_%2.txt").arg(baseFilename, nowString); + + qDebug() << "stdout for " << _program << " being written to: " << _stdoutFilename; + qDebug() << "stderr for " << _program << " being written to: " << _stderrFilename; + + // reset the stdout and stderr file positions + _stdoutFilePos = 0; + _stderrFilePos = 0; + + // clear our LogViewer + _logViewer->clear(); + + // reset our output and error files + setStandardOutputFile(_stdoutFilename); + setStandardErrorFile(_stderrFilename); + + _lastArgList = arguments; + + QProcess::start(_program, arguments); +} + +void BackgroundProcess::processStarted() { + qDebug() << "process " << _program << " started."; +} + +void BackgroundProcess::processError() { + qDebug() << "process error for" << _program << "-" << errorString(); +} + +void BackgroundProcess::receivedStandardOutput() { + QString output; + + QFile file(_stdoutFilename); + + if (!file.open(QIODevice::ReadOnly)) return; + + if (file.size() > _stdoutFilePos) { + file.seek(_stdoutFilePos); + output = file.readAll(); + _stdoutFilePos = file.pos(); + } + + file.close(); + + if (!output.isEmpty() && !output.isNull()) { + _logViewer->appendStandardOutput(output); + } +} + +void BackgroundProcess::receivedStandardError() { + QString output; + + QFile file(_stderrFilename); + + if (!file.open(QIODevice::ReadOnly)) return; + + if (file.size() > _stderrFilePos) { + file.seek(_stderrFilePos); + output = file.readAll(); + _stderrFilePos = file.pos(); + } + + file.close(); + + if (!output.isEmpty() && !output.isNull()) { + _logViewer->appendStandardError(output); + } +} diff --git a/stack-manager/src/BackgroundProcess.h b/stack-manager/src/BackgroundProcess.h new file mode 100644 index 0000000000..fd652ba73e --- /dev/null +++ b/stack-manager/src/BackgroundProcess.h @@ -0,0 +1,48 @@ +// +// BackgroundProcess.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/03/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_BackgroundProcess_h +#define hifi_BackgroundProcess_h + +#include + +#include +#include +#include + +class BackgroundProcess : public QProcess +{ + Q_OBJECT +public: + BackgroundProcess(const QString& program, QObject* parent = 0); + + LogViewer* getLogViewer() { return _logViewer; } + + const QStringList& getLastArgList() const { return _lastArgList; } + + void start(const QStringList& arguments); + +private slots: + void processStarted(); + void processError(); + void receivedStandardOutput(); + void receivedStandardError(); + +private: + QString _program; + QStringList _lastArgList; + QString _logFilePath; + LogViewer* _logViewer; + QTimer _logTimer; + QString _stdoutFilename; + QString _stderrFilename; + qint64 _stdoutFilePos; + qint64 _stderrFilePos; +}; + +#endif diff --git a/stack-manager/src/DownloadManager.cpp b/stack-manager/src/DownloadManager.cpp new file mode 100644 index 0000000000..f97ba1f680 --- /dev/null +++ b/stack-manager/src/DownloadManager.cpp @@ -0,0 +1,164 @@ +// +// DownloadManager.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "DownloadManager.h" +#include "GlobalData.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +DownloadManager::DownloadManager(QNetworkAccessManager* manager, QWidget* parent) : + QWidget(parent), + _manager(manager) +{ + setBaseSize(500, 250); + + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(10, 10, 10, 10); + QLabel* label = new QLabel; + label->setText("Download Manager"); + label->setStyleSheet("font-size: 19px;"); + label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + + _table = new QTableWidget; + _table->setEditTriggers(QTableWidget::NoEditTriggers); + _table->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + _table->setColumnCount(3); + _table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + _table->setHorizontalHeaderLabels(QStringList() << "Name" << "Progress" << "Status"); + layout->addWidget(_table); + + setLayout(layout); +} + +DownloadManager::~DownloadManager() { + _downloaderHash.clear(); +} + +void DownloadManager::downloadFile(const QUrl& url) { + for (int i = 0; i < _downloaderHash.size(); ++i) { + if (_downloaderHash.keys().at(i)->getUrl() == url) { + qDebug() << "Downloader for URL " << url << " already initialised."; + return; + } + } + + Downloader* downloader = new Downloader(url); + connect(downloader, SIGNAL(downloadCompleted(QUrl)), SLOT(onDownloadCompleted(QUrl))); + connect(downloader, SIGNAL(downloadStarted(Downloader*,QUrl)), + SLOT(onDownloadStarted(Downloader*,QUrl))); + connect(downloader, SIGNAL(downloadFailed(QUrl)), SLOT(onDownloadFailed(QUrl))); + connect(downloader, SIGNAL(downloadProgress(QUrl,int)), SLOT(onDownloadProgress(QUrl,int))); + connect(downloader, SIGNAL(installingFiles(QUrl)), SLOT(onInstallingFiles(QUrl))); + connect(downloader, SIGNAL(filesSuccessfullyInstalled(QUrl)), SLOT(onFilesSuccessfullyInstalled(QUrl))); + connect(downloader, SIGNAL(filesInstallationFailed(QUrl)), SLOT(onFilesInstallationFailed(QUrl))); + downloader->start(_manager); +} + +void DownloadManager::onDownloadStarted(Downloader* downloader, const QUrl& url) { + int rowIndex = _table->rowCount(); + _table->setRowCount(rowIndex + 1); + QTableWidgetItem* nameItem = new QTableWidgetItem(QFileInfo(url.toString()).fileName()); + _table->setItem(rowIndex, 0, nameItem); + QProgressBar* progressBar = new QProgressBar; + _table->setCellWidget(rowIndex, 1, progressBar); + QTableWidgetItem* statusItem = new QTableWidgetItem; + if (QFile(QDir::toNativeSeparators(GlobalData::getInstance().getClientsLaunchPath() + "/" + QFileInfo(url.toString()).fileName())).exists()) { + statusItem->setText("Updating"); + } else { + statusItem->setText("Downloading"); + } + _table->setItem(rowIndex, 2, statusItem); + _downloaderHash.insert(downloader, rowIndex); +} + +void DownloadManager::onDownloadCompleted(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Download Complete"); +} + +void DownloadManager::onDownloadProgress(const QUrl& url, int percentage) { + qobject_cast(_table->cellWidget(downloaderRowIndexForUrl(url), 1))->setValue(percentage); +} + +void DownloadManager::onDownloadFailed(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Download Failed"); + _downloaderHash.remove(downloaderForUrl(url)); +} + +void DownloadManager::onInstallingFiles(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Installing"); +} + +void DownloadManager::onFilesSuccessfullyInstalled(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Successfully Installed"); + _downloaderHash.remove(downloaderForUrl(url)); + emit fileSuccessfullyInstalled(url); + if (_downloaderHash.size() == 0) { + close(); + } +} + +void DownloadManager::onFilesInstallationFailed(const QUrl& url) { + _table->item(downloaderRowIndexForUrl(url), 2)->setText("Installation Failed"); + _downloaderHash.remove(downloaderForUrl(url)); +} + +void DownloadManager::closeEvent(QCloseEvent*) { + if (_downloaderHash.size() > 0) { + QMessageBox msgBox; + msgBox.setText("There are active downloads that need to be installed for the proper functioning of Stack Manager. Do you want to stop the downloads and exit?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int ret = msgBox.exec(); + switch (ret) { + case QMessageBox::Yes: + qApp->quit(); + break; + case QMessageBox::No: + msgBox.close(); + break; + } + } +} + +int DownloadManager::downloaderRowIndexForUrl(const QUrl& url) { + QHash::const_iterator i = _downloaderHash.constBegin(); + while (i != _downloaderHash.constEnd()) { + if (i.key()->getUrl() == url) { + return i.value(); + } else { + ++i; + } + } + + return -1; +} + +Downloader* DownloadManager::downloaderForUrl(const QUrl& url) { + QHash::const_iterator i = _downloaderHash.constBegin(); + while (i != _downloaderHash.constEnd()) { + if (i.key()->getUrl() == url) { + return i.key(); + } else { + ++i; + } + } + + return NULL; +} diff --git a/stack-manager/src/DownloadManager.h b/stack-manager/src/DownloadManager.h new file mode 100644 index 0000000000..1cef0ae118 --- /dev/null +++ b/stack-manager/src/DownloadManager.h @@ -0,0 +1,52 @@ +// +// DownloadManager.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_DownloadManager_h +#define hifi_DownloadManager_h + +#include +#include +#include +#include +#include + +#include "Downloader.h" + +class DownloadManager : public QWidget { + Q_OBJECT +public: + DownloadManager(QNetworkAccessManager* manager, QWidget* parent = 0); + ~DownloadManager(); + + void downloadFile(const QUrl& url); + +private slots: + void onDownloadStarted(Downloader* downloader, const QUrl& url); + void onDownloadCompleted(const QUrl& url); + void onDownloadProgress(const QUrl& url, int percentage); + void onDownloadFailed(const QUrl& url); + void onInstallingFiles(const QUrl& url); + void onFilesSuccessfullyInstalled(const QUrl& url); + void onFilesInstallationFailed(const QUrl& url); + +protected: + void closeEvent(QCloseEvent*); + +signals: + void fileSuccessfullyInstalled(const QUrl& url); + +private: + QTableWidget* _table; + QNetworkAccessManager* _manager; + QHash _downloaderHash; + + int downloaderRowIndexForUrl(const QUrl& url); + Downloader* downloaderForUrl(const QUrl& url); +}; + +#endif diff --git a/stack-manager/src/Downloader.cpp b/stack-manager/src/Downloader.cpp new file mode 100644 index 0000000000..db3b397b96 --- /dev/null +++ b/stack-manager/src/Downloader.cpp @@ -0,0 +1,133 @@ +// +// Downloader.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "Downloader.h" +#include "GlobalData.h" + +#include +#include + +#include +#include +#include +#include +#include + +Downloader::Downloader(const QUrl& url, QObject* parent) : + QObject(parent) +{ + _url = url; +} + +void Downloader::start(QNetworkAccessManager* manager) { + qDebug() << "Downloader::start() for URL - " << _url; + QNetworkRequest req(_url); + QNetworkReply* reply = manager->get(req); + emit downloadStarted(this, _url); + connect(reply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(error(QNetworkReply::NetworkError))); + connect(reply, SIGNAL(downloadProgress(qint64,qint64)), SLOT(downloadProgress(qint64,qint64))); + connect(reply, SIGNAL(finished()), SLOT(downloadFinished())); +} + +void Downloader::error(QNetworkReply::NetworkError error) { + QNetworkReply* reply = qobject_cast(sender()); + qDebug() << reply->errorString(); + reply->deleteLater(); +} + +void Downloader::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + int percentage = bytesReceived*100/bytesTotal; + emit downloadProgress(_url, percentage); +} + +void Downloader::downloadFinished() { + qDebug() << "Downloader::downloadFinished() for URL - " << _url; + QNetworkReply* reply = qobject_cast(sender()); + if (reply->error() != QNetworkReply::NoError) { + qDebug() << reply->errorString(); + emit downloadFailed(_url); + return; + } + emit downloadCompleted(_url); + + QString fileName = QFileInfo(_url.toString()).fileName(); + QString fileDir = GlobalData::getInstance().getClientsLaunchPath(); + QString filePath = fileDir + fileName; + + QFile file(filePath); + + // remove file if already exists + if (file.exists()) { + file.remove(); + } + + if (file.open(QIODevice::WriteOnly)) { + if (fileName == "assignment-client" || fileName == "assignment-client.exe" || + fileName == "domain-server" || fileName == "domain-server.exe") { + file.setPermissions(QFile::ExeOwner | QFile::ReadOwner | QFile::WriteOwner); + } else { + file.setPermissions(QFile::ReadOwner | QFile::WriteOwner); + } + emit installingFiles(_url); + file.write(reply->readAll()); + bool error = false; + file.close(); + + if (fileName.endsWith(".zip")) { // we need to unzip the file now + QuaZip zip(QFileInfo(file).absoluteFilePath()); + if (zip.open(QuaZip::mdUnzip)) { + QuaZipFile zipFile(&zip); + for(bool f = zip.goToFirstFile(); f; f = zip.goToNextFile()) { + if (zipFile.open(QIODevice::ReadOnly)) { + QFile newFile(QDir::toNativeSeparators(fileDir + "/" + zipFile.getActualFileName())); + if (zipFile.getActualFileName().endsWith("/")) { + QDir().mkpath(QFileInfo(newFile).absolutePath()); + zipFile.close(); + continue; + } + + // remove file if already exists + if (newFile.exists()) { + newFile.remove(); + } + + if (newFile.open(QIODevice::WriteOnly)) { + newFile.setPermissions(QFile::ReadOwner | QFile::WriteOwner); + newFile.write(zipFile.readAll()); + newFile.close(); + } else { + error = true; + qDebug() << "Could not open archive file for writing: " << zip.getCurrentFileName(); + emit filesInstallationFailed(_url); + break; + } + } else { + error = true; + qDebug() << "Could not open archive file: " << zip.getCurrentFileName(); + emit filesInstallationFailed(_url); + break; + } + zipFile.close(); + } + zip.close(); + } else { + error = true; + emit filesInstallationFailed(_url); + qDebug() << "Could not open zip file for extraction."; + } + } + if (!error) + emit filesSuccessfullyInstalled(_url); + } else { + emit filesInstallationFailed(_url); + qDebug() << "Could not open file: " << filePath; + } + reply->deleteLater(); +} + + diff --git a/stack-manager/src/Downloader.h b/stack-manager/src/Downloader.h new file mode 100644 index 0000000000..f5b02214e0 --- /dev/null +++ b/stack-manager/src/Downloader.h @@ -0,0 +1,45 @@ +// +// Downloader.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 07/09/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_Downloader_h +#define hifi_Downloader_h + +#include +#include +#include +#include + +class Downloader : public QObject +{ + Q_OBJECT +public: + explicit Downloader(const QUrl& url, QObject* parent = 0); + + const QUrl& getUrl() { return _url; } + + void start(QNetworkAccessManager* manager); + +private slots: + void error(QNetworkReply::NetworkError error); + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void downloadFinished(); + +signals: + void downloadStarted(Downloader* downloader, const QUrl& url); + void downloadCompleted(const QUrl& url); + void downloadProgress(const QUrl& url, int percentage); + void downloadFailed(const QUrl& url); + void installingFiles(const QUrl& url); + void filesSuccessfullyInstalled(const QUrl& url); + void filesInstallationFailed(const QUrl& url); + +private: + QUrl _url; +}; + +#endif diff --git a/stack-manager/src/GlobalData.cpp b/stack-manager/src/GlobalData.cpp new file mode 100644 index 0000000000..ecc5ed520d --- /dev/null +++ b/stack-manager/src/GlobalData.cpp @@ -0,0 +1,84 @@ +// +// GlobalData.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 6/25/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "GlobalData.h" +#include "StackManagerVersion.h" + +#include +#include +#include +#include + +GlobalData& GlobalData::getInstance() { + static GlobalData staticInstance; + return staticInstance; +} + +GlobalData::GlobalData() { + QString urlBase = URL_BASE; +#if defined Q_OS_OSX + _platform = "mac"; +#elif defined Q_OS_WIN32 + _platform = "win"; +#elif defined Q_OS_LINUX + _platform = "linux"; +#endif + + _resourcePath = "resources/"; + _assignmentClientExecutable = "assignment-client"; + _domainServerExecutable = "domain-server"; + QString applicationSupportDirectory = QStandardPaths::writableLocation(QStandardPaths::DataLocation); + if (PR_BUILD) { + applicationSupportDirectory += "/pr-binaries"; + } + + _clientsLaunchPath = QDir::toNativeSeparators(applicationSupportDirectory + "/"); + _clientsResourcePath = QDir::toNativeSeparators(applicationSupportDirectory + "/" + _resourcePath); + + _assignmentClientExecutablePath = QDir::toNativeSeparators(_clientsLaunchPath + _assignmentClientExecutable); + if (_platform == "win") { + _assignmentClientExecutablePath.append(".exe"); + } + _domainServerExecutablePath = QDir::toNativeSeparators(_clientsLaunchPath + _domainServerExecutable); + if (_platform == "win") { + _domainServerExecutablePath.append(".exe"); + } + + _requirementsURL = urlBase + "/binaries/" + _platform + "/requirements/requirements.zip"; + _requirementsZipPath = _clientsLaunchPath + "requirements.zip"; + _requirementsMD5URL = urlBase + "/binaries/" + _platform + "/requirements/requirements.md5"; + _assignmentClientURL = urlBase + "/binaries/" + _platform + "/assignment-client" + (_platform == "win" ? "/assignment-client.exe" : "/assignment-client"); + _domainServerResourcesURL = urlBase + "/binaries/" + _platform + "/domain-server/resources.zip"; + _domainServerResourcesZipPath = _clientsLaunchPath + "resources.zip"; + _domainServerResourcesMD5URL = urlBase + "/binaries/" + _platform + "/domain-server/resources.md5"; + _domainServerURL = urlBase + "/binaries/" + _platform + "/domain-server" + (_platform == "win" ? "/domain-server.exe" : "/domain-server"); + + _assignmentClientMD5URL = urlBase + "/binaries/" + _platform + "/assignment-client/assignment-client.md5"; + _domainServerMD5URL = urlBase + "/binaries/" + _platform + "/domain-server/domain-server.md5"; + + _defaultDomain = "localhost"; + _logsPath = QDir::toNativeSeparators(_clientsLaunchPath + "logs/"); + _availableAssignmentTypes.insert("audio-mixer", 0); + _availableAssignmentTypes.insert("avatar-mixer", 1); + _availableAssignmentTypes.insert("entity-server", 6); + + // allow user to override path to binaries so that they can run their own builds + _hifiBuildDirectory = ""; + + _domainServerBaseUrl = "http://localhost:40100"; +} + + +void GlobalData::setHifiBuildDirectory(const QString hifiBuildDirectory) { + _hifiBuildDirectory = hifiBuildDirectory; + _clientsLaunchPath = QDir::toNativeSeparators(_hifiBuildDirectory + "/assignment-client/"); + _clientsResourcePath = QDir::toNativeSeparators(_clientsLaunchPath + "/" + _resourcePath); + _logsPath = QDir::toNativeSeparators(_clientsLaunchPath + "logs/"); + _assignmentClientExecutablePath = QDir::toNativeSeparators(_clientsLaunchPath + _assignmentClientExecutable); + _domainServerExecutablePath = QDir::toNativeSeparators(_hifiBuildDirectory + "/domain-server/" + _domainServerExecutable); +} diff --git a/stack-manager/src/GlobalData.h b/stack-manager/src/GlobalData.h new file mode 100644 index 0000000000..58c9a93526 --- /dev/null +++ b/stack-manager/src/GlobalData.h @@ -0,0 +1,75 @@ +// +// GlobalData.h +// StackManagerQt/src +// +// Created by Mohammed Nafees on 6/25/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_GlobalData_h +#define hifi_GlobalData_h + +#include +#include + +class GlobalData { +public: + static GlobalData& getInstance(); + + QString getPlatform() { return _platform; } + QString getClientsLaunchPath() { return _clientsLaunchPath; } + QString getClientsResourcesPath() { return _clientsResourcePath; } + QString getAssignmentClientExecutablePath() { return _assignmentClientExecutablePath; } + QString getDomainServerExecutablePath() { return _domainServerExecutablePath; } + QString getRequirementsURL() { return _requirementsURL; } + QString getRequirementsZipPath() { return _requirementsZipPath; } + QString getRequirementsMD5URL() { return _requirementsMD5URL; } + QString getAssignmentClientURL() { return _assignmentClientURL; } + QString getAssignmentClientMD5URL() { return _assignmentClientMD5URL; } + QString getDomainServerURL() { return _domainServerURL; } + QString getDomainServerResourcesURL() { return _domainServerResourcesURL; } + QString getDomainServerResourcesZipPath() { return _domainServerResourcesZipPath; } + QString getDomainServerResourcesMD5URL() { return _domainServerResourcesMD5URL; } + QString getDomainServerMD5URL() { return _domainServerMD5URL; } + QString getDefaultDomain() { return _defaultDomain; } + QString getLogsPath() { return _logsPath; } + QHash getAvailableAssignmentTypes() { return _availableAssignmentTypes; } + + void setHifiBuildDirectory(const QString hifiBuildDirectory); + bool isGetHifiBuildDirectorySet() { return _hifiBuildDirectory != ""; } + + void setDomainServerBaseUrl(const QString domainServerBaseUrl) { _domainServerBaseUrl = domainServerBaseUrl; } + QString getDomainServerBaseUrl() { return _domainServerBaseUrl; } + +private: + GlobalData(); + + QString _platform; + QString _clientsLaunchPath; + QString _clientsResourcePath; + QString _assignmentClientExecutablePath; + QString _domainServerExecutablePath; + QString _requirementsURL; + QString _requirementsZipPath; + QString _requirementsMD5URL; + QString _assignmentClientURL; + QString _assignmentClientMD5URL; + QString _domainServerURL; + QString _domainServerResourcesURL; + QString _domainServerResourcesZipPath; + QString _domainServerResourcesMD5URL; + QString _domainServerMD5URL; + QString _defaultDomain; + QString _logsPath; + QString _hifiBuildDirectory; + + QString _resourcePath; + QString _assignmentClientExecutable; + QString _domainServerExecutable; + + QHash _availableAssignmentTypes; + + QString _domainServerBaseUrl; +}; + +#endif diff --git a/stack-manager/src/StackManagerVersion.h.in b/stack-manager/src/StackManagerVersion.h.in new file mode 100644 index 0000000000..402e19a056 --- /dev/null +++ b/stack-manager/src/StackManagerVersion.h.in @@ -0,0 +1,16 @@ +// +// StackManagerVersion.h +// StackManagerQt +// +// Created by Kai Ludwig on 02/16/15. +// Copyright 2015 High Fidelity, Inc. +// +// Declaration of version and build data +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +const QString BUILD_VERSION = "@BUILD_SEQ@"; +const QString URL_BASE = "@BASE_URL@"; +const bool PR_BUILD = @PR_BUILD@; diff --git a/stack-manager/src/main.cpp b/stack-manager/src/main.cpp new file mode 100644 index 0000000000..b5b715c75a --- /dev/null +++ b/stack-manager/src/main.cpp @@ -0,0 +1,15 @@ +// +// main.cpp +// StackManagerQt/src +// +// Created by Mohammed Nafees on 06/27/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "AppDelegate.h" + +int main(int argc, char* argv[]) +{ + AppDelegate app(argc, argv); + return app.exec(); +} diff --git a/stack-manager/src/resources.qrc b/stack-manager/src/resources.qrc new file mode 100644 index 0000000000..508edcc728 --- /dev/null +++ b/stack-manager/src/resources.qrc @@ -0,0 +1,9 @@ + + + ../assets/logo-larger.png + ../assets/assignment-run.svg + ../assets/assignment-stop.svg + ../assets/server-start.svg + ../assets/server-stop.svg + + diff --git a/stack-manager/src/ui/AssignmentWidget.cpp b/stack-manager/src/ui/AssignmentWidget.cpp new file mode 100644 index 0000000000..51fe067eb3 --- /dev/null +++ b/stack-manager/src/ui/AssignmentWidget.cpp @@ -0,0 +1,61 @@ +// +// AssignmentWidget.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/18/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "AssignmentWidget.h" + +#include +#include + +#include "AppDelegate.h" + +AssignmentWidget::AssignmentWidget(QWidget* parent) : + QWidget(parent), + _processID(0), + _isRunning(false), + _scriptID(QUuid::createUuid()) +{ + setFont(QFont("sans-serif")); + + QHBoxLayout* layout = new QHBoxLayout; + + _runButton = new SvgButton(this); + _runButton->setFixedSize(59, 32); + _runButton->setSvgImage(":/assignment-run.svg"); + _runButton->setCheckable(true); + _runButton->setChecked(false); + + QLabel* label = new QLabel; + label->setText("Pool ID"); + + _poolIDLineEdit = new QLineEdit; + _poolIDLineEdit->setPlaceholderText("Optional"); + + layout->addWidget(_runButton, 5); + layout->addWidget(label); + layout->addWidget(_poolIDLineEdit); + + setLayout(layout); + + connect(_runButton, &QPushButton::clicked, this, &AssignmentWidget::toggleRunningState); +} + +void AssignmentWidget::toggleRunningState() { + if (_isRunning && _processID > 0) { + AppDelegate::getInstance()->stopScriptedAssignment(_scriptID); + _runButton->setSvgImage(":/assignment-run.svg"); + update(); + _poolIDLineEdit->setEnabled(true); + _isRunning = false; + } else { + _processID = AppDelegate::getInstance()->startScriptedAssignment(_scriptID, _poolIDLineEdit->text()); + _runButton->setSvgImage(":/assignment-stop.svg"); + update(); + _poolIDLineEdit->setEnabled(false); + _isRunning = true; + } +} diff --git a/stack-manager/src/ui/AssignmentWidget.h b/stack-manager/src/ui/AssignmentWidget.h new file mode 100644 index 0000000000..3e52d7f1af --- /dev/null +++ b/stack-manager/src/ui/AssignmentWidget.h @@ -0,0 +1,37 @@ +// +// AssignmentWidget.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/18/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_AssignmentWidget_h +#define hifi_AssignmentWidget_h + +#include +#include +#include + +#include "SvgButton.h" + +class AssignmentWidget : public QWidget +{ + Q_OBJECT +public: + AssignmentWidget(QWidget* parent = 0); + + bool isRunning() { return _isRunning; } + +public slots: + void toggleRunningState(); + +private: + int _processID; + bool _isRunning; + SvgButton* _runButton; + QLineEdit* _poolIDLineEdit; + QUuid _scriptID; +}; + +#endif diff --git a/stack-manager/src/ui/LogViewer.cpp b/stack-manager/src/ui/LogViewer.cpp new file mode 100644 index 0000000000..12b6f33f88 --- /dev/null +++ b/stack-manager/src/ui/LogViewer.cpp @@ -0,0 +1,63 @@ +// +// LogViewer.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 07/10/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "LogViewer.h" +#include "GlobalData.h" + +#include +#include +#include + +LogViewer::LogViewer(QWidget* parent) : + QWidget(parent) +{ + QVBoxLayout* layout = new QVBoxLayout; + QLabel* outputLabel = new QLabel; + outputLabel->setText("Standard Output:"); + outputLabel->setStyleSheet("font-size: 13pt;"); + + layout->addWidget(outputLabel); + + _outputView = new QTextEdit; + _outputView->setUndoRedoEnabled(false); + _outputView->setReadOnly(true); + + layout->addWidget(_outputView); + + QLabel* errorLabel = new QLabel; + errorLabel->setText("Standard Error:"); + errorLabel->setStyleSheet("font-size: 13pt;"); + + layout->addWidget(errorLabel); + + _errorView = new QTextEdit; + _errorView->setUndoRedoEnabled(false); + _errorView->setReadOnly(true); + + layout->addWidget(_errorView); + setLayout(layout); +} + +void LogViewer::clear() { + _outputView->clear(); + _errorView->clear(); +} + +void LogViewer::appendStandardOutput(const QString& output) { + QTextCursor cursor = _outputView->textCursor(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(output); + _outputView->ensureCursorVisible(); +} + +void LogViewer::appendStandardError(const QString& error) { + QTextCursor cursor = _errorView->textCursor(); + cursor.movePosition(QTextCursor::End); + cursor.insertText(error); + _errorView->ensureCursorVisible(); +} diff --git a/stack-manager/src/ui/LogViewer.h b/stack-manager/src/ui/LogViewer.h new file mode 100644 index 0000000000..b4321cc886 --- /dev/null +++ b/stack-manager/src/ui/LogViewer.h @@ -0,0 +1,31 @@ +// +// LogViewer.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 07/10/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_LogViewer_h +#define hifi_LogViewer_h + +#include +#include + +class LogViewer : public QWidget +{ + Q_OBJECT +public: + explicit LogViewer(QWidget* parent = 0); + + void clear(); + + void appendStandardOutput(const QString& output); + void appendStandardError(const QString& error); + +private: + QTextEdit* _outputView; + QTextEdit* _errorView; +}; + +#endif diff --git a/stack-manager/src/ui/MainWindow.cpp b/stack-manager/src/ui/MainWindow.cpp new file mode 100644 index 0000000000..59551933f4 --- /dev/null +++ b/stack-manager/src/ui/MainWindow.cpp @@ -0,0 +1,329 @@ +// +// MainWindow.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/17/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "MainWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AppDelegate.h" +#include "AssignmentWidget.h" +#include "GlobalData.h" +#include "StackManagerVersion.h" + +const int GLOBAL_X_PADDING = 55; +const int TOP_Y_PADDING = 25; +const int REQUIREMENTS_TEXT_TOP_MARGIN = 19; +//const int HORIZONTAL_RULE_TOP_MARGIN = 25; + +const int BUTTON_PADDING_FIX = -5; + +//const int ASSIGNMENT_LAYOUT_RESIZE_FACTOR = 56; +//const int ASSIGNMENT_LAYOUT_WIDGET_STRETCH = 0; + +const QColor lightGrayColor = QColor(205, 205, 205); +const QColor darkGrayColor = QColor(84, 84, 84); +const QColor redColor = QColor(189, 54, 78); +const QColor greenColor = QColor(3, 150, 126); + +const QString SHARE_BUTTON_COPY_LINK_TEXT = "Copy link"; + +MainWindow::MainWindow() : + QWidget(), + _domainServerRunning(false), + _startServerButton(NULL), + _stopServerButton(NULL), + _serverAddressLabel(NULL), + _viewLogsButton(NULL), + _settingsButton(NULL), + _copyLinkButton(NULL), + _contentSetButton(NULL), + _logsWidget(NULL), + _localHttpPortSharedMem(NULL) +{ + // Set build version + QCoreApplication::setApplicationVersion(BUILD_VERSION); + + setWindowTitle("High Fidelity Stack Manager (build " + QCoreApplication::applicationVersion() + ")"); + const int WINDOW_FIXED_WIDTH = 640; + const int WINDOW_INITIAL_HEIGHT = 170; + + if (GlobalData::getInstance().getPlatform() == "win") { + const int windowsYCoord = 30; + setGeometry(qApp->desktop()->availableGeometry().width() / 2 - WINDOW_FIXED_WIDTH / 2, windowsYCoord, + WINDOW_FIXED_WIDTH, WINDOW_INITIAL_HEIGHT); + } else if (GlobalData::getInstance().getPlatform() == "linux") { + const int linuxYCoord = 30; + setGeometry(qApp->desktop()->availableGeometry().width() / 2 - WINDOW_FIXED_WIDTH / 2, linuxYCoord, + WINDOW_FIXED_WIDTH, WINDOW_INITIAL_HEIGHT + 40); + } else { + const int unixYCoord = 0; + setGeometry(qApp->desktop()->availableGeometry().width() / 2 - WINDOW_FIXED_WIDTH / 2, unixYCoord, + WINDOW_FIXED_WIDTH, WINDOW_INITIAL_HEIGHT); + } + setFixedWidth(WINDOW_FIXED_WIDTH); + setMaximumHeight(qApp->desktop()->availableGeometry().height()); + setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowMinimizeButtonHint | Qt::WindowCloseButtonHint); + setMouseTracking(true); + setStyleSheet("font-family: 'Helvetica', 'Arial', 'sans-serif';"); + + const int SERVER_BUTTON_HEIGHT = 47; + + _startServerButton = new SvgButton(this); + + QPixmap scaledStart(":/server-start.svg"); + scaledStart.scaledToHeight(SERVER_BUTTON_HEIGHT); + + _startServerButton->setGeometry((width() / 2.0f) - (scaledStart.width() / 2.0f), + TOP_Y_PADDING, + scaledStart.width(), + scaledStart.height()); + _startServerButton->setSvgImage(":/server-start.svg"); + + _stopServerButton = new SvgButton(this); + _stopServerButton->setSvgImage(":/server-stop.svg"); + _stopServerButton->setGeometry(GLOBAL_X_PADDING, TOP_Y_PADDING, + scaledStart.width(), scaledStart.height()); + + const int SERVER_ADDRESS_LABEL_LEFT_MARGIN = 20; + const int SERVER_ADDRESS_LABEL_TOP_MARGIN = 17; + _serverAddressLabel = new QLabel(this); + _serverAddressLabel->move(_stopServerButton->geometry().right() + SERVER_ADDRESS_LABEL_LEFT_MARGIN, + TOP_Y_PADDING + SERVER_ADDRESS_LABEL_TOP_MARGIN); + _serverAddressLabel->setOpenExternalLinks(true); + + const int SECONDARY_BUTTON_ROW_TOP_MARGIN = 10; + + int secondaryButtonY = _stopServerButton->geometry().bottom() + SECONDARY_BUTTON_ROW_TOP_MARGIN; + + _viewLogsButton = new QPushButton("View logs", this); + _viewLogsButton->adjustSize(); + _viewLogsButton->setGeometry(GLOBAL_X_PADDING + BUTTON_PADDING_FIX, secondaryButtonY, + _viewLogsButton->width(), _viewLogsButton->height()); + + _settingsButton = new QPushButton("Settings", this); + _settingsButton->adjustSize(); + _settingsButton->setGeometry(_viewLogsButton->geometry().right(), secondaryButtonY, + _settingsButton->width(), _settingsButton->height()); + + _copyLinkButton = new QPushButton(SHARE_BUTTON_COPY_LINK_TEXT, this); + _copyLinkButton->adjustSize(); + _copyLinkButton->setGeometry(_settingsButton->geometry().right(), secondaryButtonY, + _copyLinkButton->width(), _copyLinkButton->height()); + + // add the drop down for content sets + _contentSetButton = new QPushButton("Get content set", this); + _contentSetButton->adjustSize(); + _contentSetButton->setGeometry(_copyLinkButton->geometry().right(), secondaryButtonY, + _contentSetButton->width(), _contentSetButton->height()); + + const QSize logsWidgetSize = QSize(500, 500); + _logsWidget = new QTabWidget; + _logsWidget->setUsesScrollButtons(true); + _logsWidget->setElideMode(Qt::ElideMiddle); + _logsWidget->setWindowFlags(Qt::CustomizeWindowHint | Qt::WindowTitleHint | + Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint); + _logsWidget->resize(logsWidgetSize); + + connect(_startServerButton, &QPushButton::clicked, this, &MainWindow::toggleDomainServerButton); + connect(_stopServerButton, &QPushButton::clicked, this, &MainWindow::toggleDomainServerButton); + connect(_copyLinkButton, &QPushButton::clicked, this, &MainWindow::handleCopyLinkButton); + connect(_contentSetButton, &QPushButton::clicked, this, &MainWindow::showContentSetPage); + connect(_viewLogsButton, &QPushButton::clicked, _logsWidget, &QTabWidget::show); + connect(_settingsButton, &QPushButton::clicked, this, &MainWindow::openSettings); + + AppDelegate* app = AppDelegate::getInstance(); + // update the current server address label and change it if the AppDelegate says the address has changed + updateServerAddressLabel(); + connect(app, &AppDelegate::domainAddressChanged, this, &MainWindow::updateServerAddressLabel); + connect(app, &AppDelegate::domainAddressChanged, this, &MainWindow::updateServerBaseUrl); + + // handle response for content set download + connect(app, &AppDelegate::contentSetDownloadResponse, this, &MainWindow::handleContentSetDownloadResponse); + + // handle response for index path change + connect(app, &AppDelegate::indexPathChangeResponse, this, &MainWindow::handleIndexPathChangeResponse); + + // handle stack state change + connect(app, &AppDelegate::stackStateChanged, this, &MainWindow::toggleContent); + + toggleContent(false); + +} + +void MainWindow::updateServerAddressLabel() { + AppDelegate* app = AppDelegate::getInstance(); + + _serverAddressLabel->setText("

Accessible at: " + "getServerAddress() + "\">" + "" + app->getServerAddress() + + "

"); + _serverAddressLabel->adjustSize(); +} + +void MainWindow::updateServerBaseUrl() { + quint16 localPort; + + if (getLocalServerPortFromSharedMemory("domain-server.local-http-port", _localHttpPortSharedMem, localPort)) { + GlobalData::getInstance().setDomainServerBaseUrl(QString("http://localhost:") + QString::number(localPort)); + } +} + + +void MainWindow::handleCopyLinkButton() { + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(AppDelegate::getInstance()->getServerAddress()); +} + +void MainWindow::showContentSetPage() { + const QString CONTENT_SET_HTML_URL = "http://hifi-public.s3.amazonaws.com/content-sets/content-sets.html"; + + // show a QWebView for the content set page + QWebView* contentSetWebView = new QWebView(); + contentSetWebView->setUrl(CONTENT_SET_HTML_URL); + + // have the widget delete on close + contentSetWebView->setAttribute(Qt::WA_DeleteOnClose); + + // setup the page viewport to be the right size + const QSize CONTENT_SET_VIEWPORT_SIZE = QSize(800, 480); + contentSetWebView->resize(CONTENT_SET_VIEWPORT_SIZE); + + // have our app delegate handle a click on one of the content sets + contentSetWebView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); + connect(contentSetWebView->page(), &QWebPage::linkClicked, AppDelegate::getInstance(), &AppDelegate::downloadContentSet); + connect(contentSetWebView->page(), &QWebPage::linkClicked, contentSetWebView, &QWebView::close); + + contentSetWebView->show(); +} + +void MainWindow::handleContentSetDownloadResponse(bool wasSuccessful) { + if (wasSuccessful) { + QMessageBox::information(this, "New content set", + "Your new content set has been downloaded and your assignment-clients have been restarted."); + } else { + QMessageBox::information(this, "Error", "There was a problem downloading that content set. Please try again!"); + } +} + +void MainWindow::handleIndexPathChangeResponse(bool wasSuccessful) { + if (!wasSuccessful) { + QString errorMessage = "The content set was downloaded successfully but there was a problem changing your \ + domain-server index path.\n\nIf you want users to jump to the new content set when they come to your domain \ + please try and re-download the content set."; + QMessageBox::information(this, "Error", errorMessage); + } +} + +void MainWindow::setRequirementsLastChecked(const QString& lastCheckedDateTime) { + _requirementsLastCheckedDateTime = lastCheckedDateTime; +} + +void MainWindow::setUpdateNotification(const QString& updateNotification) { + _updateNotification = updateNotification; +} + +void MainWindow::toggleContent(bool isRunning) { + _stopServerButton->setVisible(isRunning); + _startServerButton->setVisible(!isRunning); + _domainServerRunning = isRunning; + _serverAddressLabel->setVisible(isRunning); + _viewLogsButton->setVisible(isRunning); + _settingsButton->setVisible(isRunning); + _copyLinkButton->setVisible(isRunning); + _contentSetButton->setVisible(isRunning); + update(); +} + +void MainWindow::paintEvent(QPaintEvent *) { + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QFont font("Helvetica"); + font.insertSubstitutions("Helvetica", QStringList() << "Arial" << "sans-serif"); + + int currentY = (_domainServerRunning ? _viewLogsButton->geometry().bottom() : _startServerButton->geometry().bottom()) + + REQUIREMENTS_TEXT_TOP_MARGIN; + + if (!_updateNotification.isEmpty()) { + font.setBold(true); + font.setUnderline(false); + if (GlobalData::getInstance().getPlatform() == "linux") { + font.setPointSize(14); + } + painter.setFont(font); + painter.setPen(redColor); + + QString updateNotificationString = ">>> " + _updateNotification + " <<<"; + float fontWidth = QFontMetrics(font).width(updateNotificationString) + GLOBAL_X_PADDING; + + painter.drawText(QRectF(_domainServerRunning ? ((width() - fontWidth) / 2.0f) : GLOBAL_X_PADDING, + currentY, + fontWidth, + QFontMetrics(font).height()), + updateNotificationString); + } + else if (!_requirementsLastCheckedDateTime.isEmpty()) { + font.setBold(false); + font.setUnderline(false); + if (GlobalData::getInstance().getPlatform() == "linux") { + font.setPointSize(14); + } + painter.setFont(font); + painter.setPen(darkGrayColor); + + QString requirementsString = "Requirements are up to date as of " + _requirementsLastCheckedDateTime; + float fontWidth = QFontMetrics(font).width(requirementsString); + + painter.drawText(QRectF(_domainServerRunning ? GLOBAL_X_PADDING : ((width() - fontWidth)/ 2.0f), + currentY, + fontWidth, + QFontMetrics(font).height()), + "Requirements are up to date as of " + _requirementsLastCheckedDateTime); + } +} + +void MainWindow::toggleDomainServerButton() { + AppDelegate::getInstance()->toggleStack(!_domainServerRunning); +} + +void MainWindow::openSettings() { + QDesktopServices::openUrl(QUrl(GlobalData::getInstance().getDomainServerBaseUrl() + "/settings/")); +} + + +// XXX this code is duplicate of LimitedNodeList::getLocalServerPortFromSharedMemory +bool MainWindow::getLocalServerPortFromSharedMemory(const QString key, QSharedMemory*& sharedMem, quint16& localPort) { + if (!sharedMem) { + sharedMem = new QSharedMemory(key, this); + + if (!sharedMem->attach(QSharedMemory::ReadOnly)) { + qWarning() << "Could not attach to shared memory at key" << key; + } + } + + if (sharedMem->isAttached()) { + sharedMem->lock(); + memcpy(&localPort, sharedMem->data(), sizeof(localPort)); + sharedMem->unlock(); + return true; + } + + return false; +} diff --git a/stack-manager/src/ui/MainWindow.h b/stack-manager/src/ui/MainWindow.h new file mode 100644 index 0000000000..6b6669ce3a --- /dev/null +++ b/stack-manager/src/ui/MainWindow.h @@ -0,0 +1,67 @@ +// +// MainWindow.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/17/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_MainWindow_h +#define hifi_MainWindow_h + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "SvgButton.h" + +class MainWindow : public QWidget { + Q_OBJECT +public: + MainWindow(); + + void setRequirementsLastChecked(const QString& lastCheckedDateTime); + void setUpdateNotification(const QString& updateNotification); + QTabWidget* getLogsWidget() { return _logsWidget; } + bool getLocalServerPortFromSharedMemory(const QString key, QSharedMemory*& sharedMem, quint16& localPort); + +protected: + virtual void paintEvent(QPaintEvent*); + +private slots: + void toggleDomainServerButton(); + void openSettings(); + void updateServerAddressLabel(); + void updateServerBaseUrl(); + void handleCopyLinkButton(); + void showContentSetPage(); + + void handleContentSetDownloadResponse(bool wasSuccessful); + void handleIndexPathChangeResponse(bool wasSuccessful); +private: + void toggleContent(bool isRunning); + + bool _domainServerRunning; + + QString _requirementsLastCheckedDateTime; + QString _updateNotification; + SvgButton* _startServerButton; + SvgButton* _stopServerButton; + QLabel* _serverAddressLabel; + QPushButton* _viewLogsButton; + QPushButton* _settingsButton; + QPushButton* _copyLinkButton; + QPushButton* _contentSetButton; + QTabWidget* _logsWidget; + + QSharedMemory* _localHttpPortSharedMem; // memory shared with domain server +}; + +#endif diff --git a/stack-manager/src/ui/SvgButton.cpp b/stack-manager/src/ui/SvgButton.cpp new file mode 100644 index 0000000000..0d646ff0d1 --- /dev/null +++ b/stack-manager/src/ui/SvgButton.cpp @@ -0,0 +1,32 @@ +// +// SvgButton.cpp +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/20/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#include "SvgButton.h" + +#include +#include +#include + +SvgButton::SvgButton(QWidget* parent) : + QAbstractButton(parent) +{ +} + +void SvgButton::enterEvent(QEvent*) { + setCursor(QCursor(Qt::PointingHandCursor)); +} + +void SvgButton::setSvgImage(const QString& svg) { + _svgImage = svg; +} + +void SvgButton::paintEvent(QPaintEvent*) { + QPainter painter(this); + QSvgRenderer renderer(_svgImage); + renderer.render(&painter); +} diff --git a/stack-manager/src/ui/SvgButton.h b/stack-manager/src/ui/SvgButton.h new file mode 100644 index 0000000000..b4d44631ec --- /dev/null +++ b/stack-manager/src/ui/SvgButton.h @@ -0,0 +1,33 @@ +// +// SvgButton.h +// StackManagerQt/src/ui +// +// Created by Mohammed Nafees on 10/20/14. +// Copyright (c) 2014 High Fidelity. All rights reserved. +// + +#ifndef hifi_SvgButton_h +#define hifi_SvgButton_h + +#include +#include +#include + +class SvgButton : public QAbstractButton +{ + Q_OBJECT +public: + explicit SvgButton(QWidget* parent = 0); + + void setSvgImage(const QString& svg); + +protected: + virtual void enterEvent(QEvent*); + virtual void paintEvent(QPaintEvent*); + +private: + QString _svgImage; + +}; + +#endif diff --git a/stack-manager/windows_icon.rc b/stack-manager/windows_icon.rc new file mode 100644 index 0000000000..125e4c19db --- /dev/null +++ b/stack-manager/windows_icon.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON DISCARDABLE "assets/icon.ico" \ No newline at end of file diff --git a/tests/animation/CMakeLists.txt b/tests/animation/CMakeLists.txt index a66e391f69..2a07f6f429 100644 --- a/tests/animation/CMakeLists.txt +++ b/tests/animation/CMakeLists.txt @@ -3,7 +3,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared animation gpu fbx model networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/audio/CMakeLists.txt b/tests/audio/CMakeLists.txt index f7dd1d23b9..239bc41fd0 100644 --- a/tests/audio/CMakeLists.txt +++ b/tests/audio/CMakeLists.txt @@ -3,7 +3,7 @@ macro (SETUP_TESTCASE_DEPENDENCIES) # link in the shared libraries link_hifi_libraries(shared audio networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/controllers/CMakeLists.txt b/tests/controllers/CMakeLists.txt index d1c8464dd5..cf1152da02 100644 --- a/tests/controllers/CMakeLists.txt +++ b/tests/controllers/CMakeLists.txt @@ -16,4 +16,4 @@ if (WIN32) target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES}) endif() -copy_dlls_beside_windows_executable() \ No newline at end of file +package_libraries_for_deployment() \ No newline at end of file diff --git a/tests/entities/CMakeLists.txt b/tests/entities/CMakeLists.txt index b2c7969386..81b8b86368 100644 --- a/tests/entities/CMakeLists.txt +++ b/tests/entities/CMakeLists.txt @@ -9,4 +9,4 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries(entities avatars shared octree gpu model fbx networking animation environment) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt index 2c9ee23c47..3d83c310cf 100644 --- a/tests/gpu-test/CMakeLists.txt +++ b/tests/gpu-test/CMakeLists.txt @@ -4,4 +4,4 @@ AUTOSCRIBE_SHADER_LIB(gpu model render-utils) setup_hifi_project(Quick Gui OpenGL Script Widgets) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(networking gl gpu procedural shared fbx model model-networking animation script-engine render-utils ) -copy_dlls_beside_windows_executable() \ No newline at end of file +package_libraries_for_deployment() \ No newline at end of file diff --git a/tests/jitter/CMakeLists.txt b/tests/jitter/CMakeLists.txt index 98be42530c..c10961d687 100644 --- a/tests/jitter/CMakeLists.txt +++ b/tests/jitter/CMakeLists.txt @@ -4,7 +4,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro() setup_hifi_testcase() diff --git a/tests/networking/CMakeLists.txt b/tests/networking/CMakeLists.txt index efa744e4c9..03578ba01d 100644 --- a/tests/networking/CMakeLists.txt +++ b/tests/networking/CMakeLists.txt @@ -4,7 +4,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared networking) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/octree/CMakeLists.txt b/tests/octree/CMakeLists.txt index e2a756105a..9f9315ba2b 100644 --- a/tests/octree/CMakeLists.txt +++ b/tests/octree/CMakeLists.txt @@ -4,7 +4,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared octree gpu model fbx networking environment entities avatars audio animation script-engine physics) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase(Script Network) diff --git a/tests/physics/CMakeLists.txt b/tests/physics/CMakeLists.txt index f789a7b2ba..cc3df5ea8e 100644 --- a/tests/physics/CMakeLists.txt +++ b/tests/physics/CMakeLists.txt @@ -3,7 +3,7 @@ macro (SETUP_TESTCASE_DEPENDENCIES) target_bullet() link_hifi_libraries(shared physics) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase(Script) diff --git a/tests/recording/CMakeLists.txt b/tests/recording/CMakeLists.txt index a523947f52..4e881fcbd9 100644 --- a/tests/recording/CMakeLists.txt +++ b/tests/recording/CMakeLists.txt @@ -3,7 +3,7 @@ set(TARGET_NAME recording-test) setup_hifi_project(Test) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") link_hifi_libraries(shared recording) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() # FIXME convert to unit tests # Declare dependencies @@ -11,6 +11,6 @@ copy_dlls_beside_windows_executable() # # link in the shared libraries # link_hifi_libraries(shared recording) # -# copy_dlls_beside_windows_executable() +# package_libraries_for_deployment() #endmacro () #setup_hifi_testcase() diff --git a/tests/render-utils/CMakeLists.txt b/tests/render-utils/CMakeLists.txt index 865db4dad5..25221a1e86 100644 --- a/tests/render-utils/CMakeLists.txt +++ b/tests/render-utils/CMakeLists.txt @@ -8,4 +8,4 @@ set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries link_hifi_libraries(render-utils gl gpu shared) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tests/shaders/CMakeLists.txt b/tests/shaders/CMakeLists.txt index fd58a5911f..3ba29c5626 100644 --- a/tests/shaders/CMakeLists.txt +++ b/tests/shaders/CMakeLists.txt @@ -17,4 +17,4 @@ include_directories("${PROJECT_BINARY_DIR}/../../libraries/render-utils/") include_directories("${PROJECT_BINARY_DIR}/../../libraries/entities-renderer/") include_directories("${PROJECT_BINARY_DIR}/../../libraries/model/") -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tests/shared/CMakeLists.txt b/tests/shared/CMakeLists.txt index 7bddb4b2ed..740014ea75 100644 --- a/tests/shared/CMakeLists.txt +++ b/tests/shared/CMakeLists.txt @@ -5,7 +5,7 @@ macro (setup_testcase_dependencies) # link in the shared libraries link_hifi_libraries(shared) - copy_dlls_beside_windows_executable() + package_libraries_for_deployment() endmacro () setup_hifi_testcase() diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index 82fc23e680..8fda001e14 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -13,4 +13,4 @@ endif() # link in the shared libraries link_hifi_libraries(shared networking gl gpu ui) -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tools/mtc/CMakeLists.txt b/tools/mtc/CMakeLists.txt index 16c812673d..c7134a869f 100644 --- a/tools/mtc/CMakeLists.txt +++ b/tools/mtc/CMakeLists.txt @@ -1,4 +1,4 @@ set(TARGET_NAME mtc) setup_hifi_project() -copy_dlls_beside_windows_executable() +package_libraries_for_deployment() diff --git a/tools/udt-test/CMakeLists.txt b/tools/udt-test/CMakeLists.txt index 648ef6f00c..f21e3e9aea 100644 --- a/tools/udt-test/CMakeLists.txt +++ b/tools/udt-test/CMakeLists.txt @@ -2,5 +2,4 @@ set(TARGET_NAME udt-test) setup_hifi_project() link_hifi_libraries(networking shared) - -copy_dlls_beside_windows_executable() \ No newline at end of file +package_libraries_for_deployment() \ No newline at end of file