Merge pull request #11997 from ZappoMan/oldPropsFilters

giving more info to entity edit filters to allow more flexibility
This commit is contained in:
Ryan Huffman 2018-02-20 12:57:44 -08:00 committed by GitHub
commit acda90577a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 408 additions and 17 deletions

View file

@ -41,8 +41,8 @@ QList<EntityItemID> EntityEditFilters::getZonesByPosition(glm::vec3& position) {
return zones;
}
bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged,
EntityTree::FilterType filterType, EntityItemID& itemID) {
bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut,
bool& wasChanged, EntityTree::FilterType filterType, EntityItemID& itemID, EntityItemPointer& existingEntity) {
// get the ids of all the zones (plus the global entity edit filter) that the position
// lies within
@ -61,6 +61,17 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper
if (filterData.rejectAll) {
return false;
}
// check to see if this filter wants to filter this message type
if ((!filterData.wantsToFilterEdit && filterType == EntityTree::FilterType::Edit) ||
(!filterData.wantsToFilterPhysics && filterType == EntityTree::FilterType::Physics) ||
(!filterData.wantsToFilterDelete && filterType == EntityTree::FilterType::Delete) ||
(!filterData.wantsToFilterAdd && filterType == EntityTree::FilterType::Add)) {
wasChanged = false;
return true; // accept the message
}
auto oldProperties = propertiesIn.getDesiredProperties();
auto specifiedProperties = propertiesIn.getChangedProperties();
propertiesIn.setDesiredProperties(specifiedProperties);
@ -68,16 +79,62 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper
propertiesIn.setDesiredProperties(oldProperties);
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 << filterType;
// get the current properties for then entity and include them for the filter call
if (existingEntity && filterData.wantsOriginalProperties) {
auto currentProperties = existingEntity->getProperties(filterData.includedOriginalProperties);
QScriptValue currentValues = currentProperties.copyToScriptValue(filterData.engine, false, true, true);
args << currentValues;
}
// get the zone properties
if (filterData.wantsZoneProperties) {
auto zoneEntity = _tree->findEntityByEntityItemID(id);
if (zoneEntity) {
auto zoneProperties = zoneEntity->getProperties(filterData.includedZoneProperties);
QScriptValue zoneValues = zoneProperties.copyToScriptValue(filterData.engine, false, true, true);
if (filterData.wantsZoneBoundingBox) {
bool success = true;
AABox aaBox = zoneEntity->getAABox(success);
if (success) {
QScriptValue boundingBox = filterData.engine->newObject();
QScriptValue bottomRightNear = vec3toScriptValue(filterData.engine, aaBox.getCorner());
QScriptValue topFarLeft = vec3toScriptValue(filterData.engine, aaBox.calcTopFarLeft());
QScriptValue center = vec3toScriptValue(filterData.engine, aaBox.calcCenter());
QScriptValue boundingBoxDimensions = vec3toScriptValue(filterData.engine, aaBox.getDimensions());
boundingBox.setProperty("brn", bottomRightNear);
boundingBox.setProperty("tfl", topFarLeft);
boundingBox.setProperty("center", center);
boundingBox.setProperty("dimensions", boundingBoxDimensions);
zoneValues.setProperty("boundingBox", boundingBox);
}
}
// If this is an add or delete, or original properties weren't requested
// there won't be original properties in the args, but zone properties need
// to be the fourth parameter, so we need to pad the args accordingly
int EXPECTED_ARGS = 3;
if (args.length() < EXPECTED_ARGS) {
args << QScriptValue();
}
assert(args.length() == EXPECTED_ARGS); // we MUST have 3 args by now!
args << zoneValues;
}
}
QScriptValue result = filterData.filterFn.call(_nullObjectForFilter, args);
if (filterData.uncaughtExceptions()) {
return false;
}
if (result.isObject()){
if (result.isObject()) {
// make propertiesIn reflect the changes, for next filter...
propertiesIn.copyFromScriptValue(result, false);
@ -86,6 +143,17 @@ bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& proper
// Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON.
auto out = QJsonValue::fromVariant(result.toVariant());
wasChanged |= (in != out);
} else if (result.isBool()) {
// if the filter returned false, then it's authoritative
if (!result.toBool()) {
return false;
}
// otherwise, assume it wants to pass all properties
propertiesOut = propertiesIn;
wasChanged = false;
} else {
return false;
}
@ -182,8 +250,8 @@ static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName
void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) {
qDebug() << "script request completed for entity " << entityID;
auto scriptRequest = qobject_cast<ResourceRequest*>(sender());
const QString urlString = scriptRequest->getUrl().toString();
if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) {
const QString urlString = scriptRequest->getUrl().toString();
auto scriptContents = scriptRequest->getData();
qInfo() << "Downloaded script:" << scriptContents;
QScriptProgram program(scriptContents, urlString);
@ -207,6 +275,7 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) {
entitiesObject.setProperty("ADD_FILTER_TYPE", EntityTree::FilterType::Add);
entitiesObject.setProperty("EDIT_FILTER_TYPE", EntityTree::FilterType::Edit);
entitiesObject.setProperty("PHYSICS_FILTER_TYPE", EntityTree::FilterType::Physics);
entitiesObject.setProperty("DELETE_FILTER_TYPE", EntityTree::FilterType::Delete);
global.setProperty("Entities", entitiesObject);
filterData.filterFn = global.property("filter");
if (!filterData.filterFn.isFunction()) {
@ -214,8 +283,86 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) {
delete engine;
filterData.rejectAll=true;
}
// if the wantsToFilterEdit is a boolean evaluate as a boolean, otherwise assume true
QScriptValue wantsToFilterAddValue = filterData.filterFn.property("wantsToFilterAdd");
filterData.wantsToFilterAdd = wantsToFilterAddValue.isBool() ? wantsToFilterAddValue.toBool() : true;
// if the wantsToFilterEdit is a boolean evaluate as a boolean, otherwise assume true
QScriptValue wantsToFilterEditValue = filterData.filterFn.property("wantsToFilterEdit");
filterData.wantsToFilterEdit = wantsToFilterEditValue.isBool() ? wantsToFilterEditValue.toBool() : true;
// if the wantsToFilterPhysics is a boolean evaluate as a boolean, otherwise assume true
QScriptValue wantsToFilterPhysicsValue = filterData.filterFn.property("wantsToFilterPhysics");
filterData.wantsToFilterPhysics = wantsToFilterPhysicsValue.isBool() ? wantsToFilterPhysicsValue.toBool() : true;
// if the wantsToFilterDelete is a boolean evaluate as a boolean, otherwise assume false
QScriptValue wantsToFilterDeleteValue = filterData.filterFn.property("wantsToFilterDelete");
filterData.wantsToFilterDelete = wantsToFilterDeleteValue.isBool() ? wantsToFilterDeleteValue.toBool() : false;
// check to see if the filterFn has properties asking for Original props
QScriptValue wantsOriginalPropertiesValue = filterData.filterFn.property("wantsOriginalProperties");
// if the wantsOriginalProperties is a boolean, or a string, or list of strings, then evaluate as follows:
// - boolean - true - include all original properties
// false - no properties at all
// - string - empty - no properties at all
// any valid property - include just that property in the Original properties
// - list of strings - include only those properties in the Original properties
if (wantsOriginalPropertiesValue.isBool()) {
filterData.wantsOriginalProperties = wantsOriginalPropertiesValue.toBool();
} else if (wantsOriginalPropertiesValue.isString()) {
auto stringValue = wantsOriginalPropertiesValue.toString();
filterData.wantsOriginalProperties = !stringValue.isEmpty();
if (filterData.wantsOriginalProperties) {
EntityPropertyFlagsFromScriptValue(wantsOriginalPropertiesValue, filterData.includedOriginalProperties);
}
} else if (wantsOriginalPropertiesValue.isArray()) {
EntityPropertyFlagsFromScriptValue(wantsOriginalPropertiesValue, filterData.includedOriginalProperties);
filterData.wantsOriginalProperties = !filterData.includedOriginalProperties.isEmpty();
}
// check to see if the filterFn has properties asking for Zone props
QScriptValue wantsZonePropertiesValue = filterData.filterFn.property("wantsZoneProperties");
// if the wantsZoneProperties is a boolean, or a string, or list of strings, then evaluate as follows:
// - boolean - true - include all Zone properties
// false - no properties at all
// - string - empty - no properties at all
// any valid property - include just that property in the Zone properties
// - list of strings - include only those properties in the Zone properties
if (wantsZonePropertiesValue.isBool()) {
filterData.wantsZoneProperties = wantsZonePropertiesValue.toBool();
filterData.wantsZoneBoundingBox = filterData.wantsZoneProperties; // include this too
} else if (wantsZonePropertiesValue.isString()) {
auto stringValue = wantsZonePropertiesValue.toString();
filterData.wantsZoneProperties = !stringValue.isEmpty();
if (filterData.wantsZoneProperties) {
if (stringValue == "boundingBox") {
filterData.wantsZoneBoundingBox = true;
} else {
EntityPropertyFlagsFromScriptValue(wantsZonePropertiesValue, filterData.includedZoneProperties);
}
}
} else if (wantsZonePropertiesValue.isArray()) {
auto length = wantsZonePropertiesValue.property("length").toInteger();
for (int i = 0; i < length; i++) {
auto stringValue = wantsZonePropertiesValue.property(i).toString();
if (!stringValue.isEmpty()) {
filterData.wantsZoneProperties = true;
// boundingBox is a special case since it's not a true EntityPropertyFlag, so we
// need to detect it here.
if (stringValue == "boundingBox") {
filterData.wantsZoneBoundingBox = true;
break; // we can break here, since there are no other special cases
}
}
}
if (filterData.wantsZoneProperties) {
EntityPropertyFlagsFromScriptValue(wantsZonePropertiesValue, filterData.includedZoneProperties);
}
}
_lock.lockForWrite();
_filterDataMap.insert(entityID, filterData);
_lock.unlock();
@ -227,6 +374,7 @@ void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) {
}
}
} else if (scriptRequest) {
const QString urlString = scriptRequest->getUrl().toString();
qCritical() << "Failed to download script at" << urlString;
// See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure.
qCritical() << "ResourceRequest error was" << scriptRequest->getResult();

View file

@ -28,6 +28,18 @@ class EntityEditFilters : public QObject, public Dependency {
public:
struct FilterData {
QScriptValue filterFn;
bool wantsOriginalProperties { false };
bool wantsZoneProperties { false };
bool wantsToFilterAdd { true };
bool wantsToFilterEdit { true };
bool wantsToFilterPhysics { true };
bool wantsToFilterDelete { true };
EntityPropertyFlags includedOriginalProperties;
EntityPropertyFlags includedZoneProperties;
bool wantsZoneBoundingBox { false };
std::function<bool()> uncaughtExceptions;
QScriptEngine* engine;
bool rejectAll;
@ -43,7 +55,7 @@ public:
void removeFilter(EntityItemID entityID);
bool filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged,
EntityTree::FilterType filterType, EntityItemID& entityID);
EntityTree::FilterType filterType, EntityItemID& entityID, EntityItemPointer& existingEntity);
signals:
void filterAdded(EntityItemID id, bool success);

View file

@ -588,7 +588,10 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
if (entity->getLocked()) {
shouldDelete = false;
} else {
_entityTree->deleteEntity(entityID);
// only delete local entities, server entities will round trip through the server filters
if (entity->getClientOnly()) {
_entityTree->deleteEntity(entityID);
}
}
}
});

