Merge branch 'reset-hud-on-driving' of https://github.com/howard-stearns/hifi into reset-hud-on-driving

This commit is contained in:
Howard Stearns 2016-05-27 10:31:42 -07:00
commit abd2b150f5
39 changed files with 488 additions and 296 deletions

View file

@ -55,12 +55,20 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
if (message->getSize() == 0) {
return;
}
QDataStream packetStream(message->getMessage());
// read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it
NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr());
QByteArray myProtocolVersion = protocolVersionsSignature();
if (nodeConnection.protocolVersion != myProtocolVersion) {
QString protocolVersionError = "Protocol version mismatch - Domain version:" + QCoreApplication::applicationVersion();
qDebug() << "Protocol Version mismatch - denying connection.";
sendConnectionDeniedPacket(protocolVersionError, message->getSenderSockAddr(),
DomainHandler::ConnectionRefusedReason::ProtocolMismatch);
return;
}
if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) {
qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection.";
return;
@ -370,7 +378,8 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
} else {
if (!senderSockAddr.isNull()) {
qDebug() << "Error decrypting username signature for " << username << "- denying connection.";
sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr);
sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
}
// free up the public key, we don't need it anymore
@ -382,13 +391,15 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
// we can't let this user in since we couldn't convert their public key to an RSA key we could use
if (!senderSockAddr.isNull()) {
qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection.";
sendConnectionDeniedPacket("Couldn't convert data to RSA key.", senderSockAddr);
sendConnectionDeniedPacket("Couldn't convert data to RSA key.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
}
}
} else {
if (!senderSockAddr.isNull()) {
qDebug() << "Insufficient data to decrypt username signature - denying connection.";
sendConnectionDeniedPacket("Insufficient data", senderSockAddr);
sendConnectionDeniedPacket("Insufficient data", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
}
}
@ -402,7 +413,8 @@ bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByt
if (username.isEmpty()) {
qDebug() << "Connect request denied - no username provided.";
sendConnectionDeniedPacket("No username provided", senderSockAddr);
sendConnectionDeniedPacket("No username provided", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
return false;
}
@ -416,7 +428,8 @@ bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByt
}
} else {
qDebug() << "Connect request denied for user" << username << "- not in allowed users list.";
sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr);
sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::NotAuthorized);
return false;
}
@ -452,7 +465,8 @@ bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteA
// deny connection from this user
qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection.";
sendConnectionDeniedPacket("Too many connected users.", senderSockAddr);
sendConnectionDeniedPacket("Too many connected users.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::TooManyUsers);
return false;
}
@ -516,16 +530,20 @@ void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) {
}
}
void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr) {
void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
DomainHandler::ConnectionRefusedReason reasonCode) {
// this is an agent and we've decided we won't let them connect - send them a packet to deny connection
QByteArray utfString = reason.toUtf8();
quint16 payloadSize = utfString.size();
// setup the DomainConnectionDenied packet
auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, payloadSize + sizeof(payloadSize));
auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied,
payloadSize + sizeof(payloadSize) + sizeof(uint8_t));
// pack in the reason the connection was denied (the client displays this)
if (payloadSize > 0) {
uint8_t reasonCodeWire = (uint8_t)reasonCode;
connectionDeniedPacket->writePrimitive(reasonCodeWire);
connectionDeniedPacket->writePrimitive(payloadSize);
connectionDeniedPacket->write(utfString);
}

View file

@ -19,6 +19,8 @@
#include <QtCore/QObject>
#include <QtNetwork/QNetworkReply>
#include <DomainHandler.h>
#include <NLPacket.h>
#include <Node.h>
#include <UUIDHasher.h>
@ -74,7 +76,8 @@ private:
const HifiSockAddr& senderSockAddr);
void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr);
void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr);
void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown);
void pingPunchForConnectingPeer(const SharedNetworkPeer& peer);

View file

@ -303,6 +303,31 @@ const QString FULL_AUTOMATIC_NETWORKING_VALUE = "full";
const QString IP_ONLY_AUTOMATIC_NETWORKING_VALUE = "ip";
const QString DISABLED_AUTOMATIC_NETWORKING_VALUE = "disabled";
bool DomainServer::packetVersionMatch(const udt::Packet& packet) {
PacketType headerType = NLPacket::typeInHeader(packet);
PacketVersion headerVersion = NLPacket::versionInHeader(packet);
auto nodeList = DependencyManager::get<LimitedNodeList>();
// This implements a special case that handles OLD clients which don't know how to negotiate matching
// protocol versions. We know these clients will sent DomainConnectRequest with older versions. We also
// know these clients will show a warning dialog if they get an EntityData with a protocol version they
// don't understand, so we can send them an empty EntityData with our latest version and they will
// warn the user that the protocol is not compatible
if (headerType == PacketType::DomainConnectRequest &&
headerVersion < static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions)) {
auto packetWithBadVersion = NLPacket::create(PacketType::EntityData);
nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr());
return false;
}
// let the normal nodeList implementation handle all other packets.
return nodeList->isPacketVerified(packet);
}
void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
const QString CUSTOM_LOCAL_PORT_OPTION = "metaverse.local_port";
@ -376,6 +401,9 @@ void DomainServer::setupNodeListAndAssignments(const QUuid& sessionUUID) {
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
// set a custum packetVersionMatch as the verify packet operator for the udt::Socket
nodeList->setPacketFilterOperator(&DomainServer::packetVersionMatch);
}
const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";

