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 659372267c..b04d55b9eb 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,48 +892,151 @@ 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) {
+ var inputs = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ':not([data-category]) input[data-changed="true"]');
+
+ var empty = false;
+
+ _.each(inputs, function(input){
+ var inputVal = $(input).val();
+
+ if (inputVal.length === 0) {
+ empty = true
+
+ markParentRowInvalid(input);
+ return;
+ }
+ });
+
+ if (empty) {
+ showErrorMessage("Error", "Empty field(s)");
+ inputsValid = false;
+ return
+ }
+
+ // 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 duplicateKey = false;
+
+ _.each(newKeys, function(keyCell) {
+ var keyVal = $(keyCell).children('input').val();
+
+ if (keyVal.indexOf(' ') !== -1) {
+ keyWithSpaces = true;
+ markParentRowInvalid(keyCell);
+ 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 (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
- 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 {
+ // 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);
- delete formJSON["security"]["verify_http_password"];
}
}
- }
- console.log("----- SAVING ------");
- console.log(formJSON);
+ // verify that the password and confirmation match before saving
+ var canPost = true;
- // re-enable all inputs
- $("input").each(function(){
- $(this).prop('disabled', false);
- });
+ if (formJSON["security"]) {
+ var password = formJSON["security"]["http_password"];
+ var verify_password = formJSON["security"]["verify_http_password"];
- // remove focus from the button
- $(this).blur();
+ 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"];
+ }
+ }
+ }
- // POST the form JSON to the domain-server settings.json endpoint so the settings are saved
- if (canPost) {
- 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();
+
+ if (canPost) {
+ // POST the form JSON to the domain-server settings.json endpoint so the settings are saved
+ postSettings(formJSON);
+ }
}
}
@@ -1110,8 +1214,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 += ""
@@ -1137,7 +1242,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns,
return html;
}
-function makeTableInputs(setting, initialValues, categoryValue) {
+function makeTableHiddenInputs(setting, initialValues, categoryValue) {
var html = "
";
@@ -1148,7 +1253,7 @@ function makeTableInputs(setting, initialValues, categoryValue) {
if (setting.key) {
html += "\
- \
+ \
| "
}
@@ -1157,14 +1262,14 @@ function makeTableInputs(setting, initialValues, categoryValue) {
if (col.type === "checkbox") {
html +=
"" +
- "" +
" | ";
} else {
html +=
"" +
- "" +
" | ";
@@ -1244,49 +1349,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")) {
@@ -1308,56 +1381,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");
}
});
@@ -1387,7 +1447,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/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/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/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
index 60bb29f85f..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));
diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h
index 29d463b915..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);
diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
index bc8c7c222e..e6902228c5 100644
--- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
@@ -504,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 7f3c273772..353819e01d 100644
--- a/libraries/entities/src/EntityItem.cpp
+++ b/libraries/entities/src/EntityItem.cpp
@@ -693,6 +693,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;
@@ -1273,7 +1280,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());
@@ -1884,6 +1891,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 6fb5d14329..85c3fc74f6 100644
--- a/libraries/entities/src/EntityScriptingInterface.cpp
+++ b/libraries/entities/src/EntityScriptingInterface.cpp
@@ -231,6 +231,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());
@@ -444,6 +445,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()) {
diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp
index 5fa86f6745..848a4ad96e 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);
}
@@ -1111,7 +1111,15 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
endUpdate = usecTimestampNow();
_totalUpdates++;
} else if (message.getType() == PacketType::EntityAdd) {
- if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
+ 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());
@@ -1126,7 +1134,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()) {
@@ -1136,10 +1144,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 =
diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp
index b0bdc34b52..02cee9a03a 100644
--- a/libraries/physics/src/EntityMotionState.cpp
+++ b/libraries/physics/src/EntityMotionState.cpp
@@ -582,6 +582,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()) {
diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp
index 5e47ed8b0f..333371ed76 100644
--- a/libraries/render-utils/src/MeshPartPayload.cpp
+++ b/libraries/render-utils/src/MeshPartPayload.cpp
@@ -374,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]));
@@ -612,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 1f3778c34a..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.
@@ -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/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/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js
index 972d95e9e9..86820c990a 100644
--- a/scripts/system/controllers/handControllerGrab.js
+++ b/scripts/system/controllers/handControllerGrab.js
@@ -853,7 +853,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;
@@ -1703,7 +1703,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;
diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html
new file mode 100644
index 0000000000..23de9cf0b0
--- /dev/null
+++ b/scripts/system/html/users.html
@@ -0,0 +1,241 @@
+
+
+
+ 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 65551b2140..75ca2e514f 100644
--- a/scripts/system/libraries/WebTablet.js
+++ b/scripts/system/libraries/WebTablet.js
@@ -24,6 +24,7 @@ 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 NO_HANDS = -1;
var TABLET_URL = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx";
@@ -35,18 +36,21 @@ var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-wi
// * 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);
@@ -64,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,
@@ -198,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() {
@@ -228,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);
@@ -288,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);
@@ -370,6 +378,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/tablet-users.js b/scripts/system/tablet-users.js
new file mode 100644
index 0000000000..7930892395
--- /dev/null
+++ b/scripts/system/tablet-users.js
@@ -0,0 +1,67 @@
+"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";
+ 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": "sendUsername",
+ "data": {"username": myUsername}
+ };
+ print("sending username: " + myUsername);
+ 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();
+ }
+ }
+
+ 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);
+ }
+});