View file

@ -1105,7 +1105,7 @@ bool EntityTree::filterProperties(EntityItemPointer& existingEntity, EntityItemP
if (entityEditFilters) {
auto position = existingEntity ? existingEntity->getWorldPosition() : propertiesIn.getPosition();
auto entityID = existingEntity ? existingEntity->getEntityItemID() : EntityItemID();
accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID);
accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID, existingEntity);
}
return accepted;
@ -1868,6 +1868,36 @@ void EntityTree::forgetEntitiesDeletedBefore(quint64 sinceTime) {
}
bool EntityTree::shouldEraseEntity(EntityItemID entityID, const SharedNodePointer& sourceNode) {
EntityItemPointer existingEntity;
auto startLookup = usecTimestampNow();
existingEntity = findEntityByEntityItemID(entityID);
auto endLookup = usecTimestampNow();
_totalLookupTime += endLookup - startLookup;
auto startFilter = usecTimestampNow();
FilterType filterType = FilterType::Delete;
EntityItemProperties dummyProperties;
bool wasChanged = false;
bool allowed = (sourceNode->isAllowedEditor()) || filterProperties(existingEntity, dummyProperties, dummyProperties, wasChanged, filterType);
auto endFilter = usecTimestampNow();
_totalFilterTime += endFilter - startFilter;
if (allowed) {
if (wantEditLogging() || wantTerseEditLogging()) {
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityID;
}
} else if (wantEditLogging() || wantTerseEditLogging()) {
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] attempted to deleteentity. ID:" << entityID << " Filter rejected erase.";
}
return allowed;
}
// TODO: consider consolidating processEraseMessageDetails() and processEraseMessage()
int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode) {
#ifdef EXTRA_ERASE_DEBUGGING
@ -1895,12 +1925,10 @@ int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePo
#endif
EntityItemID entityItemID(entityID);
entityItemIDsToDelete << entityItemID;
if (wantEditLogging() || wantTerseEditLogging()) {
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID;
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
}
}
deleteEntities(entityItemIDsToDelete, true, true);
}
@ -1946,10 +1974,9 @@ int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, cons
#endif
EntityItemID entityItemID(entityID);
entityItemIDsToDelete << entityItemID;
if (wantEditLogging() || wantTerseEditLogging()) {
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID;
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
}
}