View file

@ -99,6 +99,8 @@ private:
void optionallyGetTemporaryName(const QStringList& arguments);
static bool packetVersionMatch(const udt::Packet& packet);
bool resetAccountManagerAccessToken();
void setupAutomaticNetworking();

View file

@ -19,6 +19,16 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c
if (isConnectRequest) {
dataStream >> newHeader.connectUUID;
// Read out the protocol version signature from the connect message
char* rawBytes;
uint length;
dataStream.readBytes(rawBytes, length);
newHeader.protocolVersion = QByteArray(rawBytes, length);
// NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator.
delete[] rawBytes;
}
dataStream >> newHeader.nodeType

View file

@ -28,6 +28,8 @@ public:
HifiSockAddr senderSockAddr;
QList<NodeType_t> interestList;
QString placeName;
QByteArray protocolVersion;
};

View file

@ -16,8 +16,10 @@
{ "from": "Hydra.L0", "to": "Standard.Back" },
{ "from": "Hydra.R0", "to": "Standard.Start" },
{ "from": [ "Hydra.L1", "Hydra.L2", "Hydra.L3", "Hydra.L4" ], "to": "Standard.LeftPrimaryThumb" },
{ "from": [ "Hydra.R1", "Hydra.R2", "Hydra.R3", "Hydra.R4" ], "to": "Standard.RightPrimaryThumb" },
{ "from": [ "Hydra.L1", "Hydra.L3" ], "to": "Standard.LeftPrimaryThumb" },
{ "from": [ "Hydra.R1", "Hydra.R3" ], "to": "Standard.RightPrimaryThumb" },
{ "from": [ "Hydra.R2", "Hydra.R4" ], "to": "Standard.RightSecondaryThumb" },
{ "from": [ "Hydra.L2", "Hydra.L4" ], "to": "Standard.LeftSecondaryThumb" },
{ "from": "Hydra.LeftHand", "to": "Standard.LeftHand" },
{ "from": "Hydra.RightHand", "to": "Standard.RightHand" }

View file

@ -1,7 +1,7 @@
{
"name": "Neuron to Standard",
"channels": [
{ "from": "Hydra.LeftHand", "to": "Standard.LeftHand" },
{ "from": "Hydra.RightHand", "to": "Standard.RightHand" }
{ "from": "Neuron.LeftHand", "to": "Standard.LeftHand" },
{ "from": "Neuron.RightHand", "to": "Standard.RightHand" }
]
}

View file

