diff --git a/README.md b/README.md index 48e0de03af..44bfb94634 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,9 @@ In a new Terminal window, run: Any target can be terminated with Ctrl-C (SIGINT) in the associated Terminal window. -This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option. +This assignment-client will grab one assignment from the domain-server. You can tell the assignment-client what type you want it to be with the `-t` option. You can also run an assignment-client that forks off *n* assignment-clients with the `-n` option. The `-min` and `-max` options allow you to set a range of required assignment-clients, this allows you to have flexibility in the number of assignment-clients that are running. See `--help` for more options. - ./assignment-client -n 4 + ./assignment-client --min 6 --max 20 To test things out you'll want to run the Interface client. diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 457787013f..61164ee8d7 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -593,10 +593,15 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes // parse the identity packet and update the change timestamp if appropriate AvatarData::Identity identity; AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity); - if (avatar.processAvatarIdentity(identity)) { + bool identityChanged = false; + bool displayNameChanged = false; + avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged); + if (identityChanged) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->flagIdentityChange(); - nodeData->setAvatarSessionDisplayNameMustChange(true); + if (displayNameChanged) { + nodeData->setAvatarSessionDisplayNameMustChange(true); + } } } } diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 83a1b1ef84..dc1a693590 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -294,7 +294,9 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio } if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) { - // Fetch script from file synchronously. We don't want the server processing edits while a restarting entity server is fetching from a DOS'd source. + // Tell the tree that we have a filter, so that it doesn't accept edits until we have a filter function set up. + std::static_pointer_cast(_tree)->setHasEntityFilter(true); + // Now fetch script from file asynchronously. QUrl scriptURL(_entityEditFilter); // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) @@ -315,8 +317,6 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString()); scriptRequest->send(); qDebug() << "script request sent"; - _scriptRequestLoop.exec(); // Block here, but allow the request to be processed and its signals to be handled. - qDebug() << "script request event loop complete"; } } @@ -367,11 +367,7 @@ void EntityServer::scriptRequestFinished() { return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter); }); scriptRequest->deleteLater(); - qDebug() << "script request ending event loop. running:" << _scriptRequestLoop.isRunning(); - if (_scriptRequestLoop.isRunning()) { - _scriptRequestLoop.quit(); - } - qDebug() << "script request event loop quit"; + qDebug() << "script request filter processed"; return; } } @@ -386,11 +382,6 @@ void EntityServer::scriptRequestFinished() { // Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities. qDebug() << "script request failure causing stop"; stop(); - qDebug() << "script request ending event loop. running:" << _scriptRequestLoop.isRunning(); - if (_scriptRequestLoop.isRunning()) { - _scriptRequestLoop.quit(); - } - qDebug() << "script request event loop quit"; } void EntityServer::nodeAdded(SharedNodePointer node) { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 25270c9dd5..f142145d5f 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -80,7 +80,6 @@ private: QString _entityEditFilter{}; QScriptEngine _entityEditFilterEngine{}; - QEventLoop _scriptRequestLoop{}; }; #endif // hifi_EntityServer_h diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index b085eefb0c..d4d4b42e10 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi6.zip - URL_MD5 fcac808c1ba0b0f5b44ea06e2612ebab + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip + URL_MD5 bc2861e50852dd590cdc773a14a041a7 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 58b1df08c1..20d2711743 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -372,6 +372,13 @@ "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.", "value-hidden": true }, + { + "name": "verify_http_password", + "label": "Verify HTTP Password", + "type": "password", + "help": "Must match the password entered above for change to be saved.", + "value-hidden": true + }, { "name": "maximum_user_capacity", "label": "Maximum User Capacity", diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index ad426671a4..553f408e15 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -125,6 +125,10 @@ tr.new-row { background-color: #dff0d8; } +tr.invalid-input { + background-color: #f2dede; +} + .graphable-stat { text-align: center; color: #5286BC; diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index c31d6e2dfc..3ed7d02364 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -38,14 +38,15 @@ var Settings = { DOMAIN_ID_SELECTOR: '[name="metaverse.id"]', ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]', PLACES_TABLE_ID: 'places-table', - FORM_ID: 'settings-form' + FORM_ID: 'settings-form', + INVALID_ROW_CLASS: 'invalid-input' }; var viewHelpers = { getFormGroup: function(keypath, setting, values, isAdvanced) { form_group = "
"; setting_value = _(values).valueForKeyPath(keypath); @@ -215,8 +216,8 @@ $(document).ready(function(){ sibling = sibling.next(); } - if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { - sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click(); + // for tables with categories we add the entry and setup the new row on enter + if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) { sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click(); // set focus to the first input in the new row @@ -891,39 +892,146 @@ function reloadSettings(callback) { }); } +function validateInputs() { + // check if any new values are bad + var tables = $('table'); + + var inputsValid = true; + + var tables = $('table'); + + // clear any current invalid rows + $('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS); + + function markParentRowInvalid(rowChild) { + $(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS); + } + + _.each(tables, function(table) { + // validate keys specificially for spaces and equality to an existing key + var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key'); + + var keyWithSpaces = false; + var empty = false; + var duplicateKey = false; + + _.each(newKeys, function(keyCell) { + var keyVal = $(keyCell).children('input').val(); + + if (keyVal.indexOf(' ') !== -1) { + keyWithSpaces = true; + markParentRowInvalid(keyCell); + return; + } + + // make sure the key isn't empty + if (keyVal.length === 0) { + empty = true + + markParentRowInvalid(input); + return; + } + + // make sure we don't have duplicate keys in the table + var otherKeys = $(table).find('td.key').not(keyCell); + _.each(otherKeys, function(otherKeyCell) { + var keyInput = $(otherKeyCell).children('input'); + + if (keyInput.length) { + if ($(keyInput).val() == keyVal) { + duplicateKey = true; + } + } else if ($(otherKeyCell).html() == keyVal) { + duplicateKey = true; + } + + if (duplicateKey) { + markParentRowInvalid(keyCell); + return; + } + }); + + }); + + if (keyWithSpaces) { + showErrorMessage("Error", "Key contains spaces"); + inputsValid = false; + return + } + + if (empty) { + showErrorMessage("Error", "Empty field(s)"); + inputsValid = false; + return + } + + if (duplicateKey) { + showErrorMessage("Error", "Two keys cannot be identical"); + inputsValid = false; + return; + } + }); + + return inputsValid; +} var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!"; function saveSettings() { - // disable any inputs not changed - $("input:not([data-changed])").each(function(){ - $(this).prop('disabled', true); - }); - // grab a JSON representation of the form via form2js - var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); + if (validateInputs()) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - // check if we've set the basic http password - if so convert it to base64 - if (formJSON["security"]) { - var password = formJSON["security"]["http_password"]; - if (password && password.length > 0) { - formJSON["security"]["http_password"] = sha256_digest(password); + // disable any inputs not changed + $("input:not([data-changed])").each(function(){ + $(this).prop('disabled', true); + }); + + // grab a JSON representation of the form via form2js + var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); + + // check if we've set the basic http password - if so convert it to base64 + if (formJSON["security"]) { + var password = formJSON["security"]["http_password"]; + if (password && password.length > 0) { + formJSON["security"]["http_password"] = sha256_digest(password); + } + } + + // verify that the password and confirmation match before saving + var canPost = true; + + if (formJSON["security"]) { + var password = formJSON["security"]["http_password"]; + var verify_password = formJSON["security"]["verify_http_password"]; + + if (password && password.length > 0) { + if (password != verify_password) { + bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); + canPost = false; + } else { + formJSON["security"]["http_password"] = sha256_digest(password); + delete formJSON["security"]["verify_http_password"]; + } + } + } + + console.log("----- SAVING ------"); + console.log(formJSON); + + // re-enable all inputs + $("input").each(function(){ + $(this).prop('disabled', false); + }); + + // remove focus from the button + $(this).blur(); + + if (canPost) { + // POST the form JSON to the domain-server settings.json endpoint so the settings are saved + postSettings(formJSON); } } - - console.log("----- SAVING ------"); - console.log(formJSON); - - // re-enable all inputs - $("input").each(function(){ - $(this).prop('disabled', false); - }); - - // remove focus from the button - $(this).blur(); - - // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - postSettings(formJSON); } $('body').on('click', '.save-button', function(e){ @@ -1100,8 +1208,9 @@ function makeTable(setting, keypath, setting_value) { if (setting.can_add_new_categories) { html += makeTableCategoryInput(setting, numVisibleColumns); } + if (setting.can_add_new_rows || setting.can_add_new_categories) { - html += makeTableInputs(setting, {}, ""); + html += makeTableHiddenInputs(setting, {}, ""); } } html += "" @@ -1127,7 +1236,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, return html; } -function makeTableInputs(setting, initialValues, categoryValue) { +function makeTableHiddenInputs(setting, initialValues, categoryValue) { var html = ""; @@ -1138,7 +1247,7 @@ function makeTableInputs(setting, initialValues, categoryValue) { if (setting.key) { html += "\ - \ + \ " } @@ -1147,14 +1256,14 @@ function makeTableInputs(setting, initialValues, categoryValue) { if (col.type === "checkbox") { html += "" + - "" + ""; } else { html += "" + - "" + ""; @@ -1234,49 +1343,17 @@ function addTableRow(row) { var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS); + var input_clone = row.clone(); + if (!isArray) { - // Check key spaces - var key = row.children(".key").children("input").val() - if (key.indexOf(' ') !== -1) { - showErrorMessage("Error", "Key contains spaces") - return - } - // Check keys with the same name - var equals = false; - _.each(columns.children(".key"), function(element) { - if ($(element).text() === key) { - equals = true - return - } - }) - if (equals) { - showErrorMessage("Error", "Two keys cannot be identical") - return - } + // show the key input + var keyInput = row.children(".key").children("input"); } - // Check empty fields - var empty = false; - _.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) { - if ($(element).val().length === 0) { - empty = true - return - } - }) - - if (empty) { - showErrorMessage("Error", "Empty field(s)") - return - } - - var input_clone = row.clone() - // Change input row to data row - var table = row.parents("table") - var setting_name = table.attr("name") - var full_name = setting_name + "." + key - row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS) - row.removeClass("inputs") + var table = row.parents("table"); + var setting_name = table.attr("name"); + row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS); _.each(row.children(), function(element) { if ($(element).hasClass("numbered")) { @@ -1298,56 +1375,43 @@ function addTableRow(row) { anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES) } else if ($(element).hasClass("key")) { var input = $(element).children("input") - $(element).html(input.val()) - input.remove() + input.show(); } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { - // Hide inputs - var input = $(element).find("input") - var isCheckbox = false; - var isTime = false; - if (input.hasClass("table-checkbox")) { - input = $(input).parent(); - isCheckbox = true; - } else if (input.hasClass("table-time")) { - input = $(input).parent(); - isTime = true; - } + // show inputs + var input = $(element).find("input"); + input.show(); - var val = input.val(); - if (isCheckbox) { - // don't hide the checkbox - val = $(input).find("input").is(':checked'); - } else if (isTime) { - // don't hide the time - } else { - input.attr("type", "hidden") - } + var isCheckbox = input.hasClass("table-checkbox"); if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length - var key = $(element).attr('name') + var key = $(element).attr('name'); // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length if (isCheckbox) { - $(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } else { input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) } } else { - input.attr("name", full_name + "." + $(element).attr("name")) + // because the name of the setting in question requires the key + // setup a hook to change the HTML name of the element whenever the key changes + var colName = $(element).attr("name"); + keyInput.on('change', function(){ + input.attr("name", setting_name + "." + $(this).val() + "." + colName); + }); } if (isCheckbox) { $(input).find("input").attr("data-changed", "true"); } else { input.attr("data-changed", "true"); - $(element).append(val); } } else { - console.log("Unknown table element") + console.log("Unknown table element"); } }); @@ -1377,7 +1441,12 @@ function deleteTableRow($row) { $row.empty(); if (!isArray) { - $row.html(""); + if ($row.attr('name')) { + $row.html(""); + } else { + // for rows that didn't have a key, simply remove the row + $row.remove(); + } } else { if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) { // This is the last row of the category, so delete the header diff --git a/interface/resources/QtWebEngine/UIDelegates/Menu.qml b/interface/resources/QtWebEngine/UIDelegates/Menu.qml index 21c5e71394..5176d9d11e 100644 --- a/interface/resources/QtWebEngine/UIDelegates/Menu.qml +++ b/interface/resources/QtWebEngine/UIDelegates/Menu.qml @@ -33,6 +33,7 @@ Item { propagateComposedEvents: true acceptedButtons: "AllButtons" onClicked: { + menu.visible = false; menu.done(); mouse.accepted = false; } diff --git a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml index 561a8926e1..1890fcb81d 100644 --- a/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml +++ b/interface/resources/QtWebEngine/UIDelegates/MenuItem.qml @@ -32,6 +32,7 @@ Item { MouseArea { anchors.fill: parent onClicked: { + menu.visible = false; root.triggered(); menu.done(); } diff --git a/interface/resources/html/img/controls-help-gamepad.png b/interface/resources/html/img/controls-help-gamepad.png index cb77dbdabd..c9d2aa14ec 100644 Binary files a/interface/resources/html/img/controls-help-gamepad.png and b/interface/resources/html/img/controls-help-gamepad.png differ diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 7478ca9c2b..b55b9c517d 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -34,6 +34,7 @@ Item { property bool isMyCard: false property bool selected: false property bool isAdmin: false + property bool currentlyEditingDisplayName: false /* User image commented out for now - will probably be re-introduced later. Column { @@ -104,6 +105,7 @@ Item { focus = false myDisplayName.border.width = 0 color = hifi.colors.darkGray + currentlyEditingDisplayName = false } } MouseArea { @@ -115,10 +117,12 @@ Item { myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); myDisplayNameText.focus = true myDisplayNameText.color = "black" + currentlyEditingDisplayName = true } onDoubleClicked: { myDisplayNameText.selectAll(); myDisplayNameText.focus = true; + currentlyEditingDisplayName = true } onEntered: myDisplayName.color = hifi.colors.lightGrayText onExited: myDisplayName.color = hifi.colors.textFieldLightBackground diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index f3554727dd..923b09b9ef 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -51,6 +51,7 @@ Rectangle { // This is the container for the PAL Rectangle { + property bool punctuationMode: false id: palContainer // Size width: pal.width - 50 @@ -421,6 +422,16 @@ Rectangle { onExited: adminHelpText.color = hifi.colors.redHighlight } } + HifiControls.Keyboard { + id: keyboard + raised: myCard.currentlyEditingDisplayName && HMD.active + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right + } + } } // Timer used when selecting table rows that aren't yet present in the model // (i.e. when selecting avatars using edit.js or sphere overlays) diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml index 9ad8d1476c..c6c810d25e 100644 --- a/interface/resources/qml/hifi/tablet/TabletButton.qml +++ b/interface/resources/qml/hifi/tablet/TabletButton.qml @@ -75,6 +75,14 @@ Item { source: buttonOutline } + function urlHelper(src) { + if (src.match(/\bhttp/)) { + return src; + } else { + return "../../../" + src; + } + } + Image { id: icon width: 50 @@ -84,7 +92,7 @@ Item { anchors.bottomMargin: 5 anchors.horizontalCenter: parent.horizontalCenter fillMode: Image.Stretch - source: "../../../" + tabletButton.icon + source: tabletButton.urlHelper(tabletButton.icon) } ColorOverlay { @@ -185,7 +193,7 @@ Item { PropertyChanges { target: icon - source: "../../../" + tabletButton.activeIcon + source: tabletButton.urlHelper(tabletButton.activeIcon) } }, State { diff --git a/interface/resources/qml/hifi/tablet/TabletMenu.qml b/interface/resources/qml/hifi/tablet/TabletMenu.qml index a9ca1b884c..cd3a9cacae 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenu.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenu.qml @@ -54,7 +54,10 @@ FocusScope { onEntered: iconColorOverlay.color = "#1fc6a6"; onExited: iconColorOverlay.color = "#ffffff"; // navigate back to root level menu - onClicked: buildMenu(); + onClicked: { + buildMenu(); + tabletRoot.playButtonClickSound(); + } } } @@ -79,10 +82,12 @@ FocusScope { onEntered: breadcrumbText.color = "#1fc6a6"; onExited: breadcrumbText.color = "#34a2c7"; // navigate back to parent level menu if there is one - onClicked: + onClicked: { if (breadcrumbText.text !== "Menu") { menuPopperUpper.closeLastMenu(); } + tabletRoot.playButtonClickSound(); + } } } } diff --git a/interface/resources/qml/hifi/tablet/TabletMenuView.qml b/interface/resources/qml/hifi/tablet/TabletMenuView.qml index 6411c1af02..1845396230 100644 --- a/interface/resources/qml/hifi/tablet/TabletMenuView.qml +++ b/interface/resources/qml/hifi/tablet/TabletMenuView.qml @@ -75,7 +75,10 @@ FocusScope { anchors.fill: parent hoverEnabled: true onEntered: listView.currentIndex = index - onClicked: root.selected(item) + onClicked: { + root.selected(item) + tabletRoot.playButtonClickSound(); + } } } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index ded91a5eff..cfda92e774 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -20,6 +20,7 @@ Item { SoundEffect { id: buttonClickSound + volume: 0.1 source: "../../../sounds/Gamemaster-Audio-button-click.wav" } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c96a63ce1c..af3a449114 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1202,8 +1202,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto entityScriptingInterface = DependencyManager::get(); connect(entityScriptingInterface.data(), &EntityScriptingInterface::clickDownOnEntity, [this](const EntityItemID& entityItemID, const PointerEvent& event) { - setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID); - setKeyboardFocusEntity(entityItemID); + auto entity = getEntities()->getTree()->findEntityByID(entityItemID); + if (entity && entity->wantsKeyboardFocus()) { + setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID); + setKeyboardFocusEntity(entityItemID); + } }); connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, [=](const EntityItemID& entityItemID) { @@ -4165,6 +4168,8 @@ void Application::setKeyboardFocusOverlay(unsigned int overlayID) { auto size = overlay->getSize() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR; const float OVERLAY_DEPTH = 0.0105f; setKeyboardFocusHighlight(overlay->getPosition(), overlay->getRotation(), glm::vec3(size.x, size.y, OVERLAY_DEPTH)); + } else if (_keyboardFocusHighlight) { + _keyboardFocusHighlight->setVisible(false); } } } @@ -4379,6 +4384,10 @@ void Application::update(float deltaTime) { PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("harvestChanges"); if (_physicsEngine->hasOutgoingChanges()) { + // grab the collision events BEFORE handleOutgoingChanges() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); @@ -4386,11 +4395,10 @@ void Application::update(float deltaTime) { avatarManager->handleOutgoingChanges(outgoingChanges); }); - auto collisionEvents = _physicsEngine->getCollisionEvents(); - avatarManager->handleCollisionEvents(collisionEvents); - if (!_aboutToQuit) { + // handleCollisionEvents() AFTER handleOutgoinChanges() PerformanceTimer perfTimer("entities"); + avatarManager->handleCollisionEvents(collisionEvents); // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk // deadlock.) _entitySimulation->handleCollisionEvents(collisionEvents); diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 9e6c524c2c..64e82f63da 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -900,7 +900,7 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { _skeletonModel->getAbsoluteJointRotationInRigFrame(headJointIndex, rotation); } } - return rotation; + return Quaternions::Y_180 * rotation * Quaternions::Y_180; } default: { glm::quat rotation; @@ -936,7 +936,7 @@ glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { _skeletonModel->getAbsoluteJointTranslationInRigFrame(headJointIndex, translation); } } - return translation; + return Quaternions::Y_180 * translation * Quaternions::Y_180; } default: { glm::vec3 translation; diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index f70537a952..a0f7c4e824 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -18,7 +18,7 @@ QString const ModelOverlay::TYPE = "model"; ModelOverlay::ModelOverlay() - : _model(std::make_shared(std::make_shared())), + : _model(std::make_shared(std::make_shared(), nullptr, this)), _modelTextures(QVariantMap()) { _model->init(); @@ -27,7 +27,7 @@ ModelOverlay::ModelOverlay() ModelOverlay::ModelOverlay(const ModelOverlay* modelOverlay) : Volume3DOverlay(modelOverlay), - _model(std::make_shared(std::make_shared())), + _model(std::make_shared(std::make_shared(), nullptr, this)), _modelTextures(QVariantMap()), _url(modelOverlay->_url), _updateModel(false) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1e3dc11338..2e532d67bf 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -112,6 +112,42 @@ private: bool _quit { false }; }; +void AudioInjectorsThread::prepare() { + _audio->prepareLocalAudioInjectors(); +} + +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + +static inline float convertToFloat(int16_t sample) { + return (float)sample * (1 / 32768.0f); +} + AudioClient::AudioClient() : AbstractAudioInterface(), _gate(this), @@ -127,6 +163,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), + _localInjectorsStream(0), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -144,13 +181,18 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), + _localToOutputResampler(NULL), + _localAudioThread(this), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), - _audioOutputIODevice(_receivedAudioStream, this), + _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { + // avoid putting a lock in the device callback + assert(_localSamplesAvailable.is_lock_free()); + // deprecate legacy settings { Setting::Handle::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED); @@ -176,6 +218,10 @@ AudioClient::AudioClient() : _checkDevicesThread->setPriority(QThread::LowPriority); _checkDevicesThread->start(); + // start a thread to process local injectors + _localAudioThread.setObjectName("LocalAudio Thread"); + _localAudioThread.start(); + configureReverb(); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -213,6 +259,7 @@ void AudioClient::reset() { _stats.reset(); _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } void AudioClient::audioMixerKilled() { @@ -365,7 +412,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { CoUninitialize(); } - qCDebug(audioclient) << "DEBUG [" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; return getNamedAudioDeviceForMode(mode, deviceName); #endif @@ -387,12 +434,12 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, audioFormat.setByteOrder(QAudioFormat::LittleEndian); if (!audioDevice.isFormatSupported(audioFormat)) { - qCDebug(audioclient) << "WARNING: The native format is" << audioFormat << "but isFormatSupported() failed."; + qCWarning(audioclient) << "The native format is" << audioFormat << "but isFormatSupported() failed."; return false; } // converting to/from this rate must produce an integral number of samples if (audioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) { - qCDebug(audioclient) << "WARNING: The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; + qCWarning(audioclient) << "The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; return false; } return true; @@ -726,12 +773,12 @@ QVector AudioClient::getDeviceNames(QAudio::Mode mode) { } bool AudioClient::switchInputToAudioDevice(const QString& inputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; return switchInputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName)); } bool AudioClient::switchOutputToAudioDevice(const QString& outputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; return switchOutputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName)); } @@ -762,6 +809,7 @@ void AudioClient::configureReverb() { p.wetDryMix = _reverbOptions->getWetDryMix(); _listenerReverb.setParameters(&p); + _localReverb.setParameters(&p); // used only for adding self-reverb to loopback audio p.sampleRate = _outputFormat.sampleRate(); @@ -808,6 +856,7 @@ void AudioClient::setReverb(bool reverb) { if (!_reverb) { _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } } @@ -841,36 +890,6 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } -static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); @@ -1082,14 +1101,78 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { PacketType::MicrophoneAudioWithEcho, _selectedCodecName); } -void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { +void AudioClient::prepareLocalAudioInjectors() { + if (_outputPeriod == 0) { + return; + } + + int bufferCapacity = _localInjectorsStream.getSampleCapacity(); + if (_localToOutputResampler) { + // avoid overwriting the buffer, + // instead of failing on writes because the buffer is used as a lock-free pipe + bufferCapacity -= + _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + AudioConstants::STEREO; + bufferCapacity += 1; + } + + int samplesNeeded = std::numeric_limits::max(); + while (samplesNeeded > 0) { + // lock for every write to avoid locking out the device callback + // this lock is intentional - the buffer is only lock-free in its use in the device callback + Lock lock(_localAudioMutex); + + samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); + if (samplesNeeded <= 0) { + break; + } + + // get a network frame of local injectors' audio + if (!mixLocalAudioInjectors(_localMixBuffer)) { + break; + } + + // reverb + if (_reverb) { + _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + int samples; + if (_localToOutputResampler) { + // resample to output sample rate + int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + // write to local injectors' ring buffer + samples = frames * AudioConstants::STEREO; + _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples); + + } else { + // write to local injectors' ring buffer + samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localInjectorsStream.writeSamples(_localMixBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + } + + _localSamplesAvailable.fetch_add(samples, std::memory_order_release); + samplesNeeded -= samples; + } +} + +bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { QVector injectorsToRemove; // lock the injector vector Lock lock(_injectorsMutex); - for (AudioInjector* injector : getActiveLocalAudioInjectors()) { + if (_activeLocalAudioInjectors.size() == 0) { + return false; + } + + memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); + + for (AudioInjector* injector : _activeLocalAudioInjectors) { if (injector->getLocalBuffer()) { static const int HRTF_DATASET_INDEX = 1; @@ -1098,8 +1181,8 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { qint64 bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector - memset(_scratchBuffer, 0, bytesToRead); - if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, bytesToRead)) { + memset(_localScratchBuffer, 0, bytesToRead); + if (0 < injector->getLocalBuffer()->readData((char*)_localScratchBuffer, bytesToRead)) { if (injector->isAmbisonic()) { @@ -1119,7 +1202,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { float qz = relativeOrientation.y; // Ambisonic gets spatialized into mixBuffer - injector->getLocalFOA().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else if (injector->isStereo()) { @@ -1127,7 +1210,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // stereo gets directly mixed into mixBuffer float gain = injector->getVolume(); for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - mixBuffer[i] += (float)_scratchBuffer[i] * (1/32768.0f) * gain; + mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; } } else { @@ -1136,10 +1219,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); float gain = gainForSource(distance, injector->getVolume()); - float azimuth = azimuthForSource(relativePosition); + float azimuth = azimuthForSource(relativePosition); // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } @@ -1160,8 +1243,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (AudioInjector* injector : injectorsToRemove) { qCDebug(audioclient) << "removing injector"; - getActiveLocalAudioInjectors().removeOne(injector); + _activeLocalAudioInjectors.removeOne(injector); } + + return true; } void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { @@ -1172,33 +1257,24 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - // convert network audio to float - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _mixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); - } - - // mix in active injectors - if (getActiveLocalAudioInjectors().size() > 0) { - mixLocalAudioInjectors(_mixBuffer); - } + bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); // apply stereo reverb - bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); if (hasReverb) { updateReverbOptions(); - _listenerReverb.render(_mixBuffer, _mixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples; + _listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } + // resample to output sample rate if (_networkToOutputResampler) { + const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples; + _networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } - // resample to output sample rate - _audioLimiter.render(_mixBuffer, _scratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - _networkToOutputResampler->render(_scratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - } else { - - // no resampling needed - _audioLimiter.render(_mixBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + // if no transformations were applied, we still need to copy the buffer + if (!hasReverb && !_networkToOutputResampler) { + memcpy(outputSamples, decodedSamples, decodedBuffer.size()); } } @@ -1381,6 +1457,9 @@ void AudioClient::outputNotify() { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { bool supportedFormat = false; + Lock lock(_localAudioMutex); + _localSamplesAvailable.exchange(0, std::memory_order_release); + // cleanup any previously initialized device if (_audioOutput) { _audioOutput->stop(); @@ -1391,12 +1470,24 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _loopbackOutputDevice = NULL; delete _loopbackAudioOutput; _loopbackAudioOutput = NULL; + + delete[] _outputMixBuffer; + _outputMixBuffer = NULL; + + delete[] _outputScratchBuffer; + _outputScratchBuffer = NULL; + + delete[] _localOutputMixBuffer; + _localOutputMixBuffer = NULL; } if (_networkToOutputResampler) { // if we were using an input to network resampler, delete it here delete _networkToOutputResampler; _networkToOutputResampler = NULL; + + delete _localToOutputResampler; + _localToOutputResampler = NULL; } if (!outputDeviceInfo.isNull()) { @@ -1416,6 +1507,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice assert(_outputFormat.sampleSize() == 16); _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1441,6 +1533,14 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); + int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes + _outputPeriod = periodSampleSize * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + _localOutputMixBuffer = new float[_outputPeriod]; + _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); @@ -1550,26 +1650,61 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // samples requested from OUTPUT_CHANNEL_COUNT int deviceChannelCount = _audio->_outputFormat.channelCount(); int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + // restrict samplesRequested to the size of our mix/scratch buffers + samplesRequested = std::min(samplesRequested, _audio->_outputPeriod); - int samplesPopped; - int bytesWritten; + int16_t* scratchBuffer = _audio->_outputScratchBuffer; + float* mixBuffer = _audio->_outputMixBuffer; - if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); + int networkSamplesPopped; + if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); + lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - // if required, upmix or downmix to deviceChannelCount - if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - } else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT); - } else { - lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped); + for (int i = 0; i < networkSamplesPopped; i++) { + mixBuffer[i] = convertToFloat(scratchBuffer[i]); } - bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; + + samplesRequested = networkSamplesPopped; + } + + int injectorSamplesPopped = 0; + { + Lock lock(_audio->_localAudioMutex); + bool append = networkSamplesPopped > 0; + samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire)); + if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { + _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); + } + } + + // prepare injectors for the next callback + QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection); + + int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); + int framesPopped = samplesPopped / AudioConstants::STEREO; + int bytesWritten; + if (samplesPopped > 0) { + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + // limit the audio + _audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped); + } else { + _audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped); + + // upmix or downmix to deviceChannelCount + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT; + channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels); + } else { + channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped); + } + } + + bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; } else { // nothing on network, don't grab anything from injectors, and just return 0s - // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); memset(data, 0, maxSize); bytesWritten = maxSize; } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 123da35319..699ba71ef7 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -69,9 +69,24 @@ class QIODevice; class Transform; class NLPacket; +class AudioInjectorsThread : public QThread { + Q_OBJECT + +public: + AudioInjectorsThread(AudioClient* audio) : _audio(audio) {} + +public slots : + void prepare(); + +private: + AudioClient* _audio; +}; + class AudioClient : public AbstractAudioInterface, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + + using LocalInjectorsStream = AudioMixRingBuffer; public: static const int MIN_BUFFER_FRAMES; static const int MAX_BUFFER_FRAMES; @@ -84,8 +99,10 @@ public: class AudioOutputIODevice : public QIODevice { public: - AudioOutputIODevice(MixedProcessedAudioStream& receivedAudioStream, AudioClient* audio) : - _receivedAudioStream(receivedAudioStream), _audio(audio), _unfulfilledReads(0) {}; + AudioOutputIODevice(LocalInjectorsStream& localInjectorsStream, MixedProcessedAudioStream& receivedAudioStream, + AudioClient* audio) : + _localInjectorsStream(localInjectorsStream), _receivedAudioStream(receivedAudioStream), + _audio(audio), _unfulfilledReads(0) {} void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); } void stop() { close(); } @@ -93,6 +110,7 @@ public: qint64 writeData(const char * data, qint64 maxSize) override { return 0; } int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: + LocalInjectorsStream& _localInjectorsStream; MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; @@ -129,8 +147,6 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - QVector& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } - void checkDevices(); static const float CALLBACK_ACCELERATOR_RATIO; @@ -171,6 +187,7 @@ public slots: int setOutputBufferSize(int numFrames, bool persist = true); + void prepareLocalAudioInjectors(); bool outputLocalInjector(AudioInjector* injector) override; bool shouldLoopbackInjectors() override { return _shouldEchoToServer; } @@ -218,7 +235,7 @@ protected: private: void outputFormatChanged(); - void mixLocalAudioInjectors(float* mixBuffer); + bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -262,6 +279,10 @@ private: QAudioOutput* _loopbackAudioOutput; QIODevice* _loopbackOutputDevice; AudioRingBuffer _inputRingBuffer; + LocalInjectorsStream _localInjectorsStream; + // In order to use _localInjectorsStream as a lock-free pipe, + // use it with a single producer/consumer, and track available samples + std::atomic _localSamplesAvailable { 0 }; MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; @@ -292,14 +313,28 @@ private: AudioEffectOptions* _reverbOptions; AudioReverb _sourceReverb { AudioConstants::SAMPLE_RATE }; AudioReverb _listenerReverb { AudioConstants::SAMPLE_RATE }; + AudioReverb _localReverb { AudioConstants::SAMPLE_RATE }; // possible streams needed for resample AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; + AudioSRC* _localToOutputResampler; + + // for network audio (used by network audio thread) + int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + + // for local audio (used by audio injectors thread) + float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + float* _localOutputMixBuffer { NULL }; + AudioInjectorsThread _localAudioThread; + Mutex _localAudioMutex; + + // for output audio (used by this thread) + int _outputPeriod { 0 }; + float* _outputMixBuffer { NULL }; + int16_t* _outputScratchBuffer { NULL }; - // for local hrtf-ing - float _mixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; - int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; AudioLimiter _audioLimiter; // Adds Reverb diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index 260c682cde..59b3e874d7 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -26,46 +26,51 @@ static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." }; static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." }; -AudioRingBuffer::AudioRingBuffer(int numFrameSamples, int numFramesCapacity) : +template +AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : _numFrameSamples(numFrameSamples), _frameCapacity(numFramesCapacity), _sampleCapacity(numFrameSamples * numFramesCapacity), _bufferLength(numFrameSamples * (numFramesCapacity + 1)) { if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); _nextOutput = _buffer; _endOfLastWrite = _buffer; } static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG); static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); -}; +} -AudioRingBuffer::~AudioRingBuffer() { +template +AudioRingBufferTemplate::~AudioRingBufferTemplate() { delete[] _buffer; } -void AudioRingBuffer::clear() { +template +void AudioRingBufferTemplate::clear() { _endOfLastWrite = _buffer; _nextOutput = _buffer; } -void AudioRingBuffer::reset() { +template +void AudioRingBufferTemplate::reset() { clear(); _overflowCount = 0; } -void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { +template +void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { delete[] _buffer; _numFrameSamples = numFrameSamples; _sampleCapacity = numFrameSamples * _frameCapacity; _bufferLength = numFrameSamples * (_frameCapacity + 1); if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); } else { _buffer = nullptr; } @@ -73,17 +78,29 @@ void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { reset(); } -int AudioRingBuffer::readSamples(int16_t* destination, int maxSamples) { - return readData((char*)destination, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +template +int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; } -int AudioRingBuffer::writeSamples(const int16_t* source, int maxSamples) { - return writeData((char*)source, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +template +int AudioRingBufferTemplate::appendSamples(Sample* destination, int maxSamples, bool append) { + if (append) { + return appendData((char*)destination, maxSamples * SampleSize) / SampleSize; + } else { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; + } } -int AudioRingBuffer::readData(char *data, int maxSize) { +template +int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { + return writeData((char*)source, maxSamples * SampleSize) / SampleSize; +} + +template +int AudioRingBufferTemplate::readData(char *data, int maxSize) { // only copy up to the number of samples we have available - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numReadSamples = std::min(maxSamples, samplesAvailable()); if (_nextOutput + numReadSamples > _buffer + _bufferLength) { @@ -91,22 +108,56 @@ int AudioRingBuffer::readData(char *data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; // read to the end of the buffer - memcpy(data, _nextOutput, numSamplesToEnd * sizeof(int16_t)); + memcpy(data, _nextOutput, numSamplesToEnd * SampleSize); // read the rest from the beginning of the buffer - memcpy(data + (numSamplesToEnd * sizeof(int16_t)), _buffer, (numReadSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(data + (numSamplesToEnd * SampleSize), _buffer, (numReadSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(data, _nextOutput, numReadSamples * sizeof(int16_t)); + memcpy(data, _nextOutput, numReadSamples * SampleSize); } shiftReadPosition(numReadSamples); - return numReadSamples * sizeof(int16_t); + return numReadSamples * SampleSize; } -int AudioRingBuffer::writeData(const char* data, int maxSize) { +template +int AudioRingBufferTemplate::appendData(char *data, int maxSize) { + // only copy up to the number of samples we have available + int maxSamples = maxSize / SampleSize; + int numReadSamples = std::min(maxSamples, samplesAvailable()); + + Sample* dest = reinterpret_cast(data); + Sample* output = _nextOutput; + if (_nextOutput + numReadSamples > _buffer + _bufferLength) { + // we're going to need to do two reads to get this data, it wraps around the edge + int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; + + // read to the end of the buffer + for (int i = 0; i < numSamplesToEnd; i++) { + *dest++ += *output++; + } + + // read the rest from the beginning of the buffer + output = _buffer; + for (int i = 0; i < (numReadSamples - numSamplesToEnd); i++) { + *dest++ += *output++; + } + } else { + for (int i = 0; i < numReadSamples; i++) { + *dest++ += *output++; + } + } + + shiftReadPosition(numReadSamples); + + return numReadSamples * SampleSize; +} + +template +int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { // only copy up to the number of samples we have capacity for - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numWriteSamples = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); @@ -124,20 +175,21 @@ int AudioRingBuffer::writeData(const char* data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; // write to the end of the buffer - memcpy(_endOfLastWrite, data, numSamplesToEnd * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numSamplesToEnd * SampleSize); // write the rest to the beginning of the buffer - memcpy(_buffer, data + (numSamplesToEnd * sizeof(int16_t)), (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(_buffer, data + (numSamplesToEnd * SampleSize), (numWriteSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(_endOfLastWrite, data, numWriteSamples * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numWriteSamples * SampleSize); } _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - return numWriteSamples * sizeof(int16_t); + return numWriteSamples * SampleSize; } -int AudioRingBuffer::samplesAvailable() const { +template +int AudioRingBufferTemplate::samplesAvailable() const { if (!_endOfLastWrite) { return 0; } @@ -149,31 +201,8 @@ int AudioRingBuffer::samplesAvailable() const { return sampleDifference; } -int AudioRingBuffer::addSilentSamples(int silentSamples) { - // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there - int numWriteSamples = std::min(silentSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - - if (numWriteSamples > samplesRoomFor) { - numWriteSamples = samplesRoomFor; - - qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); - } - - if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { - int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; - memset(_endOfLastWrite, 0, numSamplesToEnd * sizeof(int16_t)); - memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); - } else { - memset(_endOfLastWrite, 0, numWriteSamples * sizeof(int16_t)); - } - - _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - - return numWriteSamples; -} - -int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const { +template +typename AudioRingBufferTemplate::Sample* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { // this shift will wrap the position around to the beginning of the ring @@ -186,11 +215,37 @@ int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int } } -float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { +template +int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { + // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there + int numWriteSamples = std::min(silentSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + + if (numWriteSamples > samplesRoomFor) { + numWriteSamples = samplesRoomFor; + + qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); + } + + if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { + int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; + memset(_endOfLastWrite, 0, numSamplesToEnd * SampleSize); + memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * SampleSize); + } else { + memset(_endOfLastWrite, 0, numWriteSamples * SampleSize); + } + + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); + + return numWriteSamples; +} + +template +float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStart) const { // FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x)) float loudness = 0.0f; - const int16_t* sampleAt = frameStart; - const int16_t* bufferLastAt = _buffer + _bufferLength - 1; + const Sample* sampleAt = frameStart; + const Sample* bufferLastAt = _buffer + _bufferLength - 1; for (int i = 0; i < _numFrameSamples; ++i) { loudness += (float) std::abs(*sampleAt); @@ -203,14 +258,16 @@ float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { return loudness; } -float AudioRingBuffer::getFrameLoudness(ConstIterator frameStart) const { +template +float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStart) const { if (frameStart.isNull()) { return 0.0f; } return getFrameLoudness(&(*frameStart)); } -int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { +template +int AudioRingBufferTemplate::writeSamples(ConstIterator source, int maxSamples) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -221,7 +278,7 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { *_endOfLastWrite = *source; _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; @@ -231,7 +288,8 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { return samplesToCopy; } -int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { +template +int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -242,12 +300,16 @@ int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { - *_endOfLastWrite = (int16_t)((float)(*source) * fade); + *_endOfLastWrite = (Sample)((float)(*source) * fade); _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; ++source; } return samplesToCopy; } + +// explicit instantiations for scratch/mix buffers +template class AudioRingBufferTemplate; +template class AudioRingBufferTemplate; diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 29e7a9e998..bb32df19a2 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -21,15 +21,19 @@ const int DEFAULT_RING_BUFFER_FRAME_CAPACITY = 10; -class AudioRingBuffer { +template +class AudioRingBufferTemplate { + using Sample = T; + static const int SampleSize = sizeof(Sample); + public: - AudioRingBuffer(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); - ~AudioRingBuffer(); + AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); + ~AudioRingBufferTemplate(); // disallow copying - AudioRingBuffer(const AudioRingBuffer&) = delete; - AudioRingBuffer(AudioRingBuffer&&) = delete; - AudioRingBuffer& operator=(const AudioRingBuffer&) = delete; + AudioRingBufferTemplate(const AudioRingBufferTemplate&) = delete; + AudioRingBufferTemplate(AudioRingBufferTemplate&&) = delete; + AudioRingBufferTemplate& operator=(const AudioRingBufferTemplate&) = delete; /// Invalidate any data in the buffer void clear(); @@ -41,13 +45,27 @@ public: // FIXME: discards any data in the buffer void resizeForFrameSize(int numFrameSamples); + // Reading and writing to the buffer uses minimal shared data, such that + // in cases that avoid overwriting the buffer, a single producer/consumer + // may use this as a lock-free pipe (see audio-client/src/AudioClient.cpp). + // IMPORTANT: Avoid changes to the implementation that touch shared data unless you can + // maintain this behavior. + /// Read up to maxSamples into destination (will only read up to samplesAvailable()) /// Returns number of read samples - int readSamples(int16_t* destination, int maxSamples); + int readSamples(Sample* destination, int maxSamples); + + /// Append up to maxSamples into destination (will only read up to samplesAvailable()) + /// If append == false, behaves as readSamples + /// Returns number of appended samples + int appendSamples(Sample* destination, int maxSamples, bool append = true); + + /// Skip up to maxSamples (will only skip up to samplesAvailable()) + void skipSamples(int maxSamples) { shiftReadPosition(std::min(maxSamples, samplesAvailable())); } /// Write up to maxSamples from source (will only write up to sample capacity) /// Returns number of written samples - int writeSamples(const int16_t* source, int maxSamples); + int writeSamples(const Sample* source, int maxSamples); /// Write up to maxSamples silent samples (will only write until other data exists in the buffer) /// This method will not overwrite existing data in the buffer, instead dropping silent samples that would overflow @@ -58,13 +76,17 @@ public: /// Returns number of read bytes int readData(char* destination, int maxSize); + /// Append up to maxSize into destination + /// Returns number of read bytes + int appendData(char* destination, int maxSize); + /// Write up to maxSize from source /// Returns number of written bytes int writeData(const char* source, int maxSize); /// Returns a reference to the index-th sample offset from the current read sample - int16_t& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } - const int16_t& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + Sample& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + const Sample& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } /// Essentially discards the next numSamples from the ring buffer /// NOTE: This is not checked - it is possible to shift past written data @@ -84,41 +106,104 @@ public: class ConstIterator { public: - ConstIterator(); - ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at); + ConstIterator() : + _bufferLength(0), + _bufferFirst(NULL), + _bufferLast(NULL), + _at(NULL) {} + ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : + _bufferLength(capacity), + _bufferFirst(bufferFirst), + _bufferLast(bufferFirst + capacity - 1), + _at(at) {} ConstIterator(const ConstIterator& rhs) = default; bool isNull() const { return _at == NULL; } bool operator==(const ConstIterator& rhs) { return _at == rhs._at; } bool operator!=(const ConstIterator& rhs) { return _at != rhs._at; } - const int16_t& operator*() { return *_at; } + const Sample& operator*() { return *_at; } - ConstIterator& operator=(const ConstIterator& rhs); - ConstIterator& operator++(); - ConstIterator operator++(int); - ConstIterator& operator--(); - ConstIterator operator--(int); - const int16_t& operator[] (int i); - ConstIterator operator+(int i); - ConstIterator operator-(int i); + ConstIterator& operator=(const ConstIterator& rhs) { + _bufferLength = rhs._bufferLength; + _bufferFirst = rhs._bufferFirst; + _bufferLast = rhs._bufferLast; + _at = rhs._at; + return *this; + } + ConstIterator& operator++() { + _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; + return *this; + } + ConstIterator operator++(int) { + ConstIterator tmp(*this); + ++(*this); + return tmp; + } + ConstIterator& operator--() { + _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; + return *this; + } + ConstIterator operator--(int) { + ConstIterator tmp(*this); + --(*this); + return tmp; + } + const Sample& operator[] (int i) { + return *atShiftedBy(i); + } + ConstIterator operator+(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); + } + ConstIterator operator-(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); + } + + void readSamples(Sample* dest, int numSamples) { + auto samplesToEnd = _bufferLast - _at + 1; + + if (samplesToEnd >= numSamples) { + memcpy(dest, _at, numSamples * SampleSize); + _at += numSamples; + } else { + auto samplesFromStart = numSamples - samplesToEnd; + memcpy(dest, _at, samplesToEnd * SampleSize); + memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * SampleSize); + + _at = _bufferFirst + samplesFromStart; + } + } + void readSamplesWithFade(Sample* dest, int numSamples, float fade) { + Sample* at = _at; + for (int i = 0; i < numSamples; i++) { + *dest = (float)*at * fade; + ++dest; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + } + } - void readSamples(int16_t* dest, int numSamples); - void readSamplesWithFade(int16_t* dest, int numSamples, float fade); - void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels); - void readSamplesWithDownmix(int16_t* dest, int numSamples); private: - int16_t* atShiftedBy(int i); + Sample* atShiftedBy(int i) { + i = (_at - _bufferFirst + i) % _bufferLength; + if (i < 0) { + i += _bufferLength; + } + return _bufferFirst + i; + } int _bufferLength; - int16_t* _bufferFirst; - int16_t* _bufferLast; - int16_t* _at; + Sample* _bufferFirst; + Sample* _bufferLast; + Sample* _at; }; - ConstIterator nextOutput() const; - ConstIterator lastFrameWritten() const; + ConstIterator nextOutput() const { + return ConstIterator(_buffer, _bufferLength, _nextOutput); + } + ConstIterator lastFrameWritten() const { + return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; + } int writeSamples(ConstIterator source, int maxSamples); int writeSamplesWithFade(ConstIterator source, int maxSamples, float fade); @@ -126,8 +211,8 @@ public: float getFrameLoudness(ConstIterator frameStart) const; protected: - int16_t* shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const; - float getFrameLoudness(const int16_t* frameStart) const; + Sample* shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const; + float getFrameLoudness(const Sample* frameStart) const; int _numFrameSamples; int _frameCapacity; @@ -135,138 +220,13 @@ protected: int _bufferLength; // actual _buffer length (_sampleCapacity + 1) int _overflowCount{ 0 }; // times the ring buffer has overwritten data - int16_t* _nextOutput{ nullptr }; - int16_t* _endOfLastWrite{ nullptr }; - int16_t* _buffer{ nullptr }; + Sample* _nextOutput{ nullptr }; + Sample* _endOfLastWrite{ nullptr }; + Sample* _buffer{ nullptr }; }; -// inline the iterator: -inline AudioRingBuffer::ConstIterator::ConstIterator() : - _bufferLength(0), - _bufferFirst(NULL), - _bufferLast(NULL), - _at(NULL) {} - -inline AudioRingBuffer::ConstIterator::ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at) : - _bufferLength(capacity), - _bufferFirst(bufferFirst), - _bufferLast(bufferFirst + capacity - 1), - _at(at) {} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator=(const ConstIterator& rhs) { - _bufferLength = rhs._bufferLength; - _bufferFirst = rhs._bufferFirst; - _bufferLast = rhs._bufferLast; - _at = rhs._at; - return *this; -} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator++() { - _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; - return *this; -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator++(int) { - ConstIterator tmp(*this); - ++(*this); - return tmp; -} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator--() { - _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; - return *this; -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator--(int) { - ConstIterator tmp(*this); - --(*this); - return tmp; -} - -inline const int16_t& AudioRingBuffer::ConstIterator::operator[] (int i) { - return *atShiftedBy(i); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator+(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator-(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); -} - -inline int16_t* AudioRingBuffer::ConstIterator::atShiftedBy(int i) { - i = (_at - _bufferFirst + i) % _bufferLength; - if (i < 0) { - i += _bufferLength; - } - return _bufferFirst + i; -} - -inline void AudioRingBuffer::ConstIterator::readSamples(int16_t* dest, int numSamples) { - auto samplesToEnd = _bufferLast - _at + 1; - - if (samplesToEnd >= numSamples) { - memcpy(dest, _at, numSamples * sizeof(int16_t)); - _at += numSamples; - } else { - auto samplesFromStart = numSamples - samplesToEnd; - memcpy(dest, _at, samplesToEnd * sizeof(int16_t)); - memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * sizeof(int16_t)); - - _at = _bufferFirst + samplesFromStart; - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, int numSamples, float fade) { - int16_t* at = _at; - for (int i = 0; i < numSamples; i++) { - *dest = (float)*at * fade; - ++dest; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) { - int16_t* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) { - int16_t* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const { - return ConstIterator(_buffer, _bufferLength, _nextOutput); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::lastFrameWritten() const { - return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; -} +// expose explicit instantiations for scratch/mix buffers +using AudioRingBuffer = AudioRingBufferTemplate; +using AudioMixRingBuffer = AudioRingBufferTemplate; #endif // hifi_AudioRingBuffer_h diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c35572a415..b25140d0a8 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1336,24 +1336,27 @@ const QUrl& AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) { return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; } -bool AvatarData::processAvatarIdentity(const Identity& identity) { - bool hasIdentityChanged = false; +void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged) { if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) { setSkeletonModelURL(identity.skeletonModelURL); - hasIdentityChanged = true; + identityChanged = true; + if (_firstSkeletonCheck) { + displayNameChanged = true; + } _firstSkeletonCheck = false; } if (identity.displayName != _displayName) { _displayName = identity.displayName; - hasIdentityChanged = true; + identityChanged = true; + displayNameChanged = true; } maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName); if (identity.attachmentData != _attachmentData) { setAttachmentData(identity.attachmentData); - hasIdentityChanged = true; + identityChanged = true; } bool avatarEntityDataChanged = false; @@ -1362,10 +1365,8 @@ bool AvatarData::processAvatarIdentity(const Identity& identity) { }); if (avatarEntityDataChanged) { setAvatarEntityData(identity.avatarEntityData); - hasIdentityChanged = true; + identityChanged = true; } - - return hasIdentityChanged; } QByteArray AvatarData::identityByteArray() { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 5d989b0eee..52cf81798e 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -471,8 +471,9 @@ public: static void parseAvatarIdentityPacket(const QByteArray& data, Identity& identityOut); - // returns true if identity has changed, false otherwise. - bool processAvatarIdentity(const Identity& identity); + // identityChanged returns true if identity has changed, false otherwise. + // displayNameChanged returns true if displayName has changed, false otherwise. + void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged); QByteArray identityByteArray(); diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index f4e94b9a35..9d43bf438b 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -148,7 +148,9 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer if (!nodeList->isIgnoringNode(identity.uuid) || nodeList->getRequestsDomainListData()) { // mesh URL for a UUID, find avatar in our list auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); - avatar->processAvatarIdentity(identity); + bool identityChanged = false; + bool displayNameChanged = false; + avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged); } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index d277fd540f..1265aabbf2 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -540,7 +540,7 @@ void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const Sha std::static_pointer_cast(_tree)->processEraseMessage(message, sourceNode); } -ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority) { +ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loadingPriority, SpatiallyNestable* spatiallyNestableOverride) { ModelPointer model = nullptr; // Only create and delete models on the thread that owns the EntityTreeRenderer @@ -552,7 +552,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, float loading return model; } - model = std::make_shared(std::make_shared()); + model = std::make_shared(std::make_shared(), nullptr, spatiallyNestableOverride); model->setLoadingPriority(loadingPriority); model->init(); model->setURL(QUrl(url)); @@ -956,68 +956,29 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } } -bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (!entity) { - return false; - } - QUuid simulatorID = entity->getSimulatorID(); - if (simulatorID.isNull()) { - // Can be null if it has never moved since being created or coming out of persistence. - // However, for there to be a collission, one of the two objects must be moving. - const EntityItemID& otherID = (id == collision.idA) ? collision.idB : collision.idA; - EntityItemPointer otherEntity = entityTree->findEntityByEntityItemID(otherID); - if (!otherEntity) { - return false; - } - simulatorID = otherEntity->getSimulatorID(); - } - - if (simulatorID.isNull() || (simulatorID != myNodeID)) { - return false; - } - - return true; -} - -void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { - - if (!isCollisionOwner(myNodeID, entityTree, id, collision)) { - return; - } - - SharedSoundPointer collisionSound; - float mass = 1.0; // value doesn't get used, but set it so compiler is quiet - AACube minAACube; - bool success = false; - _tree->withReadLock([&] { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (entity) { - collisionSound = entity->getCollisionSound(); - mass = entity->computeMass(); - minAACube = entity->getMinimumAACube(success); - } - }); - if (!success) { - return; - } +void EntityTreeRenderer::playEntityCollisionSound(EntityItemPointer entity, const Collision& collision) { + assert((bool)entity); + SharedSoundPointer collisionSound = entity->getCollisionSound(); if (!collisionSound) { return; } + bool success = false; + AACube minAACube = entity->getMinimumAACube(success); + if (!success) { + return; + } + float mass = entity->computeMass(); - const float COLLISION_PENETRATION_TO_VELOCITY = 50; // as a subsitute for RELATIVE entity->getVelocity() + const float COLLISION_PENETRATION_TO_VELOCITY = 50.0f; // as a subsitute for RELATIVE entity->getVelocity() // The collision.penetration is a pretty good indicator of changed velocity AFTER the initial contact, // but that first contact depends on exactly where we hit in the physics step. // We can get a more consistent initial-contact energy reading by using the changed velocity. // Note that velocityChange is not a good indicator for continuing collisions, because it does not distinguish // between bounce and sliding along a surface. - const float linearVelocity = (collision.type == CONTACT_EVENT_TYPE_START) ? - glm::length(collision.velocityChange) : - glm::length(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; - const float energy = mass * linearVelocity * linearVelocity / 2.0f; - const glm::vec3 position = collision.contactPoint; + const float speedSquared = (collision.type == CONTACT_EVENT_TYPE_START) ? + glm::length2(collision.velocityChange) : + glm::length2(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; + const float energy = mass * speedSquared / 2.0f; const float COLLISION_ENERGY_AT_FULL_VOLUME = (collision.type == CONTACT_EVENT_TYPE_START) ? 150.0f : 5.0f; const float COLLISION_MINIMUM_VOLUME = 0.005f; const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME); @@ -1031,7 +992,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityT // Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2) const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f; const float stretchFactor = log(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2); - AudioInjector::playSound(collisionSound, volume, stretchFactor, position); + AudioInjector::playSound(collisionSound, volume, stretchFactor, collision.contactPoint); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, @@ -1041,30 +1002,28 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons if (!_tree || _shuttingDown) { return; } - // Don't respond to small continuous contacts. - const float COLLISION_MINUMUM_PENETRATION = 0.002f; - if ((collision.type == CONTACT_EVENT_TYPE_CONTINUE) && (glm::length(collision.penetration) < COLLISION_MINUMUM_PENETRATION)) { - return; - } - // See if we should play sounds EntityTreePointer entityTree = std::static_pointer_cast(_tree); const QUuid& myNodeID = DependencyManager::get()->getSessionUUID(); - playEntityCollisionSound(myNodeID, entityTree, idA, collision); - playEntityCollisionSound(myNodeID, entityTree, idB, collision); - // And now the entity scripts - if (isCollisionOwner(myNodeID, entityTree, idA, collision)) { + // trigger scripted collision sounds and events for locally owned objects + EntityItemPointer entityA = entityTree->findEntityByEntityItemID(idA); + if ((bool)entityA && myNodeID == entityA->getSimulatorID()) { + playEntityCollisionSound(entityA, collision); emit collisionWithEntity(idA, idB, collision); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); } } - - if (isCollisionOwner(myNodeID, entityTree, idB, collision)) { - emit collisionWithEntity(idB, idA, collision); + EntityItemPointer entityB = entityTree->findEntityByEntityItemID(idB); + if ((bool)entityB && myNodeID == entityB->getSimulatorID()) { + playEntityCollisionSound(entityB, collision); + // since we're swapping A and B we need to send the inverted collision + Collision invertedCollision(collision); + invertedCollision.invert(); + emit collisionWithEntity(idB, idA, invertedCollision); if (_entitiesScriptEngine) { - _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); + _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision); } } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 8c021ad184..8669a1c4d3 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -77,7 +77,7 @@ public: void reloadEntityScripts(); /// if a renderable entity item needs a model, we will allocate it for them - Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f); + Q_INVOKABLE ModelPointer allocateModel(const QString& url, float loadingPriority = 0.0f, SpatiallyNestable* spatiallyNestableOverride = nullptr); /// if a renderable entity item needs to update the URL of a model, we will handle that for the entity Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl); @@ -170,11 +170,7 @@ private: bool _wantScripts; QSharedPointer _entitiesScriptEngine; - bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); - - void playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); + static void playEntityCollisionSound(EntityItemPointer entity, const Collision& collision); bool _lastPointerEventValid; PointerEvent _lastPointerEvent; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 087fcda8e1..e6902228c5 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -320,12 +320,9 @@ bool RenderableModelEntityItem::getAnimationFrame() { glm::mat4 finalMat = (translationMat * fbxJoints[index].preTransform * rotationMat * fbxJoints[index].postTransform); _localJointTranslations[j] = extractTranslation(finalMat); - _localJointTranslationsSet[j] = true; _localJointTranslationsDirty[j] = true; _localJointRotations[j] = glmExtractRotation(finalMat); - - _localJointRotationsSet[j] = true; _localJointRotationsDirty[j] = true; } } @@ -507,8 +504,7 @@ ModelPointer RenderableModelEntityItem::getModel(QSharedPointerallocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this)); - _model->setSpatiallyNestableOverride(shared_from_this()); + _model = _myRenderer->allocateModel(getModelURL(), renderer->getEntityLoadingPriority(*this), this); _needsInitialSimulation = true; // If we need to change URLs, update it *after rendering* (to avoid access violations) } else if (QUrl(getModelURL()) != _model->getURL()) { diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index d763eea7e3..233ce7d88e 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -659,6 +659,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // NOTE: the server is authoritative for changes to simOwnerID so we always unpack ownership data // even when we would otherwise ignore the rest of the packet. + bool filterRejection = false; if (propertyFlags.getHasProperty(PROP_SIMULATION_OWNER)) { QByteArray simOwnerData; @@ -671,6 +672,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef if (wantTerseEditLogging() && _simulationOwner != newSimOwner) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << newSimOwner; } + // This is used in the custom physics setters, below. When an entity-server filter alters + // or rejects a set of properties, it clears this. In such cases, we don't want those custom + // setters to ignore what the server says. + filterRejection = newSimOwner.getID().isNull(); if (weOwnSimulation) { if (newSimOwner.getID().isNull() && !_simulationOwner.pendingRelease(lastEditedFromBufferAdjusted)) { // entity-server is trying to clear our ownership (probably at our own request) @@ -696,6 +701,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef somethingChanged = true; _simulationOwner.clearCurrentOwner(); } + } else if (newSimOwner.matchesValidID(myNodeID) && !_hasBidOnSimulation) { + // entity-server tells us that we have simulation ownership while we never requested this for this EntityItem, + // this could happen when the user reloads the cache and entity tree. + _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; + somethingChanged = true; + _simulationOwner.clearCurrentOwner(); + weOwnSimulation = false; } else if (_simulationOwner.set(newSimOwner)) { _dirtyFlags |= Simulation::DIRTY_SIMULATOR_ID; somethingChanged = true; @@ -716,55 +728,45 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // Note: duplicate packets are expected and not wrong. They may be sent for any number of // reasons and the contract is that the client handles them in an idempotent manner. auto lastEdited = lastEditedFromBufferAdjusted; - auto customUpdatePositionFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedPositionTimestamp; - bool valueChanged = value != _lastUpdatedPositionValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; - if (shouldUpdate) { + bool otherOverwrites = overwriteLocalData && !weOwnSimulation; + auto shouldUpdate = [lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { + bool simulationChanged = lastEdited > updatedTimestamp; + return otherOverwrites && simulationChanged && (valueChanged || filterRejection); + }; + auto customUpdatePositionFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedPositionTimestamp, value != _lastUpdatedPositionValue)) { updatePositionFromNetwork(value); _lastUpdatedPositionTimestamp = lastEdited; _lastUpdatedPositionValue = value; } }; - auto customUpdateRotationFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::quat value){ - bool simulationChanged = lastEdited > _lastUpdatedRotationTimestamp; - bool valueChanged = value != _lastUpdatedRotationValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; - if (shouldUpdate) { + auto customUpdateRotationFromNetwork = [this, shouldUpdate, lastEdited](glm::quat value){ + if (shouldUpdate(_lastUpdatedRotationTimestamp, value != _lastUpdatedRotationValue)) { updateRotationFromNetwork(value); _lastUpdatedRotationTimestamp = lastEdited; _lastUpdatedRotationValue = value; } }; - auto customUpdateVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedVelocityTimestamp; - bool valueChanged = value != _lastUpdatedVelocityValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; - if (shouldUpdate) { + auto customUpdateVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedVelocityTimestamp, value != _lastUpdatedVelocityValue)) { updateVelocityFromNetwork(value); _lastUpdatedVelocityTimestamp = lastEdited; _lastUpdatedVelocityValue = value; } }; - auto customUpdateAngularVelocityFromNetwork = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedAngularVelocityTimestamp; - bool valueChanged = value != _lastUpdatedAngularVelocityValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; - if (shouldUpdate) { + auto customUpdateAngularVelocityFromNetwork = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedAngularVelocityTimestamp, value != _lastUpdatedAngularVelocityValue)) { updateAngularVelocityFromNetwork(value); _lastUpdatedAngularVelocityTimestamp = lastEdited; _lastUpdatedAngularVelocityValue = value; } }; - auto customSetAcceleration = [this, lastEdited, overwriteLocalData, weOwnSimulation](glm::vec3 value){ - bool simulationChanged = lastEdited > _lastUpdatedAccelerationTimestamp; - bool valueChanged = value != _lastUpdatedAccelerationValue; - bool shouldUpdate = overwriteLocalData && !weOwnSimulation && simulationChanged && valueChanged; - if (shouldUpdate) { + auto customSetAcceleration = [this, shouldUpdate, lastEdited](glm::vec3 value){ + if (shouldUpdate(_lastUpdatedAccelerationTimestamp, value != _lastUpdatedAccelerationValue)) { setAcceleration(value); _lastUpdatedAccelerationTimestamp = lastEdited; _lastUpdatedAccelerationValue = value; @@ -1286,7 +1288,7 @@ void EntityItem::grabSimulationOwnership() { auto nodeList = DependencyManager::get(); if (_simulationOwner.matchesValidID(nodeList->getSessionUUID())) { // we already own it - _simulationOwner.promotePriority(SCRIPT_POKE_SIMULATION_PRIORITY); + _simulationOwner.promotePriority(SCRIPT_GRAB_SIMULATION_PRIORITY); } else { // we don't own it yet _simulationOwner.setPendingPriority(SCRIPT_GRAB_SIMULATION_PRIORITY, usecTimestampNow()); @@ -1897,6 +1899,10 @@ void EntityItem::setPendingOwnershipPriority(quint8 priority, const quint64& tim _simulationOwner.setPendingPriority(priority, timestamp); } +void EntityItem::rememberHasSimulationOwnershipBid() const { + _hasBidOnSimulation = true; +} + QString EntityItem::actionsToDebugString() { QString result; QVector serializedActions; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index b203de203b..e69195d53d 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -321,6 +321,7 @@ public: void updateSimulationOwner(const SimulationOwner& owner); void clearSimulationOwnership(); void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp); + void rememberHasSimulationOwnershipBid() const; const QString& getMarketplaceID() const { return _marketplaceID; } void setMarketplaceID(const QString& value) { _marketplaceID = value; } @@ -497,16 +498,16 @@ protected: mutable AABox _cachedAABox; mutable AACube _maxAACube; mutable AACube _minAACube; - mutable bool _recalcAABox = true; - mutable bool _recalcMinAACube = true; - mutable bool _recalcMaxAACube = true; + mutable bool _recalcAABox { true }; + mutable bool _recalcMinAACube { true }; + mutable bool _recalcMaxAACube { true }; float _localRenderAlpha; - float _density = ENTITY_ITEM_DEFAULT_DENSITY; // kg/m^3 + float _density { ENTITY_ITEM_DEFAULT_DENSITY }; // kg/m^3 // NOTE: _volumeMultiplier is used to allow some mass properties code exist in the EntityItem base class // rather than in all of the derived classes. If we ever collapse these classes to one we could do it a // different way. - float _volumeMultiplier = 1.0f; + float _volumeMultiplier { 1.0f }; glm::vec3 _gravity; glm::vec3 _acceleration; float _damping; @@ -516,7 +517,7 @@ protected: QString _script; /// the value of the script property QString _loadedScript; /// the value of _script when the last preload signal was sent - quint64 _scriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload + quint64 _scriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP }; /// the script loaded property used for forced reload QString _serverScripts; /// keep track of time when _serverScripts property was last changed @@ -524,7 +525,7 @@ protected: /// the value of _scriptTimestamp when the last preload signal was sent // NOTE: on construction we want this to be different from _scriptTimestamp so we intentionally bump it - quint64 _loadedScriptTimestamp{ ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 }; + quint64 _loadedScriptTimestamp { ENTITY_ITEM_DEFAULT_SCRIPT_TIMESTAMP + 1 }; QString _collisionSoundURL; SharedSoundPointer _collisionSound; @@ -562,8 +563,8 @@ protected: uint32_t _dirtyFlags; // things that have changed from EXTERNAL changes (via script or packet) but NOT from simulation // these backpointers are only ever set/cleared by friends: - EntityTreeElementPointer _element = nullptr; // set by EntityTreeElement - void* _physicsInfo = nullptr; // set by EntitySimulation + EntityTreeElementPointer _element { nullptr }; // set by EntityTreeElement + void* _physicsInfo { nullptr }; // set by EntitySimulation bool _simulated; // set by EntitySimulation bool addActionInternal(EntitySimulationPointer simulation, EntityActionPointer action); @@ -580,12 +581,15 @@ protected: // are used to keep track of and work around this situation. void checkWaitingToRemove(EntitySimulationPointer simulation = nullptr); mutable QSet _actionsToRemove; - mutable bool _actionDataDirty = false; - mutable bool _actionDataNeedsTransmit = false; + mutable bool _actionDataDirty { false }; + mutable bool _actionDataNeedsTransmit { false }; // _previouslyDeletedActions is used to avoid an action being re-added due to server round-trip lag static quint64 _rememberDeletedActionTime; mutable QHash _previouslyDeletedActions; + // per entity keep state if it ever bid on simulation, so that we can ignore false simulation ownership + mutable bool _hasBidOnSimulation { false }; + QUuid _sourceUUID; /// the server node UUID we came from bool _clientOnly { false }; @@ -594,7 +598,7 @@ protected: // physics related changes from the network to suppress any duplicates and make // sure redundant applications are idempotent glm::vec3 _lastUpdatedPositionValue; - glm::quat _lastUpdatedRotationValue; + glm::quat _lastUpdatedRotationValue; glm::vec3 _lastUpdatedVelocityValue; glm::vec3 _lastUpdatedAngularVelocityValue; glm::vec3 _lastUpdatedAccelerationValue; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 0d42c55fbd..edace784ad 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -226,6 +226,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties // and make note of it now, so we can act on it right away. propertiesWithSimID.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); entity->setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); + entity->rememberHasSimulationOwnershipBid(); } entity->setLastBroadcast(usecTimestampNow()); @@ -436,6 +437,7 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& // we make a bid for simulation ownership properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); entity->pokeSimulationOwnership(); + entity->rememberHasSimulationOwnershipBid(); } } if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) { @@ -1359,12 +1361,14 @@ bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) { _entityTree->withReadLock([&] { EntityItemPointer parent = _entityTree->findEntityByEntityItemID(parentID); - parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { - if(descendant->getID() == childID) { - isChild = true; - return; - } - }); + if (parent) { + parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { + if (descendant->getID() == childID) { + isChild = true; + return; + } + }); + } }); return isChild; diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 7c3eb7bec3..b7a8841772 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -64,7 +64,7 @@ EntityTree::~EntityTree() { } void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) { - _entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(','); + _entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',', QString::SkipEmptyParts); } @@ -927,13 +927,21 @@ void EntityTree::initEntityEditFilterEngine(QScriptEngine* engine, std::function _entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions; auto global = _entityEditFilterEngine->globalObject(); _entityEditFilterFunction = global.property("filter"); - _hasEntityEditFilter = _entityEditFilterFunction.isFunction(); + if (!_entityEditFilterFunction.isFunction()) { + qCDebug(entities) << "Filter function specified but not found. Will reject all edits."; + _entityEditFilterEngine = nullptr; // So that we don't try to call it. See filterProperties. + } + _hasEntityEditFilter = true; } -bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged) { - if (!_hasEntityEditFilter || !_entityEditFilterEngine) { +bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, bool isAdd) { + if (!_entityEditFilterEngine) { propertiesOut = propertiesIn; wasChanged = false; // not changed + if (_hasEntityEditFilter) { + qCDebug(entities) << "Rejecting properties because filter has not been set."; + return false; + } return true; // allowed } auto oldProperties = propertiesIn.getDesiredProperties(); @@ -945,6 +953,7 @@ bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItem auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter. QScriptValueList args; args << inputValues; + args << isAdd; QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args); if (_entityEditFilterHadUncaughtExceptions()) { @@ -981,6 +990,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } int processedBytes = 0; + bool isAdd = false; // we handle these types of "edit" packets switch (message.getType()) { case PacketType::EntityErase: { @@ -990,6 +1000,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } case PacketType::EntityAdd: + isAdd = true; // fall through to next case case PacketType::EntityEdit: { quint64 startDecode = 0, endDecode = 0; quint64 startLookup = 0, endLookup = 0; @@ -1032,7 +1043,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } // If this was an add, we also want to tell the client that sent this edit that the entity was not added. - if (message.getType() == PacketType::EntityAdd) { + if (isAdd) { QWriteLocker locker(&_recentlyDeletedEntitiesLock); _recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID); validEditPacket = passedWhiteList; @@ -1042,7 +1053,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } } - if ((message.getType() == PacketType::EntityAdd || + if ((isAdd || (message.getType() == PacketType::EntityEdit && properties.lifetimeChanged())) && !senderNode->getCanRez() && senderNode->getCanRezTmp()) { // this node is only allowed to rez temporary entities. if need be, cap the lifetime. @@ -1060,9 +1071,11 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c startFilter = usecTimestampNow(); bool wasChanged = false; // Having (un)lock rights bypasses the filter. - bool allowed = senderNode->isAllowedEditor() || filterProperties(properties, properties, wasChanged); + bool allowed = senderNode->isAllowedEditor() || filterProperties(properties, properties, wasChanged, isAdd); if (!allowed) { + auto timestamp = properties.getLastEdited(); properties = EntityItemProperties(); + properties.setLastEdited(timestamp); } if (!allowed || wasChanged) { bumpTimestamp(properties); @@ -1102,8 +1115,16 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); _totalUpdates++; - } else if (message.getType() == PacketType::EntityAdd) { - if (senderNode->getCanRez() || senderNode->getCanRezTmp()) { + } else if (isAdd) { + bool failedAdd = !allowed; + if (!allowed) { + qCDebug(entities) << "Filtered entity add. ID:" << entityItemID; + } else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) { + failedAdd = true; + qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() + << "] attempted to add an entity ID:" << entityItemID; + + } else { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); properties.setLastEditedBy(senderNode->getUUID()); @@ -1118,7 +1139,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c startLogging = usecTimestampNow(); if (wantEditLogging()) { qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:" - << newEntity->getEntityItemID(); + << newEntity->getEntityItemID(); qCDebug(entities) << " properties:" << properties; } if (wantTerseEditLogging()) { @@ -1128,10 +1149,14 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } endLogging = usecTimestampNow(); + } else { + failedAdd = true; + qCDebug(entities) << "Add entity failed ID:" << entityItemID; } - } else { - qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() - << "] attempted to add an entity."; + } + if (failedAdd) { // Let client know it failed, so that they don't have an entity that no one else sees. + QWriteLocker locker(&_recentlyDeletedEntitiesLock); + _recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID); } } else { static QString repeatedMessage = @@ -1552,6 +1577,8 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra return args->map->value(oldID); } EntityItemID newID = QUuid::createUuid(); + args->map->insert(oldID, newID); + EntityItemProperties properties = item->getProperties(); EntityItemID oldParentID = properties.getParentID(); if (oldParentID.isInvalidID()) { // no parent @@ -1567,6 +1594,43 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } } + if (!properties.getXNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID()); + if (neighborEntity) { + properties.setXNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getXPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID()); + if (neighborEntity) { + properties.setXPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID()); + if (neighborEntity) { + properties.setYNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID()); + if (neighborEntity) { + properties.setYPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID()); + if (neighborEntity) { + properties.setZNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID()); + if (neighborEntity) { + properties.setZPNeighborID(getMapped(neighborEntity)); + } + } + // set creation time to "now" for imported entities properties.setCreated(usecTimestampNow()); @@ -1584,7 +1648,6 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra args->otherTree->addEntity(newID, properties); }); } - args->map->insert(oldID, newID); return newID; }; diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index cc179e7de0..9b30096be5 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -267,6 +267,7 @@ public: void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID); void initEntityEditFilterEngine(QScriptEngine* engine, std::function entityEditFilterHadUncaughtExceptions); + void setHasEntityFilter(bool hasFilter) { _hasEntityEditFilter = hasFilter; } static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; @@ -356,7 +357,7 @@ protected: float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME }; - bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged); + bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, bool isAdd); bool _hasEntityEditFilter{ false }; QScriptEngine* _entityEditFilterEngine{}; QScriptValue _entityEditFilterFunction{}; diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index 39371cc3e3..3308ba36ab 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -73,6 +73,9 @@ void KeyboardMouseDevice::mousePressEvent(QMouseEvent* event) { _mousePressTime = usecTimestampNow(); _mouseMoved = false; + _mousePressPos = event->pos(); + _clickDeadspotActive = true; + eraseMouseClicked(); } @@ -84,9 +87,11 @@ void KeyboardMouseDevice::mouseReleaseEvent(QMouseEvent* event) { // input for this button we might want to add some small tolerance to this so if you do a small drag it // still counts as a click. static const int CLICK_TIME = USECS_PER_MSEC * 500; // 500 ms to click - if (!_mouseMoved && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) { + if (_clickDeadspotActive && (usecTimestampNow() - _mousePressTime < CLICK_TIME)) { _inputDevice->_buttonPressedMap.insert(_inputDevice->makeInput((Qt::MouseButton) event->button(), true).getChannel()); } + + _clickDeadspotActive = false; } void KeyboardMouseDevice::eraseMouseClicked() { @@ -109,9 +114,14 @@ void KeyboardMouseDevice::mouseMoveEvent(QMouseEvent* event) { // outside of the application window, because we don't get MouseEvents when the cursor is outside // of the application window. _lastCursor = currentPos; + _mouseMoved = true; - eraseMouseClicked(); + const int CLICK_EVENT_DEADSPOT = 6; // pixels + if (_clickDeadspotActive && (_mousePressPos - currentPos).manhattanLength() > CLICK_EVENT_DEADSPOT) { + eraseMouseClicked(); + _clickDeadspotActive = false; + } } void KeyboardMouseDevice::wheelEvent(QWheelEvent* event) { diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h index 399ca4e93d..f38b43c107 100644 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.h @@ -118,8 +118,10 @@ public: protected: QPoint _lastCursor; + QPoint _mousePressPos; quint64 _mousePressTime; bool _mouseMoved; + bool _clickDeadspotActive; glm::vec2 _lastTouch; std::shared_ptr _inputDevice { std::make_shared() }; diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index d2d7aba517..58910c66bd 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1084,17 +1084,17 @@ int Octree::encodeTreeBitstreamRecursion(OctreeElementPointer element, params.stopReason = EncodeBitstreamParams::WAS_IN_VIEW; return bytesAtThisLevel; } + } - // If we're not in delta sending mode, and we weren't asked to do a force send, and the voxel hasn't changed, - // then we can also bail early and save bits - if (!params.forceSendScene && !params.deltaView && - !element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) { - if (params.stats) { - params.stats->skippedNoChange(element); - } - params.stopReason = EncodeBitstreamParams::NO_CHANGE; - return bytesAtThisLevel; + // If we're not in delta sending mode, and we weren't asked to do a force send, and the octree element hasn't changed, + // then we can also bail early and save bits + if (!params.forceSendScene && !params.deltaView && + !element->hasChangedSince(params.lastQuerySent - CHANGE_FUDGE)) { + if (params.stats) { + params.stats->skippedNoChange(element); } + params.stopReason = EncodeBitstreamParams::NO_CHANGE; + return bytesAtThisLevel; } bool keepDiggingDeeper = true; // Assuming we're in view we have a great work ethic, we're always ready for more! diff --git a/libraries/physics/src/ContactInfo.cpp b/libraries/physics/src/ContactInfo.cpp index c2ea6e8671..085f746a73 100644 --- a/libraries/physics/src/ContactInfo.cpp +++ b/libraries/physics/src/ContactInfo.cpp @@ -13,15 +13,25 @@ void ContactInfo::update(uint32_t currentStep, const btManifoldPoint& p) { _lastStep = currentStep; - ++_numSteps; positionWorldOnB = p.m_positionWorldOnB; normalWorldOnB = p.m_normalWorldOnB; distance = p.m_distance1; -} +} + +const uint32_t STEPS_BETWEEN_CONTINUE_EVENTS = 9; ContactEventType ContactInfo::computeType(uint32_t thisStep) { - if (_lastStep != thisStep) { - return CONTACT_EVENT_TYPE_END; + if (_continueExpiry == 0) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; + return CONTACT_EVENT_TYPE_START; } - return (_numSteps == 1) ? CONTACT_EVENT_TYPE_START : CONTACT_EVENT_TYPE_CONTINUE; + return (_lastStep == thisStep) ? CONTACT_EVENT_TYPE_CONTINUE : CONTACT_EVENT_TYPE_END; +} + +bool ContactInfo::readyForContinue(uint32_t thisStep) { + if (thisStep > _continueExpiry) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; + return true; + } + return false; } diff --git a/libraries/physics/src/ContactInfo.h b/libraries/physics/src/ContactInfo.h index 11c908a414..8d05f73b61 100644 --- a/libraries/physics/src/ContactInfo.h +++ b/libraries/physics/src/ContactInfo.h @@ -19,20 +19,22 @@ class ContactInfo { -public: +public: void update(uint32_t currentStep, const btManifoldPoint& p); ContactEventType computeType(uint32_t thisStep); const btVector3& getPositionWorldOnB() const { return positionWorldOnB; } btVector3 getPositionWorldOnA() const { return positionWorldOnB + normalWorldOnB * distance; } + bool readyForContinue(uint32_t thisStep); + btVector3 positionWorldOnB; btVector3 normalWorldOnB; btScalar distance; private: - uint32_t _lastStep = 0; - uint32_t _numSteps = 0; -}; + uint32_t _lastStep { 0 }; + uint32_t _continueExpiry { 0 }; +}; #endif // hifi_ContactEvent_h diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 1833d0aba4..e9891020b3 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -203,7 +203,11 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { BT_PROFILE("kinematicIntegration"); // This is physical kinematic motion which steps strictly by the subframe count // of the physics simulation and uses full gravity for acceleration. - _entity->setAcceleration(_entity->getGravity()); + if (_entity->hasAncestorOfType(NestableType::Avatar)) { + _entity->setAcceleration(glm::vec3(0.0f)); + } else { + _entity->setAcceleration(_entity->getGravity()); + } uint32_t thisStep = ObjectMotionState::getWorldSimulationStep(); float dt = (thisStep - _lastKinematicStep) * PHYSICS_ENGINE_FIXED_SUBSTEP; _entity->stepKinematicMotion(dt); @@ -582,6 +586,8 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS; // copy _outgoingPriority into pendingPriority... _entity->setPendingOwnershipPriority(_outgoingPriority, now); + // don't forget to remember that we have made a bid + _entity->rememberHasSimulationOwnershipBid(); // ...then reset _outgoingPriority in preparation for the next frame _outgoingPriority = 0; } else if (_outgoingPriority != _entity->getSimulationPriority()) { @@ -762,6 +768,11 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma _entity->computeCollisionGroupAndFinalMask(group, mask); } +bool EntityMotionState::shouldBeLocallyOwned() const { + return (_outgoingPriority > VOLUNTEER_SIMULATION_PRIORITY && _outgoingPriority > _entity->getSimulationPriority()) || + _entity->getSimulatorID() == Physics::getSessionUUID(); +} + void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) { _outgoingPriority = glm::max(_outgoingPriority, priority); } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 194d82805f..feac47d8ec 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -78,6 +78,8 @@ public: virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; + bool shouldBeLocallyOwned() const override; + friend class PhysicalEntitySimulation; protected: diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index a7894998a8..1d258560c3 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -146,6 +146,8 @@ public: void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; } void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; } + virtual bool shouldBeLocallyOwned() const { return false; } + friend class PhysicsEngine; protected: diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index ba002d925c..f57be4eab3 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -270,7 +270,7 @@ void PhysicsEngine::stepSimulation() { } auto onSubStep = [this]() { - updateContactMap(); + this->updateContactMap(); }; int numSubsteps = _dynamicsWorld->stepSimulationWithSubstepCallback(timeStep, PHYSICS_ENGINE_MAX_NUM_SUBSTEPS, @@ -393,7 +393,6 @@ void PhysicsEngine::updateContactMap() { } const CollisionEvents& PhysicsEngine::getCollisionEvents() { - const uint32_t CONTINUE_EVENT_FILTER_FREQUENCY = 10; _collisionEvents.clear(); // scan known contacts and trigger events @@ -402,28 +401,42 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { while (contactItr != _contactMap.end()) { ContactInfo& contact = contactItr->second; ContactEventType type = contact.computeType(_numContactFrames); - if(type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) { + const btScalar SIGNIFICANT_DEPTH = -0.002f; // penetrations have negative distance + if (type != CONTACT_EVENT_TYPE_CONTINUE || + (contact.distance < SIGNIFICANT_DEPTH && + contact.readyForContinue(_numContactFrames))) { ObjectMotionState* motionStateA = static_cast(contactItr->first._a); ObjectMotionState* motionStateB = static_cast(contactItr->first._b); - glm::vec3 velocityChange = (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)) + - (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); - if (motionStateA) { + // NOTE: the MyAvatar RigidBody is the only object in the simulation that does NOT have a MotionState + // which means should we ever want to report ALL collision events against the avatar we can + // modify the logic below. + // + // We only create events when at least one of the objects is (or should be) owned in the local simulation. + if (motionStateA && (motionStateA->shouldBeLocallyOwned())) { QUuid idA = motionStateA->getObjectID(); QUuid idB; if (motionStateB) { idB = motionStateB->getObjectID(); } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnB()) + _originOffset; + glm::vec3 velocityChange = motionStateA->getObjectLinearVelocityChange() + + (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB); _collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange)); - } else if (motionStateB) { + } else if (motionStateB && (motionStateB->shouldBeLocallyOwned())) { QUuid idB = motionStateB->getObjectID(); + QUuid idA; + if (motionStateA) { + idA = motionStateA->getObjectID(); + } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset; + glm::vec3 velocityChange = motionStateB->getObjectLinearVelocityChange() + + (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)); // NOTE: we're flipping the order of A and B (so that the first objectID is never NULL) - // hence we must negate the penetration. + // hence we negate the penetration (because penetration always points from B to A). glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB); - _collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration, velocityChange)); + _collisionEvents.push_back(Collision(type, idB, idA, position, penetration, velocityChange)); } } diff --git a/libraries/render-utils/src/LightingModel.cpp b/libraries/render-utils/src/LightingModel.cpp index 5a251fc5e9..47af83da36 100644 --- a/libraries/render-utils/src/LightingModel.cpp +++ b/libraries/render-utils/src/LightingModel.cpp @@ -92,6 +92,15 @@ bool LightingModel::isAlbedoEnabled() const { return (bool)_parametersBuffer.get().enableAlbedo; } +void LightingModel::setMaterialTexturing(bool enable) { + if (enable != isMaterialTexturingEnabled()) { + _parametersBuffer.edit().enableMaterialTexturing = (float)enable; + } +} +bool LightingModel::isMaterialTexturingEnabled() const { + return (bool)_parametersBuffer.get().enableMaterialTexturing; +} + void LightingModel::setAmbientLight(bool enable) { if (enable != isAmbientLightEnabled()) { _parametersBuffer.edit().enableAmbientLight = (float)enable; @@ -150,6 +159,8 @@ void MakeLightingModel::configure(const Config& config) { _lightingModel->setSpecular(config.enableSpecular); _lightingModel->setAlbedo(config.enableAlbedo); + _lightingModel->setMaterialTexturing(config.enableMaterialTexturing); + _lightingModel->setAmbientLight(config.enableAmbientLight); _lightingModel->setDirectionalLight(config.enableDirectionalLight); _lightingModel->setPointLight(config.enablePointLight); @@ -160,5 +171,8 @@ void MakeLightingModel::configure(const Config& config) { void MakeLightingModel::run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, LightingModelPointer& lightingModel) { - lightingModel = _lightingModel; + lightingModel = _lightingModel; + + // make sure the enableTexturing flag of the render ARgs is in sync + renderContext->args->_enableTexturing = _lightingModel->isMaterialTexturingEnabled(); } \ No newline at end of file diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h index 8f3ee9b7d6..45514654f2 100644 --- a/libraries/render-utils/src/LightingModel.h +++ b/libraries/render-utils/src/LightingModel.h @@ -49,6 +49,8 @@ public: void setAlbedo(bool enable); bool isAlbedoEnabled() const; + void setMaterialTexturing(bool enable); + bool isMaterialTexturingEnabled() const; void setAmbientLight(bool enable); bool isAmbientLightEnabled() const; @@ -88,9 +90,12 @@ protected: float enableSpotLight{ 1.0f }; float showLightContour{ 0.0f }; // false by default + float enableObscurance{ 1.0f }; - glm::vec2 spares{ 0.0f }; + float enableMaterialTexturing { 1.0f }; + + float spares{ 0.0f }; Parameters() {} }; @@ -117,6 +122,8 @@ class MakeLightingModelConfig : public render::Job::Config { Q_PROPERTY(bool enableSpecular MEMBER enableSpecular NOTIFY dirty) Q_PROPERTY(bool enableAlbedo MEMBER enableAlbedo NOTIFY dirty) + Q_PROPERTY(bool enableMaterialTexturing MEMBER enableMaterialTexturing NOTIFY dirty) + Q_PROPERTY(bool enableAmbientLight MEMBER enableAmbientLight NOTIFY dirty) Q_PROPERTY(bool enableDirectionalLight MEMBER enableDirectionalLight NOTIFY dirty) Q_PROPERTY(bool enablePointLight MEMBER enablePointLight NOTIFY dirty) @@ -136,13 +143,16 @@ public: bool enableScattering{ true }; bool enableDiffuse{ true }; bool enableSpecular{ true }; + bool enableAlbedo{ true }; + bool enableMaterialTexturing { true }; bool enableAmbientLight{ true }; bool enableDirectionalLight{ true }; bool enablePointLight{ true }; bool enableSpotLight{ true }; + bool showLightContour { false }; // false by default signals: diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index fa180a654a..333371ed76 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -129,7 +129,7 @@ void MeshPartPayload::bindMesh(gpu::Batch& batch) const { } } -void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations) const { +void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::LocationsPointer locations, bool enableTextures) const { if (!_drawMaterial) { return; } @@ -147,6 +147,17 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat numUnlit++; } + if (!enableTextures) { + batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture()); + batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, nullptr); + batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, nullptr); + return; + } + // Albedo if (materialKey.isAlbedoMap()) { auto itr = textureMaps.find(model::MaterialKey::ALBEDO_MAP); @@ -271,7 +282,7 @@ void MeshPartPayload::render(RenderArgs* args) const { bindMesh(batch); // apply material properties - bindMaterial(batch, locations); + bindMaterial(batch, locations, args->_enableTexturing); if (args) { args->_details._materialSwitches++; @@ -363,12 +374,7 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& transf _transform = transform; if (clusterMatrices.size() > 0) { - _worldBound = AABox(); - for (auto& clusterMatrix : clusterMatrices) { - AABox clusterBound = _localBound; - clusterBound.transform(clusterMatrix); - _worldBound += clusterBound; - } + _worldBound = _adjustedLocalBound; _worldBound.transform(_transform); if (clusterMatrices.size() == 1) { _transform = _transform.worldTransform(Transform(clusterMatrices[0])); @@ -588,7 +594,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { bindMesh(batch); // apply material properties - bindMaterial(batch, locations); + bindMaterial(batch, locations, args->_enableTexturing); args->_details._materialSwitches++; @@ -601,3 +607,15 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { const int INDICES_PER_TRIANGLE = 3; args->_details._trianglesRendered += _drawPart._numIndices / INDICES_PER_TRIANGLE; } + +void ModelMeshPartPayload::computeAdjustedLocalBound(const QVector& clusterMatrices) { + _adjustedLocalBound = _localBound; + if (clusterMatrices.size() > 0) { + _adjustedLocalBound.transform(clusterMatrices[0]); + for (int i = 1; i < clusterMatrices.size(); ++i) { + AABox clusterBound = _localBound; + clusterBound.transform(clusterMatrices[i]); + _adjustedLocalBound += clusterBound; + } + } +} diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 7d0aeab2bd..c585c95025 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -1,5 +1,5 @@ // -// ModelMeshPartPayload.h +// MeshPartPayload.h // interface/src/renderer // // Created by Sam Gateau on 10/3/15. @@ -51,7 +51,7 @@ public: // ModelMeshPartPayload functions to perform render void drawCall(gpu::Batch& batch) const; virtual void bindMesh(gpu::Batch& batch) const; - virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations) const; + virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, bool enableTextures) const; virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const; // Payload resource cached values @@ -61,6 +61,7 @@ public: bool _hasColorAttrib { false }; model::Box _localBound; + model::Box _adjustedLocalBound; mutable model::Box _worldBound; std::shared_ptr _drawMesh; @@ -105,6 +106,8 @@ public: void initCache(); + void computeAdjustedLocalBound(const QVector& clusterMatrices); + Model* _model; int _meshIndex; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 59b997b2cc..e1627f2fd6 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -78,11 +78,12 @@ void initCollisionMaterials() { } } -Model::Model(RigPointer rig, QObject* parent) : +Model::Model(RigPointer rig, QObject* parent, SpatiallyNestable* spatiallyNestableOverride) : QObject(parent), _renderGeometry(), _collisionGeometry(), _renderWatcher(_renderGeometry), + _spatiallyNestableOverride(spatiallyNestableOverride), _translation(0.0f), _rotation(), _scale(1.0f, 1.0f, 1.0f), @@ -133,16 +134,10 @@ void Model::setRotation(const glm::quat& rotation) { updateRenderItems(); } -void Model::setSpatiallyNestableOverride(SpatiallyNestablePointer override) { - _spatiallyNestableOverride = override; - updateRenderItems(); -} - Transform Model::getTransform() const { - SpatiallyNestablePointer spatiallyNestableOverride = _spatiallyNestableOverride.lock(); - if (spatiallyNestableOverride) { + if (_spatiallyNestableOverride) { bool success; - Transform transform = spatiallyNestableOverride->getTransform(success); + Transform transform = _spatiallyNestableOverride->getTransform(success); if (success) { transform.setScale(getScale()); return transform; @@ -1149,6 +1144,8 @@ void Model::simulate(float deltaTime, bool fullUpdate) { // update the world space transforms for all joints glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset); updateRig(deltaTime, parentTransform); + + computeMeshPartLocalBounds(); } } @@ -1158,6 +1155,14 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { _rig->updateAnimations(deltaTime, parentTransform); } +void Model::computeMeshPartLocalBounds() { + for (auto& part : _modelMeshRenderItemsSet) { + assert(part->_meshIndex < _modelMeshRenderItemsSet.size()); + const Model::MeshState& state = _meshStates.at(part->_meshIndex); + part->computeAdjustedLocalBound(state.clusterMatrices); + } +} + // virtual void Model::updateClusterMatrices() { PerformanceTimer perfTimer("Model::updateClusterMatrices"); @@ -1334,6 +1339,7 @@ void Model::createVisibleRenderItemSet() { shapeID++; } } + computeMeshPartLocalBounds(); } void Model::createCollisionRenderItemSet() { diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 8b6992394f..2042a16801 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -67,7 +67,7 @@ public: static void setAbstractViewStateInterface(AbstractViewStateInterface* viewState) { _viewState = viewState; } - Model(RigPointer rig, QObject* parent = nullptr); + Model(RigPointer rig, QObject* parent = nullptr, SpatiallyNestable* spatiallyNestableOverride = nullptr); virtual ~Model(); inline ModelPointer getThisPointer() const { @@ -205,7 +205,6 @@ public: void setTranslation(const glm::vec3& translation); void setRotation(const glm::quat& rotation); - void setSpatiallyNestableOverride(SpatiallyNestablePointer ptr); const glm::vec3& getTranslation() const { return _translation; } const glm::quat& getRotation() const { return _rotation; } @@ -244,7 +243,6 @@ public: public: QVector clusterMatrices; gpu::BufferPointer clusterBuffer; - }; const MeshState& getMeshState(int index) { return _meshStates.at(index); } @@ -293,12 +291,11 @@ protected: GeometryResourceWatcher _renderWatcher; + SpatiallyNestable* _spatiallyNestableOverride; + glm::vec3 _translation; glm::quat _rotation; glm::vec3 _scale; - - SpatiallyNestableWeakPointer _spatiallyNestableOverride; - glm::vec3 _offset; static float FAKE_DIMENSION_PLACEHOLDER; @@ -319,6 +316,7 @@ protected: void scaleToFit(); void snapToRegistrationPoint(); + void computeMeshPartLocalBounds(); virtual void updateRig(float deltaTime, glm::mat4 parentTransform); /// Restores the indexed joint to its default position. diff --git a/libraries/script-engine/src/SoundEffect.cpp b/libraries/script-engine/src/SoundEffect.cpp index 6833bb1f31..1c78ae84bf 100644 --- a/libraries/script-engine/src/SoundEffect.cpp +++ b/libraries/script-engine/src/SoundEffect.cpp @@ -23,10 +23,19 @@ void SoundEffect::setSource(QUrl url) { _sound = DependencyManager::get()->getSound(_url); } +float SoundEffect::getVolume() const { + return _volume; +} + +void SoundEffect::setVolume(float volume) { + _volume = volume; +} + void SoundEffect::play(QVariant position) { AudioInjectorOptions options; options.position = vec3FromVariant(position); options.localOnly = true; + options.volume = _volume; if (_injector) { _injector->setOptions(options); _injector->restart(); diff --git a/libraries/script-engine/src/SoundEffect.h b/libraries/script-engine/src/SoundEffect.h index 5d2a5095c1..656f98dd8d 100644 --- a/libraries/script-engine/src/SoundEffect.h +++ b/libraries/script-engine/src/SoundEffect.h @@ -22,6 +22,7 @@ class AudioInjector; class SoundEffect : public QQuickItem { Q_OBJECT Q_PROPERTY(QUrl source READ getSource WRITE setSource) + Q_PROPERTY(float volume READ getVolume WRITE setVolume) public: virtual ~SoundEffect(); @@ -29,9 +30,13 @@ public: QUrl getSource() const; void setSource(QUrl url); + float getVolume() const; + void setVolume(float volume); + Q_INVOKABLE void play(QVariant position); protected: QUrl _url; + float _volume { 1.0f }; SharedSoundPointer _sound; AudioInjector* _injector { nullptr }; }; diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index b0b2d00e0f..0b7829c7fb 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -119,7 +119,7 @@ public: * @param msg {object|string} */ Q_INVOKABLE void emitScriptEvent(QVariant msg); - + Q_INVOKABLE bool onHomeScreen(); QObject* getTabletSurface(); @@ -170,14 +170,14 @@ public: /**jsdoc * Returns the current value of this button's properties * @function TabletButtonProxy#getProperties - * @returns {object} + * @returns {ButtonProperties} */ Q_INVOKABLE QVariantMap getProperties() const; /**jsdoc * Replace the values of some of this button's properties * @function TabletButtonProxy#editProperties - * @param properties {object} set of properties to change + * @param {ButtonProperties} properties - set of properties to change */ Q_INVOKABLE void editProperties(QVariantMap properties); @@ -199,4 +199,13 @@ protected: QVariantMap _properties; }; +/**jsdoc + * @typedef TabletButtonProxy.ButtonProperties + * @property {string} text - button caption + * @property {string} icon - url to button icon. (50 x 50) + * @property {string} activeText - button caption when button is active + * @property {string} activeIcon - url to button icon used when button is active. (50 x 50) + * @property {string} isActive - true when button is active. + */ + #endif // hifi_TabletScriptingInterface_h diff --git a/libraries/shared/src/CPUDetect.h b/libraries/shared/src/CPUDetect.h index c9d2eb649b..ea6d23d8d6 100644 --- a/libraries/shared/src/CPUDetect.h +++ b/libraries/shared/src/CPUDetect.h @@ -134,7 +134,7 @@ static inline bool cpuSupportsAVX() { result = true; } } - return result; + return result; } static inline bool cpuSupportsAVX2() { @@ -143,11 +143,18 @@ static inline bool cpuSupportsAVX2() { bool result = false; if (cpuSupportsAVX()) { - if (__get_cpuid(0x7, &eax, &ebx, &ecx, &edx) && ((ebx & MASK_AVX2) == MASK_AVX2)) { - result = true; + // Work around a bug where __get_cpuid(0x7) returns wrong values on older GCC + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77756 + if (__get_cpuid(0x0, &eax, &ebx, &ecx, &edx) && (eax >= 0x7)) { + + __cpuid_count(0x7, 0x0, eax, ebx, ecx, edx); + + if ((ebx & MASK_AVX2) == MASK_AVX2) { + result = true; + } } } - return result; + return result; } #else diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 984529c4ba..7f12d6cc00 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -742,6 +742,12 @@ void collisionFromScriptValue(const QScriptValue &object, Collision& collision) // TODO: implement this when we know what it means to accept collision events from JS } +void Collision::invert() { + std::swap(idA, idB); + contactPoint += penetration; + penetration *= -1.0f; +} + QScriptValue quuidToScriptValue(QScriptEngine* engine, const QUuid& uuid) { if (uuid.isNull()) { return QScriptValue::NullValue; diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 2aefd3aa47..498a8b3b3a 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -142,11 +142,13 @@ public: const glm::vec3& cPenetration, const glm::vec3& velocityChange) : type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration), velocityChange(velocityChange) { } + void invert(); // swap A and B + ContactEventType type; QUuid idA; QUuid idB; - glm::vec3 contactPoint; - glm::vec3 penetration; + glm::vec3 contactPoint; // on B in world-frame + glm::vec3 penetration; // from B towards A in world-frame glm::vec3 velocityChange; }; Q_DECLARE_METATYPE(Collision) diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h index 851e065f20..b2c05b0548 100644 --- a/libraries/shared/src/RenderArgs.h +++ b/libraries/shared/src/RenderArgs.h @@ -122,6 +122,7 @@ public: gpu::Batch* _batch = nullptr; std::shared_ptr _whiteTexture; + bool _enableTexturing { true }; RenderDetails _details; }; diff --git a/script-archive/entity-server-filter-example.js b/script-archive/entity-server-filter-example.js index 4d4f7273f1..ad44bf1583 100644 --- a/script-archive/entity-server-filter-example.js +++ b/script-archive/entity-server-filter-example.js @@ -1,54 +1,106 @@ function filter(p) { - /* block comments are ok, but not double-slash end-of-line-comments */ - + /******************************************************/ + /* General Filter Comments + /* + - Custom filters must be named "filter" and must be global + - Block comments are ok, but not double-slash end-of-line-comments + - Certain JavaScript functions are not available, like Math.sign(), as they are undefined in QT's non-conforming JS + - HiFi's scripting interface is unavailable here. That means you can't call, for example, Users.*() + */ + /******************************************************/ + + /******************************************************/ + /* Simple Filter Examples + /******************************************************/ /* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */ if (p.name) {p.name += 'x'; print('fixme name', p. name);} + /* This example clamps y. A better filter would probably zero y component of velocity and acceleration. */ if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);} - /* Can also reject altogether */ + + /* Can also reject new properties altogether by returning false */ if (p.userData) { return false; } + /* Reject if modifications made to Model properties */ if (p.modelURL || p.compoundShapeURL || p.shape || p.shapeType || p.url || p.fps || p.currentFrame || p.running || p.loop || p.firstFrame || p.lastFrame || p.hold || p.textures || p.xTextureURL || p.yTextureURL || p.zTextureURL) { return false; } + + /******************************************************/ + /* Physical Property Filter Examples + /* + NOTES about filtering physical properties: + - For now, ensure you always supply a new value for the filtered physical property + (instead of simply removing the property) + - Ensure you always specify a slightly different value for physical properties every + time your filter returns. Look to "var nearZero" below for an example). + This is necessary because Interface checks if a physical property has changed + when deciding whether to apply or reject the server's physical properties. + If a physical property's value doesn't change, Interface will reject the server's property value, + and Bullet will continue simulating the entity with stale physical properties. + Ensure that this value is not changed by such a small amount such that new values + fall within floating point precision boundaries. If you accidentally do this, prepare for many + hours of frustrating debugging :). + */ + /******************************************************/ /* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/ - var maxVelocity = 5; if (p.velocity) { + var maxVelocity = 5; + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + function sign(val) { + if (val > 0) { + return 1; + } else if (val < 0) { + return -1; + } else { + return 0; + } + } if (Math.abs(p.velocity.x) > maxVelocity) { - p.velocity.x = Math.sign(p.velocity.x) * maxVelocity; - p.acceleration.x = 0; + p.velocity.x = sign(p.velocity.x) * (maxVelocity + nearZero); + p.acceleration.x = nearZero; } if (Math.abs(p.velocity.y) > maxVelocity) { - p.velocity.y = Math.sign(p.velocity.y) * maxVelocity; - p.acceleration.y = 0; + p.velocity.y = sign(p.velocity.y) * (maxVelocity + nearZero); + p.acceleration.y = nearZero; } if (Math.abs(p.velocity.z) > maxVelocity) { - p.velocity.z = Math.sign(p.velocity.z) * maxVelocity; - p.acceleration.z = 0; + p.velocity.z = sign(p.velocity.z) * (maxVelocity + nearZero); + p.acceleration.z = nearZero; } } + /* Define an axis-aligned zone in which entities are not allowed to enter. */ /* This example zone corresponds to an area to the right of the spawnpoint in your Sandbox. It's an area near the big rock to the right. If an entity enters the zone, it'll move behind the rock.*/ - var boxMin = {x: 25.5, y: -0.48, z: -9.9}; - var boxMax = {x: 31.1, y: 4, z: -3.79}; - var zero = {x: 0.0, y: 0.0, z: 0.0}; - if (p.position) { + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + /* Define the points that create the "NO ENTITIES ALLOWED" box */ + var boxMin = {x: 25.5, y: -0.48, z: -9.9}; + var boxMax = {x: 31.1, y: 4, z: -3.79}; + /* Define the point that you want entites that enter the box to appear */ + var resetPoint = {x: 29.5, y: 0.37 + nearZero, z: -2}; var x = p.position.x; var y = p.position.y; var z = p.position.z; if ((x > boxMin.x && x < boxMax.x) && (y > boxMin.y && y < boxMax.y) && (z > boxMin.z && z < boxMax.z)) { - /* Move it to the origin of the zone */ - p.position = boxMin; - p.velocity = zero; - p.acceleration = zero; + p.position = resetPoint; + if (p.velocity) { + p.velocity = {x: 0, y: nearZero, z: 0}; + } + if (p.acceleration) { + p.acceleration = {x: 0, y: nearZero, z: 0}; + } } } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bb96fe96cf..36b0ddde85 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -24,7 +24,7 @@ var DEFAULT_SCRIPTS = [ "system/goto.js", "system/marketplaces/marketplaces.js", "system/edit.js", - "system/users.js", + "system/tablet-users.js", "system/selectAudioDevice.js", "system/notifications.js", "system/controllers/controllerDisplayManager.js", diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index 26dbc1f2bc..0ac4cbc5b5 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -25,6 +25,7 @@ Column { "Lightmap:LightingModel:enableLightmap", "Background:LightingModel:enableBackground", "ssao:AmbientOcclusion:enabled", + "Textures:LightingModel:enableMaterialTexturing", ] CheckBox { text: modelData.split(":")[0] diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 69fb01eab7..bd32d82773 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -854,7 +854,7 @@ function MyController(hand) { }; this.setState = function(newState, reason) { - if (isInEditMode() && (newState !== STATE_OFF && + if ((isInEditMode() && this.grabbedEntity !== HMD.tabletID )&& (newState !== STATE_OFF && newState !== STATE_SEARCHING && newState !== STATE_OVERLAY_STYLUS_TOUCHING)) { return; @@ -1699,7 +1699,7 @@ function MyController(hand) { }; this.isTablet = function (entityID) { - if (entityID === HMD.tabletID) { // XXX what's a better way to know this? + if (entityID === HMD.tabletID) { return true; } return false; @@ -2907,7 +2907,7 @@ function MyController(hand) { var pos3D = intersectInfo.point; if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && - !this.deadspotExpired && + !this.tabletStabbed && intersectInfo.distance < WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_TOO_CLOSE) { // they've stabbed the tablet, don't send events until they pull back this.tabletStabbed = true; @@ -2915,8 +2915,15 @@ function MyController(hand) { this.tabletStabbedPos3D = pos3D; return; } + if (this.tabletStabbed) { - return; + var origin = {x: this.tabletStabbedPos2D.x, y: this.tabletStabbedPos2D.y, z: 0}; + var point = {x: pos2D.x, y: pos2D.y, z: 0}; + var offset = Vec3.distance(origin, point); + var radius = 0.05; + if (offset < radius) { + return; + } } if (Overlays.keyboardFocusOverlay != this.grabbedOverlay) { diff --git a/scripts/system/edit.js b/scripts/system/edit.js index d49f7ad3c5..d2db83de6e 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -210,7 +210,10 @@ var toolBar = (function () { var button = toolBar.addButton({ objectName: name, imageURL: imageUrl, - buttonState: 1, + imageOffOut: 1, + imageOffIn: 2, + imageOnOut: 0, + imageOnIn: 2, alpha: 0.9, visible: true }); diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html new file mode 100644 index 0000000000..c18162ff8a --- /dev/null +++ b/scripts/system/html/users.html @@ -0,0 +1,465 @@ + + + + Users Online + + + + + + + +
+
+
Users Online
+ +
+
+
+
+

+ +
+
    +
  • Everyone (0)
  • +
  • Friends (0)
  • +
+
+
    +
    +
    +
      + +
      +
      + + + + + + + + + \ No newline at end of file diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 29561362d5..6b4f4b1b4a 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -24,31 +24,33 @@ var CAMERA_MATRIX = -7; var ROT_Y_180 = {x: 0, y: 1, z: 0, w: 0}; var TABLET_TEXTURE_RESOLUTION = { x: 480, y: 706 }; var INCHES_TO_METERS = 1 / 39.3701; - var AVATAR_SELF_ID = "{00000000-0000-0000-0000-000000000001}"; - var TABLET_URL = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx"; +var NO_HANDS = -1; // will need to be recaclulated if dimensions of fbx model change. var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269}; -var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; -var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx"; +var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; +var TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx"; // returns object with two fields: // * position - position in front of the user // * rotation - rotation of entity so it faces the user. function calcSpawnInfo(hand, height) { - var noHands = -1; var finalPosition; - if (HMD.active && hand !== noHands) { + + var headPos = (HMD.active && Camera.mode === "first person") ? HMD.position : Camera.position; + var headRot = (HMD.active && Camera.mode === "first person") ? HMD.orientation : Camera.orientation; + + if (HMD.active && hand !== NO_HANDS) { var handController = getControllerWorldLocation(hand, true); var controllerPosition = handController.position; // compute the angle of the chord with length (height / 2) - var theta = Math.asin(height / (2 * Vec3.distance(HMD.position, controllerPosition))); + var theta = Math.asin(height / (2 * Vec3.distance(headPos, controllerPosition))); // then we can use this angle to rotate the vector between the HMD position and the center of the tablet. // this vector, u, will become our new look at direction. - var d = Vec3.normalize(Vec3.subtract(HMD.position, controllerPosition)); + var d = Vec3.normalize(Vec3.subtract(headPos, controllerPosition)); var w = Vec3.normalize(Vec3.cross(Y_AXIS, d)); var q = Quat.angleAxis(theta * (180 / Math.PI), w); var u = Vec3.multiplyQbyV(q, d); @@ -66,8 +68,8 @@ function calcSpawnInfo(hand, height) { rotation: lookAtRot }; } else { - var front = Quat.getFront(Camera.orientation); - finalPosition = Vec3.sum(Camera.position, Vec3.multiply(0.6, front)); + var front = Quat.getFront(headRot); + finalPosition = Vec3.sum(headPos, Vec3.multiply(0.6, front)); var orientation = Quat.lookAt({x: 0, y: 0, z: 0}, front, {x: 0, y: 1, z: 0}); return { position: finalPosition, @@ -200,6 +202,11 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { _this.geometryChanged(geometry); }; Window.geometryChanged.connect(this.myGeometryChanged); + + this.myCameraModeChanged = function(newMode) { + _this.cameraModeChanged(newMode); + }; + Camera.modeUpdated.connect(this.myCameraModeChanged); }; WebTablet.prototype.setHomeButtonTexture = function() { @@ -230,11 +237,11 @@ WebTablet.prototype.destroy = function () { Controller.mouseReleaseEvent.disconnect(this.myMouseReleaseEvent); Window.geometryChanged.disconnect(this.myGeometryChanged); + Camera.modeUpdated.disconnect(this.myCameraModeChanged); }; WebTablet.prototype.geometryChanged = function (geometry) { if (!HMD.active) { - var NO_HANDS = -1; var tabletProperties = {}; // compute position, rotation & parentJointIndex of the tablet this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties); @@ -290,7 +297,6 @@ WebTablet.prototype.onHmdChanged = function () { Controller.mouseReleaseEvent.connect(this.myMouseReleaseEvent); } - var NO_HANDS = -1; var tabletProperties = {}; // compute position, rotation & parentJointIndex of the tablet this.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties); @@ -330,6 +336,7 @@ WebTablet.prototype.cleanUpOldTablets = function() { this.cleanUpOldTabletsOnJoint(SENSOR_TO_ROOM_MATRIX); this.cleanUpOldTabletsOnJoint(CAMERA_MATRIX); this.cleanUpOldTabletsOnJoint(65529); + this.cleanUpOldTabletsOnJoint(65534); }; WebTablet.prototype.unregister = function() { @@ -373,6 +380,18 @@ WebTablet.prototype.mousePressEvent = function (event) { } }; +WebTablet.prototype.cameraModeChanged = function (newMode) { + // reposition the tablet. + // This allows HMD.position to reflect the new camera mode. + if (HMD.active) { + var self = this; + var tabletProperties = {}; + // compute position, rotation & parentJointIndex of the tablet + self.calculateTabletAttachmentProperties(NO_HANDS, tabletProperties); + Entities.editEntity(self.tabletEntityID, tabletProperties); + } +}; + function rayIntersectPlane(planePosition, planeNormal, rayStart, rayDirection) { var rayDirectionDotPlaneNormal = Vec3.dot(rayDirection, planeNormal); if (rayDirectionDotPlaneNormal > 0.00001 || rayDirectionDotPlaneNormal < -0.00001) { diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 2932417d25..b9bae72d14 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1038,7 +1038,7 @@ SelectionDisplay = (function() { if (entityIntersection.intersects && (!overlayIntersection.intersects || (entityIntersection.distance < overlayIntersection.distance))) { - if (HMD.tabletID == entityIntersection.entityID) { + if (HMD.tabletID === entityIntersection.entityID) { return; } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 8cb13cf27e..a58615673b 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -19,7 +19,7 @@ var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); -var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; +var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; // Event bridge messages. var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; diff --git a/scripts/system/menu.js b/scripts/system/menu.js index 1b67a7995c..9858b69476 100644 --- a/scripts/system/menu.js +++ b/scripts/system/menu.js @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; +var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; (function() { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ @@ -30,4 +30,4 @@ var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet- button.clicked.disconnect(onClicked); tablet.removeButton(button); }) -}()); \ No newline at end of file +}()); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 800a1a8c42..8c53ccd59d 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -520,6 +520,7 @@ function onClicked() { Controller.mouseMoveEvent.connect(handleMouseMoveEvent); triggerMapping.enable(); triggerPressMapping.enable(); + createAudioInterval(); } else { off(); } diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js new file mode 100644 index 0000000000..0fad0c56f3 --- /dev/null +++ b/scripts/system/tablet-users.js @@ -0,0 +1,98 @@ +"use strict"; + +// +// users.js +// +// Created by Faye Li on 18 Jan 2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { // BEGIN LOCAL_SCOPE + var USERS_URL = "https://hifi-content.s3.amazonaws.com/faye/tablet-dev/users.html"; + + var FRIENDS_WINDOW_URL = "https://metaverse.highfidelity.com/user/friends"; + var FRIENDS_WINDOW_WIDTH = 290; + var FRIENDS_WINDOW_HEIGHT = 500; + var FRIENDS_WINDOW_TITLE = "Add/Remove Friends"; + + // Initialise visibility based on global service + var VISIBILITY_VALUES_SET = {}; + VISIBILITY_VALUES_SET["all"] = true; + VISIBILITY_VALUES_SET["friends"] = true; + VISIBILITY_VALUES_SET["none"] = true; + var myVisibility; + if (GlobalServices.findableBy in VISIBILITY_VALUES_SET) { + myVisibility = GlobalServices.findableBy; + } else { + // default to friends if it can't be determined + myVisibility = "friends"; + GlobalServices.findableBy = myVisibilty; + } + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: "icons/tablet-icons/people-i.svg", + text: "Users" + }); + + function onClicked() { + tablet.gotoWebScreen(USERS_URL); + } + + function onWebEventReceived(event) { + print("Script received a web event, its type is " + typeof event); + if (typeof event === "string") { + event = JSON.parse(event); + } + if (event.type === "ready") { + // send username to html + var myUsername = GlobalServices.username; + var object = { + "type": "user-info", + "data": { + "username": myUsername, + "visibility": myVisibility + } + }; + tablet.emitScriptEvent(JSON.stringify(object)); + } + if (event.type === "manage-friends") { + // open a web overlay to metaverse friends page + var friendsWindow = new OverlayWebWindow({ + title: FRIENDS_WINDOW_TITLE, + width: FRIENDS_WINDOW_WIDTH, + height: FRIENDS_WINDOW_HEIGHT, + visible: false + }); + friendsWindow.setURL(FRIENDS_WINDOW_URL); + friendsWindow.setVisible(true); + friendsWindow.raise(); + } + if (event.type === "jump-to") { + if (typeof event.data.username !== undefined) { + // teleport to selected user from the online users list + location.goToUser(event.data.username); + } + } + if (event.type === "toggle-visibility") { + if (typeof event.data.visibility !== undefined) { + // update your visibility (all, friends, or none) + myVisibility = event.data.visibility; + GlobalServices.findableBy = myVisibility; + } + } + } + + button.clicked.connect(onClicked); + tablet.webEventReceived.connect(onWebEventReceived); + + function cleanup() { + button.clicked.disconnect(onClicked); + tablet.removeButton(button); + } + + Script.scriptEnding.connect(cleanup); +}()); // END LOCAL_SCOPE diff --git a/unpublishedScripts/marketplace/teaLight/teaLight.js b/unpublishedScripts/marketplace/teaLight/teaLight.js new file mode 100644 index 0000000000..85b0a3c310 --- /dev/null +++ b/unpublishedScripts/marketplace/teaLight/teaLight.js @@ -0,0 +1,21 @@ +(function() { + var MINIMUM_LIGHT_INTENSITY = 100.0; + var MAXIMUM_LIGHT_INTENSITY = 125.0; + + // Return a random number between `low` (inclusive) and `high` (exclusive) + function randFloat(low, high) { + return low + Math.random() * (high - low); + } + + var self = this; + this.preload = function(entityID) { + self.intervalID = Script.setInterval(function() { + Entities.editEntity(entityID, { + intensity: randFloat(MINIMUM_LIGHT_INTENSITY, MAXIMUM_LIGHT_INTENSITY) + }); + }, 100); + }; + this.unload = function() { + Script.clearInterval(self.intervalID); + } +});