mirror of
https://github.com/lubosz/overte.git
synced 2025-04-27 05:35:37 +02:00
commit
5ec3bbde52
16 changed files with 385 additions and 155 deletions
assignment-client/src/entities
libraries
entities/src
EntityEditFilters.cppEntityEditFilters.hEntityItemID.cppEntityItemProperties.cppEntityItemProperties.hEntityPropertyFlags.hEntityTree.cppEntityTree.hZoneEntityItem.cppZoneEntityItem.h
networking/src/udt
scripts/system/html
|
@ -15,6 +15,7 @@
|
|||
#include <SimpleEntitySimulation.h>
|
||||
#include <ResourceCache.h>
|
||||
#include <ScriptCache.h>
|
||||
#include <EntityEditFilters.h>
|
||||
|
||||
#include "EntityServer.h"
|
||||
#include "EntityServerConsts.h"
|
||||
|
@ -71,6 +72,7 @@ OctreePointer EntityServer::createTree() {
|
|||
|
||||
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
|
||||
DependencyManager::set<AssignmentParentFinder>(tree);
|
||||
DependencyManager::set<EntityEditFilters>(std::static_pointer_cast<EntityTree>(tree));
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
@ -292,96 +294,26 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
|
|||
} else {
|
||||
tree->setEntityScriptSourceWhitelist("");
|
||||
}
|
||||
|
||||
if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) {
|
||||
// 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<EntityTree>(_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)
|
||||
if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) {
|
||||
qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer.";
|
||||
scriptRequestFinished();
|
||||
return;
|
||||
}
|
||||
auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL);
|
||||
if (!scriptRequest) {
|
||||
qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString();
|
||||
scriptRequestFinished();
|
||||
return;
|
||||
}
|
||||
// Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own.
|
||||
connect(scriptRequest, &ResourceRequest::finished, this, &EntityServer::scriptRequestFinished);
|
||||
// FIXME: handle atp rquests setup here. See Agent::requestScript()
|
||||
qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString());
|
||||
scriptRequest->send();
|
||||
qDebug() << "script request sent";
|
||||
|
||||
auto entityEditFilters = DependencyManager::get<EntityEditFilters>();
|
||||
|
||||
QString filterURL;
|
||||
if (readOptionString("entityEditFilter", settingsSectionObject, filterURL) && !filterURL.isEmpty()) {
|
||||
// connect the filterAdded signal, and block edits until you hear back
|
||||
connect(entityEditFilters.data(), &EntityEditFilters::filterAdded, this, &EntityServer::entityFilterAdded);
|
||||
|
||||
entityEditFilters->addFilter(EntityItemID(), filterURL);
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from ScriptEngine.cpp. We should make this a class method for reuse.
|
||||
// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point.
|
||||
static bool hasCorrectSyntax(const QScriptProgram& program) {
|
||||
const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());
|
||||
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||
const auto error = syntaxCheck.errorMessage();
|
||||
const auto line = QString::number(syntaxCheck.errorLineNumber());
|
||||
const auto column = QString::number(syntaxCheck.errorColumnNumber());
|
||||
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column);
|
||||
qCritical() << qPrintable(message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) {
|
||||
if (engine.hasUncaughtException()) {
|
||||
const auto backtrace = engine.uncaughtExceptionBacktrace();
|
||||
const auto exception = engine.uncaughtException().toString();
|
||||
const auto line = QString::number(engine.uncaughtExceptionLineNumber());
|
||||
engine.clearExceptions();
|
||||
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3";
|
||||
auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line);
|
||||
if (!backtrace.empty()) {
|
||||
static const auto lineSeparator = "\n ";
|
||||
message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
|
||||
void EntityServer::entityFilterAdded(EntityItemID id, bool success) {
|
||||
if (id.isInvalidID()) {
|
||||
if (success) {
|
||||
qDebug() << "entity edit filter for " << id << "added successfully";
|
||||
} else {
|
||||
qDebug() << "entity edit filter unsuccessfully added, all edits will be rejected for those without lock rights.";
|
||||
}
|
||||
qCritical() << qPrintable(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void EntityServer::scriptRequestFinished() {
|
||||
qDebug() << "script request completed";
|
||||
auto scriptRequest = qobject_cast<ResourceRequest*>(sender());
|
||||
const QString urlString = scriptRequest->getUrl().toString();
|
||||
if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) {
|
||||
auto scriptContents = scriptRequest->getData();
|
||||
qInfo() << "Downloaded script:" << scriptContents;
|
||||
QScriptProgram program(scriptContents, urlString);
|
||||
if (hasCorrectSyntax(program)) {
|
||||
_entityEditFilterEngine.evaluate(scriptContents);
|
||||
if (!hadUncaughtExceptions(_entityEditFilterEngine, urlString)) {
|
||||
std::static_pointer_cast<EntityTree>(_tree)->initEntityEditFilterEngine(&_entityEditFilterEngine, [this]() {
|
||||
return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter);
|
||||
});
|
||||
scriptRequest->deleteLater();
|
||||
qDebug() << "script request filter processed";
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (scriptRequest) {
|
||||
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();
|
||||
} else {
|
||||
qCritical() << "Failed to create script request.";
|
||||
}
|
||||
// Hard stop of the assignment client on failure. We don't want anyone to think they have a filter in place when they don't.
|
||||
// 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();
|
||||
}
|
||||
|
||||
void EntityServer::nodeAdded(SharedNodePointer node) {
|
||||
|
|
|
@ -63,13 +63,13 @@ public slots:
|
|||
virtual void nodeAdded(SharedNodePointer node) override;
|
||||
virtual void nodeKilled(SharedNodePointer node) override;
|
||||
void pruneDeletedEntities();
|
||||
void entityFilterAdded(EntityItemID id, bool success);
|
||||
|
||||
protected:
|
||||
virtual OctreePointer createTree() override;
|
||||
|
||||
private slots:
|
||||
void handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void scriptRequestFinished();
|
||||
|
||||
private:
|
||||
SimpleEntitySimulationPointer _entitySimulation;
|
||||
|
@ -77,9 +77,6 @@ private:
|
|||
|
||||
QReadWriteLock _viewerSendingStatsLock;
|
||||
QMap<QUuid, QMap<QUuid, ViewerSendingStats>> _viewerSendingStats;
|
||||
|
||||
QString _entityEditFilter{};
|
||||
QScriptEngine _entityEditFilterEngine{};
|
||||
};
|
||||
|
||||
#endif // hifi_EntityServer_h
|
||||
|
|
237
libraries/entities/src/EntityEditFilters.cpp
Normal file
237
libraries/entities/src/EntityEditFilters.cpp
Normal file
|
@ -0,0 +1,237 @@
|
|||
//
|
||||
// EntityEditFilters.cpp
|
||||
// libraries/entities/src
|
||||
//
|
||||
// Created by David Kelly on 2/7/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
|
||||
//
|
||||
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
#include <ResourceManager.h>
|
||||
#include "EntityEditFilters.h"
|
||||
|
||||
QList<EntityItemID> EntityEditFilters::getZonesByPosition(glm::vec3& position) {
|
||||
QList<EntityItemID> zones;
|
||||
QList<EntityItemID> missingZones;
|
||||
_lock.lockForRead();
|
||||
auto zoneIDs = _filterDataMap.keys();
|
||||
_lock.unlock();
|
||||
for (auto id : zoneIDs) {
|
||||
if (!id.isInvalidID()) {
|
||||
// for now, look it up in the tree (soon we need to cache or similar?)
|
||||
EntityItemPointer itemPtr = _tree->findEntityByEntityItemID(id);
|
||||
auto zone = std::dynamic_pointer_cast<ZoneEntityItem>(itemPtr);
|
||||
if (!zone) {
|
||||
// TODO: maybe remove later?
|
||||
removeFilter(id);
|
||||
} else if (zone->contains(position)) {
|
||||
zones.append(id);
|
||||
}
|
||||
} else {
|
||||
// the null id is the global filter we put in the domain server's
|
||||
// advanced entity server settings
|
||||
zones.append(id);
|
||||
}
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged,
|
||||
EntityTree::FilterType filterType, EntityItemID& itemID) {
|
||||
|
||||
// get the ids of all the zones (plus the global entity edit filter) that the position
|
||||
// lies within
|
||||
auto zoneIDs = getZonesByPosition(position);
|
||||
for (auto id : zoneIDs) {
|
||||
if (!itemID.isInvalidID() && id == itemID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the filter pair, etc...
|
||||
_lock.lockForRead();
|
||||
FilterData filterData = _filterDataMap.value(id);
|
||||
_lock.unlock();
|
||||
|
||||
if (filterData.valid()) {
|
||||
if (filterData.rejectAll) {
|
||||
return false;
|
||||
}
|
||||
auto oldProperties = propertiesIn.getDesiredProperties();
|
||||
auto specifiedProperties = propertiesIn.getChangedProperties();
|
||||
propertiesIn.setDesiredProperties(specifiedProperties);
|
||||
QScriptValue inputValues = propertiesIn.copyToScriptValue(filterData.engine, false, true, true);
|
||||
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;
|
||||
|
||||
QScriptValue result = filterData.filterFn.call(_nullObjectForFilter, args);
|
||||
if (filterData.uncaughtExceptions()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.isObject()){
|
||||
// make propertiesIn reflect the changes, for next filter...
|
||||
propertiesIn.copyFromScriptValue(result, false);
|
||||
|
||||
// and update propertiesOut too. TODO: this could be more efficient...
|
||||
propertiesOut.copyFromScriptValue(result, false);
|
||||
// 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 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we made it here,
|
||||
return true;
|
||||
}
|
||||
|
||||
void EntityEditFilters::removeFilter(EntityItemID entityID) {
|
||||
QWriteLocker writeLock(&_lock);
|
||||
FilterData filterData = _filterDataMap.value(entityID);
|
||||
if (filterData.valid()) {
|
||||
delete filterData.engine;
|
||||
}
|
||||
_filterDataMap.remove(entityID);
|
||||
}
|
||||
|
||||
void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) {
|
||||
|
||||
QUrl scriptURL(filterURL);
|
||||
|
||||
// setting it to an empty string is same as removing
|
||||
if (filterURL.size() == 0) {
|
||||
removeFilter(entityID);
|
||||
return;
|
||||
}
|
||||
|
||||
// The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp)
|
||||
if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) {
|
||||
qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer.";
|
||||
scriptRequestFinished(entityID);
|
||||
return;
|
||||
}
|
||||
|
||||
// first remove any existing info for this entity
|
||||
removeFilter(entityID);
|
||||
|
||||
// reject all edits until we load the script
|
||||
FilterData filterData;
|
||||
filterData.rejectAll = true;
|
||||
|
||||
_lock.lockForWrite();
|
||||
_filterDataMap.insert(entityID, filterData);
|
||||
_lock.unlock();
|
||||
|
||||
auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL);
|
||||
if (!scriptRequest) {
|
||||
qWarning() << "Could not create ResourceRequest for Entity Edit filter script at" << scriptURL.toString();
|
||||
scriptRequestFinished(entityID);
|
||||
return;
|
||||
}
|
||||
// Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own.
|
||||
connect(scriptRequest, &ResourceRequest::finished, this, [this, entityID]{ EntityEditFilters::scriptRequestFinished(entityID);} );
|
||||
// FIXME: handle atp rquests setup here. See Agent::requestScript()
|
||||
qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString());
|
||||
scriptRequest->send();
|
||||
qDebug() << "script request sent for entity " << entityID;
|
||||
}
|
||||
|
||||
// Copied from ScriptEngine.cpp. We should make this a class method for reuse.
|
||||
// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point.
|
||||
static bool hasCorrectSyntax(const QScriptProgram& program) {
|
||||
const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode());
|
||||
if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {
|
||||
const auto error = syntaxCheck.errorMessage();
|
||||
const auto line = QString::number(syntaxCheck.errorLineNumber());
|
||||
const auto column = QString::number(syntaxCheck.errorColumnNumber());
|
||||
const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column);
|
||||
qCritical() << qPrintable(message);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) {
|
||||
if (engine.hasUncaughtException()) {
|
||||
const auto backtrace = engine.uncaughtExceptionBacktrace();
|
||||
const auto exception = engine.uncaughtException().toString();
|
||||
const auto line = QString::number(engine.uncaughtExceptionLineNumber());
|
||||
engine.clearExceptions();
|
||||
|
||||
static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3";
|
||||
auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line);
|
||||
if (!backtrace.empty()) {
|
||||
static const auto lineSeparator = "\n ";
|
||||
message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator));
|
||||
}
|
||||
qCritical() << qPrintable(message);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
auto scriptContents = scriptRequest->getData();
|
||||
qInfo() << "Downloaded script:" << scriptContents;
|
||||
QScriptProgram program(scriptContents, urlString);
|
||||
if (hasCorrectSyntax(program)) {
|
||||
// create a QScriptEngine for this script
|
||||
QScriptEngine* engine = new QScriptEngine();
|
||||
engine->evaluate(scriptContents);
|
||||
if (!hadUncaughtExceptions(*engine, urlString)) {
|
||||
// put the engine in the engine map (so we don't leak them, etc...)
|
||||
FilterData filterData;
|
||||
filterData.engine = engine;
|
||||
filterData.rejectAll = false;
|
||||
|
||||
// define the uncaughtException function
|
||||
QScriptEngine& engineRef = *engine;
|
||||
filterData.uncaughtExceptions = [this, &engineRef, urlString]() { return hadUncaughtExceptions(engineRef, urlString); };
|
||||
|
||||
// now get the filter function
|
||||
auto global = engine->globalObject();
|
||||
auto entitiesObject = engine->newObject();
|
||||
entitiesObject.setProperty("ADD_FILTER_TYPE", EntityTree::FilterType::Add);
|
||||
entitiesObject.setProperty("EDIT_FILTER_TYPE", EntityTree::FilterType::Edit);
|
||||
entitiesObject.setProperty("PHYSICS_FILTER_TYPE", EntityTree::FilterType::Physics);
|
||||
global.setProperty("Entities", entitiesObject);
|
||||
filterData.filterFn = global.property("filter");
|
||||
if (!filterData.filterFn.isFunction()) {
|
||||
qDebug() << "Filter function specified but not found. Will reject all edits for those without lock rights.";
|
||||
delete engine;
|
||||
filterData.rejectAll=true;
|
||||
}
|
||||
|
||||
|
||||
_lock.lockForWrite();
|
||||
_filterDataMap.insert(entityID, filterData);
|
||||
_lock.unlock();
|
||||
|
||||
qDebug() << "script request filter processed for entity id " << entityID;
|
||||
|
||||
emit filterAdded(entityID, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (scriptRequest) {
|
||||
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();
|
||||
} else {
|
||||
qCritical() << "Failed to create script request.";
|
||||
}
|
||||
emit filterAdded(entityID, false);
|
||||
}
|
65
libraries/entities/src/EntityEditFilters.h
Normal file
65
libraries/entities/src/EntityEditFilters.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// EntityEditFilters.h
|
||||
// libraries/entities/src
|
||||
//
|
||||
// Created by David Kelly on 2/7/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
|
||||
//
|
||||
#ifndef hifi_EntityEditFilters_h
|
||||
#define hifi_EntityEditFilters_h
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QScriptValue>
|
||||
#include <QScriptEngine>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "EntityItemID.h"
|
||||
#include "EntityItemProperties.h"
|
||||
#include "EntityTree.h"
|
||||
|
||||
class EntityEditFilters : public QObject, public Dependency {
|
||||
Q_OBJECT
|
||||
public:
|
||||
struct FilterData {
|
||||
QScriptValue filterFn;
|
||||
std::function<bool()> uncaughtExceptions;
|
||||
QScriptEngine* engine;
|
||||
bool rejectAll;
|
||||
|
||||
FilterData(): engine(nullptr), rejectAll(false) {};
|
||||
bool valid() { return (rejectAll || (engine != nullptr && filterFn.isFunction() && uncaughtExceptions)); }
|
||||
};
|
||||
|
||||
EntityEditFilters() {};
|
||||
EntityEditFilters(EntityTreePointer tree ): _tree(tree) {};
|
||||
|
||||
void addFilter(EntityItemID entityID, QString filterURL);
|
||||
void removeFilter(EntityItemID entityID);
|
||||
|
||||
bool filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged,
|
||||
EntityTree::FilterType filterType, EntityItemID& entityID);
|
||||
|
||||
signals:
|
||||
void filterAdded(EntityItemID id, bool success);
|
||||
|
||||
private slots:
|
||||
void scriptRequestFinished(EntityItemID entityID);
|
||||
|
||||
private:
|
||||
QList<EntityItemID> getZonesByPosition(glm::vec3& position);
|
||||
|
||||
EntityTreePointer _tree {};
|
||||
bool _rejectAll {false};
|
||||
QScriptValue _nullObjectForFilter{};
|
||||
|
||||
QReadWriteLock _lock;
|
||||
QMap<EntityItemID, FilterData> _filterDataMap;
|
||||
};
|
||||
|
||||
#endif //hifi_EntityEditFilters_h
|
|
@ -19,6 +19,7 @@
|
|||
#include "RegisteredMetaTypes.h"
|
||||
#include "EntityItemID.h"
|
||||
|
||||
int entityItemIDTypeID = qRegisterMetaType<EntityItemID>();
|
||||
|
||||
EntityItemID::EntityItemID() : QUuid()
|
||||
{
|
||||
|
|
|
@ -332,6 +332,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
|
|||
|
||||
CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed);
|
||||
CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed);
|
||||
CHECK_PROPERTY_CHANGE(PROP_FILTER_URL, filterURL);
|
||||
|
||||
CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly);
|
||||
CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID);
|
||||
|
@ -509,6 +510,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
|
|||
|
||||
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FLYING_ALLOWED, flyingAllowed);
|
||||
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GHOSTING_ALLOWED, ghostingAllowed);
|
||||
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FILTER_URL, filterURL);
|
||||
}
|
||||
|
||||
// Web only
|
||||
|
@ -751,6 +753,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
|
|||
|
||||
COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed);
|
||||
COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed);
|
||||
COPY_PROPERTY_FROM_QSCRIPTVALUE(filterURL, QString, setFilterURL);
|
||||
|
||||
COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly);
|
||||
COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID);
|
||||
|
@ -879,6 +882,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
|
|||
|
||||
COPY_PROPERTY_IF_CHANGED(flyingAllowed);
|
||||
COPY_PROPERTY_IF_CHANGED(ghostingAllowed);
|
||||
COPY_PROPERTY_IF_CHANGED(filterURL);
|
||||
|
||||
COPY_PROPERTY_IF_CHANGED(clientOnly);
|
||||
COPY_PROPERTY_IF_CHANGED(owningAvatarID);
|
||||
|
@ -1063,6 +1067,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue
|
|||
|
||||
ADD_PROPERTY_TO_MAP(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool);
|
||||
ADD_PROPERTY_TO_MAP(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool);
|
||||
ADD_PROPERTY_TO_MAP(PROP_FILTER_URL, FilterURL, filterURL, QString);
|
||||
|
||||
ADD_PROPERTY_TO_MAP(PROP_DPI, DPI, dpi, uint16_t);
|
||||
|
||||
|
@ -1311,6 +1316,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem
|
|||
|
||||
APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, properties.getFlyingAllowed());
|
||||
APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, properties.getGhostingAllowed());
|
||||
APPEND_ENTITY_PROPERTY(PROP_FILTER_URL, properties.getFilterURL());
|
||||
}
|
||||
|
||||
if (properties.getType() == EntityTypes::PolyVox) {
|
||||
|
@ -1605,6 +1611,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
|
|||
|
||||
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FLYING_ALLOWED, bool, setFlyingAllowed);
|
||||
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed);
|
||||
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FILTER_URL, QString, setFilterURL);
|
||||
}
|
||||
|
||||
if (properties.getType() == EntityTypes::PolyVox) {
|
||||
|
@ -1808,6 +1815,7 @@ void EntityItemProperties::markAllChanged() {
|
|||
|
||||
_flyingAllowedChanged = true;
|
||||
_ghostingAllowedChanged = true;
|
||||
_filterURLChanged = true;
|
||||
|
||||
_clientOnlyChanged = true;
|
||||
_owningAvatarIDChanged = true;
|
||||
|
@ -2150,7 +2158,9 @@ QList<QString> EntityItemProperties::listChangedProperties() {
|
|||
if (ghostingAllowedChanged()) {
|
||||
out += "ghostingAllowed";
|
||||
}
|
||||
|
||||
if (filterURLChanged()) {
|
||||
out += "filterURL";
|
||||
}
|
||||
if (dpiChanged()) {
|
||||
out += "dpi";
|
||||
}
|
||||
|
|
|
@ -215,6 +215,7 @@ public:
|
|||
|
||||
DEFINE_PROPERTY(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool, ZoneEntityItem::DEFAULT_FLYING_ALLOWED);
|
||||
DEFINE_PROPERTY(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool, ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED);
|
||||
DEFINE_PROPERTY(PROP_FILTER_URL, FilterURL, filterURL, QString, ZoneEntityItem::DEFAULT_FILTER_URL);
|
||||
|
||||
DEFINE_PROPERTY(PROP_CLIENT_ONLY, ClientOnly, clientOnly, bool, false);
|
||||
DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID);
|
||||
|
@ -458,6 +459,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
|
|||
|
||||
DEBUG_PROPERTY_IF_CHANGED(debug, properties, FlyingAllowed, flyingAllowed, "");
|
||||
DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, "");
|
||||
DEBUG_PROPERTY_IF_CHANGED(debug, properties, FilterURL, filterURL, "");
|
||||
|
||||
DEBUG_PROPERTY_IF_CHANGED(debug, properties, ClientOnly, clientOnly, "");
|
||||
DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, "");
|
||||
|
|
|
@ -185,6 +185,8 @@ enum EntityPropertyList {
|
|||
|
||||
PROP_SERVER_SCRIPTS,
|
||||
|
||||
PROP_FILTER_URL,
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ATTENTION: add new properties to end of list just ABOVE this line
|
||||
PROP_AFTER_LAST_ITEM,
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include "EntitiesLogging.h"
|
||||
#include "RecurseOctreeToMapOperator.h"
|
||||
#include "LogHandler.h"
|
||||
#include "EntityEditFilters.h"
|
||||
|
||||
static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50;
|
||||
const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour
|
||||
|
@ -923,55 +924,14 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList<Q
|
|||
}
|
||||
}
|
||||
|
||||
void EntityTree::initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> entityEditFilterHadUncaughtExceptions) {
|
||||
_entityEditFilterEngine = engine;
|
||||
_entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions;
|
||||
auto global = _entityEditFilterEngine->globalObject();
|
||||
_entityEditFilterFunction = global.property("filter");
|
||||
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.
|
||||
}
|
||||
auto entitiesObject = _entityEditFilterEngine->newObject();
|
||||
entitiesObject.setProperty("ADD_FILTER_TYPE", FilterType::Add);
|
||||
entitiesObject.setProperty("EDIT_FILTER_TYPE", FilterType::Edit);
|
||||
entitiesObject.setProperty("PHYSICS_FILTER_TYPE", FilterType::Physics);
|
||||
global.setProperty("Entities", entitiesObject);
|
||||
_hasEntityEditFilter = true;
|
||||
}
|
||||
|
||||
bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) {
|
||||
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();
|
||||
auto specifiedProperties = propertiesIn.getChangedProperties();
|
||||
propertiesIn.setDesiredProperties(specifiedProperties);
|
||||
QScriptValue inputValues = propertiesIn.copyToScriptValue(_entityEditFilterEngine, false, true, true);
|
||||
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;
|
||||
|
||||
QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args);
|
||||
if (_entityEditFilterHadUncaughtExceptions()) {
|
||||
result = QScriptValue();
|
||||
}
|
||||
|
||||
bool accepted = result.isObject(); // filters should return null or false to completely reject edit or add
|
||||
if (accepted) {
|
||||
propertiesOut.copyFromScriptValue(result, false);
|
||||
// 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;
|
||||
bool EntityTree::filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) {
|
||||
bool accepted = true;
|
||||
auto entityEditFilters = DependencyManager::get<EntityEditFilters>();
|
||||
if (entityEditFilters) {
|
||||
auto position = existingEntity ? existingEntity->getPosition() : propertiesIn.getPosition();
|
||||
auto entityID = existingEntity ? existingEntity->getEntityItemID() : EntityItemID();
|
||||
accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID);
|
||||
}
|
||||
|
||||
return accepted;
|
||||
|
@ -1076,11 +1036,16 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
|||
// an existing entity... handle appropriately
|
||||
if (validEditPacket) {
|
||||
|
||||
// search for the entity by EntityItemID
|
||||
startLookup = usecTimestampNow();
|
||||
EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID);
|
||||
endLookup = usecTimestampNow();
|
||||
|
||||
startFilter = usecTimestampNow();
|
||||
bool wasChanged = false;
|
||||
// Having (un)lock rights bypasses the filter, unless it's a physics result.
|
||||
FilterType filterType = isPhysics ? FilterType::Physics : (isAdd ? FilterType::Add : FilterType::Edit);
|
||||
bool allowed = (!isPhysics && senderNode->isAllowedEditor()) || filterProperties(properties, properties, wasChanged, filterType);
|
||||
bool allowed = (!isPhysics && senderNode->isAllowedEditor()) || filterProperties(existingEntity, properties, properties, wasChanged, filterType);
|
||||
if (!allowed) {
|
||||
auto timestamp = properties.getLastEdited();
|
||||
properties = EntityItemProperties();
|
||||
|
@ -1093,10 +1058,6 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
|
|||
}
|
||||
endFilter = usecTimestampNow();
|
||||
|
||||
// search for the entity by EntityItemID
|
||||
startLookup = usecTimestampNow();
|
||||
EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID);
|
||||
endLookup = usecTimestampNow();
|
||||
if (existingEntity && !isAdd) {
|
||||
|
||||
if (suppressDisallowedScript) {
|
||||
|
@ -1767,3 +1728,4 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const {
|
|||
}
|
||||
return entity->getJointNames();
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ typedef std::shared_ptr<EntityTree> EntityTreePointer;
|
|||
#include "EntityTreeElement.h"
|
||||
#include "DeleteEntityOperator.h"
|
||||
|
||||
class EntityEditFilters;
|
||||
class Model;
|
||||
using ModelPointer = std::shared_ptr<Model>;
|
||||
using ModelWeakPointer = std::weak_ptr<Model>;
|
||||
|
@ -271,9 +272,6 @@ public:
|
|||
|
||||
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
|
||||
|
||||
void initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> entityEditFilterHadUncaughtExceptions);
|
||||
void setHasEntityFilter(bool hasFilter) { _hasEntityEditFilter = hasFilter; }
|
||||
|
||||
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
|
||||
|
||||
public slots:
|
||||
|
@ -362,13 +360,8 @@ protected:
|
|||
|
||||
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
|
||||
|
||||
bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType);
|
||||
bool filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType);
|
||||
bool _hasEntityEditFilter{ false };
|
||||
QScriptEngine* _entityEditFilterEngine{};
|
||||
QScriptValue _entityEditFilterFunction{};
|
||||
QScriptValue _nullObjectForFilter{};
|
||||
std::function<bool()> _entityEditFilterHadUncaughtExceptions;
|
||||
|
||||
QStringList _entityScriptSourceWhitelist;
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include "EntityTree.h"
|
||||
#include "EntityTreeElement.h"
|
||||
#include "ZoneEntityItem.h"
|
||||
#include "EntityEditFilters.h"
|
||||
|
||||
bool ZoneEntityItem::_zonesArePickable = false;
|
||||
bool ZoneEntityItem::_drawZoneBoundaries = false;
|
||||
|
@ -28,7 +29,7 @@ const ShapeType ZoneEntityItem::DEFAULT_SHAPE_TYPE = SHAPE_TYPE_BOX;
|
|||
const QString ZoneEntityItem::DEFAULT_COMPOUND_SHAPE_URL = "";
|
||||
const bool ZoneEntityItem::DEFAULT_FLYING_ALLOWED = true;
|
||||
const bool ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED = true;
|
||||
|
||||
const QString ZoneEntityItem::DEFAULT_FILTER_URL = "";
|
||||
|
||||
EntityItemPointer ZoneEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
|
||||
EntityItemPointer entity { new ZoneEntityItem(entityID) };
|
||||
|
@ -61,6 +62,7 @@ EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredPr
|
|||
|
||||
COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed);
|
||||
COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed);
|
||||
COPY_ENTITY_PROPERTY_TO_PROPERTIES(filterURL, getFilterURL);
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
@ -79,6 +81,7 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) {
|
|||
|
||||
SET_ENTITY_PROPERTY_FROM_PROPERTIES(flyingAllowed, setFlyingAllowed);
|
||||
SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed);
|
||||
SET_ENTITY_PROPERTY_FROM_PROPERTIES(filterURL, setFilterURL);
|
||||
|
||||
bool somethingChangedInSkybox = _skyboxProperties.setProperties(properties);
|
||||
|
||||
|
@ -128,6 +131,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
|
|||
|
||||
READ_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, bool, setFlyingAllowed);
|
||||
READ_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed);
|
||||
READ_ENTITY_PROPERTY(PROP_FILTER_URL, QString, setFilterURL);
|
||||
|
||||
return bytesRead;
|
||||
}
|
||||
|
@ -147,6 +151,7 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p
|
|||
|
||||
requestedProperties += PROP_FLYING_ALLOWED;
|
||||
requestedProperties += PROP_GHOSTING_ALLOWED;
|
||||
requestedProperties += PROP_FILTER_URL;
|
||||
|
||||
return requestedProperties;
|
||||
}
|
||||
|
@ -177,6 +182,7 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits
|
|||
|
||||
APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, getFlyingAllowed());
|
||||
APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, getGhostingAllowed());
|
||||
APPEND_ENTITY_PROPERTY(PROP_FILTER_URL, getFilterURL());
|
||||
}
|
||||
|
||||
void ZoneEntityItem::debugDump() const {
|
||||
|
@ -215,3 +221,13 @@ bool ZoneEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const
|
|||
|
||||
return _zonesArePickable;
|
||||
}
|
||||
|
||||
void ZoneEntityItem::setFilterURL(QString url) {
|
||||
_filterURL = url;
|
||||
if (DependencyManager::isSet<EntityEditFilters>()) {
|
||||
auto entityEditFilters = DependencyManager::get<EntityEditFilters>();
|
||||
qCDebug(entities) << "adding filter " << url << "for zone" << getEntityItemID();
|
||||
entityEditFilters->addFilter(getEntityItemID(), url);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,8 @@ public:
|
|||
void setFlyingAllowed(bool value) { _flyingAllowed = value; }
|
||||
bool getGhostingAllowed() const { return _ghostingAllowed; }
|
||||
void setGhostingAllowed(bool value) { _ghostingAllowed = value; }
|
||||
QString getFilterURL() const { return _filterURL; }
|
||||
void setFilterURL(const QString url);
|
||||
|
||||
virtual bool supportsDetailedRayIntersection() const override { return true; }
|
||||
virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
|
||||
|
@ -87,6 +89,7 @@ public:
|
|||
static const QString DEFAULT_COMPOUND_SHAPE_URL;
|
||||
static const bool DEFAULT_FLYING_ALLOWED;
|
||||
static const bool DEFAULT_GHOSTING_ALLOWED;
|
||||
static const QString DEFAULT_FILTER_URL;
|
||||
|
||||
protected:
|
||||
KeyLightPropertyGroup _keyLightProperties;
|
||||
|
@ -101,6 +104,7 @@ protected:
|
|||
|
||||
bool _flyingAllowed { DEFAULT_FLYING_ALLOWED };
|
||||
bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED };
|
||||
QString _filterURL { DEFAULT_FILTER_URL };
|
||||
|
||||
static bool _drawZoneBoundaries;
|
||||
static bool _zonesArePickable;
|
||||
|
|
|
@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
|||
case PacketType::EntityEdit:
|
||||
case PacketType::EntityData:
|
||||
case PacketType::EntityPhysics:
|
||||
return VERSION_ENTITIES_PHYSICS_PACKET;
|
||||
return VERSION_ENTITIES_ZONE_FILTERS;
|
||||
case PacketType::EntityQuery:
|
||||
return static_cast<PacketVersion>(EntityQueryPacketVersion::JsonFilter);
|
||||
case PacketType::AvatarIdentity:
|
||||
|
|
|
@ -204,6 +204,7 @@ const PacketVersion VERSION_ENTITIES_ARROW_ACTION = 64;
|
|||
const PacketVersion VERSION_ENTITIES_LAST_EDITED_BY = 65;
|
||||
const PacketVersion VERSION_ENTITIES_SERVER_SCRIPTS = 66;
|
||||
const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67;
|
||||
const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68;
|
||||
|
||||
enum class EntityQueryPacketVersion: PacketVersion {
|
||||
JsonFilter = 18
|
||||
|
|
|
@ -450,6 +450,11 @@
|
|||
<input type="checkbox" id="property-zone-ghosting-allowed">
|
||||
<label for="property-zone-ghosting-allowed">Ghosting allowed</label>
|
||||
</div>
|
||||
<hr class="zone-group zone-section">
|
||||
<div class="zone-group zone-section property url ">
|
||||
<label for="property-zone-filter-url">Filter URL</label>
|
||||
<input type="text" id="property-zone-filter-url">
|
||||
</div>
|
||||
<div class="sub-section-header zone-group zone-section keylight-section">
|
||||
<label>Key Light</label>
|
||||
</div>
|
||||
|
|
|
@ -697,6 +697,7 @@ function loaded() {
|
|||
|
||||
var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed");
|
||||
var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed");
|
||||
var elZoneFilterURL = document.getElementById("property-zone-filter-url");
|
||||
|
||||
var elPolyVoxSections = document.querySelectorAll(".poly-vox-section");
|
||||
allSections.push(elPolyVoxSections);
|
||||
|
@ -1032,6 +1033,7 @@ function loaded() {
|
|||
|
||||
elZoneFlyingAllowed.checked = properties.flyingAllowed;
|
||||
elZoneGhostingAllowed.checked = properties.ghostingAllowed;
|
||||
elZoneFilterURL.value = properties.filterURL;
|
||||
|
||||
showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox');
|
||||
} else if (properties.type == "PolyVox") {
|
||||
|
@ -1387,7 +1389,8 @@ function loaded() {
|
|||
|
||||
elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed'));
|
||||
elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed'));
|
||||
|
||||
elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL'));
|
||||
|
||||
var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction(
|
||||
'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ);
|
||||
elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction);
|
||||
|
|
Loading…
Reference in a new issue