mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-13 02:05:19 +02:00
Merge branch 'master' into feature/platform
This commit is contained in:
commit
cadcb8b7c3
26 changed files with 831 additions and 63 deletions
|
@ -82,6 +82,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
|
||||
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
|
||||
this, "handleOctreePacket");
|
||||
packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnership");
|
||||
|
||||
packetReceiver.registerListenerForTypes({
|
||||
PacketType::ReplicatedAvatarIdentity,
|
||||
|
@ -367,10 +368,13 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
|
|||
return;
|
||||
}
|
||||
|
||||
bool sendIdentity = false;
|
||||
if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
|
||||
AvatarData& avatar = nodeData->getAvatar();
|
||||
const QString& existingBaseDisplayName = nodeData->getAvatar().getSessionDisplayName();
|
||||
MixerAvatar& avatar = nodeData->getAvatar();
|
||||
bool sendIdentity = avatar.needsIdentityUpdate();
|
||||
if (sendIdentity) {
|
||||
nodeData->flagIdentityChange();
|
||||
}
|
||||
if (nodeData->getAvatarSessionDisplayNameMustChange()) {
|
||||
const QString& existingBaseDisplayName = avatar.getSessionDisplayName();
|
||||
if (!existingBaseDisplayName.isEmpty()) {
|
||||
SessionDisplayName existingDisplayName { existingBaseDisplayName };
|
||||
|
||||
|
@ -414,10 +418,11 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
|
|||
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
|
||||
// since this packet includes a change to either the skeleton model URL or the display name
|
||||
// it needs a new sequence number
|
||||
nodeData->getAvatar().pushIdentitySequenceNumber();
|
||||
avatar.pushIdentitySequenceNumber();
|
||||
|
||||
// tell node whose name changed about its new session display name or avatar.
|
||||
sendIdentityPacket(nodeData, node);
|
||||
avatar.setNeedsIdentityUpdate(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1123,6 +1128,16 @@ void AvatarMixer::entityChange() {
|
|||
_dirtyHeroStatus = true;
|
||||
}
|
||||
|
||||
void AvatarMixer::handleChallengeOwnership(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
if (senderNode->getType() == NodeType::Agent && senderNode->getLinkedData()) {
|
||||
auto clientData = static_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
|
||||
auto avatar = clientData->getAvatarSharedPointer();
|
||||
if (avatar) {
|
||||
avatar->handleChallengeResponse(message.data());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarMixer::aboutToFinish() {
|
||||
DependencyManager::destroy<ResourceManager>();
|
||||
DependencyManager::destroy<ResourceCacheSharedItems>();
|
||||
|
|
|
@ -65,6 +65,7 @@ private slots:
|
|||
void domainSettingsRequestComplete();
|
||||
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
|
||||
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void handleChallengeOwnership(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
void start();
|
||||
|
||||
private:
|
||||
|
|
|
@ -81,6 +81,10 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
|
|||
}
|
||||
assert(_packetQueue.empty());
|
||||
|
||||
if (_avatar) {
|
||||
_avatar->processCertifyEvents();
|
||||
}
|
||||
|
||||
return packetsProcessed;
|
||||
}
|
||||
|
||||
|
@ -200,6 +204,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
|||
if (traitType == AvatarTraits::SkeletonModelURL) {
|
||||
// special handling for skeleton model URL, since we need to make sure it is in the whitelist
|
||||
checkSkeletonURLAgainstWhitelist(slaveSharedData, sendingNode, packetTraitVersion);
|
||||
_avatar->fetchAvatarFST();
|
||||
}
|
||||
|
||||
anyTraitsChanged = true;
|
||||
|
|
|
@ -157,6 +157,11 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
|
|||
++simpleReceivedIt;
|
||||
}
|
||||
|
||||
if (bytesWritten > 0 && sendingAvatar->isCertifyFailed()) {
|
||||
// Resend identity packet if certification failed:
|
||||
sendingAvatar->setNeedsIdentityUpdate();
|
||||
}
|
||||
|
||||
// enumerate the received instanced trait versions
|
||||
auto instancedReceivedIt = lastReceivedVersions.instancedCBegin();
|
||||
while (instancedReceivedIt != lastReceivedVersions.instancedCEnd()) {
|
||||
|
|
345
assignment-client/src/avatars/MixerAvatar.cpp
Normal file
345
assignment-client/src/avatars/MixerAvatar.cpp
Normal file
|
@ -0,0 +1,345 @@
|
|||
//
|
||||
// MixerAvatar.cpp
|
||||
// assignment-client/src/avatars
|
||||
//
|
||||
// Created by Simon Walton April 2019
|
||||
// Copyright 2019 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 "MixerAvatar.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include <QCryptographicHash>
|
||||
#include <QApplication>
|
||||
|
||||
#include <ResourceManager.h>
|
||||
#include <NetworkAccessManager.h>
|
||||
#include <NetworkingConstants.h>
|
||||
#include <EntityItem.h>
|
||||
#include <EntityItemProperties.h>
|
||||
#include "ClientTraitsHandler.h"
|
||||
#include "AvatarLogging.h"
|
||||
|
||||
void MixerAvatar::fetchAvatarFST() {
|
||||
_verifyState = nonCertified;
|
||||
|
||||
_pendingEvent = false;
|
||||
|
||||
QUrl avatarURL = getSkeletonModelURL();
|
||||
if (avatarURL.isEmpty() || avatarURL.isLocalFile() || avatarURL.scheme() == "qrc") {
|
||||
// Not network FST.
|
||||
return;
|
||||
}
|
||||
_certificateIdFromURL.clear();
|
||||
_certificateIdFromFST.clear();
|
||||
_marketplaceIdFromURL.clear();
|
||||
_marketplaceIdFromFST.clear();
|
||||
auto resourceManager = DependencyManager::get<ResourceManager>();
|
||||
|
||||
// Match UUID + (optionally) URL cert
|
||||
static const QRegularExpression marketIdRegex{
|
||||
"^https://.*?highfidelity\\.com/api/.*?/commerce/entity_edition/([-0-9a-z]{36})(.*?certificate_id=([\\w/+%]+)|.*).*$"
|
||||
};
|
||||
auto marketIdMatch = marketIdRegex.match(avatarURL.toDisplayString());
|
||||
if (marketIdMatch.hasMatch()) {
|
||||
QMutexLocker certifyLocker(&_avatarCertifyLock);
|
||||
_marketplaceIdFromURL = marketIdMatch.captured(1);
|
||||
if (marketIdMatch.lastCapturedIndex() == 3) {
|
||||
_certificateIdFromURL = QUrl::fromPercentEncoding(marketIdMatch.captured(3).toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
ResourceRequest* fstRequest = resourceManager->createResourceRequest(this, avatarURL);
|
||||
if (fstRequest) {
|
||||
QMutexLocker certifyLocker(&_avatarCertifyLock);
|
||||
|
||||
_avatarRequest = fstRequest;
|
||||
_verifyState = requestingFST;
|
||||
connect(fstRequest, &ResourceRequest::finished, this, &MixerAvatar::fstRequestComplete);
|
||||
fstRequest->send();
|
||||
} else {
|
||||
qCDebug(avatars) << "Couldn't create FST request for" << avatarURL;
|
||||
_verifyState = error;
|
||||
}
|
||||
_needsIdentityUpdate = true;
|
||||
}
|
||||
|
||||
void MixerAvatar::fstRequestComplete() {
|
||||
ResourceRequest* fstRequest = static_cast<ResourceRequest*>(QObject::sender());
|
||||
QMutexLocker certifyLocker(&_avatarCertifyLock);
|
||||
if (fstRequest == _avatarRequest) {
|
||||
auto result = fstRequest->getResult();
|
||||
if (result != ResourceRequest::Success) {
|
||||
_verifyState = error;
|
||||
qCDebug(avatars) << "FST request for" << fstRequest->getUrl() << "failed:" << result;
|
||||
} else {
|
||||
_avatarFSTContents = fstRequest->getData();
|
||||
_verifyState = receivedFST;
|
||||
_pendingEvent = true;
|
||||
}
|
||||
_avatarRequest->deleteLater();
|
||||
_avatarRequest = nullptr;
|
||||
} else {
|
||||
qCDebug(avatars) << "Incorrect request for" << getDisplayName();
|
||||
}
|
||||
}
|
||||
|
||||
bool MixerAvatar::generateFSTHash() {
|
||||
if (_avatarFSTContents.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
QByteArray hashJson = canonicalJson(_avatarFSTContents);
|
||||
QCryptographicHash fstHash(QCryptographicHash::Sha256);
|
||||
fstHash.addData(hashJson);
|
||||
_certificateHash = fstHash.result();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MixerAvatar::validateFSTHash(const QString& publicKey) {
|
||||
// Guess we should refactor this stuff into a Authorization namespace ...
|
||||
return EntityItemProperties::verifySignature(publicKey, _certificateHash,
|
||||
QByteArray::fromBase64(_certificateIdFromFST.toUtf8()));
|
||||
}
|
||||
|
||||
QByteArray MixerAvatar::canonicalJson(const QString fstFile) {
|
||||
QStringList fstLines = fstFile.split("\n", QString::SkipEmptyParts);
|
||||
static const QString fstKeywordsReg {
|
||||
"(marketplaceID|itemDescription|itemCategories|itemArtist|itemLicenseUrl|limitedRun|itemName|"
|
||||
"filename|texdir|script|editionNumber|certificateID)"
|
||||
};
|
||||
QRegularExpression fstLineRegExp { QString("^\\s*") + fstKeywordsReg + "\\s*=\\s*(\\S.*)$" };
|
||||
QStringListIterator fstLineIter(fstLines);
|
||||
|
||||
QJsonObject certifiedItems;
|
||||
QStringList scripts;
|
||||
while (fstLineIter.hasNext()) {
|
||||
auto line = fstLineIter.next();
|
||||
auto lineMatch = fstLineRegExp.match(line);
|
||||
if (lineMatch.hasMatch()) {
|
||||
QString key = lineMatch.captured(1);
|
||||
if (key == "certificateID") {
|
||||
_certificateIdFromFST = lineMatch.captured(2);
|
||||
} else if (key == "itemDescription") {
|
||||
// Item description can be multiline - intermediate lines end in <CR>
|
||||
QString itemDesc = lineMatch.captured(2);
|
||||
while (itemDesc.endsWith('\r') && fstLineIter.hasNext()) {
|
||||
itemDesc += '\n' + fstLineIter.next();
|
||||
}
|
||||
certifiedItems[key] = QJsonValue(itemDesc);
|
||||
} else if (key == "limitedRun" || key == "editionNumber") {
|
||||
double value = lineMatch.captured(2).toDouble();
|
||||
if (value != 0.0) {
|
||||
certifiedItems[key] = QJsonValue(value);
|
||||
}
|
||||
} else if (key == "script") {
|
||||
scripts.append(lineMatch.captured(2).trimmed());
|
||||
} else {
|
||||
certifiedItems[key] = QJsonValue(lineMatch.captured(2));
|
||||
if (key == "marketplaceID") {
|
||||
_marketplaceIdFromFST = lineMatch.captured(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!scripts.empty()) {
|
||||
scripts.sort();
|
||||
certifiedItems["script"] = QJsonArray::fromStringList(scripts);
|
||||
}
|
||||
|
||||
QJsonDocument jsonDocCertifiedItems(certifiedItems);
|
||||
//Example working form:
|
||||
//return R"({"editionNumber":34,"filename":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/Hifi_Toon_Male_3.fbx","itemArtist":"EgyMax",
|
||||
//"itemCategories":"Avatars","itemDescription":"This is my first avatar. I hope you like it. More will come","itemName":"Bridger","limitedRun":-1,
|
||||
//"marketplaceID":"7f142fde-541a-4902-b33a-25fa89dfba21","texdir":"http://mpassets.highfidelity.com/7f142fde-541a-4902-b33a-25fa89dfba21-v1/Bridger/textures"})";
|
||||
return jsonDocCertifiedItems.toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
void MixerAvatar::ownerRequestComplete() {
|
||||
QMutexLocker certifyLocker(&_avatarCertifyLock);
|
||||
QNetworkReply* networkReply = static_cast<QNetworkReply*>(QObject::sender());
|
||||
|
||||
if (networkReply->error() == QNetworkReply::NoError) {
|
||||
_dynamicMarketResponse = networkReply->readAll();
|
||||
_verifyState = ownerResponse;
|
||||
_pendingEvent = true;
|
||||
} else {
|
||||
auto jsonData = QJsonDocument::fromJson(networkReply->readAll())["data"];
|
||||
if (!jsonData.isUndefined() && !jsonData.toObject()["message"].isUndefined()) {
|
||||
qCDebug(avatars) << "Owner lookup failed for" << getDisplayName() << ":"
|
||||
<< jsonData.toObject()["message"].toString();
|
||||
_verifyState = error;
|
||||
_pendingEvent = false;
|
||||
}
|
||||
}
|
||||
networkReply->deleteLater();
|
||||
}
|
||||
|
||||
void MixerAvatar::processCertifyEvents() {
|
||||
if (!_pendingEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMutexLocker certifyLocker(&_avatarCertifyLock);
|
||||
switch (_verifyState) {
|
||||
|
||||
case receivedFST:
|
||||
{
|
||||
generateFSTHash();
|
||||
if (_certificateIdFromFST.length() != 0) {
|
||||
QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
|
||||
bool staticVerification = validateFSTHash(marketplacePublicKey);
|
||||
_verifyState = staticVerification ? staticValidation : verificationFailed;
|
||||
|
||||
if (_verifyState == staticValidation) {
|
||||
static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
|
||||
auto& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
QNetworkRequest networkRequest;
|
||||
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
|
||||
requestURL.setPath(POP_MARKETPLACE_API);
|
||||
networkRequest.setUrl(requestURL);
|
||||
|
||||
QJsonObject request;
|
||||
request["certificate_id"] = _certificateIdFromFST;
|
||||
_verifyState = requestingOwner;
|
||||
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
|
||||
connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
|
||||
} else {
|
||||
_needsIdentityUpdate = true;
|
||||
_pendingEvent = false;
|
||||
qCDebug(avatars) << "Avatar" << getDisplayName() << "FAILED static certification";
|
||||
}
|
||||
} else { // FST doesn't have a certificate, so noncertified rather than failed:
|
||||
_pendingEvent = false;
|
||||
_verifyState = nonCertified;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ownerResponse:
|
||||
{
|
||||
QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8());
|
||||
QString ownerPublicKey;
|
||||
bool ownerValid = false;
|
||||
if (responseJson["status"].toString() == "success") {
|
||||
QJsonValue jsonData = responseJson["data"];
|
||||
if (jsonData.isObject()) {
|
||||
auto ownerJson = jsonData["transfer_recipient_key"];
|
||||
if (ownerJson.isString()) {
|
||||
ownerPublicKey = ownerJson.toString();
|
||||
}
|
||||
auto transferStatusJson = jsonData["transfer_status"];
|
||||
if (transferStatusJson.isArray() && transferStatusJson.toArray()[0].toString() == "confirmed") {
|
||||
ownerValid = true;
|
||||
}
|
||||
}
|
||||
if (ownerValid && !ownerPublicKey.isEmpty()) {
|
||||
if (ownerPublicKey.startsWith("-----BEGIN ")){
|
||||
_ownerPublicKey = ownerPublicKey;
|
||||
} else {
|
||||
_ownerPublicKey = "-----BEGIN PUBLIC KEY-----\n"
|
||||
+ ownerPublicKey
|
||||
+ "\n-----END PUBLIC KEY-----\n";
|
||||
}
|
||||
sendOwnerChallenge();
|
||||
_verifyState = challengeClient;
|
||||
} else {
|
||||
_verifyState = error;
|
||||
}
|
||||
} else {
|
||||
qCDebug(avatars) << "Get owner status failed for " << getDisplayName() << _marketplaceIdFromURL <<
|
||||
"message:" << responseJson["message"].toString();
|
||||
_verifyState = error;
|
||||
}
|
||||
_pendingEvent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case challengeResponse:
|
||||
{
|
||||
if (_challengeResponse.length() < 8) {
|
||||
_verifyState = error;
|
||||
_pendingEvent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
int avatarIDLength;
|
||||
int signedNonceLength;
|
||||
{
|
||||
QDataStream responseStream(_challengeResponse);
|
||||
responseStream.setByteOrder(QDataStream::LittleEndian);
|
||||
responseStream >> avatarIDLength >> signedNonceLength;
|
||||
}
|
||||
QByteArray avatarID(_challengeResponse.data() + 2 * sizeof(int), avatarIDLength);
|
||||
QByteArray signedNonce(_challengeResponse.data() + 2 * sizeof(int) + avatarIDLength, signedNonceLength);
|
||||
|
||||
bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash,
|
||||
QByteArray::fromBase64(signedNonce));
|
||||
_verifyState = challengeResult ? verificationSucceeded : verificationFailed;
|
||||
_needsIdentityUpdate = true;
|
||||
if (_verifyState == verificationFailed) {
|
||||
qCDebug(avatars) << "Dynamic verification FAILED for " << getDisplayName() << getSessionUUID();
|
||||
} else {
|
||||
qCDebug(avatars) << "Dynamic verification SUCCEEDED for " << getDisplayName() << getSessionUUID();
|
||||
}
|
||||
_pendingEvent = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case requestingOwner:
|
||||
{ // Qt networking done on this thread:
|
||||
QCoreApplication::processEvents();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
qCDebug(avatars) << "Unexpected verify state" << _verifyState;
|
||||
break;
|
||||
|
||||
} // close switch
|
||||
}
|
||||
|
||||
void MixerAvatar::sendOwnerChallenge() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
QByteArray avatarID = ("{" + _marketplaceIdFromFST + "}").toUtf8();
|
||||
QByteArray nonce = QUuid::createUuid().toByteArray();
|
||||
|
||||
auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
|
||||
2 * sizeof(int) + nonce.length() + avatarID.length(), true);
|
||||
challengeOwnershipPacket->writePrimitive(avatarID.length());
|
||||
challengeOwnershipPacket->writePrimitive(nonce.length());
|
||||
challengeOwnershipPacket->write(avatarID);
|
||||
challengeOwnershipPacket->write(nonce);
|
||||
|
||||
nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) );
|
||||
QCryptographicHash nonceHash(QCryptographicHash::Sha256);
|
||||
nonceHash.addData(nonce);
|
||||
_challengeNonceHash = nonceHash.result();
|
||||
|
||||
static constexpr int CHALLENGE_TIMEOUT_MS = 10 * 1000; // 10 s
|
||||
_challengeTimeout.setInterval(CHALLENGE_TIMEOUT_MS);
|
||||
_challengeTimeout.connect(&_challengeTimeout, &QTimer::timeout, [this]() {
|
||||
_verifyState = verificationFailed;
|
||||
_needsIdentityUpdate = true;
|
||||
});
|
||||
}
|
||||
|
||||
void MixerAvatar::handleChallengeResponse(ReceivedMessage* response) {
|
||||
QByteArray avatarID;
|
||||
QByteArray encryptedNonce;
|
||||
QMutexLocker certifyLocker(&_avatarCertifyLock);
|
||||
if (_verifyState == challengeClient) {
|
||||
_challengeTimeout.stop();
|
||||
_challengeResponse = response->readAll();
|
||||
_verifyState = challengeResponse;
|
||||
_pendingEvent = true;
|
||||
}
|
||||
}
|
|
@ -17,14 +17,55 @@
|
|||
|
||||
#include <AvatarData.h>
|
||||
|
||||
class ResourceRequest;
|
||||
|
||||
class MixerAvatar : public AvatarData {
|
||||
public:
|
||||
bool getNeedsHeroCheck() const { return _needsHeroCheck; }
|
||||
void setNeedsHeroCheck(bool needsHeroCheck = true)
|
||||
{ _needsHeroCheck = needsHeroCheck; }
|
||||
void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
|
||||
|
||||
void fetchAvatarFST();
|
||||
virtual bool isCertifyFailed() const override { return _verifyState == verificationFailed; }
|
||||
bool needsIdentityUpdate() const { return _needsIdentityUpdate; }
|
||||
void setNeedsIdentityUpdate(bool value = true) { _needsIdentityUpdate = value; }
|
||||
|
||||
void processCertifyEvents();
|
||||
void handleChallengeResponse(ReceivedMessage* response);
|
||||
|
||||
private:
|
||||
bool _needsHeroCheck { false };
|
||||
|
||||
// Avatar certification/verification:
|
||||
enum VerifyState { nonCertified, requestingFST, receivedFST, staticValidation, requestingOwner, ownerResponse,
|
||||
challengeClient, challengeResponse, verified, verificationFailed, verificationSucceeded, error };
|
||||
Q_ENUM(VerifyState);
|
||||
VerifyState _verifyState { nonCertified };
|
||||
std::atomic<bool> _pendingEvent { false };
|
||||
QMutex _avatarCertifyLock;
|
||||
ResourceRequest* _avatarRequest { nullptr };
|
||||
QString _marketplaceIdFromURL;
|
||||
QString _marketplaceIdFromFST;
|
||||
QByteArray _avatarFSTContents;
|
||||
QByteArray _certificateHash;
|
||||
QString _certificateIdFromURL;
|
||||
QString _certificateIdFromFST;
|
||||
QString _dynamicMarketResponse;
|
||||
QString _ownerPublicKey;
|
||||
QByteArray _challengeNonceHash;
|
||||
QByteArray _challengeResponse;
|
||||
QTimer _challengeTimeout;
|
||||
bool _needsIdentityUpdate { false };
|
||||
|
||||
bool generateFSTHash();
|
||||
bool validateFSTHash(const QString& publicKey);
|
||||
QByteArray canonicalJson(const QString fstFile);
|
||||
void sendOwnerChallenge();
|
||||
|
||||
static const QString VERIFY_FAIL_MODEL;
|
||||
|
||||
private slots:
|
||||
void fstRequestComplete();
|
||||
void ownerRequestComplete();
|
||||
};
|
||||
|
||||
using MixerAvatarSharedPointer = std::shared_ptr<MixerAvatar>;
|
||||
|
|
|
@ -1307,8 +1307,8 @@
|
|||
"name": "connection_rate",
|
||||
"label": "Connection Rate",
|
||||
"help": "Number of new agents that can connect to the mixer every second",
|
||||
"placeholder": "50",
|
||||
"default": "50",
|
||||
"placeholder": "10000000",
|
||||
"default": "10000000",
|
||||
"advanced": true
|
||||
},
|
||||
{
|
||||
|
|
BIN
interface/resources/images/AvatarTheftBanner.png
Normal file
BIN
interface/resources/images/AvatarTheftBanner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
BIN
interface/resources/meshes/mannequin/man_stolen.fbx
Normal file
BIN
interface/resources/meshes/mannequin/man_stolen.fbx
Normal file
Binary file not shown.
86
interface/resources/meshes/verifyFailed.fst
Normal file
86
interface/resources/meshes/verifyFailed.fst
Normal file
|
@ -0,0 +1,86 @@
|
|||
name = mannequin2
|
||||
type = body+head
|
||||
scale = 1
|
||||
filename = mannequin/man_stolen.fbx
|
||||
texdir = textures
|
||||
joint = jointEyeLeft = LeftEye
|
||||
joint = jointRightHand = RightHand
|
||||
joint = jointHead = Head
|
||||
joint = jointEyeRight = RightEye
|
||||
joint = jointNeck = Neck
|
||||
joint = jointLeftHand = LeftHand
|
||||
joint = jointLean = Spine
|
||||
joint = jointRoot = Hips
|
||||
freeJoint = LeftArm
|
||||
freeJoint = LeftForeArm
|
||||
freeJoint = RightArm
|
||||
freeJoint = RightForeArm
|
||||
jointIndex = RightHandPinky2 = 19
|
||||
jointIndex = LeftHandPinky3 = 44
|
||||
jointIndex = RightToeBase = 9
|
||||
jointIndex = LeftHandRing4 = 49
|
||||
jointIndex = LeftHandPinky1 = 42
|
||||
jointIndex = LeftHandRing1 = 46
|
||||
jointIndex = LeftLeg = 2
|
||||
jointIndex = RightHandIndex4 = 29
|
||||
jointIndex = LeftHandRing3 = 48
|
||||
jointIndex = RightShoulder = 14
|
||||
jointIndex = RightArm = 15
|
||||
jointIndex = Neck = 62
|
||||
jointIndex = RightHandMiddle2 = 35
|
||||
jointIndex = HeadTop_End = 66
|
||||
jointIndex = LeftHandRing2 = 47
|
||||
jointIndex = RightHandThumb1 = 30
|
||||
jointIndex = RightHandRing3 = 24
|
||||
jointIndex = LeftHandIndex3 = 52
|
||||
jointIndex = LeftForeArm = 40
|
||||
jointIndex = face = 68
|
||||
jointIndex = LeftToe_End = 5
|
||||
jointIndex = RightHandThumb3 = 32
|
||||
jointIndex = RightEye = 65
|
||||
jointIndex = Spine = 11
|
||||
jointIndex = LeftEye = 64
|
||||
jointIndex = LeftToeBase = 4
|
||||
jointIndex = LeftHandIndex4 = 53
|
||||
jointIndex = RightHandPinky4 = 21
|
||||
jointIndex = RightHandMiddle1 = 34
|
||||
jointIndex = Spine1 = 12
|
||||
jointIndex = LeftHandIndex2 = 51
|
||||
jointIndex = RightToe_End = 10
|
||||
jointIndex = RightHand = 17
|
||||
jointIndex = LeftUpLeg = 1
|
||||
jointIndex = RightHandRing1 = 22
|
||||
jointIndex = RightUpLeg = 6
|
||||
jointIndex = RightHandMiddle4 = 37
|
||||
jointIndex = Head = 63
|
||||
jointIndex = RightHandMiddle3 = 36
|
||||
jointIndex = RightHandIndex1 = 26
|
||||
jointIndex = LeftHandMiddle4 = 61
|
||||
jointIndex = LeftHandPinky4 = 45
|
||||
jointIndex = Hips = 0
|
||||
jointIndex = body = 67
|
||||
jointIndex = RightHandThumb2 = 31
|
||||
jointIndex = LeftHandThumb2 = 55
|
||||
jointIndex = RightHandThumb4 = 33
|
||||
jointIndex = RightHandPinky3 = 20
|
||||
jointIndex = LeftHandPinky2 = 43
|
||||
jointIndex = LeftShoulder = 38
|
||||
jointIndex = RightHandIndex3 = 28
|
||||
jointIndex = LeftHandThumb4 = 57
|
||||
jointIndex = RightLeg = 7
|
||||
jointIndex = RightHandIndex2 = 27
|
||||
jointIndex = LeftHandMiddle3 = 60
|
||||
jointIndex = RightHandRing4 = 25
|
||||
jointIndex = LeftHandThumb1 = 54
|
||||
jointIndex = LeftArm = 39
|
||||
jointIndex = LeftHandThumb3 = 56
|
||||
jointIndex = LeftHandMiddle1 = 58
|
||||
jointIndex = RightHandPinky1 = 18
|
||||
jointIndex = Spine2 = 13
|
||||
jointIndex = RightHandRing2 = 23
|
||||
jointIndex = RightForeArm = 16
|
||||
jointIndex = LeftHandIndex1 = 50
|
||||
jointIndex = RightFoot = 8
|
||||
jointIndex = LeftHandMiddle2 = 59
|
||||
jointIndex = LeftHand = 41
|
||||
jointIndex = LeftFoot = 3
|
|
@ -3385,6 +3385,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona
|
|||
DependencyManager::get<KeyboardScriptingInterface>().data());
|
||||
|
||||
if (setAdditionalContextProperties) {
|
||||
qDebug() << "setting additional context properties!";
|
||||
auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
|
||||
auto flags = tabletScriptingInterface->getFlags();
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
#include "MyAvatar.h"
|
||||
#include "DebugDraw.h"
|
||||
#include "SceneScriptingInterface.h"
|
||||
#include "ui/AvatarCertifyBanner.h"
|
||||
|
||||
// 50 times per second - target is 45hz, but this helps account for any small deviations
|
||||
// in the update loop - this also results in ~30hz when in desktop mode which is essentially
|
||||
|
@ -178,6 +179,13 @@ void AvatarManager::updateMyAvatar(float deltaTime) {
|
|||
_lastSendAvatarDataTime = now;
|
||||
_myAvatarSendRate.increment();
|
||||
}
|
||||
|
||||
static AvatarCertifyBanner theftBanner;
|
||||
if (_myAvatar->isCertifyFailed()) {
|
||||
theftBanner.show(_myAvatar->getSessionUUID());
|
||||
} else {
|
||||
theftBanner.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
76
interface/src/ui/AvatarCertifyBanner.cpp
Normal file
76
interface/src/ui/AvatarCertifyBanner.cpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// AvatarCertifyBanner.h
|
||||
// interface/src/ui
|
||||
//
|
||||
// Created by Simon Walton, April 2019
|
||||
// Copyright 2019 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 "AvatarCertifyBanner.h"
|
||||
|
||||
#include <QtGui/QDesktopServices>
|
||||
|
||||
#include "ui/TabletScriptingInterface.h"
|
||||
#include "EntityTreeRenderer.h"
|
||||
|
||||
namespace {
|
||||
const QUrl AVATAR_THEFT_BANNER_IMAGE = PathUtils::resourcesUrl("images/AvatarTheftBanner.png");
|
||||
const QString AVATAR_THEFT_BANNER_SCRIPT { "/system/clickToAvatarApp.js" };
|
||||
}
|
||||
|
||||
AvatarCertifyBanner::AvatarCertifyBanner(QQuickItem* parent) {
|
||||
}
|
||||
|
||||
void AvatarCertifyBanner::show(const QUuid& avatarID) {
|
||||
if (!_active) {
|
||||
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = entityTreeRenderer->getTree();
|
||||
if (!entityTree) {
|
||||
return;
|
||||
}
|
||||
const bool tabletShown = DependencyManager::get<TabletScriptingInterface>()->property("tabletShown").toBool();
|
||||
const auto& position = tabletShown ? glm::vec3(0.0f, 0.0f, -1.8f) : glm::vec3(0.0f, 0.0f, -0.7f);
|
||||
const float scaleFactor = tabletShown ? 2.6f : 1.0f;
|
||||
|
||||
EntityItemProperties entityProperties;
|
||||
entityProperties.setType(EntityTypes::Image);
|
||||
entityProperties.setEntityHostType(entity::HostType::LOCAL);
|
||||
entityProperties.setImageURL(AVATAR_THEFT_BANNER_IMAGE.toString());
|
||||
entityProperties.setName("hifi-avatar-notification-banner");
|
||||
entityProperties.setParentID(avatarID);
|
||||
entityProperties.setParentJointIndex(CAMERA_MATRIX_INDEX);
|
||||
entityProperties.setLocalPosition(position);
|
||||
entityProperties.setDimensions(glm::vec3(1.0f, 1.0f, 0.3f) * scaleFactor);
|
||||
entityProperties.setRenderLayer(tabletShown ? RenderLayer::WORLD : RenderLayer::FRONT);
|
||||
entityProperties.getGrab().setGrabbable(false);
|
||||
QString scriptPath = QUrl(PathUtils::defaultScriptsLocation("")).toString() + AVATAR_THEFT_BANNER_SCRIPT;
|
||||
entityProperties.setScript(scriptPath);
|
||||
entityProperties.setVisible(true);
|
||||
|
||||
entityTree->withWriteLock([&] {
|
||||
auto entityTreeItem = entityTree->addEntity(_bannerID, entityProperties);
|
||||
entityTreeItem->setLocalPosition(position);
|
||||
});
|
||||
|
||||
_active = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarCertifyBanner::clear() {
|
||||
if (_active) {
|
||||
auto entityTreeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||
EntityTreePointer entityTree = entityTreeRenderer->getTree();
|
||||
if (!entityTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
entityTree->withWriteLock([&] {
|
||||
entityTree->deleteEntity(_bannerID);
|
||||
});
|
||||
|
||||
_active = false;
|
||||
}
|
||||
}
|
34
interface/src/ui/AvatarCertifyBanner.h
Normal file
34
interface/src/ui/AvatarCertifyBanner.h
Normal file
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// AvatarCertifyBanner.h
|
||||
// interface/src/ui
|
||||
//
|
||||
// Created by Simon Walton, April 2019
|
||||
// Copyright 2019 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_AvatarCertifyBanner_h
|
||||
#define hifi_AvatarCertifyBanner_h
|
||||
|
||||
#include <QUuid>
|
||||
#include "OffscreenQmlElement.h"
|
||||
#include "EntityItemID.h"
|
||||
|
||||
class EntityItemID;
|
||||
|
||||
class AvatarCertifyBanner : QObject {
|
||||
Q_OBJECT
|
||||
HIFI_QML_DECL
|
||||
public:
|
||||
AvatarCertifyBanner(QQuickItem* parent = nullptr);
|
||||
void show(const QUuid& avatarID);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
const EntityItemID _bannerID { QUuid::createUuid() };
|
||||
bool _active { false };
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarCertifyBanner_h
|
|
@ -1955,8 +1955,7 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
|
|||
>> identity.attachmentData
|
||||
>> identity.displayName
|
||||
>> identity.sessionDisplayName
|
||||
>> identity.isReplicated
|
||||
>> identity.lookAtSnappingEnabled
|
||||
>> identity.identityFlags
|
||||
;
|
||||
|
||||
if (incomingSequenceNumber > _identitySequenceNumber) {
|
||||
|
@ -1971,8 +1970,22 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
|
|||
}
|
||||
maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName);
|
||||
|
||||
if (identity.isReplicated != _isReplicated) {
|
||||
_isReplicated = identity.isReplicated;
|
||||
bool flagValue;
|
||||
flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::isReplicated);
|
||||
if ( flagValue != _isReplicated) {
|
||||
_isReplicated = flagValue;
|
||||
identityChanged = true;
|
||||
}
|
||||
|
||||
flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::lookAtSnapping);
|
||||
if ( flagValue != _lookAtSnappingEnabled) {
|
||||
setProperty("lookAtSnappingEnabled", flagValue);
|
||||
identityChanged = true;
|
||||
}
|
||||
|
||||
flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::verificationFailed);
|
||||
if (flagValue != _verificationFailed) {
|
||||
_verificationFailed = flagValue;
|
||||
identityChanged = true;
|
||||
}
|
||||
|
||||
|
@ -1981,11 +1994,6 @@ void AvatarData::processAvatarIdentity(QDataStream& packetStream, bool& identity
|
|||
identityChanged = true;
|
||||
}
|
||||
|
||||
if (identity.lookAtSnappingEnabled != _lookAtSnappingEnabled) {
|
||||
setProperty("lookAtSnappingEnabled", identity.lookAtSnappingEnabled);
|
||||
identityChanged = true;
|
||||
}
|
||||
|
||||
#ifdef WANT_DEBUG
|
||||
qCDebug(avatars) << __FUNCTION__
|
||||
<< "identity.uuid:" << identity.uuid
|
||||
|
@ -2195,17 +2203,27 @@ void AvatarData::prepareResetTraitInstances() {
|
|||
QByteArray AvatarData::identityByteArray(bool setIsReplicated) const {
|
||||
QByteArray identityData;
|
||||
QDataStream identityStream(&identityData, QIODevice::Append);
|
||||
using namespace AvatarDataPacket;
|
||||
|
||||
// when mixers send identity packets to agents, they simply forward along the last incoming sequence number they received
|
||||
// whereas agents send a fresh outgoing sequence number when identity data has changed
|
||||
IdentityFlags identityFlags = IdentityFlag::none;
|
||||
if (_isReplicated || setIsReplicated) {
|
||||
identityFlags.setFlag(IdentityFlag::isReplicated);
|
||||
}
|
||||
if (_lookAtSnappingEnabled) {
|
||||
identityFlags.setFlag(IdentityFlag::lookAtSnapping);
|
||||
}
|
||||
if (isCertifyFailed()) {
|
||||
identityFlags.setFlag(IdentityFlag::verificationFailed);
|
||||
}
|
||||
|
||||
identityStream << getSessionUUID()
|
||||
<< (udt::SequenceNumber::Type) _identitySequenceNumber
|
||||
<< _attachmentData
|
||||
<< _displayName
|
||||
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
|
||||
<< (_isReplicated || setIsReplicated)
|
||||
<< _lookAtSnappingEnabled;
|
||||
<< identityFlags;
|
||||
|
||||
return identityData;
|
||||
}
|
||||
|
|
|
@ -378,6 +378,10 @@ namespace AvatarDataPacket {
|
|||
|
||||
static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE;
|
||||
|
||||
// AvatarIdentity packet:
|
||||
enum class IdentityFlag: quint32 {none, isReplicated = 0x1, lookAtSnapping = 0x2, verificationFailed = 0x4};
|
||||
Q_DECLARE_FLAGS(IdentityFlags, IdentityFlag)
|
||||
|
||||
struct SendStatus {
|
||||
HasFlags itemFlags { 0 };
|
||||
bool sendUUID { false };
|
||||
|
@ -1182,6 +1186,7 @@ public:
|
|||
QString sessionDisplayName;
|
||||
bool isReplicated;
|
||||
bool lookAtSnappingEnabled;
|
||||
AvatarDataPacket::IdentityFlags identityFlags;
|
||||
};
|
||||
|
||||
// identityChanged returns true if identity has changed, false otherwise.
|
||||
|
@ -1213,6 +1218,7 @@ public:
|
|||
_sessionDisplayName = sessionDisplayName;
|
||||
markIdentityDataChanged();
|
||||
}
|
||||
virtual bool isCertifyFailed() const { return _verificationFailed; }
|
||||
|
||||
/**jsdoc
|
||||
* Gets information about the models currently attached to your avatar.
|
||||
|
@ -1694,6 +1700,7 @@ protected:
|
|||
QString _displayName;
|
||||
QString _sessionDisplayName { };
|
||||
bool _lookAtSnappingEnabled { true };
|
||||
bool _verificationFailed { false };
|
||||
|
||||
quint64 _errorLogExpiry; ///< time in future when to log an error
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
#include "Profile.h"
|
||||
|
||||
static const QString VERIFY_FAIL_MODEL { "/meshes/verifyFailed.fst" };
|
||||
|
||||
void AvatarReplicas::addReplica(const QUuid& parentID, AvatarSharedPointer replica) {
|
||||
if (parentID == QUuid()) {
|
||||
return;
|
||||
|
@ -324,6 +326,10 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
|
|||
bool displayNameChanged = false;
|
||||
// In this case, the "sendingNode" is the Avatar Mixer.
|
||||
avatar->processAvatarIdentity(avatarIdentityStream, identityChanged, displayNameChanged);
|
||||
if (avatar->isCertifyFailed() && identityUUID != EMPTY) {
|
||||
qCDebug(avatars) << "Avatar" << avatar->getSessionDisplayName() << "marked as VERIFY-FAILED";
|
||||
avatar->setSkeletonModelURL(PathUtils::resourcesUrl(VERIFY_FAIL_MODEL));
|
||||
}
|
||||
_replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,14 @@ QVariant readBinaryArray(QDataStream& in, int& position) {
|
|||
quint32 compressedLength;
|
||||
|
||||
in >> arrayLength;
|
||||
if (arrayLength > std::numeric_limits<int>::max() / sizeof(T)) { // Upcoming byte containers are limited to max signed int
|
||||
throw QString("FBX file most likely corrupt: binary data exceeds data limits");
|
||||
}
|
||||
in >> encoding;
|
||||
in >> compressedLength;
|
||||
if (compressedLength > std::numeric_limits<int>::max() / sizeof(T)) { // Upcoming byte containers are limited to max signed int
|
||||
throw QString("FBX file most likely corrupt: compressed binary data exceeds data limits");
|
||||
}
|
||||
position += sizeof(quint32) * 3;
|
||||
|
||||
QVector<T> values;
|
||||
|
|
|
@ -261,6 +261,61 @@ void Midi::MidiCleanup() {
|
|||
}
|
||||
#endif
|
||||
|
||||
/**jsdoc
|
||||
* A MIDI message.
|
||||
* <p><strong>Warning:</strong> The <code>status</code> property is NOT a MIDI status value.</p>
|
||||
* @typedef {object} Midi.MidiMessage
|
||||
* @property {number} device - Device number.
|
||||
* @property {Midi.RawMidiMessage} raw - Raw MIDI message.
|
||||
* @property {number} status - Channel + status. <em>Legacy value.</em>
|
||||
* @property {number} channel - Channel: <code>1</code> – <code>16</code>.
|
||||
* @property {number} type - Status: {@link Midi.MidiStatus}; <code>8</code> – <code>15</code>.
|
||||
* @property {number} note - Note: <code>0</code> – <code>127</code>.
|
||||
* @property {number} velocity - Note velocity: <code>0</code> – <code>127</code>. (<code>0</code> means "note off".)
|
||||
* @property {number} bend - Pitch bend: <code>-8192</code> – <code>8191</code>.
|
||||
* @property {number} program - Program change: <code>0</code> – <code>127</code>.
|
||||
*/
|
||||
/**jsdoc
|
||||
* An integer DWORD (unsigned 32 bit) message with bits having values as follows:
|
||||
* <table>
|
||||
* <tbody>
|
||||
* <tr>
|
||||
* <td width=25%><code>00000000</code></td>
|
||||
* <td width=25%><code>0vvvvvvv</code></td>
|
||||
* <td width=25%><code>0nnnnnnn</code></td>
|
||||
* <td width=12%><code>1sss</code></td>
|
||||
* <td width=12%><code>cccc</code></td>
|
||||
* </tbody>
|
||||
* </table>
|
||||
* <p>Where:</p>
|
||||
* <ul>
|
||||
* <li><code>v</code> = Velocity.
|
||||
* <li><code>n</code> = Note.
|
||||
* <li><code>s</code> = Status - {@link Midi.MidiStatus}
|
||||
* <li><code>c</code> = Channel.
|
||||
* </ul>
|
||||
* <p>The number in the first bit of each byte denotes whether it is a command (1) or data (0).
|
||||
* @typedef {number} Midi.RawMidiMessage
|
||||
*/
|
||||
/**jsdoc
|
||||
* <p>A MIDI status value. The following MIDI status values are supported:</p>
|
||||
* <table>
|
||||
* <thead>
|
||||
* <tr><th>Value</th><th>Description</th>
|
||||
* </thead>
|
||||
* <tbody>
|
||||
* <tr><td><code>8</code></td><td>Note off.</td></tr>
|
||||
* <tr><td><code>9</code></td><td>Note on.</td></tr>
|
||||
* <tr><td><code>10</code></td><td>Polyphonic key pressure.</td></tr>
|
||||
* <tr><td><code>11</code></td><td>Control change.</td></tr>
|
||||
* <tr><td><code>12</code></td><td>Program change.</td></tr>
|
||||
* <tr><td><code>13</code></td><td>Channel pressure.</td></tr>
|
||||
* <tr><td><code>14</code></td><td>Pitch bend.</td></tr>
|
||||
* <tr><td><code>15</code></td><td>System message.</td></tr>
|
||||
* </tbody>
|
||||
* </table>
|
||||
* @typedef {number} Midi.MidiStatus
|
||||
*/
|
||||
void Midi::midiReceived(int device, int raw, int channel, int status, int type, int note, int velocity, int bend, int program) {
|
||||
QVariantMap eventData;
|
||||
eventData["device"] = device;
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
#include <string>
|
||||
|
||||
/**jsdoc
|
||||
* The <code>Midi</code> API provides the ability to connect Interface with musical instruments and other external or virtual
|
||||
* devices via the MIDI protocol. For further information and examples, see the tutorial:
|
||||
* <a href="https://docs.highfidelity.com/en/rc81/script/midi-tutorial.html">Use MIDI to Control Your Environment</a>.</p>
|
||||
*
|
||||
* <p><strong>Note:</strong> Only works on Windows.</p>
|
||||
*
|
||||
* @namespace Midi
|
||||
*
|
||||
* @hifi-interface
|
||||
|
@ -49,88 +55,112 @@ private:
|
|||
void MidiCleanup();
|
||||
|
||||
signals:
|
||||
void midiNote(QVariantMap eventData);
|
||||
void midiMessage(QVariantMap eventData);
|
||||
void midiReset();
|
||||
|
||||
public slots:
|
||||
|
||||
/**jsdoc
|
||||
* Send Raw MIDI packet to a particular device.
|
||||
* Triggered when a connected device sends an output.
|
||||
* @function Midi.midiNote
|
||||
* @param {Midi.MidiMessage} message - The MIDI message.
|
||||
* @returns {Signal}
|
||||
* @deprecated This signal is deprecated and will be removed. Use {@link Midi.midiMessage|midiMessage} instead.
|
||||
*/
|
||||
void midiNote(QVariantMap eventData);
|
||||
|
||||
/**jsdoc
|
||||
* Triggered when a connected device sends an output.
|
||||
* @function Midi.midiMessage
|
||||
* @param {Midi.MidiMessage} message - The MIDI message.
|
||||
* @returns {Signal}
|
||||
*/
|
||||
void midiMessage(QVariantMap eventData);
|
||||
|
||||
/**jsdoc
|
||||
* Triggered when the system detects there was a reset such as when a device is plugged in or unplugged.
|
||||
* @function Midi.midiReset
|
||||
* @returns {Signal}
|
||||
*/
|
||||
void midiReset();
|
||||
|
||||
public slots:
|
||||
|
||||
/**jsdoc
|
||||
* Sends a raw MIDI packet to a particular device.
|
||||
* @function Midi.sendRawDword
|
||||
* @param {number} device - Integer device number.
|
||||
* @param {number} raw - Integer (DWORD) raw MIDI message.
|
||||
* @param {Midi.RawMidiMessage} raw - Raw MIDI message.
|
||||
*/
|
||||
Q_INVOKABLE void sendRawDword(int device, int raw);
|
||||
|
||||
/**jsdoc
|
||||
* Send MIDI message to a particular device.
|
||||
* Sends a MIDI message to a particular device.
|
||||
* @function Midi.sendMidiMessage
|
||||
* @param {number} device - Integer device number.
|
||||
* @param {number} channel - Integer channel number.
|
||||
* @param {number} type - 0x8 is note off, 0x9 is note on (if velocity=0, note off), etc.
|
||||
* @param {number} note - MIDI note number.
|
||||
* @param {number} velocity - Note velocity (0 means note off).
|
||||
* @param {Midi.MidiStatus} type - Integer status value.
|
||||
* @param {number} note - Note number.
|
||||
* @param {number} velocity - Note velocity. (<code>0</code> means "note off".)
|
||||
* @comment The "type" parameter has that name to match up with {@link Midi.MidiMessage}.
|
||||
*/
|
||||
Q_INVOKABLE void sendMidiMessage(int device, int channel, int type, int note, int velocity);
|
||||
|
||||
/**jsdoc
|
||||
* Play a note on all connected devices.
|
||||
* Plays a note on all connected devices.
|
||||
* @function Midi.playMidiNote
|
||||
* @param {number} status - 0x80 is note off, 0x90 is note on (if velocity=0, note off), etc.
|
||||
* @param {number} note - MIDI note number.
|
||||
* @param {number} velocity - Note velocity (0 means note off).
|
||||
* @param {MidiStatus} status - Note status.
|
||||
* @param {number} note - Note number.
|
||||
* @param {number} velocity - Note velocity. (<code>0</code> means "note off".)
|
||||
*/
|
||||
Q_INVOKABLE void playMidiNote(int status, int note, int velocity);
|
||||
|
||||
/**jsdoc
|
||||
* Turn off all notes on all connected devices.
|
||||
* Turns off all notes on all connected MIDI devices.
|
||||
* @function Midi.allNotesOff
|
||||
*/
|
||||
Q_INVOKABLE void allNotesOff();
|
||||
|
||||
/**jsdoc
|
||||
* Clean up and re-discover attached devices.
|
||||
* Cleans up and rediscovers attached MIDI devices.
|
||||
* @function Midi.resetDevices
|
||||
*/
|
||||
Q_INVOKABLE void resetDevices();
|
||||
|
||||
/**jsdoc
|
||||
* Get a list of inputs/outputs.
|
||||
* Gets a list of MIDI input or output devices.
|
||||
* @function Midi.listMidiDevices
|
||||
* @param {boolean} output
|
||||
* @param {boolean} output - <code>true</code> to list output devices, <code>false</code> to list input devices.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
Q_INVOKABLE QStringList listMidiDevices(bool output);
|
||||
|
||||
/**jsdoc
|
||||
* Block an input/output by name.
|
||||
* Blocks a MIDI device's input or output.
|
||||
* @function Midi.blockMidiDevice
|
||||
* @param {string} name
|
||||
* @param {boolean} output
|
||||
* @param {string} name - The name of the MIDI device to block.
|
||||
* @param {boolean} output - <code>true</code> to block the device's output, <code>false</code> to block its input.
|
||||
*/
|
||||
Q_INVOKABLE void blockMidiDevice(QString name, bool output);
|
||||
|
||||
/**jsdoc
|
||||
* Unblock an input/output by name.
|
||||
* Unblocks a MIDI device's input or output.
|
||||
* @function Midi.unblockMidiDevice
|
||||
* @param {string} name
|
||||
* @param {boolean} output
|
||||
* @param {string} name- The name of the MIDI device to unblock.
|
||||
* @param {boolean} output - <code>true</code> to unblock the device's output, <code>false</code> to unblock its input.
|
||||
*/
|
||||
Q_INVOKABLE void unblockMidiDevice(QString name, bool output);
|
||||
|
||||
/**jsdoc
|
||||
* Repeat all incoming notes to all outputs (default disabled).
|
||||
* Enables or disables repeating all incoming notes to all outputs. (Default is disabled.)
|
||||
* @function Midi.thruModeEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable repeating all incoming notes to all output, <code>false</code> to
|
||||
* disable.
|
||||
*/
|
||||
Q_INVOKABLE void thruModeEnable(bool enable);
|
||||
|
||||
|
||||
/**jsdoc
|
||||
* Broadcast on all unblocked devices.
|
||||
* Enables or disables broadcasts to all unblocked devices.
|
||||
* @function Midi.broadcastEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to have "send" functions broadcast to all devices, <code>false</code> to
|
||||
* have them send to specific output devices.
|
||||
*/
|
||||
Q_INVOKABLE void broadcastEnable(bool enable);
|
||||
|
||||
|
@ -138,50 +168,58 @@ signals:
|
|||
/// filter by event types
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables note off events.
|
||||
* @function Midi.typeNoteOffEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typeNoteOffEnable(bool enable);
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables note on events.
|
||||
* @function Midi.typeNoteOnEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typeNoteOnEnable(bool enable);
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables poly key pressure events.
|
||||
* @function Midi.typePolyKeyPressureEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typePolyKeyPressureEnable(bool enable);
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables control change events.
|
||||
* @function Midi.typeControlChangeEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typeControlChangeEnable(bool enable);
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables program change events.
|
||||
* @function Midi.typeProgramChangeEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typeProgramChangeEnable(bool enable);
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables channel pressure events.
|
||||
* @function Midi.typeChanPressureEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typeChanPressureEnable(bool enable);
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables pitch bend events.
|
||||
* @function Midi.typePitchBendEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typePitchBendEnable(bool enable);
|
||||
|
||||
/**jsdoc
|
||||
* Enables or disables system message events.
|
||||
* @function Midi.typeSystemMessageEnable
|
||||
* @param {boolean} enable
|
||||
* @param {boolean} enable - <code>true</code> to enable, <code>false</code> to disable.
|
||||
*/
|
||||
Q_INVOKABLE void typeSystemMessageEnable(bool enable);
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ void CongestionControl::setMaxBandwidth(int maxBandwidth) {
|
|||
void CongestionControl::setPacketSendPeriod(double newSendPeriod) {
|
||||
Q_ASSERT_X(newSendPeriod >= 0, "CongestionControl::setPacketPeriod", "Can not set a negative packet send period");
|
||||
|
||||
auto packetsPerSecond = (double)_maxBandwidth / (BITS_PER_BYTE * _mss);
|
||||
auto packetsPerSecond = _mss > 0 ? (double)_maxBandwidth / (BITS_PER_BYTE * _mss) : -1.0;
|
||||
if (packetsPerSecond > 0.0) {
|
||||
// anytime the packet send period is about to be increased, make sure it stays below the minimum period,
|
||||
// calculated based on the maximum desired bandwidth
|
||||
|
|
|
@ -38,10 +38,10 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
|||
return static_cast<PacketVersion>(EntityQueryPacketVersion::ConicalFrustums);
|
||||
case PacketType::AvatarIdentity:
|
||||
case PacketType::AvatarData:
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::HandControllerSection);
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SendVerificationFailed);
|
||||
case PacketType::BulkAvatarData:
|
||||
case PacketType::KillAvatar:
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::HandControllerSection);
|
||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SendVerificationFailed);
|
||||
case PacketType::MessagesData:
|
||||
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
|
||||
// ICE packets
|
||||
|
|
|
@ -333,6 +333,7 @@ enum class AvatarMixerPacketVersion : PacketVersion {
|
|||
SendMaxTranslationDimension,
|
||||
FBXJointOrderChange,
|
||||
HandControllerSection,
|
||||
SendVerificationFailed
|
||||
};
|
||||
|
||||
enum class DomainConnectRequestVersion : PacketVersion {
|
||||
|
|
|
@ -392,6 +392,10 @@ void Scene::updateItems(const Transaction::Updates& transactions) {
|
|||
void Scene::transitionItems(const Transaction::TransitionAdds& transactions) {
|
||||
auto transitionStage = getStage<TransitionStage>(TransitionStage::getName());
|
||||
|
||||
if (!transitionStage) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& add : transactions) {
|
||||
auto itemId = std::get<0>(add);
|
||||
// Access the true item
|
||||
|
@ -433,6 +437,10 @@ void Scene::reApplyTransitions(const Transaction::TransitionReApplies& transacti
|
|||
void Scene::queryTransitionItems(const Transaction::TransitionQueries& transactions) {
|
||||
auto transitionStage = getStage<TransitionStage>(TransitionStage::getName());
|
||||
|
||||
if (!transitionStage) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& query : transactions) {
|
||||
auto itemId = std::get<0>(query);
|
||||
// Access the true item
|
||||
|
@ -553,11 +561,14 @@ void Scene::setItemTransition(ItemID itemId, Index transitionId) {
|
|||
}
|
||||
|
||||
void Scene::resetItemTransition(ItemID itemId) {
|
||||
auto transitionStage = getStage<TransitionStage>(TransitionStage::getName());
|
||||
if (!transitionStage) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& item = _items[itemId];
|
||||
TransitionStage::Index transitionId = item.getTransitionId();
|
||||
if (!render::TransitionStage::isIndexInvalid(transitionId)) {
|
||||
auto transitionStage = getStage<TransitionStage>(TransitionStage::getName());
|
||||
|
||||
auto finishedOperators = _transitionFinishedOperatorMap[transitionId];
|
||||
for (auto finishedOperator : finishedOperators) {
|
||||
if (finishedOperator) {
|
||||
|
|
|
@ -252,7 +252,9 @@ private slots:
|
|||
_finished = true;
|
||||
auto offscreenUi = DependencyManager::get<OffscreenUi>();
|
||||
emit response(_result);
|
||||
offscreenUi->removeModalDialog(qobject_cast<QObject*>(this));
|
||||
if (!offscreenUi.isNull()) {
|
||||
offscreenUi->removeModalDialog(qobject_cast<QObject*>(this));
|
||||
}
|
||||
disconnect(_dialog);
|
||||
}
|
||||
};
|
||||
|
|
7
scripts/system/clickToAvatarApp.js
Normal file
7
scripts/system/clickToAvatarApp.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
(function () {
|
||||
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
|
||||
this.clickDownOnEntity = function (entityID, mouseEvent) {
|
||||
tablet.loadQMLSource("hifi/AvatarApp.qml");
|
||||
};
|
||||
}
|
||||
);
|
Loading…
Reference in a new issue