@ -1,24 +1,26 @@
{
"name": "Vive to Standard",
"channels": [
{ "from": "Vive.LY", "when": "Vive.LS", "filters": ["invert" ,{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LY" },
{ "from": "Vive.LX", "when": "Vive.LS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.LX" },
{ "from": "Vive.LY", "when": "Vive.LSOuter", "filters": ["invert"], "to": "Standard.LY" },
{ "from": "Vive.LX", "when": "Vive.LSOuter", "to": "Standard.LX" },
{ "from": "Vive.LT", "to": "Standard.LT" },
{ "from": "Vive.LeftGrip", "to": "Standard.LB" },
{ "from": "Vive.LS", "to": "Standard.LS" },
{ "from": "Vive.LSTouch", "to": "Standard.LSTouch" },
{ "from": "Vive.RY", "when": "Vive.RS", "filters": ["invert", { "type": "deadZone", "min": 0.6 }], "to": "Standard.RY" },
{ "from": "Vive.RX", "when": "Vive.RS", "filters": [{ "type": "deadZone", "min": 0.6 }], "to": "Standard.RX" },
{ "from": "Vive.RY", "when": "Vive.RSOuter", "filters": ["invert"], "to": "Standard.RY" },
{ "from": "Vive.RX", "when": "Vive.RSOuter", "to": "Standard.RX" },
{ "from": "Vive.RT", "to": "Standard.RT" },
{ "from": "Vive.RightGrip", "to": "Standard.RB" },
{ "from": "Vive.RS", "to": "Standard.RS" },
{ "from": "Vive.RSTouch", "to": "Standard.RSTouch" },
{ "from": "Vive.LeftApplicationMenu", "to": "Standard.Back" },
{ "from": "Vive.RightApplicationMenu", "to": "Standard.Start" },
{ "from": "Vive.LSCenter", "to": "Standard.LeftPrimaryThumb" },
{ "from": "Vive.LeftApplicationMenu", "to": "Standard.LeftSecondaryThumb" },
{ "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" },
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand" },
{ "from": "Vive.RightHand", "to": "Standard.RightHand" }

View file

@ -630,6 +630,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
connect(&domainHandler, &DomainHandler::resetting, nodeList.data(), &NodeList::resetDomainServerCheckInVersion);
connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused);
// update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one
const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND;
@ -652,7 +654,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated);
connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID);
connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID);
connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset);
connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, this, &Application::limitOfSilentDomainCheckInsReached);
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch);
// connect to appropriate slots on AccountManager
@ -1062,6 +1064,24 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
firstRun.set(false);
}
void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) {
switch (static_cast<DomainHandler::ConnectionRefusedReason>(reasonCode)) {
case DomainHandler::ConnectionRefusedReason::ProtocolMismatch:
notifyPacketVersionMismatch();
break;
case DomainHandler::ConnectionRefusedReason::TooManyUsers:
case DomainHandler::ConnectionRefusedReason::Unknown: {
QString message = "Unable to connect to the location you are visiting.\n";
message += reasonMessage;
OffscreenUi::warning("", message);
break;
}
default:
// nothing to do.
break;
}
}
QString Application::getUserAgent() {
if (QThread::currentThread() != thread()) {
QString userAgent;
@ -4572,6 +4592,17 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const {
Physics::setSessionUUID(sessionUUID);
}
// If we're not getting anything back from the domain server checkin, it might be that the domain speaks an
// older version of the DomainConnectRequest protocol. We will attempt to send and older version of DomainConnectRequest.
// We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we
// need to be downgraded to talk to it).
void Application::limitOfSilentDomainCheckInsReached() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->downgradeDomainServerCheckInVersion(); // attempt to use an older domain checkin version
nodeList->reset();
}
bool Application::askToSetAvatarUrl(const QString& url) {
QUrl realUrl(url);
if (realUrl.isLocalFile()) {

View file

@ -261,6 +261,10 @@ public slots:
void resetSensors(bool andReload = false);
void setActiveFaceTracker() const;
#if (PR_BUILD || DEV_BUILD)
void sendWrongProtocolVersionsSignature(bool checked) { ::sendWrongProtocolVersionsSignature(checked); }
#endif
#ifdef HAVE_IVIEWHMD
void setActiveEyeTracker();
void calibrateEyeTracker1Point();
@ -314,6 +318,8 @@ private slots:
bool displayAvatarAttachmentConfirmationDialog(const QString& name) const;
void setSessionUUID(const QUuid& sessionUUID) const;
void limitOfSilentDomainCheckInsReached();
void domainChanged(const QString& domainHostname);
void updateWindowTitle() const;
void nodeAdded(SharedNodePointer node) const;
@ -322,6 +328,7 @@ private slots:
static void packetSent(quint64 length);
void updateDisplayMode();
void updateInputModes();
void domainConnectionRefused(const QString& reasonMessage, int reason);
private:
static void initDisplay();

View file

@ -545,6 +545,13 @@ Menu::Menu() {
addActionToQMenuAndActionHash(networkMenu, MenuOption::BandwidthDetails, 0,
dialogsManager.data(), SLOT(bandwidthDetails()));
#if (PR_BUILD || DEV_BUILD)
addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false,
qApp, SLOT(sendWrongProtocolVersionsSignature(bool)));
#endif
// Developer > Timing >>>
MenuWrapper* timingMenu = developerMenu->addMenu("Timing");

View file

@ -167,6 +167,7 @@ namespace MenuOption {
const QString RunTimingTests = "Run Timing Tests";
const QString ScriptEditor = "Script Editor...";
const QString ScriptedMotorControl = "Enable Scripted Motor Control";
const QString SendWrongProtocolVersion = "Send wrong protocol version";
const QString SetHomeLocation = "Set Home Location";
const QString ShowDSConnectTable = "Show Domain Connection Timing";
const QString ShowBordersEntityNodes = "Show Entity Nodes";

View file

@ -855,7 +855,7 @@ void MyAvatar::loadData() {
setDisplayName(settings.value("displayName").toString());
setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString());
setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool());
setSnapTurn(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool());
setClearOverlayWhenDriving(settings.value("clearOverlayWhenDriving", _clearOverlayWhenDriving).toBool());
settings.endGroup();

View file

@ -45,7 +45,7 @@ public slots:
signals:
void domainChanged(const QString& domainHostname);
void svoImportRequested(const QString& url);
void domainConnectionRefused(const QString& reason);
void domainConnectionRefused(const QString& reasonMessage, int reasonCode);
private slots:
WebWindowClass* doCreateWebWindow(const QString& title, const QString& url, int width, int height);

View file

@ -43,6 +43,8 @@ namespace controller {
LEFT_SECONDARY_THUMB_TOUCH,
LS_TOUCH,
LEFT_THUMB_UP,
LS_CENTER,
LS_OUTER,
RIGHT_PRIMARY_THUMB,
RIGHT_SECONDARY_THUMB,
@ -50,6 +52,8 @@ namespace controller {
RIGHT_SECONDARY_THUMB_TOUCH,
RS_TOUCH,
RIGHT_THUMB_UP,
RS_CENTER,
RS_OUTER,
LEFT_PRIMARY_INDEX,
LEFT_SECONDARY_INDEX,

View file

@ -103,7 +103,6 @@ void DomainHandler::hardReset() {
_sockAddr.clear();
_hasCheckedForAccessToken = false;
_domainConnectionRefusals.clear();
// clear any pending path we may have wanted to ask the previous DS about
_pendingPath.clear();
@ -142,6 +141,9 @@ void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const
// set the new hostname
_hostname = hostname;
// FIXME - is this the right place???
_domainConnectionRefusals.clear();
qCDebug(networking) << "Updated domain hostname to" << _hostname;
// re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname
@ -349,34 +351,58 @@ void DomainHandler::processICEResponsePacket(QSharedPointer<ReceivedMessage> mes
}
}
bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) {
switch (reasonCode) {
case ConnectionRefusedReason::LoginError:
case ConnectionRefusedReason::NotAuthorized:
return true;
default:
case ConnectionRefusedReason::Unknown:
case ConnectionRefusedReason::ProtocolMismatch:
case ConnectionRefusedReason::TooManyUsers:
return false;
}
return false;
}
void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message) {
// Read deny reason from packet
uint8_t reasonCodeWire;
message->readPrimitive(&reasonCodeWire);
ConnectionRefusedReason reasonCode = static_cast<ConnectionRefusedReason>(reasonCodeWire);
quint16 reasonSize;
message->readPrimitive(&reasonSize);
QString reason = QString::fromUtf8(message->readWithoutCopy(reasonSize));
auto reasonText = message->readWithoutCopy(reasonSize);
QString reasonMessage = QString::fromUtf8(reasonText);
// output to the log so the user knows they got a denied connection request
// and check and signal for an access token so that we can make sure they are logged in
qCWarning(networking) << "The domain-server denied a connection request: " << reason;
qCWarning(networking) << "Make sure you are logged in.";
qCWarning(networking) << "The domain-server denied a connection request: " << reasonMessage;
if (!_domainConnectionRefusals.contains(reason)) {
_domainConnectionRefusals.append(reason);
emit domainConnectionRefused(reason);
if (!_domainConnectionRefusals.contains(reasonMessage)) {
_domainConnectionRefusals.append(reasonMessage);
emit domainConnectionRefused(reasonMessage, (int)reasonCode);
}
auto accountManager = DependencyManager::get<AccountManager>();
if (!_hasCheckedForAccessToken) {
accountManager->checkAndSignalForAccessToken();
_hasCheckedForAccessToken = true;
}
// Some connection refusal reasons imply that a login is required. If so, suggest a new login
if (reasonSuggestsLogin(reasonCode)) {
qCWarning(networking) << "Make sure you are logged in.";
static const int CONNECTION_DENIALS_FOR_KEYPAIR_REGEN = 3;
if (!_hasCheckedForAccessToken) {
accountManager->checkAndSignalForAccessToken();
_hasCheckedForAccessToken = true;
}
// force a re-generation of key-pair after CONNECTION_DENIALS_FOR_KEYPAIR_REGEN failed connection attempts
if (++_connectionDenialsSinceKeypairRegen >= CONNECTION_DENIALS_FOR_KEYPAIR_REGEN) {
accountManager->generateNewUserKeypair();
_connectionDenialsSinceKeypairRegen = 0;
static const int CONNECTION_DENIALS_FOR_KEYPAIR_REGEN = 3;
// force a re-generation of key-pair after CONNECTION_DENIALS_FOR_KEYPAIR_REGEN failed connection attempts
if (++_connectionDenialsSinceKeypairRegen >= CONNECTION_DENIALS_FOR_KEYPAIR_REGEN) {
accountManager->generateNewUserKeypair();
_connectionDenialsSinceKeypairRegen = 0;
}
}
}

View file

@ -84,6 +84,15 @@ public:
bool isSocketKnown() const { return !_sockAddr.getAddress().isNull(); }
void softReset();
enum class ConnectionRefusedReason : uint8_t {
Unknown,
ProtocolMismatch,
LoginError,
NotAuthorized,
TooManyUsers
};
public slots:
void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid());
void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id);
@ -115,9 +124,10 @@ signals:
void settingsReceived(const QJsonObject& domainSettingsObject);
void settingsReceiveFail();
void domainConnectionRefused(QString reason);
void domainConnectionRefused(QString reasonMessage, int reason);
private:
bool reasonSuggestsLogin(ConnectionRefusedReason reasonCode);
void sendDisconnectPacket();
void hardReset();

View file

@ -221,6 +221,10 @@ public:
void setConnectionMaxBandwidth(int maxBandwidth) { _nodeSocket.setConnectionMaxBandwidth(maxBandwidth); }
void setPacketFilterOperator(udt::PacketFilterOperator filterOperator) { _nodeSocket.setPacketFilterOperator(filterOperator); }
bool packetVersionMatch(const udt::Packet& packet);
bool isPacketVerified(const udt::Packet& packet);
public slots:
void reset();
void eraseAllNodes();
@ -269,8 +273,6 @@ protected:
void setLocalSocket(const HifiSockAddr& sockAddr);
bool isPacketVerified(const udt::Packet& packet);
bool packetVersionMatch(const udt::Packet& packet);
bool packetSourceAndHashMatch(const udt::Packet& packet);
void processSTUNResponse(std::unique_ptr<udt::BasePacket> packet);

View file

@ -24,8 +24,8 @@ int NLPacket::maxPayloadSize(PacketType type, bool isPartOfMessage) {
return Packet::maxPayloadSize(isPartOfMessage) - NLPacket::localHeaderSize(type);
}
std::unique_ptr<NLPacket> NLPacket::create(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage) {
auto packet = std::unique_ptr<NLPacket>(new NLPacket(type, size, isReliable, isPartOfMessage));
std::unique_ptr<NLPacket> NLPacket::create(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage, PacketVersion version) {
auto packet = std::unique_ptr<NLPacket>(new NLPacket(type, size, isReliable, isPartOfMessage, version));
packet->open(QIODevice::ReadWrite);
@ -61,10 +61,10 @@ std::unique_ptr<NLPacket> NLPacket::createCopy(const NLPacket& other) {
return std::unique_ptr<NLPacket>(new NLPacket(other));
}
NLPacket::NLPacket(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage) :
NLPacket::NLPacket(PacketType type, qint64 size, bool isReliable, bool isPartOfMessage, PacketVersion version) :
Packet((size == -1) ? -1 : NLPacket::localHeaderSize(type) + size, isReliable, isPartOfMessage),
_type(type),
_version(versionForPacketType(type))
_version((version == 0) ? versionForPacketType(type) : version)
{
adjustPayloadStartAndCapacity(NLPacket::localHeaderSize(_type));

View file

@ -38,7 +38,7 @@ public:
sizeof(PacketType) + sizeof(PacketVersion) + NUM_BYTES_RFC4122_UUID + NUM_BYTES_MD5_HASH;
static std::unique_ptr<NLPacket> create(PacketType type, qint64 size = -1,
bool isReliable = false, bool isPartOfMessage = false);
bool isReliable = false, bool isPartOfMessage = false, PacketVersion version = 0);
static std::unique_ptr<NLPacket> fromReceivedPacket(std::unique_ptr<char[]> data, qint64 size,
const HifiSockAddr& senderSockAddr);
@ -73,7 +73,7 @@ public:
protected:
NLPacket(PacketType type, qint64 size = -1, bool forceReliable = false, bool isPartOfMessage = false);
NLPacket(PacketType type, qint64 size = -1, bool forceReliable = false, bool isPartOfMessage = false, PacketVersion version = 0);
NLPacket(std::unique_ptr<char[]> data, qint64 size, const HifiSockAddr& senderSockAddr);
NLPacket(const NLPacket& other);

View file

@ -292,7 +292,8 @@ void NodeList::sendDomainServerCheckIn() {
return;
}
auto domainPacket = NLPacket::create(domainPacketType);
auto packetVersion = (domainPacketType == PacketType::DomainConnectRequest) ? _domainConnectRequestVersion : 0;
auto domainPacket = NLPacket::create(domainPacketType, -1, false, false, packetVersion);
QDataStream packetStream(domainPacket.get());
@ -312,12 +313,20 @@ void NodeList::sendDomainServerCheckIn() {
// pack the connect UUID for this connect request
packetStream << connectUUID;
// include the protocol version signature in our connect request
if (_domainConnectRequestVersion >= static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions)) {
QByteArray protocolVersionSig = protocolVersionsSignature();
packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size());
}
}
// pack our data to send to the domain-server including
// the hostname information (so the domain-server can see which place name we came in on)
packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList()
<< DependencyManager::get<AddressManager>()->getPlaceName();
packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
if (_domainConnectRequestVersion >= static_cast<PacketVersion>(DomainConnectRequestVersion::HasHostname)) {
packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
}
if (!_domainHandler.isConnected()) {
DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();

View file

@ -68,6 +68,9 @@ public:
void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
/// downgrades the DomainConnnectRequest PacketVersion to attempt to probe for older domain servers
void downgradeDomainServerCheckInVersion() { _domainConnectRequestVersion--; }
public slots:
void reset();
void sendDomainServerCheckIn();
@ -85,6 +88,9 @@ public slots:
void processICEPingPacket(QSharedPointer<ReceivedMessage> message);
void resetDomainServerCheckInVersion()
{ _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); }
signals:
void limitOfSilentDomainCheckInsReached();
void receivedDomainServerList();
@ -123,6 +129,8 @@ private:
HifiSockAddr _assignmentServerSocket;
bool _isShuttingDown { false };
QTimer _keepAlivePingTimer;
PacketVersion _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest);
};
#endif // hifi_NodeList_h

View file

@ -12,7 +12,9 @@
#include "PacketHeaders.h"
#include <math.h>
#include <mutex>
#include <QtCore/QDataStream>
#include <QtCore/QDebug>
#include <QtCore/QMetaEnum>
@ -47,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::EntityAdd:
case PacketType::EntityEdit:
case PacketType::EntityData:
return VERSION_ENTITIES_NO_FLY_ZONES;
return VERSION_ENTITIES_MORE_SHAPES;
case PacketType::AvatarIdentity:
case PacketType::AvatarData:
case PacketType::BulkAvatarData:
@ -60,9 +62,13 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::AssetUpload:
// Removal of extension from Asset requests
return 18;
case PacketType::DomainConnectionDenied:
return static_cast<PacketVersion>(DomainConnectionDeniedVersion::IncludesReasonCode);
case PacketType::DomainConnectRequest:
// addition of referring hostname information
return 18;
return static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions);
default:
return 17;
}
@ -82,3 +88,36 @@ QDebug operator<<(QDebug debug, const PacketType& type) {
debug.nospace().noquote() << (uint8_t) type << " (" << typeName << ")";
return debug.space();
}
#if (PR_BUILD || DEV_BUILD)
static bool sendWrongProtocolVersion = false;
void sendWrongProtocolVersionsSignature(bool sendWrongVersion) {
sendWrongProtocolVersion = sendWrongVersion;
}
#endif
QByteArray protocolVersionsSignature() {
static QByteArray protocolVersionSignature;
static std::once_flag once;
std::call_once(once, [&] {
QByteArray buffer;
QDataStream stream(&buffer, QIODevice::WriteOnly);
uint8_t numberOfProtocols = static_cast<uint8_t>(PacketType::LAST_PACKET_TYPE) + 1;
stream << numberOfProtocols;
for (uint8_t packetType = 0; packetType < numberOfProtocols; packetType++) {
uint8_t packetTypeVersion = static_cast<uint8_t>(versionForPacketType(static_cast<PacketType>(packetType)));
stream << packetTypeVersion;
}
QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(buffer);
protocolVersionSignature = hash.result();
});
#if (PR_BUILD || DEV_BUILD)
if (sendWrongProtocolVersion) {
return QByteArray("INCORRECTVERSION"); // only for debugging version checking
}
#endif
return protocolVersionSignature;
}