View file

@ -57,7 +57,8 @@ public:
enum FilterType {
Add,
Edit,
Physics
Physics,
Delete
};
EntityTree(bool shouldReaverage = false);
virtual ~EntityTree();
@ -193,6 +194,8 @@ public:
int processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode);
int processEraseMessageDetails(const QByteArray& buffer, const SharedNodePointer& sourceNode);
bool shouldEraseEntity(EntityItemID entityID, const SharedNodePointer& sourceNode);
EntityTreeElementPointer getContainingElement(const EntityItemID& entityItemID) /*const*/;
void addEntityMapEntry(EntityItemPointer entity);

View file

@ -0,0 +1,41 @@
//
// keep-in-zone-example.js
//
//
// Created by Brad Hefta-Gaub to use Entities on Dec. 15, 2017
// Copyright 2017 High Fidelity, Inc.
//
// This sample entity edit filter script will keep any entity inside the bounding box of the zone this filter is applied to.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
function filter(properties, type, originalProperties, zoneProperties) {
var nearZero = 0.0001 * Math.random() + 0.001;
/* Clamp position changes to bounding box of zone.*/
function clamp(val, min, max) {
/* Random near-zero value used as "zero" to prevent two sequential updates from being
exactly the same (which would cause them to be ignored) */
if (val > max) {
val = max - nearZero;
} else if (val < min) {
val = min + nearZero;
}
return val;
}
if (properties.position) {
properties.position.x = clamp(properties.position.x, zoneProperties.boundingBox.brn.x, zoneProperties.boundingBox.tfl.x);
properties.position.y = clamp(properties.position.y, zoneProperties.boundingBox.brn.y, zoneProperties.boundingBox.tfl.y);
properties.position.z = clamp(properties.position.z, zoneProperties.boundingBox.brn.z, zoneProperties.boundingBox.tfl.z);
}
return properties;
}
filter.wantsOriginalProperties = true;
filter.wantsZoneProperties = true;
filter;

View file

@ -0,0 +1,45 @@
//
// position-example.js
//
//
// Created by Brad Hefta-Gaub to use Entities on Dec. 15, 2017
// Copyright 2017 High Fidelity, Inc.
//
// This sample entity edit filter script will only allow position to be changed by no more than 5 meters on any axis.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
function filter(properties, type, originalProperties) {
/* Clamp position changes.*/
var maxChange = 5;
function clamp(val, min, max) {
if (val > max) {
val = max;
} else if (val < min) {
val = min;
}
return val;
}
if (properties.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;
var maxFudgeChange = (maxChange + nearZero);
properties.position.x = clamp(properties.position.x, originalProperties.position.x
- maxFudgeChange, originalProperties.position.x + maxFudgeChange);
properties.position.y = clamp(properties.position.y, originalProperties.position.y
- maxFudgeChange, originalProperties.position.y + maxFudgeChange);
properties.position.z = clamp(properties.position.z, originalProperties.position.z
- maxFudgeChange, originalProperties.position.z + maxFudgeChange);
}
return properties;
}
filter.wantsOriginalProperties = "position";
filter;

View file

@ -0,0 +1,40 @@
//
// prevent-add-delete-or-edit-of-entities-with-name-of-zone.js
//
//
// Created by Brad Hefta-Gaub to use Entities on Feb. 14, 2018
// Copyright 2018 High Fidelity, Inc.
//
// This sample entity edit filter script will get all edits, adds, physcis, and deletes, but will only block
// deletes, and will pass through all others.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
function filter(properties, type, originalProperties, zoneProperties) {
// for adds, check the new properties, if the name is the same as the zone, prevent the add
// note: this case tests the example where originalProperties will be unknown, since as an
// add message, there is no existing entity to get proprties from. But we need to make sure
// zoneProperties is still set in the correct 4th parameter.
if (type == Entities.ADD_FILTER_TYPE) {
if (properties.name == zoneProperties.name) {
return false;
}
} else {
// for edits or deletes, check the "original" property for the entity against the zone's name
if (originalProperties.name == zoneProperties.name) {
return false;
}
}
return properties;
}
filter.wantsToFilterAdd = true; // do run on add
filter.wantsToFilterEdit = true; // do not run on edit
filter.wantsToFilterPhysics = false; // do not run on physics
filter.wantsToFilterDelete = true; // do not run on delete
filter.wantsOriginalProperties = "name";
filter.wantsZoneProperties = "name";
filter;