View file

@ -61,7 +61,7 @@ public:
AssignmentClientStatus,
NoisyMute,
AvatarIdentity,
AvatarBillboard,
TYPE_UNUSED_1,
DomainConnectRequest,
DomainServerRequireDTLS,
NodeJsonStats,
@ -94,7 +94,8 @@ public:
ICEServerHeartbeatDenied,
AssetMappingOperation,
AssetMappingOperationReply,
ICEServerHeartbeatACK
ICEServerHeartbeatACK,
LAST_PACKET_TYPE = ICEServerHeartbeatACK
};
};
@ -109,6 +110,11 @@ extern const QSet<PacketType> NON_SOURCED_PACKETS;
extern const QSet<PacketType> RELIABLE_PACKETS;
PacketVersion versionForPacketType(PacketType packetType);
QByteArray protocolVersionsSignature(); /// returns a unqiue signature for all the current protocols
#if (PR_BUILD || DEV_BUILD)
void sendWrongProtocolVersionsSignature(bool sendWrongVersion); /// for debugging version negotiation
#endif
uint qHash(const PacketType& key, uint seed);
QDebug operator<<(QDebug debug, const PacketType& type);
@ -172,6 +178,7 @@ const PacketVersion VERSION_ENTITITES_HAVE_COLLISION_MASK = 55;
const PacketVersion VERSION_ATMOSPHERE_REMOVED = 56;
const PacketVersion VERSION_LIGHT_HAS_FALLOFF_RADIUS = 57;
const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58;
const PacketVersion VERSION_ENTITIES_MORE_SHAPES = 59;
enum class AvatarMixerPacketVersion : PacketVersion {
TranslationSupport = 17,
@ -180,4 +187,15 @@ enum class AvatarMixerPacketVersion : PacketVersion {
AbsoluteSixByteRotations
};
enum class DomainConnectRequestVersion : PacketVersion {
NoHostname = 17,
HasHostname,
HasProtocolVersions
};
enum class DomainConnectionDeniedVersion : PacketVersion {
ReasonMessageOnly = 17,
IncludesReasonCode
};
#endif // hifi_PacketHeaders_h

View file

@ -19,7 +19,7 @@
class ScriptAudioInjector : public QObject {
Q_OBJECT
Q_PROPERTY(bool isPlaying READ isPlaying)
Q_PROPERTY(bool playing READ isPlaying)
Q_PROPERTY(float loudness READ getLoudness)
Q_PROPERTY(AudioInjectorOptions options WRITE setOptions READ getOptions)
public:

View file

@ -282,7 +282,22 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u
for (uint32_t i = 0; i < vr::k_unControllerStateAxisCount; i++) {
handleAxisEvent(deltaTime, i, controllerState.rAxis[i].x, controllerState.rAxis[i].y, isLeftHand);
}
}
// pseudo buttons the depend on both of the above for-loops
partitionTouchpad(controller::LS, controller::LX, controller::LY, controller::LS_CENTER, controller::LS_OUTER);
partitionTouchpad(controller::RS, controller::RX, controller::RY, controller::RS_CENTER, controller::RS_OUTER);
}
}
}
void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPseudoButton, int outerPseudoButton) {
// Populate the L/RS_CENTER/OUTER pseudo buttons, corresponding to a partition of the L/RS space based on the X/Y values.
const float CENTER_DEADBAND = 0.6f;
if (_buttonPressedMap.find(sButton) != _buttonPressedMap.end()) {
float absX = abs(_axisStateMap[xAxis]);
float absY = abs(_axisStateMap[yAxis]);
bool isCenter = (absX < CENTER_DEADBAND) && (absY < CENTER_DEADBAND); // square deadband
_buttonPressedMap.insert(isCenter ? centerPseudoButton : outerPseudoButton);
}
}
@ -443,6 +458,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI
// touch pad press
makePair(LS, "LS"),
makePair(RS, "RS"),
// Differentiate where we are in the touch pad click
makePair(LS_CENTER, "LSCenter"),
makePair(LS_OUTER, "LSOuter"),
makePair(RS_CENTER, "RSCenter"),
makePair(RS_OUTER, "RSOuter"),
// triggers
makePair(LT, "LT"),