View file

@ -0,0 +1,26 @@
//
// prevent-add-of-entities-named-bob-example.js
//
//
// Created by Brad Hefta-Gaub to use Entities on Jan. 25, 2018
// Copyright 2018 High Fidelity, Inc.
//
// This sample entity edit filter script will get all edits, adds, physcis, and deletes, but will only block
// deletes, and will pass through all others.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
function filter(properties, type) {
if (properties.name == "bob") {
return false;
}
return properties;
}
filter.wantsToFilterAdd = true; // do run on add
filter.wantsToFilterEdit = false; // do not run on edit
filter.wantsToFilterPhysics = false; // do not run on physics
filter.wantsToFilterDelete = false; // do not run on delete
filter;

View file

@ -0,0 +1,22 @@
//
// prevent-all-deletes.js
//
//
// Created by Brad Hefta-Gaub to use Entities on Jan. 25, 2018
// Copyright 2018 High Fidelity, Inc.
//
// This sample entity edit filter script will prevent deletes of any entities.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
function filter() {
return false; // all deletes are blocked
}
filter.wantsToFilterAdd = false; // don't run on adds
filter.wantsToFilterEdit = false; // don't run on edits
filter.wantsToFilterPhysics = false; // don't run on physics
filter.wantsToFilterDelete = true; // do run on deletes
filter;

View file

@ -0,0 +1,24 @@
//
// prevent-delete-in-zone-example.js
//
//
// Created by Brad Hefta-Gaub to use Entities on Jan. 25, 2018
// Copyright 2018 High Fidelity, Inc.
//
// This sample entity edit filter script will get all edits, adds, physcis, and deletes, but will only block
// deletes, and will pass through all others.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
function filter(properties, type) {
if (type == Entities.DELETE_FILTER_TYPE) {
return false;
}
return properties;
}
filter.wantsToFilterDelete = true; // do run on deletes
filter;