View file

@ -61,6 +61,7 @@ private:
void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand);
void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat,
const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand);
void ViveControllerManager::InputDevice::partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int outerPseudoButton);
class FilteredStick {
public:

View file

@ -110,7 +110,7 @@ function updateBirds(deltaTime) {
}
// Check whether to play a chirp
if (playSounds && (!birds[i].audioId || !birds[i].audioId.isPlaying) && (Math.random() < ((numPlaying > 0) ? SOUND_PROBABILITY / numPlaying : SOUND_PROBABILITY))) {
if (playSounds && (!birds[i].audioId || !birds[i].audioId.playing) && (Math.random() < ((numPlaying > 0) ? SOUND_PROBABILITY / numPlaying : SOUND_PROBABILITY))) {
var options = {
position: properties.position,
volume: BIRD_MASTER_VOLUME
@ -129,7 +129,7 @@ function updateBirds(deltaTime) {
} else if (birds[i].audioId) {
// If bird is playing a chirp
if (!birds[i].audioId.isPlaying) {
if (!birds[i].audioId.playing) {
Entities.editEntity(birds[i].entityId, { dimensions: { x: BIRD_SIZE, y: BIRD_SIZE, z: BIRD_SIZE }});
numPlaying--;
}

View file

@ -146,7 +146,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n
return;
}
that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC
if (!that.injector.isPlaying) { // Subtle: a looping sound will not check playbackGap.
if (!that.injector.playing) { // Subtle: a looping sound will not check playbackGap.
if (repeat()) { // WAITING => PLAYING
// Setup next play just once, now. Changes won't be looked at while we wait.
that.playAfter = randomizedNextPlay();
@ -208,7 +208,7 @@ function updateAllEntityData() { // A fast update of all entities we know about.
stats.entities++;
if (datum.url) {
stats.sounds++;
if (datum.injector && datum.injector.isPlaying) {
if (datum.injector && datum.injector.playing) {
stats.playing++;
}
}

View file

@ -395,7 +395,7 @@ function update(deltaTime) {
Overlays.editOverlay(descriptionText, { position: textOverlayPosition() });
// if the reticle is up then we may need to play the next muzak
if (currentMuzakInjector && !currentMuzakInjector.isPlaying) {
if (currentMuzakInjector && !currentMuzakInjector.playing) {
playNextMuzak();
}
}

View file

@ -21,7 +21,7 @@ var CHATTER_VOLUME = 0.20
var EXTRA_VOLUME = 0.25
function playChatter() {
if (chatter.downloaded && !chatter.isPlaying) {
if (chatter.downloaded && !chatter.playing) {
Audio.playSound(chatter, { loop: true, volume: CHATTER_VOLUME });
}
}
@ -31,7 +31,7 @@ chatter.ready.connect(playChatter);
var currentInjector = null;
function playRandomExtras() {
if ((!currentInjector || !currentInjector.isPlaying) && (Math.random() < (1.0 / 1800.0))) {
if ((!currentInjector || !currentInjector.playing) && (Math.random() < (1.0 / 1800.0))) {
// play a random extra sound about every 30s
currentInjector = Audio.playSound(
extras[Math.floor(Math.random() * extras.length)],

View file

@ -106,7 +106,7 @@ function checkHands(deltaTime) {
var chord = Controller.getValue(chordTrigger);
if (volume > 1.0) volume = 1.0;
if ((chord > 0.1) && audioInjector && audioInjector.isPlaying) {
if ((chord > 0.1) && audioInjector && audioInjector.playing) {
// If chord finger trigger pulled, stop current chord
print("stopping chord because cord trigger pulled");
audioInjector.stop();
@ -160,7 +160,7 @@ function checkHands(deltaTime) {
}
function stopAudio(killInjector) {
if (audioInjector && audioInjector.isPlaying) {
if (audioInjector && audioInjector.playing) {
print("stopped sound");
audioInjector.stop();
}
@ -212,4 +212,3 @@ function scriptEnding() {
Script.update.connect(checkHands);
Script.scriptEnding.connect(scriptEnding);
Controller.keyPressEvent.connect(keyPressEvent);

View file

@ -340,7 +340,7 @@ function moveRats() {
var metaRat = getMetaRatByRat(rat);
if (metaRat !== undefined) {
if (metaRat.injector !== undefined) {
if (metaRat.injector.isPlaying === true) {
if (metaRat.injector.playing === true) {
metaRat.injector.options = {
loop: true,
position: ratPosition

View file

@ -186,7 +186,7 @@
this.move = function(mouseEvent) {
this.updatePosition(mouseEvent);
if (this.moveInjector === null || !this.moveInjector.isPlaying) {
if (this.moveInjector === null || !this.moveInjector.playing) {
this.playMoveSound();
}
};
@ -245,7 +245,7 @@
}
}
if (this.turnInjector === null || !this.turnInjector.isPlaying) {
if (this.turnInjector === null || !this.turnInjector.playing) {
this.playTurnSound();
}
};

View file

@ -75,7 +75,7 @@ function maybePlaySound(deltaTime) {
//print("number playing = " + numPlaying);
}
for (var i = 0; i < playing.length; i++) {
if (!playing[i].audioId.isPlaying) {
if (!playing[i].audioId.playing) {
Entities.deleteEntity(playing[i].entityId);
if (useLights) {
Entities.deleteEntity(playing[i].lightId);

View file

@ -378,7 +378,7 @@ function update(deltaTime) {
Overlays.editOverlay(descriptionText, { position: textOverlayPosition() });
// if the reticle is up then we may need to play the next muzak
if (currentMuzakInjector && !currentMuzakInjector.isPlaying) {
if (currentMuzakInjector && !currentMuzakInjector.playing) {
playNextMuzak();
}
}

View file

@ -47,7 +47,7 @@ function checkSound(deltaTime) {
volume: 1.0,
loop: false } );
started = true;
} else if (!soundPlaying.isPlaying) {
} else if (!soundPlaying.playing) {
soundPlaying.restart();
started = true;
}
@ -93,4 +93,3 @@ function scriptEnding() {
// Connect a call back that happens every frame
Script.scriptEnding.connect(scriptEnding);
Script.update.connect(checkSound);

View file

@ -22,10 +22,7 @@
// (For now, the thumb buttons on both controllers are always on.)
// When over a HUD element, the reticle is shown where the active hand controller beam intersects the HUD.
// Otherwise, the active hand controller shows a red ball where a click will act.
//
// Bugs:
// On Windows, the upper left corner of Interface must be in the upper left corner of the screen, and the title bar must be 50px high. (System bug.)
// While hardware mouse move switches to mouse move, hardware mouse click (without amove) does not.
// UTILITIES -------------
@ -269,76 +266,24 @@ function toggleHand() {
}
}
// Create clickMappings as needed, on demand.
var clickMappings = {}, clickMapping, clickMapToggle;
var hardware; // undefined
function checkHardware() {
var newHardware = Controller.Hardware.Hydra ? 'Hydra' : (Controller.Hardware.Vive ? 'Vive' : null); // not undefined
if (hardware === newHardware) {
return;
}
print('Setting mapping for new controller hardware:', newHardware);
if (clickMapToggle) {
clickMapToggle.setState(false);
}
hardware = newHardware;
if (clickMappings[hardware]) {
clickMapping = clickMappings[hardware];
} else {
clickMapping = Controller.newMapping(Script.resolvePath('') + '-click-' + hardware);
Script.scriptEnding.connect(clickMapping.disable);
function mapToAction(button, action) {
clickMapping.from(Controller.Hardware[hardware][button]).peek().to(Controller.Actions[action]);
}
function makeHandToggle(button, hand, optionalWhen) {
var whenThunk = optionalWhen || function () {
return true;
};
function maybeToggle() {
if (activeHand !== Controller.Standard[hand]) {
toggleHand();
}
var clickMapping = Controller.newMapping(Script.resolvePath('') + '-click');
Script.scriptEnding.connect(clickMapping.disable);
}
clickMapping.from(Controller.Hardware[hardware][button]).peek().when(whenThunk).to(maybeToggle);
}
function makeViveWhen(click, x, y) {
var viveClick = Controller.Hardware.Vive[click],
viveX = Controller.Standard[x], // Standard after filtering by mapping
viveY = Controller.Standard[y];
return function () {
var clickValue = Controller.getValue(viveClick);
var xValue = Controller.getValue(viveX);
var yValue = Controller.getValue(viveY);
return clickValue && !xValue && !yValue;
};
}
switch (hardware) {
case 'Hydra':
makeHandToggle('R3', 'RightHand');
makeHandToggle('L3', 'LeftHand');
mapToAction('R3', 'ReticleClick');
mapToAction('L3', 'ReticleClick');
mapToAction('R4', 'ContextMenu');
mapToAction('L4', 'ContextMenu');
break;
case 'Vive':
// When touchpad click is NOT treated as movement, treat as left click
makeHandToggle('RS', 'RightHand', makeViveWhen('RS', 'RX', 'RY'));
makeHandToggle('LS', 'LeftHand', makeViveWhen('LS', 'LX', 'LY'));
clickMapping.from(Controller.Hardware.Vive.RS).when(makeViveWhen('RS', 'RX', 'RY')).to(Controller.Actions.ReticleClick);
clickMapping.from(Controller.Hardware.Vive.LS).when(makeViveWhen('LS', 'LX', 'LY')).to(Controller.Actions.ReticleClick);
mapToAction('RightApplicationMenu', 'ContextMenu');
mapToAction('LeftApplicationMenu', 'ContextMenu');
break;
}
clickMappings[hardware] = clickMapping;
clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(Controller.Actions.ReticleClick);
clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(Controller.Actions.ReticleClick);
clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(Controller.Actions.ContextMenu);
clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(Controller.Actions.ContextMenu);
clickMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(function (on) {
if (on && (activeHand !== Controller.Standard.RightHand)) {
toggleHand();
}
clickMapToggle = new LatchedToggle(clickMapping.enable, clickMapping.disable);
clickMapToggle.setState(true);
}
checkHardware();
});
clickMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(function (on) {
if (on && (activeHand !== Controller.Standard.LeftHand)) {
toggleHand();
}
});
clickMapping.enable();
// VISUAL AID -----------
// Same properties as handControllerGrab search sphere
@ -415,8 +360,8 @@ function update() {
return turnOffVisualization();
}
var controllerPose = Controller.getPoseValue(activeHand);
// Vive is effectively invalid when not in HMD
if (!controllerPose.valid || ((hardware === 'Vive') && !HMD.active)) {
// Valid if any plugged-in hand controller is "on". (uncradled Hydra, green-lighted Vive...)
if (!controllerPose.valid) {
return turnOffVisualization();
} // Controller is cradled.
var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation),
@ -458,7 +403,6 @@ Script.scriptEnding.connect(function () {
var SETTINGS_CHANGE_RECHECK_INTERVAL = 10 * 1000; // milliseconds
function checkSettings() {
updateFieldOfView();
checkHardware();
}
checkSettings();
var settingsChecker = Script.setInterval(checkSettings, SETTINGS_CHANGE_RECHECK_INTERVAL);