mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-08 07:22:25 +02:00
Merge remote-tracking branch 'upstream/master' into perf
This commit is contained in:
commit
70f4d0c634
271 changed files with 3312 additions and 2640 deletions
|
@ -25,16 +25,19 @@
|
|||
#include <AudioInjectorManager.h>
|
||||
#include <AssetClient.h>
|
||||
#include <DebugDraw.h>
|
||||
#include <EntityScriptingInterface.h>
|
||||
#include <LocationScriptingInterface.h>
|
||||
#include <MessagesClient.h>
|
||||
#include <NetworkAccessManager.h>
|
||||
#include <NodeList.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
#include <ResourceCache.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <ScriptCache.h>
|
||||
#include <ScriptEngines.h>
|
||||
#include <SoundCacheScriptingInterface.h>
|
||||
#include <SoundCache.h>
|
||||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
#include <UsersScriptingInterface.h>
|
||||
#include <UUID.h>
|
||||
|
||||
|
@ -50,11 +53,11 @@
|
|||
#include <EntityScriptingInterface.h> // TODO: consider moving to scriptengine.h
|
||||
|
||||
#include "entities/AssignmentParentFinder.h"
|
||||
#include "AssignmentDynamicFactory.h"
|
||||
#include "RecordingScriptingInterface.h"
|
||||
#include "AbstractAudioInterface.h"
|
||||
#include "AgentScriptingInterface.h"
|
||||
|
||||
|
||||
static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
|
||||
|
||||
Agent::Agent(ReceivedMessage& message) :
|
||||
|
@ -63,6 +66,18 @@ Agent::Agent(ReceivedMessage& message) :
|
|||
_audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO),
|
||||
_avatarAudioTimer(this)
|
||||
{
|
||||
DependencyManager::set<ScriptableAvatar>();
|
||||
|
||||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||
DependencyManager::set<AssignmentDynamicFactory>();
|
||||
|
||||
DependencyManager::set<AnimationCache>();
|
||||
DependencyManager::set<AnimationCacheScriptingInterface>();
|
||||
DependencyManager::set<EntityScriptingInterface>(false);
|
||||
|
||||
DependencyManager::set<ResourceScriptingInterface>();
|
||||
DependencyManager::set<UserActivityLoggerScriptingInterface>();
|
||||
|
||||
_entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT);
|
||||
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
|
||||
|
||||
|
@ -99,7 +114,6 @@ Agent::Agent(ReceivedMessage& message) :
|
|||
this, "handleOctreePacket");
|
||||
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
|
||||
|
||||
|
||||
// 100Hz timer for audio
|
||||
const int TARGET_INTERVAL_MSEC = 10; // 10ms
|
||||
connect(&_avatarAudioTimer, &QTimer::timeout, this, &Agent::processAgentAvatarAudio);
|
||||
|
@ -351,6 +365,8 @@ void Agent::executeScript() {
|
|||
// setup an Avatar for the script to use
|
||||
auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
|
||||
|
||||
scriptedAvatar->setID(getSessionUUID());
|
||||
|
||||
connect(_scriptEngine.data(), SIGNAL(update(float)),
|
||||
scriptedAvatar.data(), SLOT(update(float)), Qt::ConnectionType::QueuedConnection);
|
||||
scriptedAvatar->setForceFaceTrackerConnected(true);
|
||||
|
@ -439,7 +455,7 @@ void Agent::executeScript() {
|
|||
encodedBuffer = audio;
|
||||
}
|
||||
|
||||
AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, false,
|
||||
AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, false,
|
||||
audioTransform, scriptedAvatar->getWorldPosition(), glm::vec3(0),
|
||||
packetType, _selectedCodecName);
|
||||
});
|
||||
|
@ -447,11 +463,6 @@ void Agent::executeScript() {
|
|||
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
|
||||
_scriptEngine->registerGlobalObject("AvatarList", avatarHashMap.data());
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket");
|
||||
packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar");
|
||||
packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket");
|
||||
|
||||
// register ourselves to the script engine
|
||||
_scriptEngine->registerGlobalObject("Agent", new AgentScriptingInterface(this));
|
||||
|
||||
|
@ -597,6 +608,11 @@ void Agent::setIsAvatar(bool isAvatar) {
|
|||
}
|
||||
|
||||
QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
|
||||
|
||||
_entityEditSender.setMyAvatar(nullptr);
|
||||
} else {
|
||||
auto scriptableAvatar = DependencyManager::get<ScriptableAvatar>();
|
||||
_entityEditSender.setMyAvatar(scriptableAvatar.data());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -847,6 +863,11 @@ void Agent::aboutToFinish() {
|
|||
DependencyManager::destroy<recording::Recorder>();
|
||||
DependencyManager::destroy<recording::ClipCache>();
|
||||
DependencyManager::destroy<ScriptEngine>();
|
||||
|
||||
DependencyManager::destroy<AssignmentDynamicFactory>();
|
||||
|
||||
DependencyManager::destroy<ScriptableAvatar>();
|
||||
|
||||
QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
|
||||
|
||||
// cleanup codec & encoder
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include <QtCore/QTimer>
|
||||
#include <QUuid>
|
||||
|
||||
#include <ClientTraitsHandler.h>
|
||||
#include <EntityEditPacketSender.h>
|
||||
#include <EntityTree.h>
|
||||
#include <ScriptEngine.h>
|
||||
|
|
|
@ -21,10 +21,7 @@
|
|||
#include <shared/QtHelpers.h>
|
||||
#include <AccountManager.h>
|
||||
#include <AddressManager.h>
|
||||
#include <AnimationCacheScriptingInterface.h>
|
||||
#include <Assignment.h>
|
||||
#include <AvatarHashMap.h>
|
||||
#include <EntityScriptingInterface.h>
|
||||
#include <LogHandler.h>
|
||||
#include <LogUtils.h>
|
||||
#include <LimitedNodeList.h>
|
||||
|
@ -32,16 +29,12 @@
|
|||
#include <udt/PacketHeaders.h>
|
||||
#include <SharedUtil.h>
|
||||
#include <ShutdownEventListener.h>
|
||||
#include <SoundCache.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <UserActivityLoggerScriptingInterface.h>
|
||||
|
||||
#include <Trace.h>
|
||||
#include <StatTracker.h>
|
||||
|
||||
#include "AssignmentClientLogging.h"
|
||||
#include "AssignmentDynamicFactory.h"
|
||||
#include "AssignmentFactory.h"
|
||||
#include "avatars/ScriptableAvatar.h"
|
||||
|
||||
const QString ASSIGNMENT_CLIENT_TARGET_NAME = "assignment-client";
|
||||
const long long ASSIGNMENT_REQUEST_INTERVAL_MSECS = 1 * 1000;
|
||||
|
@ -57,21 +50,11 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
|
|||
DependencyManager::set<StatTracker>();
|
||||
DependencyManager::set<AccountManager>();
|
||||
|
||||
auto scriptableAvatar = DependencyManager::set<ScriptableAvatar>();
|
||||
auto addressManager = DependencyManager::set<AddressManager>();
|
||||
|
||||
// create a NodeList as an unassigned client, must be after addressManager
|
||||
auto nodeList = DependencyManager::set<NodeList>(NodeType::Unassigned, listenPort);
|
||||
|
||||
auto animationCache = DependencyManager::set<AnimationCache>();
|
||||
DependencyManager::set<AnimationCacheScriptingInterface>();
|
||||
auto entityScriptingInterface = DependencyManager::set<EntityScriptingInterface>(false);
|
||||
|
||||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||
auto dynamicFactory = DependencyManager::set<AssignmentDynamicFactory>();
|
||||
DependencyManager::set<ResourceScriptingInterface>();
|
||||
DependencyManager::set<UserActivityLoggerScriptingInterface>();
|
||||
|
||||
nodeList->startThread();
|
||||
// set the logging target to the the CHILD_TARGET_NAME
|
||||
LogHandler::getInstance().setTargetName(ASSIGNMENT_CLIENT_TARGET_NAME);
|
||||
|
|
|
@ -366,7 +366,7 @@ AudioMixerClientData* AudioMixer::getOrCreateClientData(Node* node) {
|
|||
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
|
||||
if (!clientData) {
|
||||
node->setLinkedData(std::unique_ptr<NodeData> { new AudioMixerClientData(node->getUUID()) });
|
||||
node->setLinkedData(std::unique_ptr<NodeData> { new AudioMixerClientData(node->getUUID(), node->getLocalID()) });
|
||||
clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
|
||||
connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector);
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@
|
|||
#include "AudioHelpers.h"
|
||||
#include "AudioMixer.h"
|
||||
|
||||
AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) :
|
||||
NodeData(nodeID),
|
||||
AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
|
||||
NodeData(nodeID, nodeLocalID),
|
||||
audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO),
|
||||
_ignoreZone(*this),
|
||||
_outgoingMixedAudioSequenceNumber(0),
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
class AudioMixerClientData : public NodeData {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AudioMixerClientData(const QUuid& nodeID);
|
||||
AudioMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
|
||||
~AudioMixerClientData();
|
||||
|
||||
using SharedStreamPointer = std::shared_ptr<PositionalAudioStream>;
|
||||
|
|
|
@ -39,7 +39,8 @@ const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer";
|
|||
const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
|
||||
|
||||
AvatarMixer::AvatarMixer(ReceivedMessage& message) :
|
||||
ThreadedAssignment(message)
|
||||
ThreadedAssignment(message),
|
||||
_slavePool(&_slaveSharedData)
|
||||
{
|
||||
// make sure we hear about node kills so we can tell the other nodes
|
||||
connect(DependencyManager::get<NodeList>().data(), &NodeList::nodeKilled, this, &AvatarMixer::handleAvatarKilled);
|
||||
|
@ -54,6 +55,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
|
|||
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
|
||||
packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket");
|
||||
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
|
||||
|
||||
packetReceiver.registerListenerForTypes({
|
||||
PacketType::ReplicatedAvatarIdentity,
|
||||
|
@ -337,17 +339,7 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
|
|||
sendIdentity = true;
|
||||
qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID();
|
||||
}
|
||||
if (nodeData && nodeData->getAvatarSkeletonModelUrlMustChange()) { // never true for an empty _avatarWhitelist
|
||||
nodeData->setAvatarSkeletonModelUrlMustChange(false);
|
||||
AvatarData& avatar = nodeData->getAvatar();
|
||||
static const QUrl emptyURL("");
|
||||
QUrl url = avatar.cannonicalSkeletonModelURL(emptyURL);
|
||||
if (!isAvatarInWhitelist(url)) {
|
||||
qCDebug(avatars) << "Forbidden avatar" << nodeData->getNodeID() << avatar.getSkeletonModelURL() << "replaced with" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
|
||||
avatar.setSkeletonModelURL(_replacementAvatar);
|
||||
sendIdentity = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (sendIdentity && !node->isUpstream()) {
|
||||
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
|
||||
|
@ -359,22 +351,6 @@ void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
|
|||
}
|
||||
}
|
||||
|
||||
bool AvatarMixer::isAvatarInWhitelist(const QUrl& url) {
|
||||
// The avatar is in the whitelist if:
|
||||
// 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND
|
||||
// 2. The avatar's URL's path starts with the path of that same URL in the whitelist
|
||||
for (const auto& whiteListedPrefix : _avatarWhitelist) {
|
||||
auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix);
|
||||
// check if this script URL matches the whitelist domain and, optionally, is beneath the path
|
||||
if (url.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 &&
|
||||
url.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) {
|
||||
// throttle using a modified proportional-integral controller
|
||||
const float FRAME_TIME = USECS_PER_SECOND / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
|
||||
|
@ -494,7 +470,8 @@ void AvatarMixer::handleAvatarKilled(SharedNodePointer avatarNode) {
|
|||
QMetaObject::invokeMethod(node->getLinkedData(),
|
||||
"cleanupKilledNode",
|
||||
Qt::AutoConnection,
|
||||
Q_ARG(const QUuid&, QUuid(avatarNode->getUUID())));
|
||||
Q_ARG(const QUuid&, QUuid(avatarNode->getUUID())),
|
||||
Q_ARG(Node::LocalID, avatarNode->getLocalID()));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -564,7 +541,8 @@ void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMess
|
|||
// ...For those nodes, reset the lastBroadcastTime to 0
|
||||
// so that the AvatarMixer will send Identity data to us
|
||||
[&](const SharedNodePointer& node) {
|
||||
nodeData->setLastBroadcastTime(node->getUUID(), 0);
|
||||
nodeData->setLastBroadcastTime(node->getUUID(), 0);
|
||||
nodeData->resetSentTraitData(node->getLocalID());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -587,8 +565,7 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
|
|||
// parse the identity packet and update the change timestamp if appropriate
|
||||
bool identityChanged = false;
|
||||
bool displayNameChanged = false;
|
||||
bool skeletonModelUrlChanged = false;
|
||||
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
|
||||
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged);
|
||||
|
||||
if (identityChanged) {
|
||||
QMutexLocker nodeDataLocker(&nodeData->getMutex());
|
||||
|
@ -596,9 +573,6 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
|
|||
if (displayNameChanged) {
|
||||
nodeData->setAvatarSessionDisplayNameMustChange(true);
|
||||
}
|
||||
if (skeletonModelUrlChanged && !_avatarWhitelist.isEmpty()) {
|
||||
nodeData->setAvatarSkeletonModelUrlMustChange(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -615,10 +589,10 @@ void AvatarMixer::handleAvatarIdentityRequestPacket(QSharedPointer<ReceivedMessa
|
|||
QUuid avatarID(QUuid::fromRfc4122(message->getMessage()) );
|
||||
if (!avatarID.isNull()) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto node = nodeList->nodeWithUUID(avatarID);
|
||||
if (node) {
|
||||
QMutexLocker lock(&node->getMutex());
|
||||
AvatarMixerClientData* avatarClientData = dynamic_cast<AvatarMixerClientData*>(node->getLinkedData());
|
||||
auto requestedNode = nodeList->nodeWithUUID(avatarID);
|
||||
|
||||
if (requestedNode) {
|
||||
AvatarMixerClientData* avatarClientData = static_cast<AvatarMixerClientData*>(requestedNode->getLinkedData());
|
||||
if (avatarClientData) {
|
||||
const AvatarData& avatarData = avatarClientData->getAvatar();
|
||||
QByteArray serializedAvatar = avatarData.identityByteArray();
|
||||
|
@ -627,6 +601,11 @@ void AvatarMixer::handleAvatarIdentityRequestPacket(QSharedPointer<ReceivedMessa
|
|||
nodeList->sendPacketList(std::move(identityPackets), *senderNode);
|
||||
++_sumIdentityPackets;
|
||||
}
|
||||
|
||||
AvatarMixerClientData* senderData = static_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
|
||||
if (senderData) {
|
||||
senderData->resetSentTraitData(requestedNode->getLocalID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -652,23 +631,24 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage>
|
|||
while (message->getBytesLeftToRead()) {
|
||||
// parse out the UUID being ignored from the packet
|
||||
QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
if (nodeList->nodeWithUUID(ignoredUUID)) {
|
||||
auto ignoredNode = nodeList->nodeWithUUID(ignoredUUID);
|
||||
if (ignoredNode) {
|
||||
if (nodeData) {
|
||||
// Reset the lastBroadcastTime for the ignored avatar to 0
|
||||
// so the AvatarMixer knows it'll have to send identity data about the ignored avatar
|
||||
// to the ignorer if the ignorer unignores.
|
||||
nodeData->setLastBroadcastTime(ignoredUUID, 0);
|
||||
nodeData->resetSentTraitData(ignoredNode->getLocalID());
|
||||
}
|
||||
|
||||
|
||||
// Reset the lastBroadcastTime for the ignorer (FROM THE PERSPECTIVE OF THE IGNORED) to 0
|
||||
// so the AvatarMixer knows it'll have to send identity data about the ignorer
|
||||
// to the ignored if the ignorer unignores.
|
||||
auto ignoredNode = nodeList->nodeWithUUID(ignoredUUID);
|
||||
AvatarMixerClientData* ignoredNodeData = reinterpret_cast<AvatarMixerClientData*>(ignoredNode->getLinkedData());
|
||||
if (ignoredNodeData) {
|
||||
ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0);
|
||||
ignoredNodeData->resetSentTraitData(senderNode->getLocalID());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -903,7 +883,7 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node
|
|||
auto clientData = dynamic_cast<AvatarMixerClientData*>(node->getLinkedData());
|
||||
|
||||
if (!clientData) {
|
||||
node->setLinkedData(std::unique_ptr<NodeData> { new AvatarMixerClientData(node->getUUID()) });
|
||||
node->setLinkedData(std::unique_ptr<NodeData> { new AvatarMixerClientData(node->getUUID(), node->getLocalID()) });
|
||||
clientData = dynamic_cast<AvatarMixerClientData*>(node->getLinkedData());
|
||||
auto& avatar = clientData->getAvatar();
|
||||
avatar.setDomainMinimumHeight(_domainMinimumHeight);
|
||||
|
@ -991,20 +971,22 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
|
|||
qCDebug(avatars) << "This domain requires a minimum avatar height of" << _domainMinimumHeight
|
||||
<< "and a maximum avatar height of" << _domainMaximumHeight;
|
||||
|
||||
const QString AVATAR_WHITELIST_DEFAULT{ "" };
|
||||
static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist";
|
||||
_avatarWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION].toString(AVATAR_WHITELIST_DEFAULT).split(',', QString::KeepEmptyParts);
|
||||
_slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION]
|
||||
.toString().split(',', QString::KeepEmptyParts);
|
||||
|
||||
static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar";
|
||||
_replacementAvatar = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION].toString(REPLACEMENT_AVATAR_DEFAULT);
|
||||
_slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION]
|
||||
.toString();
|
||||
|
||||
if ((_avatarWhitelist.count() == 1) && _avatarWhitelist[0].isEmpty()) {
|
||||
_avatarWhitelist.clear(); // KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok).
|
||||
if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) {
|
||||
// KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok).
|
||||
_slaveSharedData.skeletonURLWhitelist.clear();
|
||||
}
|
||||
|
||||
if (_avatarWhitelist.isEmpty()) {
|
||||
if (_slaveSharedData.skeletonURLWhitelist.isEmpty()) {
|
||||
qCDebug(avatars) << "All avatars are allowed.";
|
||||
} else {
|
||||
qCDebug(avatars) << "Avatars other than" << _avatarWhitelist << "will be replaced by" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
|
||||
qCDebug(avatars) << "Avatars other than" << _slaveSharedData.skeletonURLWhitelist << "will be replaced by" << (_slaveSharedData.skeletonReplacementURL.isEmpty() ? "default" : _slaveSharedData.skeletonReplacementURL.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,6 @@ private slots:
|
|||
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
|
||||
void start();
|
||||
|
||||
|
||||
private:
|
||||
AvatarMixerClientData* getOrCreateClientData(SharedNodePointer node);
|
||||
std::chrono::microseconds timeFrame(p_high_resolution_clock::time_point& timestamp);
|
||||
|
@ -69,11 +68,6 @@ private:
|
|||
void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
|
||||
|
||||
void manageIdentityData(const SharedNodePointer& node);
|
||||
bool isAvatarInWhitelist(const QUrl& url);
|
||||
|
||||
const QString REPLACEMENT_AVATAR_DEFAULT{ "" };
|
||||
QStringList _avatarWhitelist { };
|
||||
QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT };
|
||||
|
||||
void optionallyReplicatePacket(ReceivedMessage& message, const Node& node);
|
||||
|
||||
|
@ -83,7 +77,6 @@ private:
|
|||
float _trailingMixRatio { 0.0f };
|
||||
float _throttlingRatio { 0.0f };
|
||||
|
||||
|
||||
int _sumListeners { 0 };
|
||||
int _numStatFrames { 0 };
|
||||
int _numTightLoopFrames { 0 };
|
||||
|
@ -126,9 +119,8 @@ private:
|
|||
|
||||
RateCounter<> _loopRate; // this is the rate that the main thread tight loop runs
|
||||
|
||||
|
||||
AvatarMixerSlavePool _slavePool;
|
||||
|
||||
SlaveSharedData _slaveSharedData;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarMixer_h
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
#include <DependencyManager.h>
|
||||
#include <NodeList.h>
|
||||
|
||||
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID) :
|
||||
NodeData(nodeID)
|
||||
#include "AvatarMixerSlave.h"
|
||||
|
||||
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
|
||||
NodeData(nodeID, nodeLocalID)
|
||||
{
|
||||
// in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID
|
||||
_avatar->setID(nodeID);
|
||||
|
@ -47,7 +49,7 @@ void AvatarMixerClientData::queuePacket(QSharedPointer<ReceivedMessage> message,
|
|||
_packetQueue.push(message);
|
||||
}
|
||||
|
||||
int AvatarMixerClientData::processPackets() {
|
||||
int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData) {
|
||||
int packetsProcessed = 0;
|
||||
SharedNodePointer node = _packetQueue.node;
|
||||
assert(_packetQueue.empty() || node);
|
||||
|
@ -62,6 +64,9 @@ int AvatarMixerClientData::processPackets() {
|
|||
case PacketType::AvatarData:
|
||||
parseData(*packet);
|
||||
break;
|
||||
case PacketType::SetAvatarTraits:
|
||||
processSetTraitsMessage(*packet, slaveSharedData, *node);
|
||||
break;
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
@ -87,6 +92,113 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) {
|
|||
// compute the offset to the data payload
|
||||
return _avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()));
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||
const SlaveSharedData& slaveSharedData, Node& sendingNode) {
|
||||
// pull the trait version from the message
|
||||
AvatarTraits::TraitVersion packetTraitVersion;
|
||||
message.readPrimitive(&packetTraitVersion);
|
||||
|
||||
bool anyTraitsChanged = false;
|
||||
|
||||
while (message.getBytesLeftToRead() > 0) {
|
||||
// for each trait in the packet, apply it if the trait version is newer than what we have
|
||||
|
||||
AvatarTraits::TraitType traitType;
|
||||
message.readPrimitive(&traitType);
|
||||
|
||||
if (AvatarTraits::isSimpleTrait(traitType)) {
|
||||
AvatarTraits::TraitWireSize traitSize;
|
||||
message.readPrimitive(&traitSize);
|
||||
|
||||
if (packetTraitVersion > _lastReceivedTraitVersions[traitType]) {
|
||||
_avatar->processTrait(traitType, message.read(traitSize));
|
||||
_lastReceivedTraitVersions[traitType] = packetTraitVersion;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
anyTraitsChanged = true;
|
||||
} else {
|
||||
message.seek(message.getPosition() + traitSize);
|
||||
}
|
||||
} else {
|
||||
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
AvatarTraits::TraitWireSize traitSize;
|
||||
message.readPrimitive(&traitSize);
|
||||
|
||||
auto& instanceVersionRef = _lastReceivedTraitVersions.getInstanceValueRef(traitType, instanceID);
|
||||
|
||||
if (packetTraitVersion > instanceVersionRef) {
|
||||
if (traitSize == AvatarTraits::DELETED_TRAIT_SIZE) {
|
||||
_avatar->processDeletedTraitInstance(traitType, instanceID);
|
||||
|
||||
// to track a deleted instance but keep version information
|
||||
// the avatar mixer uses the negative value of the sent version
|
||||
instanceVersionRef = -packetTraitVersion;
|
||||
} else {
|
||||
_avatar->processTraitInstance(traitType, instanceID, message.read(traitSize));
|
||||
instanceVersionRef = packetTraitVersion;
|
||||
}
|
||||
|
||||
anyTraitsChanged = true;
|
||||
} else {
|
||||
message.seek(message.getPosition() + traitSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyTraitsChanged) {
|
||||
_lastReceivedTraitsChange = std::chrono::steady_clock::now();
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedData &slaveSharedData, Node& sendingNode,
|
||||
AvatarTraits::TraitVersion traitVersion) {
|
||||
const auto& whitelist = slaveSharedData.skeletonURLWhitelist;
|
||||
|
||||
if (!whitelist.isEmpty()) {
|
||||
bool inWhitelist = false;
|
||||
auto avatarURL = _avatar->getSkeletonModelURL();
|
||||
|
||||
// The avatar is in the whitelist if:
|
||||
// 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND
|
||||
// 2. The avatar's URL's path starts with the path of that same URL in the whitelist
|
||||
for (const auto& whiteListedPrefix : whitelist) {
|
||||
auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix);
|
||||
// check if this script URL matches the whitelist domain and, optionally, is beneath the path
|
||||
if (avatarURL.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 &&
|
||||
avatarURL.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) {
|
||||
inWhitelist = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inWhitelist) {
|
||||
// make sure we're not unecessarily overriding the default avatar with the default avatar
|
||||
if (_avatar->getWireSafeSkeletonModelURL() != slaveSharedData.skeletonReplacementURL) {
|
||||
// we need to change this avatar's skeleton URL, and send them a traits packet informing them of the change
|
||||
qDebug() << "Overwriting avatar URL" << _avatar->getWireSafeSkeletonModelURL()
|
||||
<< "to replacement" << slaveSharedData.skeletonReplacementURL << "for" << sendingNode.getUUID();
|
||||
_avatar->setSkeletonModelURL(slaveSharedData.skeletonReplacementURL);
|
||||
|
||||
auto packet = NLPacket::create(PacketType::SetAvatarTraits, -1, true);
|
||||
|
||||
// the returned set traits packet uses the trait version from the incoming packet
|
||||
// so the client knows they should not overwrite if they have since changed the trait
|
||||
_avatar->packTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion);
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
nodeList->sendPacket(std::move(packet), sendingNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) const {
|
||||
// return the matching PacketSequenceNumber, or the default if we don't have it
|
||||
auto nodeMatch = _lastBroadcastTimes.find(nodeUUID);
|
||||
|
@ -116,6 +228,9 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe
|
|||
killPacket->writePrimitive(KillAvatarReason::YourAvatarEnteredTheirBubble);
|
||||
}
|
||||
setLastBroadcastTime(other->getUUID(), 0);
|
||||
|
||||
resetSentTraitData(other->getLocalID());
|
||||
|
||||
DependencyManager::get<NodeList>()->sendPacket(std::move(killPacket), *self);
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +241,11 @@ void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self,
|
|||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::resetSentTraitData(Node::LocalID nodeLocalID) {
|
||||
_lastSentTraitsTimestamps[nodeLocalID] = TraitsCheckTimestamp();
|
||||
_sentTraitVersions[nodeLocalID].reset();
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::readViewFrustumPacket(const QByteArray& message) {
|
||||
_currentViewFrustums.clear();
|
||||
|
||||
|
@ -164,3 +284,20 @@ void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
|
|||
jsonObject["recent_other_av_in_view"] = _recentOtherAvatarsInView;
|
||||
jsonObject["recent_other_av_out_of_view"] = _recentOtherAvatarsOutOfView;
|
||||
}
|
||||
|
||||
AvatarMixerClientData::TraitsCheckTimestamp AvatarMixerClientData::getLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar) const {
|
||||
auto it = _lastSentTraitsTimestamps.find(otherAvatar);
|
||||
|
||||
if (it != _lastSentTraitsTimestamps.end()) {
|
||||
return it->second;
|
||||
} else {
|
||||
return TraitsCheckTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarMixerClientData::cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID) {
|
||||
removeLastBroadcastSequenceNumber(nodeUUID);
|
||||
removeLastBroadcastTime(nodeUUID);
|
||||
_lastSentTraitsTimestamps.erase(nodeLocalID);
|
||||
_sentTraitVersions.erase(nodeLocalID);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
#include <QtCore/QUrl>
|
||||
|
||||
#include <AvatarData.h>
|
||||
#include <AssociatedTraitValues.h>
|
||||
#include <NodeData.h>
|
||||
#include <NumericalConstants.h>
|
||||
#include <udt/PacketHeaders.h>
|
||||
|
@ -33,10 +34,12 @@
|
|||
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
|
||||
const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
|
||||
|
||||
struct SlaveSharedData;
|
||||
|
||||
class AvatarMixerClientData : public NodeData {
|
||||
Q_OBJECT
|
||||
public:
|
||||
AvatarMixerClientData(const QUuid& nodeID = QUuid());
|
||||
AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
|
||||
virtual ~AvatarMixerClientData() {}
|
||||
using HRCTime = p_high_resolution_clock::time_point;
|
||||
|
||||
|
@ -54,10 +57,7 @@ public:
|
|||
void setLastBroadcastTime(const QUuid& nodeUUID, uint64_t broadcastTime) { _lastBroadcastTimes[nodeUUID] = broadcastTime; }
|
||||
Q_INVOKABLE void removeLastBroadcastTime(const QUuid& nodeUUID) { _lastBroadcastTimes.erase(nodeUUID); }
|
||||
|
||||
Q_INVOKABLE void cleanupKilledNode(const QUuid& nodeUUID) {
|
||||
removeLastBroadcastSequenceNumber(nodeUUID);
|
||||
removeLastBroadcastTime(nodeUUID);
|
||||
}
|
||||
Q_INVOKABLE void cleanupKilledNode(const QUuid& nodeUUID, Node::LocalID nodeLocalID);
|
||||
|
||||
uint16_t getLastReceivedSequenceNumber() const { return _lastReceivedSequenceNumber; }
|
||||
|
||||
|
@ -65,8 +65,6 @@ public:
|
|||
void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); }
|
||||
bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; }
|
||||
void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; }
|
||||
bool getAvatarSkeletonModelUrlMustChange() const { return _avatarSkeletonModelUrlMustChange; }
|
||||
void setAvatarSkeletonModelUrlMustChange(bool set = true) { _avatarSkeletonModelUrlMustChange = set; }
|
||||
|
||||
void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; }
|
||||
void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; }
|
||||
|
@ -118,7 +116,26 @@ public:
|
|||
QVector<JointData>& getLastOtherAvatarSentJoints(QUuid otherAvatar) { return _lastOtherAvatarSentJoints[otherAvatar]; }
|
||||
|
||||
void queuePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node);
|
||||
int processPackets(); // returns number of packets processed
|
||||
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
|
||||
|
||||
void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode);
|
||||
void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode,
|
||||
AvatarTraits::TraitVersion traitVersion);
|
||||
|
||||
using TraitsCheckTimestamp = std::chrono::steady_clock::time_point;
|
||||
|
||||
TraitsCheckTimestamp getLastReceivedTraitsChange() const { return _lastReceivedTraitsChange; }
|
||||
|
||||
AvatarTraits::TraitVersions& getLastReceivedTraitVersions() { return _lastReceivedTraitVersions; }
|
||||
const AvatarTraits::TraitVersions& getLastReceivedTraitVersions() const { return _lastReceivedTraitVersions; }
|
||||
|
||||
TraitsCheckTimestamp getLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar) const;
|
||||
void setLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar, TraitsCheckTimestamp sendPoint)
|
||||
{ _lastSentTraitsTimestamps[otherAvatar] = sendPoint; }
|
||||
|
||||
AvatarTraits::TraitVersions& getLastSentTraitVersions(Node::LocalID otherAvatar) { return _sentTraitVersions[otherAvatar]; }
|
||||
|
||||
void resetSentTraitData(Node::LocalID nodeID);
|
||||
|
||||
private:
|
||||
struct PacketQueue : public std::queue<QSharedPointer<ReceivedMessage>> {
|
||||
|
@ -156,6 +173,12 @@ private:
|
|||
int _recentOtherAvatarsOutOfView { 0 };
|
||||
QString _baseDisplayName{}; // The santized key used in determinging unique sessionDisplayName, so that we can remove from dictionary.
|
||||
bool _requestsDomainListData { false };
|
||||
|
||||
AvatarTraits::TraitVersions _lastReceivedTraitVersions;
|
||||
TraitsCheckTimestamp _lastReceivedTraitsChange;
|
||||
|
||||
std::unordered_map<Node::LocalID, TraitsCheckTimestamp> _lastSentTraitsTimestamps;
|
||||
std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions> _sentTraitVersions;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarMixerClientData_h
|
||||
|
|
|
@ -59,7 +59,7 @@ void AvatarMixerSlave::processIncomingPackets(const SharedNodePointer& node) {
|
|||
auto nodeData = dynamic_cast<AvatarMixerClientData*>(node->getLinkedData());
|
||||
if (nodeData) {
|
||||
_stats.nodesProcessed++;
|
||||
_stats.packetsProcessed += nodeData->processPackets();
|
||||
_stats.packetsProcessed += nodeData->processPackets(*_sharedData);
|
||||
}
|
||||
auto end = usecTimestampNow();
|
||||
_stats.processIncomingPacketsElapsedTime += (end - start);
|
||||
|
@ -79,6 +79,107 @@ int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData,
|
|||
}
|
||||
}
|
||||
|
||||
qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
||||
const AvatarMixerClientData* sendingNodeData,
|
||||
NLPacketList& traitsPacketList) {
|
||||
|
||||
auto otherNodeLocalID = sendingNodeData->getNodeLocalID();
|
||||
|
||||
// Perform a simple check with two server clock time points
|
||||
// to see if there is any new traits data for this avatar that we need to send
|
||||
auto timeOfLastTraitsSent = listeningNodeData->getLastOtherAvatarTraitsSendPoint(otherNodeLocalID);
|
||||
auto timeOfLastTraitsChange = sendingNodeData->getLastReceivedTraitsChange();
|
||||
|
||||
qint64 bytesWritten = 0;
|
||||
|
||||
if (timeOfLastTraitsChange > timeOfLastTraitsSent) {
|
||||
// there is definitely new traits data to send
|
||||
|
||||
// add the avatar ID to mark the beginning of traits for this avatar
|
||||
bytesWritten += traitsPacketList.write(sendingNodeData->getNodeID().toRfc4122());
|
||||
|
||||
auto sendingAvatar = sendingNodeData->getAvatarSharedPointer();
|
||||
|
||||
// compare trait versions so we can see what exactly needs to go out
|
||||
auto& lastSentVersions = listeningNodeData->getLastSentTraitVersions(otherNodeLocalID);
|
||||
const auto& lastReceivedVersions = sendingNodeData->getLastReceivedTraitVersions();
|
||||
|
||||
auto simpleReceivedIt = lastReceivedVersions.simpleCBegin();
|
||||
while (simpleReceivedIt != lastReceivedVersions.simpleCEnd()) {
|
||||
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(lastReceivedVersions.simpleCBegin(),
|
||||
simpleReceivedIt));
|
||||
|
||||
auto lastReceivedVersion = *simpleReceivedIt;
|
||||
auto& lastSentVersionRef = lastSentVersions[traitType];
|
||||
|
||||
if (lastReceivedVersions[traitType] > lastSentVersionRef) {
|
||||
// there is an update to this trait, add it to the traits packet
|
||||
bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion);
|
||||
|
||||
// update the last sent version
|
||||
lastSentVersionRef = lastReceivedVersion;
|
||||
}
|
||||
|
||||
++simpleReceivedIt;
|
||||
}
|
||||
|
||||
// enumerate the received instanced trait versions
|
||||
auto instancedReceivedIt = lastReceivedVersions.instancedCBegin();
|
||||
while (instancedReceivedIt != lastReceivedVersions.instancedCEnd()) {
|
||||
auto traitType = instancedReceivedIt->traitType;
|
||||
|
||||
// get or create the sent trait versions for this trait type
|
||||
auto& sentIDValuePairs = lastSentVersions.getInstanceIDValuePairs(traitType);
|
||||
|
||||
// enumerate each received instance
|
||||
for (auto& receivedInstance : instancedReceivedIt->instances) {
|
||||
auto instanceID = receivedInstance.id;
|
||||
const auto receivedVersion = receivedInstance.value;
|
||||
|
||||
// to track deletes and maintain version information for traits
|
||||
// the mixer stores the negative value of the received version when a trait instance is deleted
|
||||
bool isDeleted = receivedVersion < 0;
|
||||
const auto absoluteReceivedVersion = std::abs(receivedVersion);
|
||||
|
||||
// look for existing sent version for this instance
|
||||
auto sentInstanceIt = std::find_if(sentIDValuePairs.begin(), sentIDValuePairs.end(),
|
||||
[instanceID](auto& sentInstance)
|
||||
{
|
||||
return sentInstance.id == instanceID;
|
||||
});
|
||||
|
||||
if (!isDeleted && (sentInstanceIt == sentIDValuePairs.end() || receivedVersion > sentInstanceIt->value)) {
|
||||
// this instance version exists and has never been sent or is newer so we need to send it
|
||||
bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion);
|
||||
|
||||
if (sentInstanceIt != sentIDValuePairs.end()) {
|
||||
sentInstanceIt->value = receivedVersion;
|
||||
} else {
|
||||
sentIDValuePairs.emplace_back(instanceID, receivedVersion);
|
||||
}
|
||||
} else if (isDeleted && sentInstanceIt != sentIDValuePairs.end() && absoluteReceivedVersion > sentInstanceIt->value) {
|
||||
// this instance version was deleted and we haven't sent the delete to this client yet
|
||||
bytesWritten += AvatarTraits::packInstancedTraitDelete(traitType, instanceID, traitsPacketList, absoluteReceivedVersion);
|
||||
|
||||
// update the last sent version for this trait instance to the absolute value of the deleted version
|
||||
sentInstanceIt->value = absoluteReceivedVersion;
|
||||
}
|
||||
}
|
||||
|
||||
++instancedReceivedIt;
|
||||
}
|
||||
|
||||
// write a null trait type to mark the end of trait data for this avatar
|
||||
bytesWritten += traitsPacketList.writePrimitive(AvatarTraits::NullTrait);
|
||||
|
||||
// since we send all traits for this other avatar, update the time of last traits sent
|
||||
// to match the time of last traits change
|
||||
listeningNodeData->setLastOtherAvatarTraitsSendPoint(otherNodeLocalID, timeOfLastTraitsChange);
|
||||
}
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode) {
|
||||
if (AvatarMixer::shouldReplicateTo(agentNode, destinationNode)) {
|
||||
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray(true);
|
||||
|
@ -138,6 +239,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
// keep track of outbound data rate specifically for avatar data
|
||||
int numAvatarDataBytes = 0;
|
||||
int identityBytesSent = 0;
|
||||
int traitBytesSent = 0;
|
||||
|
||||
// max number of avatarBytes per frame
|
||||
auto maxAvatarBytesPerFrame = (_maxKbpsPerNode * BYTES_PER_KILOBIT) / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
|
||||
|
@ -326,6 +428,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
// loop through our sorted avatars and allocate our bandwidth to them accordingly
|
||||
|
||||
int remainingAvatars = (int)sortedAvatars.size();
|
||||
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
|
||||
while (!sortedAvatars.empty()) {
|
||||
const auto avatarData = sortedAvatars.top().getAvatar();
|
||||
sortedAvatars.pop();
|
||||
|
@ -392,11 +495,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
|
||||
quint64 start = usecTimestampNow();
|
||||
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
|
||||
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
|
||||
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition,
|
||||
&lastSentJointsForOther);
|
||||
quint64 end = usecTimestampNow();
|
||||
_stats.toByteArrayElapsedTime += (end - start);
|
||||
|
||||
auto maxAvatarDataBytes = avatarPacketList->getMaxSegmentSize() - NUM_BYTES_RFC4122_UUID;
|
||||
static auto maxAvatarDataBytes = avatarPacketList->getMaxSegmentSize() - NUM_BYTES_RFC4122_UUID;
|
||||
if (bytes.size() > maxAvatarDataBytes) {
|
||||
qCWarning(avatars) << "otherAvatar.toByteArray() for" << otherNode->getUUID()
|
||||
<< "resulted in very large buffer of" << bytes.size() << "bytes - dropping facial data";
|
||||
|
@ -445,6 +549,11 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
|
||||
quint64 endAvatarDataPacking = usecTimestampNow();
|
||||
_stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking);
|
||||
|
||||
// use helper to add any changed traits to our packet list
|
||||
traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList);
|
||||
|
||||
traitsPacketList->getDataSize();
|
||||
}
|
||||
|
||||
quint64 startPacketSending = usecTimestampNow();
|
||||
|
@ -461,6 +570,14 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
|||
// record the bytes sent for other avatar data in the AvatarMixerClientData
|
||||
nodeData->recordSentAvatarData(numAvatarDataBytes);
|
||||
|
||||
// close the current traits packet list
|
||||
traitsPacketList->closeCurrentPacket();
|
||||
|
||||
if (traitsPacketList->getNumPackets() >= 1) {
|
||||
// send the traits packet list
|
||||
nodeList->sendPacketList(std::move(traitsPacketList), *node);
|
||||
}
|
||||
|
||||
// record the number of avatars held back this frame
|
||||
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
|
||||
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
|
||||
|
|
|
@ -78,11 +78,16 @@ public:
|
|||
jobElapsedTime += rhs.jobElapsedTime;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
struct SlaveSharedData {
|
||||
QStringList skeletonURLWhitelist;
|
||||
QUrl skeletonReplacementURL;
|
||||
};
|
||||
|
||||
class AvatarMixerSlave {
|
||||
public:
|
||||
AvatarMixerSlave(SlaveSharedData* sharedData) : _sharedData(sharedData) {};
|
||||
using ConstIter = NodeList::const_iterator;
|
||||
|
||||
void configure(ConstIter begin, ConstIter end);
|
||||
|
@ -99,6 +104,10 @@ private:
|
|||
int sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
|
||||
int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode);
|
||||
|
||||
qint64 addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
||||
const AvatarMixerClientData* sendingNodeData,
|
||||
NLPacketList& traitsPacketList);
|
||||
|
||||
void broadcastAvatarDataToAgent(const SharedNodePointer& node);
|
||||
void broadcastAvatarDataToDownstreamMixer(const SharedNodePointer& node);
|
||||
|
||||
|
@ -111,6 +120,7 @@ private:
|
|||
float _throttlingRatio { 0.0f };
|
||||
|
||||
AvatarMixerSlaveStats _stats;
|
||||
SlaveSharedData* _sharedData;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarMixerSlave_h
|
||||
|
|
|
@ -168,7 +168,7 @@ void AvatarMixerSlavePool::resize(int numThreads) {
|
|||
if (numThreads > _numThreads) {
|
||||
// start new slaves
|
||||
for (int i = 0; i < numThreads - _numThreads; ++i) {
|
||||
auto slave = new AvatarMixerSlaveThread(*this);
|
||||
auto slave = new AvatarMixerSlaveThread(*this, _slaveSharedData);
|
||||
slave->start();
|
||||
_slaves.emplace_back(slave);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,8 @@ class AvatarMixerSlaveThread : public QThread, public AvatarMixerSlave {
|
|||
using Lock = std::unique_lock<Mutex>;
|
||||
|
||||
public:
|
||||
AvatarMixerSlaveThread(AvatarMixerSlavePool& pool) : _pool(pool) {}
|
||||
AvatarMixerSlaveThread(AvatarMixerSlavePool& pool, SlaveSharedData* slaveSharedData) :
|
||||
AvatarMixerSlave(slaveSharedData), _pool(pool) {};
|
||||
|
||||
void run() override final;
|
||||
|
||||
|
@ -59,7 +60,8 @@ class AvatarMixerSlavePool {
|
|||
public:
|
||||
using ConstIter = NodeList::const_iterator;
|
||||
|
||||
AvatarMixerSlavePool(int numThreads = QThread::idealThreadCount()) { setNumThreads(numThreads); }
|
||||
AvatarMixerSlavePool(SlaveSharedData* slaveSharedData, int numThreads = QThread::idealThreadCount()) :
|
||||
_slaveSharedData(slaveSharedData) { setNumThreads(numThreads); }
|
||||
~AvatarMixerSlavePool() { resize(0); }
|
||||
|
||||
// Jobs the slave pool can do...
|
||||
|
@ -98,6 +100,8 @@ private:
|
|||
Queue _queue;
|
||||
ConstIter _begin;
|
||||
ConstIter _end;
|
||||
|
||||
SlaveSharedData* _slaveSharedData;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarMixerSlavePool_h
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// ScriptableAvatar.cpp
|
||||
//
|
||||
// assignment-client/src/avatars
|
||||
//
|
||||
// Created by Clement on 7/22/14.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
|
@ -16,9 +16,13 @@
|
|||
#include <glm/gtx/transform.hpp>
|
||||
|
||||
#include <shared/QtHelpers.h>
|
||||
#include <GLMHelpers.h>
|
||||
#include <AnimUtil.h>
|
||||
#include <ClientTraitsHandler.h>
|
||||
#include <GLMHelpers.h>
|
||||
|
||||
ScriptableAvatar::ScriptableAvatar() {
|
||||
_clientTraitsHandler = std::unique_ptr<ClientTraitsHandler>(new ClientTraitsHandler(this));
|
||||
}
|
||||
|
||||
QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) {
|
||||
_globalPosition = getWorldPosition();
|
||||
|
@ -61,6 +65,7 @@ AnimationDetails ScriptableAvatar::getAnimationDetails() {
|
|||
void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
|
||||
_bind.reset();
|
||||
_animSkeleton.reset();
|
||||
|
||||
AvatarData::setSkeletonModelURL(skeletonModelURL);
|
||||
}
|
||||
|
||||
|
@ -137,4 +142,6 @@ void ScriptableAvatar::update(float deltatime) {
|
|||
_animation.clear();
|
||||
}
|
||||
}
|
||||
|
||||
_clientTraitsHandler->sendChangedTraitsToMixer();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// ScriptableAvatar.h
|
||||
//
|
||||
// assignment-client/src/avatars
|
||||
//
|
||||
// Created by Clement on 7/22/14.
|
||||
// Copyright 2014 High Fidelity, Inc.
|
||||
|
@ -123,7 +123,9 @@
|
|||
class ScriptableAvatar : public AvatarData, public Dependency {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
|
||||
ScriptableAvatar();
|
||||
|
||||
/**jsdoc
|
||||
* @function Avatar.startAnimation
|
||||
* @param {string} url
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include <NetworkingConstants.h>
|
||||
#include <AddressManager.h>
|
||||
|
||||
#include "../AssignmentDynamicFactory.h"
|
||||
#include "AssignmentParentFinder.h"
|
||||
#include "EntityNodeData.h"
|
||||
#include "EntityServerConsts.h"
|
||||
|
@ -42,6 +43,9 @@ EntityServer::EntityServer(ReceivedMessage& message) :
|
|||
DependencyManager::set<ResourceCacheSharedItems>();
|
||||
DependencyManager::set<ScriptCache>();
|
||||
|
||||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||
DependencyManager::set<AssignmentDynamicFactory>();
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd,
|
||||
PacketType::EntityClone,
|
||||
|
@ -71,6 +75,8 @@ EntityServer::~EntityServer() {
|
|||
void EntityServer::aboutToFinish() {
|
||||
DependencyManager::get<ResourceManager>()->cleanup();
|
||||
|
||||
DependencyManager::destroy<AssignmentDynamicFactory>();
|
||||
|
||||
OctreeServer::aboutToFinish();
|
||||
}
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ bool EntityTreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, O
|
|||
// Send EntityQueryInitialResultsComplete reliable packet ...
|
||||
auto initialCompletion = NLPacket::create(PacketType::EntityQueryInitialResultsComplete,
|
||||
sizeof(OCTREE_PACKET_SEQUENCE), true);
|
||||
initialCompletion->writePrimitive(OCTREE_PACKET_SEQUENCE(nodeData->getSequenceNumber() - 1U));
|
||||
initialCompletion->writePrimitive(OCTREE_PACKET_SEQUENCE(nodeData->getSequenceNumber()));
|
||||
DependencyManager::get<NodeList>()->sendPacket(std::move(initialCompletion), *node);
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
#include <plugins/CodecPlugin.h>
|
||||
#include <plugins/PluginManager.h>
|
||||
#include <ResourceManager.h>
|
||||
#include <ResourceScriptingInterface.h>
|
||||
#include <ScriptCache.h>
|
||||
#include <ScriptEngines.h>
|
||||
#include <SoundCacheScriptingInterface.h>
|
||||
|
@ -32,6 +33,7 @@
|
|||
|
||||
#include <EntityScriptClient.h> // for EntityScriptServerServices
|
||||
|
||||
#include "../AssignmentDynamicFactory.h"
|
||||
#include "EntityScriptServerLogging.h"
|
||||
#include "../entities/AssignmentParentFinder.h"
|
||||
|
||||
|
@ -55,7 +57,11 @@ int EntityScriptServer::_entitiesScriptEngineCount = 0;
|
|||
EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssignment(message) {
|
||||
qInstallMessageHandler(messageHandler);
|
||||
|
||||
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
|
||||
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
|
||||
DependencyManager::set<AssignmentDynamicFactory>();
|
||||
|
||||
DependencyManager::set<EntityScriptingInterface>(false)->setPacketSender(&_entityEditSender);
|
||||
DependencyManager::set<ResourceScriptingInterface>();
|
||||
|
||||
DependencyManager::set<ResourceManager>();
|
||||
DependencyManager::set<PluginManager>();
|
||||
|
@ -81,9 +87,6 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig
|
|||
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
|
||||
|
||||
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket");
|
||||
packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar");
|
||||
packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket");
|
||||
|
||||
packetReceiver.registerListener(PacketType::ReloadEntityServerScript, this, "handleReloadEntityServerScriptPacket");
|
||||
packetReceiver.registerListener(PacketType::EntityScriptGetStatus, this, "handleEntityScriptGetStatusPacket");
|
||||
|
@ -458,8 +461,11 @@ void EntityScriptServer::resetEntitiesScriptEngine() {
|
|||
auto newEngineSP = qSharedPointerCast<EntitiesScriptEngineProvider>(newEngine);
|
||||
DependencyManager::get<EntityScriptingInterface>()->setEntitiesScriptEngine(newEngineSP);
|
||||
|
||||
disconnect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated,
|
||||
this, &EntityScriptServer::updateEntityPPS);
|
||||
if (_entitiesScriptEngine) {
|
||||
disconnect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated,
|
||||
this, &EntityScriptServer::updateEntityPPS);
|
||||
}
|
||||
|
||||
_entitiesScriptEngine.swap(newEngine);
|
||||
connect(_entitiesScriptEngine.data(), &ScriptEngine::entityScriptDetailsUpdated,
|
||||
this, &EntityScriptServer::updateEntityPPS);
|
||||
|
@ -490,6 +496,21 @@ void EntityScriptServer::shutdownScriptEngine() {
|
|||
_shuttingDown = true;
|
||||
|
||||
clear(); // always clear() on shutdown
|
||||
|
||||
auto scriptEngines = DependencyManager::get<ScriptEngines>();
|
||||
scriptEngines->shutdownScripting();
|
||||
|
||||
_entitiesScriptEngine.clear();
|
||||
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
// our entity tree is going to go away so tell that to the EntityScriptingInterface
|
||||
entityScriptingInterface->setEntityTree(nullptr);
|
||||
|
||||
// Should always be true as they are singletons.
|
||||
if (entityScriptingInterface->getPacketSender() == &_entityEditSender) {
|
||||
// The packet sender is about to go away.
|
||||
entityScriptingInterface->setPacketSender(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void EntityScriptServer::addingEntity(const EntityItemID& entityID) {
|
||||
|
@ -562,24 +583,19 @@ void EntityScriptServer::handleOctreePacket(QSharedPointer<ReceivedMessage> mess
|
|||
void EntityScriptServer::aboutToFinish() {
|
||||
shutdownScriptEngine();
|
||||
|
||||
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
|
||||
// our entity tree is going to go away so tell that to the EntityScriptingInterface
|
||||
entityScriptingInterface->setEntityTree(nullptr);
|
||||
|
||||
// Should always be true as they are singletons.
|
||||
if (entityScriptingInterface->getPacketSender() == &_entityEditSender) {
|
||||
// The packet sender is about to go away.
|
||||
entityScriptingInterface->setPacketSender(nullptr);
|
||||
}
|
||||
|
||||
DependencyManager::destroy<AssignmentDynamicFactory>();
|
||||
DependencyManager::destroy<AssignmentParentFinder>();
|
||||
|
||||
DependencyManager::get<ResourceManager>()->cleanup();
|
||||
|
||||
DependencyManager::destroy<PluginManager>();
|
||||
|
||||
DependencyManager::destroy<ResourceScriptingInterface>();
|
||||
DependencyManager::destroy<EntityScriptingInterface>();
|
||||
|
||||
// cleanup the AudioInjectorManager (and any still running injectors)
|
||||
DependencyManager::destroy<AudioInjectorManager>();
|
||||
|
||||
DependencyManager::destroy<ScriptEngines>();
|
||||
DependencyManager::destroy<EntityScriptServerServices>();
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ set(EXTERNAL_NAME serverless-content)
|
|||
|
||||
ExternalProject_Add(
|
||||
${EXTERNAL_NAME}
|
||||
URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC70v2.zip
|
||||
URL_MD5 35fcc8e635e71d0b00a08455a2582448
|
||||
URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC72.zip
|
||||
URL_MD5 b1d8faf9266bfbff88274a484911eb99
|
||||
CONFIGURE_COMMAND ""
|
||||
BUILD_COMMAND ""
|
||||
INSTALL_COMMAND ""
|
||||
|
|
|
@ -8,290 +8,224 @@
|
|||
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
#
|
||||
|
||||
function(global_append varName varValue)
|
||||
get_property(LOCAL_LIST GLOBAL PROPERTY ${varName})
|
||||
list(APPEND LOCAL_LIST ${varValue})
|
||||
set_property(GLOBAL PROPERTY ${varName} ${LOCAL_LIST})
|
||||
endfunction()
|
||||
|
||||
function(AUTOSCRIBE_SHADER SHADER_FILE)
|
||||
# Grab include files
|
||||
foreach(includeFile ${ARGN})
|
||||
list(APPEND SHADER_INCLUDE_FILES ${includeFile})
|
||||
endforeach()
|
||||
|
||||
foreach(SHADER_INCLUDE ${SHADER_INCLUDE_FILES})
|
||||
get_filename_component(INCLUDE_DIR ${SHADER_INCLUDE} PATH)
|
||||
list(APPEND SHADER_INCLUDES_PATHS ${INCLUDE_DIR})
|
||||
endforeach()
|
||||
|
||||
#Extract the unique include shader paths
|
||||
set(INCLUDES ${HIFI_LIBRARIES_SHADER_INCLUDE_FILES})
|
||||
#message(${TARGET_NAME} Hifi for includes ${INCLUDES})
|
||||
foreach(EXTRA_SHADER_INCLUDE ${INCLUDES})
|
||||
list(APPEND SHADER_INCLUDES_PATHS ${EXTRA_SHADER_INCLUDE})
|
||||
endforeach()
|
||||
|
||||
list(REMOVE_DUPLICATES SHADER_INCLUDES_PATHS)
|
||||
#message(ready for includes ${SHADER_INCLUDES_PATHS})
|
||||
|
||||
# make the scribe include arguments
|
||||
set(SCRIBE_INCLUDES)
|
||||
foreach(INCLUDE_PATH ${SHADER_INCLUDES_PATHS})
|
||||
set(SCRIBE_INCLUDES ${SCRIBE_INCLUDES} -I ${INCLUDE_PATH}/)
|
||||
endforeach()
|
||||
|
||||
# Define the final name of the generated shader file
|
||||
get_filename_component(SHADER_TARGET ${SHADER_FILE} NAME_WE)
|
||||
get_filename_component(SHADER_EXT ${SHADER_FILE} EXT)
|
||||
if(SHADER_EXT STREQUAL .slv)
|
||||
set(SHADER_TYPE vert)
|
||||
elseif(${SHADER_EXT} STREQUAL .slf)
|
||||
set(SHADER_TYPE frag)
|
||||
elseif(${SHADER_EXT} STREQUAL .slg)
|
||||
set(SHADER_TYPE geom)
|
||||
endif()
|
||||
file(MAKE_DIRECTORY "${SHADERS_DIR}/${SHADER_LIB}")
|
||||
set(SHADER_TARGET "${SHADERS_DIR}/${SHADER_LIB}/${SHADER_TARGET}.${SHADER_TYPE}")
|
||||
set(SCRIBE_COMMAND scribe)
|
||||
|
||||
# Target dependant Custom rule on the SHADER_FILE
|
||||
if (APPLE)
|
||||
set(GLPROFILE MAC_GL)
|
||||
elseif (ANDROID)
|
||||
set(GLPROFILE LINUX_GL)
|
||||
set(SCRIBE_COMMAND ${NATIVE_SCRIBE})
|
||||
elseif (UNIX)
|
||||
set(GLPROFILE LINUX_GL)
|
||||
else ()
|
||||
set(GLPROFILE PC_GL)
|
||||
endif()
|
||||
set(SCRIBE_ARGS -T ${SHADER_TYPE} -D GLPROFILE ${GLPROFILE} ${SCRIBE_INCLUDES} -o ${SHADER_TARGET} ${SHADER_FILE})
|
||||
add_custom_command(
|
||||
OUTPUT ${SHADER_TARGET}
|
||||
COMMAND ${SCRIBE_COMMAND} ${SCRIBE_ARGS}
|
||||
DEPENDS ${SHADER_FILE} ${SCRIBE_COMMAND} ${SHADER_INCLUDE_FILES}
|
||||
)
|
||||
|
||||
#output the generated file name
|
||||
set(AUTOSCRIBE_SHADER_RETURN ${SHADER_TARGET} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
macro(AUTOSCRIBE_APPEND_SHADER_SOURCES)
|
||||
if (NOT("${ARGN}" STREQUAL ""))
|
||||
set_property(GLOBAL PROPERTY ${TARGET_NAME}_SHADER_SOURCES "${ARGN}")
|
||||
global_append(GLOBAL_SHADER_LIBS ${TARGET_NAME})
|
||||
global_append(GLOBAL_SHADER_SOURCES "${ARGN}")
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
macro(AUTOSCRIBE_SHADER_LIB)
|
||||
unset(HIFI_LIBRARIES_SHADER_INCLUDE_FILES)
|
||||
set(SRC_FOLDER "${CMAKE_SOURCE_DIR}/libraries/${TARGET_NAME}/src")
|
||||
|
||||
file(GLOB_RECURSE SHADER_INCLUDE_FILES ${SRC_FOLDER}/*.slh)
|
||||
file(GLOB_RECURSE SHADER_VERTEX_FILES ${SRC_FOLDER}/*.slv)
|
||||
file(GLOB_RECURSE SHADER_FRAGMENT_FILES ${SRC_FOLDER}/*.slf)
|
||||
file(GLOB_RECURSE SHADER_GEOMETRY_FILES ${SRC_FOLDER}/*.slg)
|
||||
file(GLOB_RECURSE SHADER_COMPUTE_FILES ${SRC_FOLDER}/*.slc)
|
||||
|
||||
unset(SHADER_SOURCE_FILES)
|
||||
list(APPEND SHADER_SOURCE_FILES ${SHADER_VERTEX_FILES})
|
||||
list(APPEND SHADER_SOURCE_FILES ${SHADER_FRAGMENT_FILES})
|
||||
list(APPEND SHADER_SOURCE_FILES ${SHADER_GEOMETRY_FILES})
|
||||
list(APPEND SHADER_SOURCE_FILES ${SHADER_COMPUTE_FILES})
|
||||
|
||||
unset(LOCAL_SHADER_SOURCES)
|
||||
list(APPEND LOCAL_SHADER_SOURCES ${SHADER_SOURCE_FILES})
|
||||
list(APPEND LOCAL_SHADER_SOURCES ${SHADER_INCLUDE_FILES})
|
||||
|
||||
AUTOSCRIBE_APPEND_SHADER_SOURCES(${LOCAL_SHADER_SOURCES})
|
||||
|
||||
file(RELATIVE_PATH RELATIVE_LIBRARY_DIR_PATH ${CMAKE_CURRENT_SOURCE_DIR} "${HIFI_LIBRARY_DIR}")
|
||||
foreach(HIFI_LIBRARY ${ARGN})
|
||||
list(APPEND HIFI_LIBRARIES_SHADER_INCLUDE_FILES ${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src)
|
||||
endforeach()
|
||||
endmacro()
|
||||
|
||||
macro(AUTOSCRIBE_PROGRAM)
|
||||
set(oneValueArgs NAME VERTEX FRAGMENT GEOMETRY COMPUTE)
|
||||
cmake_parse_arguments(AUTOSCRIBE_PROGRAM "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
if (NOT (DEFINED AUTOSCRIBE_PROGRAM_NAME))
|
||||
message(FATAL_ERROR "Programs must have a name and both a vertex and fragment shader")
|
||||
endif()
|
||||
if (NOT (DEFINED AUTOSCRIBE_PROGRAM_VERTEX))
|
||||
set(AUTOSCRIBE_PROGRAM_VERTEX ${AUTOSCRIBE_PROGRAM_NAME})
|
||||
endif()
|
||||
if (NOT (DEFINED AUTOSCRIBE_PROGRAM_FRAGMENT))
|
||||
set(AUTOSCRIBE_PROGRAM_FRAGMENT ${AUTOSCRIBE_PROGRAM_NAME})
|
||||
endif()
|
||||
|
||||
if (NOT (${AUTOSCRIBE_PROGRAM_VERTEX} MATCHES ".*::.*"))
|
||||
set(AUTOSCRIBE_PROGRAM_VERTEX "vertex::${AUTOSCRIBE_PROGRAM_VERTEX}")
|
||||
endif()
|
||||
if (NOT (${AUTOSCRIBE_PROGRAM_FRAGMENT} MATCHES ".*::.*"))
|
||||
set(AUTOSCRIBE_PROGRAM_FRAGMENT "fragment::${AUTOSCRIBE_PROGRAM_FRAGMENT}")
|
||||
endif()
|
||||
|
||||
unset(AUTOSCRIBE_PROGRAM_MAP)
|
||||
list(APPEND AUTOSCRIBE_PROGRAM_MAP AUTOSCRIBE_PROGRAM_VERTEX)
|
||||
list(APPEND AUTOSCRIBE_PROGRAM_MAP ${AUTOSCRIBE_PROGRAM_VERTEX})
|
||||
list(APPEND AUTOSCRIBE_PROGRAM_MAP AUTOSCRIBE_PROGRAM_FRAGMENT)
|
||||
list(APPEND AUTOSCRIBE_PROGRAM_MAP ${AUTOSCRIBE_PROGRAM_FRAGMENT})
|
||||
global_append(${TARGET_NAME}_PROGRAMS ${AUTOSCRIBE_PROGRAM_NAME})
|
||||
set_property(GLOBAL PROPERTY ${AUTOSCRIBE_PROGRAM_NAME} "${AUTOSCRIBE_PROGRAM_MAP}")
|
||||
endmacro()
|
||||
|
||||
macro(unpack_map)
|
||||
set(MAP_VAR "${ARGN}")
|
||||
list(LENGTH MAP_VAR MAP_SIZE)
|
||||
MATH(EXPR MAP_ENTRY_RANGE "(${MAP_SIZE} / 2) - 1")
|
||||
foreach(INDEX RANGE ${MAP_ENTRY_RANGE})
|
||||
MATH(EXPR INDEX_NAME "${INDEX} * 2")
|
||||
MATH(EXPR INDEX_VAL "${INDEX_NAME} + 1")
|
||||
list(GET MAP_VAR ${INDEX_NAME} VAR_NAME)
|
||||
list(GET MAP_VAR ${INDEX_VAL} VAR_VAL)
|
||||
set(${VAR_NAME} ${VAR_VAL})
|
||||
macro(AUTOSCRIBE_SHADER)
|
||||
message(STATUS "Processing shader ${SHADER_FILE}")
|
||||
unset(SHADER_INCLUDE_FILES)
|
||||
# Grab include files
|
||||
foreach(includeFile ${ARGN})
|
||||
list(APPEND SHADER_INCLUDE_FILES ${includeFile})
|
||||
endforeach()
|
||||
endmacro()
|
||||
|
||||
macro(PROCESS_SHADER_FILE)
|
||||
AUTOSCRIBE_SHADER(${SHADER} ${ALL_SHADER_HEADERS} ${HIFI_LIBRARIES_SHADER_INCLUDE_FILES})
|
||||
file(TO_CMAKE_PATH "${AUTOSCRIBE_SHADER_RETURN}" AUTOSCRIBE_GENERATED_FILE)
|
||||
set_property(SOURCE ${AUTOSCRIBE_GENERATED_FILE} PROPERTY SKIP_AUTOMOC ON)
|
||||
source_group("Compiled/${SHADER_LIB}" FILES ${AUTOSCRIBE_GENERATED_FILE})
|
||||
set(REFLECTED_SHADER "${AUTOSCRIBE_GENERATED_FILE}.json")
|
||||
foreach(SHADER_INCLUDE ${SHADER_INCLUDE_FILES})
|
||||
get_filename_component(INCLUDE_DIR ${SHADER_INCLUDE} PATH)
|
||||
list(APPEND SHADER_INCLUDES_PATHS ${INCLUDE_DIR})
|
||||
endforeach()
|
||||
|
||||
list(REMOVE_DUPLICATES SHADER_INCLUDES_PATHS)
|
||||
#Extract the unique include shader paths
|
||||
set(INCLUDES ${HIFI_LIBRARIES_SHADER_INCLUDE_FILES})
|
||||
foreach(EXTRA_SHADER_INCLUDE ${INCLUDES})
|
||||
list(APPEND SHADER_INCLUDES_PATHS ${EXTRA_SHADER_INCLUDE})
|
||||
endforeach()
|
||||
|
||||
list(REMOVE_DUPLICATES SHADER_INCLUDES_PATHS)
|
||||
#message(ready for includes ${SHADER_INCLUDES_PATHS})
|
||||
|
||||
# make the scribe include arguments
|
||||
set(SCRIBE_INCLUDES)
|
||||
foreach(INCLUDE_PATH ${SHADER_INCLUDES_PATHS})
|
||||
set(SCRIBE_INCLUDES ${SCRIBE_INCLUDES} -I ${INCLUDE_PATH}/)
|
||||
endforeach()
|
||||
|
||||
# Define the final name of the generated shader file
|
||||
get_filename_component(SHADER_NAME ${SHADER_FILE} NAME_WE)
|
||||
get_filename_component(SHADER_EXT ${SHADER_FILE} EXT)
|
||||
if(SHADER_EXT STREQUAL .slv)
|
||||
set(SHADER_TYPE vert)
|
||||
elseif(${SHADER_EXT} STREQUAL .slf)
|
||||
set(SHADER_TYPE frag)
|
||||
elseif(${SHADER_EXT} STREQUAL .slg)
|
||||
set(SHADER_TYPE geom)
|
||||
endif()
|
||||
file(MAKE_DIRECTORY "${SHADERS_DIR}/${SHADER_LIB}")
|
||||
set(SHADER_TARGET "${SHADERS_DIR}/${SHADER_LIB}/${SHADER_NAME}.${SHADER_TYPE}")
|
||||
file(TO_CMAKE_PATH "${SHADER_TARGET}" COMPILED_SHADER)
|
||||
set(REFLECTED_SHADER "${COMPILED_SHADER}.json")
|
||||
|
||||
set(SCRIBE_ARGS -T ${SHADER_TYPE} -D GLPROFILE ${GLPROFILE} ${SCRIBE_INCLUDES} -o ${SHADER_TARGET} ${SHADER_FILE})
|
||||
|
||||
# Generate the frag/vert file
|
||||
add_custom_command(
|
||||
OUTPUT ${SHADER_TARGET}
|
||||
COMMAND ${SCRIBE_COMMAND} ${SCRIBE_ARGS}
|
||||
DEPENDS ${SHADER_FILE} ${SCRIBE_COMMAND} ${SHADER_INCLUDE_FILES})
|
||||
|
||||
# Generate the json reflection
|
||||
# FIXME move to spirv-cross for this task after we have spirv compatible shaders
|
||||
add_custom_command(
|
||||
OUTPUT ${REFLECTED_SHADER}
|
||||
COMMAND ${SHREFLECT_COMMAND} ${COMPILED_SHADER}
|
||||
DEPENDS ${SHREFLECT_DEPENDENCY} ${COMPILED_SHADER})
|
||||
|
||||
#output the generated file name
|
||||
source_group("Compiled/${SHADER_LIB}" FILES ${COMPILED_SHADER})
|
||||
set_property(SOURCE ${COMPILED_SHADER} PROPERTY SKIP_AUTOMOC ON)
|
||||
list(APPEND COMPILED_SHADERS ${COMPILED_SHADER})
|
||||
|
||||
source_group("Reflected/${SHADER_LIB}" FILES ${REFLECTED_SHADER})
|
||||
list(APPEND COMPILED_SHADERS ${AUTOSCRIBE_GENERATED_FILE})
|
||||
get_filename_component(ENUM_NAME ${SHADER} NAME_WE)
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "${ENUM_NAME} = ${SHADER_COUNT},\n")
|
||||
list(APPEND REFLECTED_SHADERS ${REFLECTED_SHADER})
|
||||
|
||||
string(CONCAT SHADER_QRC "${SHADER_QRC}" "<file alias=\"${SHADER_COUNT}\">${COMPILED_SHADER}</file>\n")
|
||||
string(CONCAT SHADER_QRC "${SHADER_QRC}" "<file alias=\"${SHADER_COUNT}_reflection\">${REFLECTED_SHADER}</file>\n")
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "${SHADER_NAME} = ${SHADER_COUNT},\n")
|
||||
|
||||
MATH(EXPR SHADER_COUNT "${SHADER_COUNT}+1")
|
||||
endmacro()
|
||||
|
||||
macro(AUTOSCRIBE_SHADER_FINISH)
|
||||
get_property(GLOBAL_SHADER_LIBS GLOBAL PROPERTY GLOBAL_SHADER_LIBS)
|
||||
list(REMOVE_DUPLICATES GLOBAL_SHADER_LIBS)
|
||||
set(SHADER_COUNT 0)
|
||||
set(PROGRAM_COUNT 0)
|
||||
macro(AUTOSCRIBE_SHADER_LIB)
|
||||
if (NOT ("${TARGET_NAME}" STREQUAL "shaders"))
|
||||
message(FATAL_ERROR "AUTOSCRIBE_SHADER_LIB can only be used by the shaders library")
|
||||
endif()
|
||||
|
||||
list(APPEND HIFI_LIBRARIES_SHADER_INCLUDE_FILES "${CMAKE_SOURCE_DIR}/libraries/${SHADER_LIB}/src")
|
||||
string(REGEX REPLACE "[-]" "_" SHADER_NAMESPACE ${SHADER_LIB})
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace ${SHADER_NAMESPACE} {\n")
|
||||
set(SRC_FOLDER "${CMAKE_SOURCE_DIR}/libraries/${ARGN}/src")
|
||||
|
||||
# Process the scribe headers
|
||||
file(GLOB_RECURSE SHADER_INCLUDE_FILES ${SRC_FOLDER}/*.slh)
|
||||
if(SHADER_INCLUDE_FILES)
|
||||
source_group("${SHADER_LIB}/Headers" FILES ${SHADER_INCLUDE_FILES})
|
||||
list(APPEND ALL_SHADER_HEADERS ${SHADER_INCLUDE_FILES})
|
||||
list(APPEND ALL_SCRIBE_SHADERS ${SHADER_INCLUDE_FILES})
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE SHADER_VERTEX_FILES ${SRC_FOLDER}/*.slv)
|
||||
if (SHADER_VERTEX_FILES)
|
||||
source_group("${SHADER_LIB}/Vertex" FILES ${SHADER_VERTEX_FILES})
|
||||
list(APPEND ALL_SCRIBE_SHADERS ${SHADER_VERTEX_FILES})
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace vertex { enum {\n")
|
||||
foreach(SHADER_FILE ${SHADER_VERTEX_FILES})
|
||||
AUTOSCRIBE_SHADER(${ALL_SHADER_HEADERS})
|
||||
endforeach()
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "}; } // vertex \n")
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE SHADER_FRAGMENT_FILES ${SRC_FOLDER}/*.slf)
|
||||
if (SHADER_FRAGMENT_FILES)
|
||||
source_group("${SHADER_LIB}/Fragment" FILES ${SHADER_FRAGMENT_FILES})
|
||||
list(APPEND ALL_SCRIBE_SHADERS ${SHADER_FRAGMENT_FILES})
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace fragment { enum {\n")
|
||||
foreach(SHADER_FILE ${SHADER_FRAGMENT_FILES})
|
||||
AUTOSCRIBE_SHADER(${ALL_SHADER_HEADERS})
|
||||
endforeach()
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "}; } // fragment \n")
|
||||
endif()
|
||||
|
||||
# FIXME add support for geometry, compute and tesselation shaders
|
||||
#file(GLOB_RECURSE SHADER_GEOMETRY_FILES ${SRC_FOLDER}/*.slg)
|
||||
#file(GLOB_RECURSE SHADER_COMPUTE_FILES ${SRC_FOLDER}/*.slc)
|
||||
|
||||
# the programs
|
||||
file(GLOB_RECURSE SHADER_PROGRAM_FILES ${SRC_FOLDER}/*.slp)
|
||||
if (SHADER_PROGRAM_FILES)
|
||||
source_group("${SHADER_LIB}/Program" FILES ${SHADER_PROGRAM_FILES})
|
||||
list(APPEND ALL_SCRIBE_SHADERS ${SHADER_PROGRAM_FILES})
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace program { enum {\n")
|
||||
foreach(PROGRAM_FILE ${SHADER_PROGRAM_FILES})
|
||||
get_filename_component(PROGRAM_NAME ${PROGRAM_FILE} NAME_WE)
|
||||
set(AUTOSCRIBE_PROGRAM_FRAGMENT ${PROGRAM_NAME})
|
||||
file(READ ${PROGRAM_FILE} PROGRAM_CONFIG)
|
||||
set(AUTOSCRIBE_PROGRAM_VERTEX ${PROGRAM_NAME})
|
||||
set(AUTOSCRIBE_PROGRAM_FRAGMENT ${PROGRAM_NAME})
|
||||
|
||||
if (NOT("${PROGRAM_CONFIG}" STREQUAL ""))
|
||||
string(REGEX MATCH ".*VERTEX +([_\\:A-Z0-9a-z]+)" MVERT ${PROGRAM_CONFIG})
|
||||
if (CMAKE_MATCH_1)
|
||||
set(AUTOSCRIBE_PROGRAM_VERTEX ${CMAKE_MATCH_1})
|
||||
endif()
|
||||
string(REGEX MATCH ".*FRAGMENT +([_:A-Z0-9a-z]+)" MFRAG ${PROGRAM_CONFIG})
|
||||
if (CMAKE_MATCH_1)
|
||||
set(AUTOSCRIBE_PROGRAM_FRAGMENT ${CMAKE_MATCH_1})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT (${AUTOSCRIBE_PROGRAM_VERTEX} MATCHES ".*::.*"))
|
||||
set(AUTOSCRIBE_PROGRAM_VERTEX "vertex::${AUTOSCRIBE_PROGRAM_VERTEX}")
|
||||
endif()
|
||||
if (NOT (${AUTOSCRIBE_PROGRAM_FRAGMENT} MATCHES ".*::.*"))
|
||||
set(AUTOSCRIBE_PROGRAM_FRAGMENT "fragment::${AUTOSCRIBE_PROGRAM_FRAGMENT}")
|
||||
endif()
|
||||
|
||||
set(PROGRAM_ENTRY "${PROGRAM_NAME} = (${AUTOSCRIBE_PROGRAM_VERTEX} << 16) | ${AUTOSCRIBE_PROGRAM_FRAGMENT},\n")
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "${PROGRAM_ENTRY}")
|
||||
string(CONCAT SHADER_PROGRAMS_ARRAY "${SHADER_PROGRAMS_ARRAY} ${SHADER_NAMESPACE}::program::${PROGRAM_NAME},\n")
|
||||
endforeach()
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "}; } // program \n")
|
||||
endif()
|
||||
|
||||
# Finish the shader enums
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "} // namespace ${SHADER_NAMESPACE}\n")
|
||||
#file(RELATIVE_PATH RELATIVE_LIBRARY_DIR_PATH ${CMAKE_CURRENT_SOURCE_DIR} "${HIFI_LIBRARY_DIR}")
|
||||
#foreach(HIFI_LIBRARY ${ARGN})
|
||||
#list(APPEND HIFI_LIBRARIES_SHADER_INCLUDE_FILES ${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src)
|
||||
#endforeach()
|
||||
#endif()
|
||||
endmacro()
|
||||
|
||||
macro(AUTOSCRIBE_SHADER_LIBS)
|
||||
set(SCRIBE_COMMAND scribe)
|
||||
set(SHREFLECT_COMMAND shreflect)
|
||||
set(SHREFLECT_DEPENDENCY shreflect)
|
||||
|
||||
# Target dependant Custom rule on the SHADER_FILE
|
||||
if (ANDROID)
|
||||
set(GLPROFILE LINUX_GL)
|
||||
set(SCRIBE_COMMAND ${NATIVE_SCRIBE})
|
||||
set(SHREFLECT_COMMAND ${NATIVE_SHREFLECT})
|
||||
unset(SHREFLECT_DEPENDENCY)
|
||||
else()
|
||||
if (APPLE)
|
||||
set(GLPROFILE MAC_GL)
|
||||
elseif(UNIX)
|
||||
set(GLPROFILE LINUX_GL)
|
||||
else()
|
||||
set(GLPROFILE PC_GL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Start the shader IDs
|
||||
set(SHADER_COUNT 1)
|
||||
set(SHADERS_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders")
|
||||
set(SHADER_ENUMS "")
|
||||
file(MAKE_DIRECTORY ${SHADERS_DIR})
|
||||
|
||||
unset(COMPILED_SHADERS)
|
||||
foreach(SHADER_LIB ${GLOBAL_SHADER_LIBS})
|
||||
get_property(LIB_SHADER_SOURCES GLOBAL PROPERTY ${SHADER_LIB}_SHADER_SOURCES)
|
||||
get_property(LIB_PROGRAMS GLOBAL PROPERTY ${SHADER_LIB}_PROGRAMS)
|
||||
list(REMOVE_DUPLICATES LIB_SHADER_SOURCES)
|
||||
string(REGEX REPLACE "[-]" "_" SHADER_NAMESPACE ${SHADER_LIB})
|
||||
list(APPEND HIFI_LIBRARIES_SHADER_INCLUDE_FILES "${CMAKE_SOURCE_DIR}/libraries/${SHADER_LIB}/src")
|
||||
|
||||
unset(VERTEX_SHADERS)
|
||||
unset(FRAGMENT_SHADERS)
|
||||
unset(SHADER_HEADERS)
|
||||
|
||||
foreach(SHADER_FILE ${LIB_SHADER_SOURCES})
|
||||
if (SHADER_FILE MATCHES ".*\\.slv")
|
||||
list(APPEND VERTEX_SHADERS ${SHADER_FILE})
|
||||
elseif (SHADER_FILE MATCHES ".*\\.slf")
|
||||
list(APPEND FRAGMENT_SHADERS ${SHADER_FILE})
|
||||
elseif (SHADER_FILE MATCHES ".*\\.slh")
|
||||
list(APPEND SHADER_HEADERS ${SHADER_FILE})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (DEFINED SHADER_HEADERS)
|
||||
list(REMOVE_DUPLICATES SHADER_HEADERS)
|
||||
source_group("${SHADER_LIB}/Headers" FILES ${SHADER_HEADERS})
|
||||
list(APPEND ALL_SHADER_HEADERS ${SHADER_HEADERS})
|
||||
list(APPEND ALL_SCRIBE_SHADERS ${SHADER_HEADERS})
|
||||
endif()
|
||||
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace ${SHADER_NAMESPACE} {\n")
|
||||
if (DEFINED VERTEX_SHADERS)
|
||||
list(REMOVE_DUPLICATES VERTEX_SHADERS)
|
||||
source_group("${SHADER_LIB}/Vertex" FILES ${VERTEX_SHADERS})
|
||||
list(APPEND ALL_SCRIBE_SHADERS ${VERTEX_SHADERS})
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace vertex { enum {\n")
|
||||
foreach(SHADER ${VERTEX_SHADERS})
|
||||
process_shader_file()
|
||||
endforeach()
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "}; } // vertex \n")
|
||||
endif()
|
||||
|
||||
if (DEFINED FRAGMENT_SHADERS)
|
||||
list(REMOVE_DUPLICATES FRAGMENT_SHADERS)
|
||||
source_group("${SHADER_LIB}/Fragment" FILES ${FRAGMENT_SHADERS})
|
||||
list(APPEND ALL_SCRIBE_SHADERS ${FRAGMENT_SHADERS})
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace fragment { enum {\n")
|
||||
foreach(SHADER ${FRAGMENT_SHADERS})
|
||||
process_shader_file()
|
||||
endforeach()
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "}; } // fragment \n")
|
||||
endif()
|
||||
|
||||
if (DEFINED LIB_PROGRAMS)
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "namespace program { enum {\n")
|
||||
foreach(LIB_PROGRAM ${LIB_PROGRAMS})
|
||||
get_property(LIB_PROGRAM_MAP GLOBAL PROPERTY ${LIB_PROGRAM})
|
||||
unpack_map(${LIB_PROGRAM_MAP})
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "${LIB_PROGRAM} = (${AUTOSCRIBE_PROGRAM_VERTEX} << 16) | ${AUTOSCRIBE_PROGRAM_FRAGMENT},\n")
|
||||
MATH(EXPR PROGRAM_COUNT "${PROGRAM_COUNT}+1")
|
||||
list(APPEND SHADER_ALL_PROGRAMS "${SHADER_NAMESPACE}::program::${LIB_PROGRAM}")
|
||||
endforeach()
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "}; } // program \n")
|
||||
endif()
|
||||
string(CONCAT SHADER_ENUMS "${SHADER_ENUMS}" "} // namespace ${SHADER_NAMESPACE}\n")
|
||||
#
|
||||
# Scribe generation & program defintiion
|
||||
#
|
||||
foreach(SHADER_LIB ${ARGN})
|
||||
AUTOSCRIBE_SHADER_LIB(${SHADER_LIB})
|
||||
endforeach()
|
||||
|
||||
set(SHADER_PROGRAMS_ARRAY "")
|
||||
foreach(SHADER_PROGRAM ${SHADER_ALL_PROGRAMS})
|
||||
string(CONCAT SHADER_PROGRAMS_ARRAY "${SHADER_PROGRAMS_ARRAY}" " ${SHADER_PROGRAM},\n")
|
||||
endforeach()
|
||||
# Generate the library files
|
||||
configure_file(
|
||||
ShaderEnums.cpp.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.cpp)
|
||||
configure_file(
|
||||
ShaderEnums.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.h)
|
||||
configure_file(
|
||||
shaders.qrc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/shaders.qrc)
|
||||
|
||||
set(SHREFLECT_COMMAND shreflect)
|
||||
set(SHREFLECT_DEPENDENCY shreflect)
|
||||
if (ANDROID)
|
||||
set(SHREFLECT_COMMAND ${NATIVE_SHREFLECT})
|
||||
unset(SHREFLECT_DEPENDENCY)
|
||||
endif()
|
||||
|
||||
set(SHADER_COUNT 0)
|
||||
foreach(COMPILED_SHADER ${COMPILED_SHADERS})
|
||||
set(REFLECTED_SHADER "${COMPILED_SHADER}.json")
|
||||
list(APPEND COMPILED_SHADER_REFLECTIONS ${REFLECTED_SHADER})
|
||||
string(CONCAT SHADER_QRC "${SHADER_QRC}" "<file alias=\"${SHADER_COUNT}\">${COMPILED_SHADER}</file>\n")
|
||||
string(CONCAT SHADER_QRC "${SHADER_QRC}" "<file alias=\"${SHADER_COUNT}_reflection\">${REFLECTED_SHADER}</file>\n")
|
||||
MATH(EXPR SHADER_COUNT "${SHADER_COUNT}+1")
|
||||
add_custom_command(
|
||||
OUTPUT ${REFLECTED_SHADER}
|
||||
COMMAND ${SHREFLECT_COMMAND} ${COMPILED_SHADER}
|
||||
DEPENDS ${SHREFLECT_DEPENDENCY} ${COMPILED_SHADER}
|
||||
)
|
||||
endforeach()
|
||||
set(AUTOSCRIBE_SHADER_LIB_SRC "${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.h;${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.cpp")
|
||||
set(QT_RESOURCES_FILE ${CMAKE_CURRENT_BINARY_DIR}/shaders.qrc)
|
||||
|
||||
# Custom targets required to force generation of the shaders via scribe
|
||||
add_custom_target(scribe_shaders SOURCES ${ALL_SCRIBE_SHADERS})
|
||||
add_custom_target(compiled_shaders SOURCES ${COMPILED_SHADERS})
|
||||
add_custom_target(reflected_shaders SOURCES ${COMPILED_SHADER_REFLECTIONS})
|
||||
add_custom_target(reflected_shaders SOURCES ${REFLECTED_SHADERS})
|
||||
set_target_properties(scribe_shaders PROPERTIES FOLDER "Shaders")
|
||||
set_target_properties(compiled_shaders PROPERTIES FOLDER "Shaders")
|
||||
set_target_properties(reflected_shaders PROPERTIES FOLDER "Shaders")
|
||||
|
||||
configure_file(
|
||||
ShaderEnums.cpp.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.cpp
|
||||
)
|
||||
configure_file(
|
||||
ShaderEnums.h.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.h
|
||||
)
|
||||
configure_file(
|
||||
shaders.qrc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/shaders.qrc
|
||||
)
|
||||
set(AUTOSCRIBE_SHADER_LIB_SRC "${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.h;${CMAKE_CURRENT_BINARY_DIR}/shaders/ShaderEnums.cpp")
|
||||
list(APPEND AUTOSCRIBE_SHADER_LIB_SRC ${COMPILED_SHADERS})
|
||||
set(QT_RESOURCES_FILE ${CMAKE_CURRENT_BINARY_DIR}/shaders.qrc)
|
||||
get_property(GLOBAL_SHADER_SOURCES GLOBAL PROPERTY GLOBAL_SHADER_SOURCES)
|
||||
list(REMOVE_DUPLICATES GLOBAL_SHADER_SOURCES)
|
||||
endmacro()
|
||||
|
|
|
@ -2439,8 +2439,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
|
|||
}
|
||||
|
||||
} else if (connection->requestOperation() == QNetworkAccessManager::DeleteOperation) {
|
||||
const QString ALL_NODE_DELETE_REGEX_STRING = QString("\\%1\\/?$").arg(URI_NODES);
|
||||
const QString NODE_DELETE_REGEX_STRING = QString("\\%1\\/(%2)\\/$").arg(URI_NODES).arg(UUID_REGEX_STRING);
|
||||
const QString ALL_NODE_DELETE_REGEX_STRING = QString("%1/?$").arg(URI_NODES);
|
||||
const QString NODE_DELETE_REGEX_STRING = QString("%1/(%2)$").arg(URI_NODES).arg(UUID_REGEX_STRING);
|
||||
|
||||
QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING);
|
||||
QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING);
|
||||
|
|
|
@ -120,6 +120,7 @@ Item {
|
|||
|
||||
TextField {
|
||||
id: usernameField
|
||||
text: Settings.getValue("wallet/savedUsername", "");
|
||||
width: parent.width
|
||||
focus: true
|
||||
label: "Username or Email"
|
||||
|
|
|
@ -60,6 +60,9 @@ Item {
|
|||
StatText {
|
||||
text: "Game Rate: " + root.gameLoopRate
|
||||
}
|
||||
StatText {
|
||||
text: "Physics Object Count: " + root.physicsObjectCount
|
||||
}
|
||||
StatText {
|
||||
visible: root.expanded
|
||||
text: root.gameUpdateStats
|
||||
|
|
|
@ -103,7 +103,7 @@ FocusScope {
|
|||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
size: hifi.fontSizes.textFieldInput
|
||||
text: comboBox.currentText
|
||||
text: comboBox.displayText ? comboBox.displayText : comboBox.currentText
|
||||
elide: Text.ElideRight
|
||||
color: comboBox.hovered || comboBox.popup.visible ? hifi.colors.baseGray : (isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText )
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ SpinBox {
|
|||
id: hifi
|
||||
}
|
||||
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
property int colorScheme: hifi.colorSchemes.light
|
||||
readonly property bool isLightColorScheme: colorScheme === hifi.colorSchemes.light
|
||||
property string label: ""
|
||||
|
@ -91,7 +92,6 @@ SpinBox {
|
|||
}
|
||||
|
||||
valueFromText: function(text, locale) {
|
||||
spinBox.value = 0; // Force valueChanged signal to be emitted so that validator fires.
|
||||
return Number.fromLocaleString(locale, text)*factor;
|
||||
}
|
||||
|
||||
|
@ -104,6 +104,8 @@ SpinBox {
|
|||
selectedTextColor: hifi.colors.black
|
||||
selectionColor: hifi.colors.primaryHighlight
|
||||
text: spinBox.textFromValue(spinBox.value, spinBox.locale) + suffix
|
||||
inputMethodHints: spinBox.inputMethodHints
|
||||
validator: spinBox.validator
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
leftPadding: spinBoxLabelInside.visible ? 30 : hifi.dimensions.textPadding
|
||||
//rightPadding: hifi.dimensions.spinnerSize
|
||||
|
|
|
@ -57,7 +57,7 @@ Rectangle {
|
|||
try {
|
||||
var marketResponse = JSON.parse(xmlhttp.responseText.trim())
|
||||
|
||||
if(marketResponse.status === 'success') {
|
||||
if (marketResponse.status === 'success') {
|
||||
avatar.modelName = marketResponse.data.title;
|
||||
}
|
||||
}
|
||||
|
@ -72,14 +72,14 @@ Rectangle {
|
|||
|
||||
function getAvatarModelName() {
|
||||
|
||||
if(currentAvatar === null) {
|
||||
if (currentAvatar === null) {
|
||||
return '';
|
||||
}
|
||||
if(currentAvatar.modelName !== undefined) {
|
||||
if (currentAvatar.modelName !== undefined) {
|
||||
return currentAvatar.modelName;
|
||||
} else {
|
||||
var marketId = allAvatars.extractMarketId(currentAvatar.avatarUrl);
|
||||
if(marketId !== '') {
|
||||
if (marketId !== '') {
|
||||
fetchAvatarModelName(marketId, currentAvatar);
|
||||
}
|
||||
}
|
||||
|
@ -103,51 +103,51 @@ Rectangle {
|
|||
property url externalAvatarThumbnailUrl: '../../images/avatarapp/guy-in-circle.svg'
|
||||
|
||||
function fromScript(message) {
|
||||
if(message.method === 'initialize') {
|
||||
if (message.method === 'initialize') {
|
||||
jointNames = message.data.jointNames;
|
||||
emitSendToScript({'method' : getAvatarsMethod});
|
||||
} else if(message.method === 'wearableUpdated') {
|
||||
} else if (message.method === 'wearableUpdated') {
|
||||
adjustWearables.refreshWearable(message.entityID, message.wearableIndex, message.properties, message.updateUI);
|
||||
} else if(message.method === 'wearablesUpdated') {
|
||||
} else if (message.method === 'wearablesUpdated') {
|
||||
var wearablesModel = currentAvatar.wearables;
|
||||
wearablesModel.clear();
|
||||
message.wearables.forEach(function(wearable) {
|
||||
wearablesModel.append(wearable);
|
||||
});
|
||||
adjustWearables.refresh(currentAvatar);
|
||||
} else if(message.method === 'scaleChanged') {
|
||||
} else if (message.method === 'scaleChanged') {
|
||||
currentAvatar.avatarScale = message.value;
|
||||
updateCurrentAvatarInBookmarks(currentAvatar);
|
||||
} else if(message.method === 'externalAvatarApplied') {
|
||||
} else if (message.method === 'externalAvatarApplied') {
|
||||
currentAvatar.avatarUrl = message.avatarURL;
|
||||
currentAvatar.thumbnailUrl = allAvatars.makeThumbnailUrl(message.avatarURL);
|
||||
currentAvatar.entry.avatarUrl = currentAvatar.avatarUrl;
|
||||
currentAvatar.modelName = undefined;
|
||||
updateCurrentAvatarInBookmarks(currentAvatar);
|
||||
} else if(message.method === 'settingChanged') {
|
||||
} else if (message.method === 'settingChanged') {
|
||||
currentAvatarSettings[message.name] = message.value;
|
||||
} else if(message.method === 'changeSettings') {
|
||||
} else if (message.method === 'changeSettings') {
|
||||
currentAvatarSettings = message.settings;
|
||||
} else if(message.method === 'bookmarkLoaded') {
|
||||
} else if (message.method === 'bookmarkLoaded') {
|
||||
setCurrentAvatar(message.data.currentAvatar, message.data.name);
|
||||
var avatarIndex = allAvatars.findAvatarIndex(currentAvatar.name);
|
||||
allAvatars.move(avatarIndex, 0, 1);
|
||||
view.setPage(0);
|
||||
} else if(message.method === 'bookmarkAdded') {
|
||||
} else if (message.method === 'bookmarkAdded') {
|
||||
var avatar = allAvatars.findAvatar(message.bookmarkName);
|
||||
if(avatar !== undefined) {
|
||||
if (avatar !== undefined) {
|
||||
var avatarObject = allAvatars.makeAvatarObject(message.bookmark, message.bookmarkName);
|
||||
for(var prop in avatarObject) {
|
||||
avatar[prop] = avatarObject[prop];
|
||||
}
|
||||
if(currentAvatar.name === message.bookmarkName) {
|
||||
if (currentAvatar.name === message.bookmarkName) {
|
||||
currentAvatar = currentAvatarModel.makeAvatarEntry(avatarObject);
|
||||
}
|
||||
} else {
|
||||
allAvatars.addAvatarEntry(message.bookmark, message.bookmarkName);
|
||||
}
|
||||
updateCurrentAvatarInBookmarks(currentAvatar);
|
||||
} else if(message.method === 'bookmarkDeleted') {
|
||||
} else if (message.method === 'bookmarkDeleted') {
|
||||
pageOfAvatars.isUpdating = true;
|
||||
|
||||
var index = pageOfAvatars.findAvatarIndex(message.name);
|
||||
|
@ -159,15 +159,16 @@ Rectangle {
|
|||
var itemsOnPage = pageOfAvatars.count;
|
||||
var newItemIndex = view.currentPage * view.itemsPerPage + itemsOnPage;
|
||||
|
||||
if(newItemIndex <= (allAvatars.count - 1)) {
|
||||
if (newItemIndex <= (allAvatars.count - 1)) {
|
||||
pageOfAvatars.append(allAvatars.get(newItemIndex));
|
||||
} else {
|
||||
if(!pageOfAvatars.hasGetAvatars())
|
||||
if (!pageOfAvatars.hasGetAvatars()) {
|
||||
pageOfAvatars.appendGetAvatars();
|
||||
}
|
||||
}
|
||||
|
||||
pageOfAvatars.isUpdating = false;
|
||||
} else if(message.method === getAvatarsMethod) {
|
||||
} else if (message.method === getAvatarsMethod) {
|
||||
var getAvatarsData = message.data;
|
||||
allAvatars.populate(getAvatarsData.bookmarks);
|
||||
setCurrentAvatar(getAvatarsData.currentAvatar, '');
|
||||
|
@ -175,16 +176,16 @@ Rectangle {
|
|||
currentAvatarSettings = getAvatarsData.currentAvatarSettings;
|
||||
|
||||
updateCurrentAvatarInBookmarks(currentAvatar);
|
||||
} else if(message.method === 'updateAvatarInBookmarks') {
|
||||
} else if (message.method === 'updateAvatarInBookmarks') {
|
||||
updateCurrentAvatarInBookmarks(currentAvatar);
|
||||
} else if(message.method === 'selectAvatarEntity') {
|
||||
} else if (message.method === 'selectAvatarEntity') {
|
||||
adjustWearables.selectWearableByID(message.entityID);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentAvatarInBookmarks(avatar) {
|
||||
var bookmarkAvatarIndex = allAvatars.findAvatarIndexByValue(avatar);
|
||||
if(bookmarkAvatarIndex === -1) {
|
||||
if (bookmarkAvatarIndex === -1) {
|
||||
avatar.name = '';
|
||||
view.setPage(0);
|
||||
} else {
|
||||
|
@ -285,6 +286,9 @@ Rectangle {
|
|||
onWearableSelected: {
|
||||
emitSendToScript({'method' : 'selectWearable', 'entityID' : id});
|
||||
}
|
||||
onAddWearable: {
|
||||
emitSendToScript({'method' : 'addWearable', 'avatarName' : avatarName, 'url' : url});
|
||||
}
|
||||
|
||||
z: 3
|
||||
}
|
||||
|
@ -491,33 +495,10 @@ Rectangle {
|
|||
anchors.verticalCenter: wearablesLabel.verticalCenter
|
||||
glyphText: "\ue02e"
|
||||
|
||||
visible: avatarWearablesCount !== 0
|
||||
onClicked: {
|
||||
adjustWearables.open(currentAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
// TextStyle3
|
||||
RalewayRegular {
|
||||
size: 22;
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: wearablesLabel.verticalCenter
|
||||
font.underline: true
|
||||
text: "Add"
|
||||
color: 'black'
|
||||
visible: avatarWearablesCount === 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
popup.showGetWearables(function() {
|
||||
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
|
||||
}, function(link) {
|
||||
emitSendToScript({'method' : 'navigate', 'url' : link})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
@ -617,8 +598,9 @@ Rectangle {
|
|||
pageOfAvatars.append(avatarItem);
|
||||
}
|
||||
|
||||
if(pageOfAvatars.count !== itemsPerPage)
|
||||
if (pageOfAvatars.count !== itemsPerPage) {
|
||||
pageOfAvatars.appendGetAvatars();
|
||||
}
|
||||
|
||||
currentPage = pageIndex;
|
||||
pageOfAvatars.isUpdating = false;
|
||||
|
@ -639,7 +621,7 @@ Rectangle {
|
|||
}
|
||||
|
||||
function removeGetAvatars() {
|
||||
if(hasGetAvatars()) {
|
||||
if (hasGetAvatars()) {
|
||||
remove(count - 1)
|
||||
}
|
||||
}
|
||||
|
@ -707,13 +689,13 @@ Rectangle {
|
|||
hoverEnabled: enabled
|
||||
|
||||
onClicked: {
|
||||
if(isInManageState) {
|
||||
if (isInManageState) {
|
||||
var currentItem = delegateRoot.GridView.view.model.get(index);
|
||||
popup.showDeleteFavorite(currentItem.name, function() {
|
||||
view.deleteAvatar(currentItem);
|
||||
});
|
||||
} else {
|
||||
if(delegateRoot.GridView.view.currentIndex !== index) {
|
||||
if (delegateRoot.GridView.view.currentIndex !== index) {
|
||||
var currentItem = delegateRoot.GridView.view.model.get(index);
|
||||
popup.showLoadFavorite(currentItem.name, function() {
|
||||
view.selectAvatar(currentItem);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Hifi 1.0 as Hifi
|
||||
import QtQuick 2.5
|
||||
import QtQuick.Layouts 1.3
|
||||
import "../../styles-uit"
|
||||
import "../../controls-uit" as HifiControlsUit
|
||||
import "../../controls" as HifiControls
|
||||
|
@ -17,6 +18,7 @@ Rectangle {
|
|||
|
||||
signal adjustWearablesOpened(var avatarName);
|
||||
signal adjustWearablesClosed(bool status, var avatarName);
|
||||
signal addWearable(string avatarName, string url);
|
||||
|
||||
property bool modified: false;
|
||||
Component.onCompleted: {
|
||||
|
@ -30,6 +32,7 @@ Rectangle {
|
|||
function open(avatar) {
|
||||
adjustWearablesOpened(avatar.name);
|
||||
|
||||
modified = false;
|
||||
visible = true;
|
||||
avatarName = avatar.name;
|
||||
wearablesModel = avatar.wearables;
|
||||
|
@ -38,43 +41,47 @@ Rectangle {
|
|||
|
||||
function refresh(avatar) {
|
||||
wearablesCombobox.model.clear();
|
||||
for(var i = 0; i < avatar.wearables.count; ++i) {
|
||||
wearablesCombobox.currentIndex = -1;
|
||||
|
||||
for (var i = 0; i < avatar.wearables.count; ++i) {
|
||||
var wearable = avatar.wearables.get(i).properties;
|
||||
for(var j = (wearable.modelURL.length - 1); j >= 0; --j) {
|
||||
if(wearable.modelURL[j] === '/') {
|
||||
wearable.text = wearable.modelURL.substring(j + 1) + ' [%jointIndex%]'.replace('%jointIndex%', jointNames[wearable.parentJointIndex]);
|
||||
for (var j = (wearable.modelURL.length - 1); j >= 0; --j) {
|
||||
if (wearable.modelURL[j] === '/') {
|
||||
wearable.text = wearable.modelURL.substring(j + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
wearablesCombobox.model.append(wearable);
|
||||
}
|
||||
|
||||
wearablesCombobox.currentIndex = 0;
|
||||
if (wearablesCombobox.model.count !== 0) {
|
||||
wearablesCombobox.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshWearable(wearableID, wearableIndex, properties, updateUI) {
|
||||
if(wearableIndex === -1) {
|
||||
if (wearableIndex === -1) {
|
||||
wearableIndex = wearablesCombobox.model.findIndexById(wearableID);
|
||||
}
|
||||
|
||||
var wearable = wearablesCombobox.model.get(wearableIndex);
|
||||
|
||||
if(!wearable) {
|
||||
if (!wearable) {
|
||||
return;
|
||||
}
|
||||
|
||||
var wearableModelItemProperties = wearablesModel.get(wearableIndex).properties;
|
||||
|
||||
for(var prop in properties) {
|
||||
for (var prop in properties) {
|
||||
wearable[prop] = properties[prop];
|
||||
wearableModelItemProperties[prop] = wearable[prop];
|
||||
|
||||
if(updateUI) {
|
||||
if(prop === 'localPosition') {
|
||||
position.set(wearable[prop]);
|
||||
} else if(prop === 'localRotationAngles') {
|
||||
rotation.set(wearable[prop]);
|
||||
} else if(prop === 'dimensions') {
|
||||
if (updateUI) {
|
||||
if (prop === 'localPosition') {
|
||||
positionVector.set(wearable[prop]);
|
||||
} else if (prop === 'localRotationAngles') {
|
||||
rotationVector.set(wearable[prop]);
|
||||
} else if (prop === 'dimensions') {
|
||||
scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x);
|
||||
}
|
||||
}
|
||||
|
@ -84,13 +91,13 @@ Rectangle {
|
|||
}
|
||||
|
||||
function getCurrentWearable() {
|
||||
return wearablesCombobox.model.get(wearablesCombobox.currentIndex)
|
||||
return wearablesCombobox.currentIndex !== -1 ? wearablesCombobox.model.get(wearablesCombobox.currentIndex) : null;
|
||||
}
|
||||
|
||||
function selectWearableByID(entityID) {
|
||||
for(var i = 0; i < wearablesCombobox.model.count; ++i) {
|
||||
for (var i = 0; i < wearablesCombobox.model.count; ++i) {
|
||||
var wearable = wearablesCombobox.model.get(i);
|
||||
if(wearable.id === entityID) {
|
||||
if (wearable.id === entityID) {
|
||||
wearablesCombobox.currentIndex = i;
|
||||
break;
|
||||
}
|
||||
|
@ -115,72 +122,216 @@ Rectangle {
|
|||
|
||||
Column {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 15
|
||||
anchors.topMargin: 12
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
spacing: 20
|
||||
width: parent.width - 30 * 2
|
||||
width: parent.width - 22 * 2
|
||||
|
||||
HifiControlsUit.ComboBox {
|
||||
id: wearablesCombobox
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
comboBox.textRole: "text"
|
||||
Column {
|
||||
width: parent.width
|
||||
|
||||
model: ListModel {
|
||||
function findIndexById(id) {
|
||||
Rectangle {
|
||||
color: hifi.colors.orangeHighlight
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 70
|
||||
visible: HMD.active
|
||||
|
||||
for(var i = 0; i < count; ++i) {
|
||||
var wearable = get(i);
|
||||
if(wearable.id === id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
RalewayRegular {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 18
|
||||
anchors.rightMargin: 18
|
||||
size: 20;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 23;
|
||||
text: "Tip: You can use hand controllers to grab and adjust your wearables"
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
comboBox.onCurrentIndexChanged: {
|
||||
var currentWearable = getCurrentWearable();
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 12 // spacing
|
||||
visible: HMD.active
|
||||
}
|
||||
|
||||
if(currentWearable) {
|
||||
position.set(currentWearable.localPosition);
|
||||
rotation.set(currentWearable.localRotationAngles);
|
||||
scalespinner.set(currentWearable.dimensions.x / currentWearable.naturalDimensions.x)
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
wearableSelected(currentWearable.id);
|
||||
RalewayBold {
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "Wearable"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
|
||||
RalewayBold {
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "<a href='#'>Get more</a>"
|
||||
linkColor: hifi.colors.blueHighlight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onLinkActivated: {
|
||||
popup.showGetWearables(function() {
|
||||
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
|
||||
}, function(link) {
|
||||
emitSendToScript({'method' : 'navigate', 'url' : link})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
RalewayBold {
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "<a href='#'>Add custom</a>"
|
||||
linkColor: hifi.colors.blueHighlight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
onLinkActivated: {
|
||||
popup.showSpecifyWearableUrl(function(url) {
|
||||
console.debug('popup.showSpecifyWearableUrl: ', url);
|
||||
addWearable(root.avatarName, url);
|
||||
modified = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControlsUit.ComboBox {
|
||||
id: wearablesCombobox
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
enabled: getCurrentWearable() !== null
|
||||
comboBox.textRole: "text"
|
||||
|
||||
model: ListModel {
|
||||
function findIndexById(id) {
|
||||
|
||||
for (var i = 0; i < count; ++i) {
|
||||
var wearable = get(i);
|
||||
if (wearable.id === id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
comboBox.onCurrentIndexChanged: {
|
||||
var currentWearable = getCurrentWearable();
|
||||
var position = currentWearable ? currentWearable.localPosition : { x : 0, y : 0, z : 0 };
|
||||
var rotation = currentWearable ? currentWearable.localRotationAngles : { x : 0, y : 0, z : 0 };
|
||||
var scale = currentWearable ? currentWearable.dimensions.x / currentWearable.naturalDimensions.x : 1.0;
|
||||
var joint = currentWearable ? currentWearable.parentJointIndex : -1;
|
||||
var soft = currentWearable ? currentWearable.relayParentJoints : false;
|
||||
|
||||
positionVector.set(position);
|
||||
rotationVector.set(rotation);
|
||||
scalespinner.set(scale);
|
||||
jointsCombobox.set(joint);
|
||||
isSoft.set(soft);
|
||||
|
||||
if (currentWearable) {
|
||||
wearableSelected(currentWearable.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
|
||||
RalewayBold {
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "Joint"
|
||||
}
|
||||
|
||||
HifiControlsUit.ComboBox {
|
||||
id: jointsCombobox
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
enabled: getCurrentWearable() !== null && !isSoft.checked
|
||||
comboBox.displayText: isSoft.checked ? 'Hips' : comboBox.currentText
|
||||
|
||||
model: jointNames
|
||||
property bool notify: false
|
||||
|
||||
function set(jointIndex) {
|
||||
notify = false;
|
||||
currentIndex = jointIndex;
|
||||
notify = true;
|
||||
}
|
||||
|
||||
function notifyJointChanged() {
|
||||
modified = true;
|
||||
var properties = {
|
||||
parentJointIndex: currentIndex,
|
||||
localPosition: {
|
||||
x: positionVector.xvalue,
|
||||
y: positionVector.yvalue,
|
||||
z: positionVector.zvalue
|
||||
},
|
||||
localRotationAngles: {
|
||||
x: rotationVector.xvalue,
|
||||
y: rotationVector.yvalue,
|
||||
z: rotationVector.zvalue,
|
||||
}
|
||||
};
|
||||
|
||||
wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties);
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (notify) notifyJointChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
|
||||
Row {
|
||||
spacing: 20
|
||||
|
||||
// TextStyle5
|
||||
FiraSansSemiBold {
|
||||
RalewayBold {
|
||||
id: positionLabel
|
||||
size: 22;
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "Position"
|
||||
}
|
||||
|
||||
// TextStyle7
|
||||
FiraSansRegular {
|
||||
size: 18;
|
||||
RalewayBold {
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 16.9;
|
||||
lineHeight: 18;
|
||||
text: "m"
|
||||
anchors.verticalCenter: positionLabel.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 {
|
||||
id: position
|
||||
id: positionVector
|
||||
backgroundColor: "lightgray"
|
||||
enabled: getCurrentWearable() !== null
|
||||
|
||||
function set(localPosition) {
|
||||
notify = false;
|
||||
|
@ -201,9 +352,9 @@ Rectangle {
|
|||
|
||||
property bool notify: false;
|
||||
|
||||
onXvalueChanged: if(notify) notifyPositionChanged();
|
||||
onYvalueChanged: if(notify) notifyPositionChanged();
|
||||
onZvalueChanged: if(notify) notifyPositionChanged();
|
||||
onXvalueChanged: if (notify) notifyPositionChanged();
|
||||
onYvalueChanged: if (notify) notifyPositionChanged();
|
||||
onZvalueChanged: if (notify) notifyPositionChanged();
|
||||
|
||||
decimals: 2
|
||||
realFrom: -10
|
||||
|
@ -214,31 +365,33 @@ Rectangle {
|
|||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
|
||||
Row {
|
||||
spacing: 20
|
||||
|
||||
// TextStyle5
|
||||
FiraSansSemiBold {
|
||||
RalewayBold {
|
||||
id: rotationLabel
|
||||
size: 22;
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "Rotation"
|
||||
}
|
||||
|
||||
// TextStyle7
|
||||
FiraSansRegular {
|
||||
size: 18;
|
||||
RalewayBold {
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 16.9;
|
||||
lineHeight: 18;
|
||||
text: "deg"
|
||||
anchors.verticalCenter: rotationLabel.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 {
|
||||
id: rotation
|
||||
id: rotationVector
|
||||
backgroundColor: "lightgray"
|
||||
enabled: getCurrentWearable() !== null
|
||||
|
||||
function set(localRotationAngles) {
|
||||
notify = false;
|
||||
|
@ -259,9 +412,9 @@ Rectangle {
|
|||
|
||||
property bool notify: false;
|
||||
|
||||
onXvalueChanged: if(notify) notifyRotationChanged();
|
||||
onYvalueChanged: if(notify) notifyRotationChanged();
|
||||
onZvalueChanged: if(notify) notifyRotationChanged();
|
||||
onXvalueChanged: if (notify) notifyRotationChanged();
|
||||
onYvalueChanged: if (notify) notifyRotationChanged();
|
||||
onZvalueChanged: if (notify) notifyRotationChanged();
|
||||
|
||||
decimals: 0
|
||||
realFrom: -180
|
||||
|
@ -270,33 +423,66 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Item {
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
height: childrenRect.height
|
||||
|
||||
// TextStyle5
|
||||
FiraSansSemiBold {
|
||||
size: 22;
|
||||
text: "Scale"
|
||||
HifiControlsUit.CheckBox {
|
||||
id: isSoft
|
||||
enabled: getCurrentWearable() !== null
|
||||
text: "Is soft"
|
||||
labelFontSize: 15
|
||||
labelFontWeight: Font.Bold
|
||||
color: Qt.black
|
||||
y: scalespinner.y
|
||||
|
||||
function set(value) {
|
||||
notify = false;
|
||||
checked = value
|
||||
notify = true;
|
||||
}
|
||||
|
||||
function notifyIsSoftChanged() {
|
||||
modified = true;
|
||||
var properties = {
|
||||
relayParentJoints: checked
|
||||
};
|
||||
|
||||
wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties);
|
||||
}
|
||||
|
||||
property bool notify: false;
|
||||
|
||||
onCheckedChanged: if (notify) notifyIsSoftChanged();
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Column {
|
||||
id: scalesColumn
|
||||
anchors.right: parent.right
|
||||
|
||||
// TextStyle5
|
||||
RalewayBold {
|
||||
id: scaleLabel
|
||||
size: 15;
|
||||
lineHeightMode: Text.FixedHeight
|
||||
lineHeight: 18;
|
||||
text: "Scale"
|
||||
}
|
||||
|
||||
HifiControlsUit.SpinBox {
|
||||
id: scalespinner
|
||||
enabled: getCurrentWearable() !== null
|
||||
decimals: 2
|
||||
realStepSize: 0.1
|
||||
realFrom: 0.1
|
||||
realTo: 3.0
|
||||
realValue: 1.0
|
||||
backgroundColor: "lightgray"
|
||||
width: position.spinboxWidth
|
||||
width: positionVector.spinboxWidth
|
||||
colorScheme: hifi.colorSchemes.light
|
||||
|
||||
property bool notify: false;
|
||||
onValueChanged: if(notify) notifyScaleChanged();
|
||||
onRealValueChanged: if (notify) notifyScaleChanged();
|
||||
|
||||
function set(value) {
|
||||
notify = false;
|
||||
|
@ -320,26 +506,34 @@ Rectangle {
|
|||
wearableUpdated(currentWearable.id, wearablesCombobox.currentIndex, properties);
|
||||
}
|
||||
}
|
||||
|
||||
HifiControlsUit.Button {
|
||||
fontSize: 18
|
||||
height: 40
|
||||
anchors.right: parent.right
|
||||
color: hifi.buttons.red;
|
||||
colorScheme: hifi.colorSchemes.light;
|
||||
text: "TAKE IT OFF"
|
||||
onClicked: wearableDeleted(root.avatarName, getCurrentWearable().id);
|
||||
enabled: wearablesCombobox.model.count !== 0
|
||||
anchors.verticalCenter: scalespinner.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
|
||||
HifiControlsUit.Button {
|
||||
fontSize: 18
|
||||
height: 40
|
||||
width: scalespinner.width
|
||||
anchors.right: parent.right
|
||||
color: hifi.buttons.red;
|
||||
colorScheme: hifi.colorSchemes.light;
|
||||
text: "TAKE IT OFF"
|
||||
onClicked: {
|
||||
modified = true;
|
||||
wearableDeleted(root.avatarName, getCurrentWearable().id);
|
||||
}
|
||||
enabled: wearablesCombobox.model.count !== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DialogButtons {
|
||||
yesButton.enabled: modified
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 30
|
||||
anchors.bottomMargin: 57
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 30
|
||||
anchors.right: parent.right
|
||||
|
|
|
@ -24,8 +24,9 @@ ListModel {
|
|||
|
||||
function makeThumbnailUrl(avatarUrl) {
|
||||
var marketId = extractMarketId(avatarUrl);
|
||||
if(marketId === '')
|
||||
if (marketId === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
var avatarThumbnailUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/%marketId%/large/hifi-mp-%marketId%.jpg"
|
||||
.split('%marketId%').join(marketId);
|
||||
|
@ -42,7 +43,6 @@ ListModel {
|
|||
'thumbnailUrl' : avatarThumbnailUrl,
|
||||
'avatarUrl' : avatar.avatarUrl,
|
||||
'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [],
|
||||
'attachments' : avatar.attachments ? avatar.attachments : [],
|
||||
'entry' : avatar,
|
||||
'getMoreAvatars' : false
|
||||
};
|
||||
|
@ -58,7 +58,7 @@ ListModel {
|
|||
|
||||
function populate(bookmarks) {
|
||||
clear();
|
||||
for(var avatarName in bookmarks) {
|
||||
for (var avatarName in bookmarks) {
|
||||
var avatar = bookmarks[avatarName];
|
||||
var avatarEntry = makeAvatarObject(avatar, avatarName);
|
||||
|
||||
|
@ -67,19 +67,19 @@ ListModel {
|
|||
}
|
||||
|
||||
function arraysAreEqual(a1, a2, comparer) {
|
||||
if(Array.isArray(a1) && Array.isArray(a2)) {
|
||||
if(a1.length !== a2.length) {
|
||||
if (Array.isArray(a1) && Array.isArray(a2)) {
|
||||
if (a1.length !== a2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(var i = 0; i < a1.length; ++i) {
|
||||
if(!comparer(a1[i], a2[i])) {
|
||||
for (var i = 0; i < a1.length; ++i) {
|
||||
if (!comparer(a1[i], a2[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if(Array.isArray(a1)) {
|
||||
} else if (Array.isArray(a1)) {
|
||||
return a1.length === 0;
|
||||
} else if(Array.isArray(a2)) {
|
||||
} else if (Array.isArray(a2)) {
|
||||
return a2.length === 0;
|
||||
}
|
||||
|
||||
|
@ -87,26 +87,26 @@ ListModel {
|
|||
}
|
||||
|
||||
function modelsAreEqual(m1, m2, comparer) {
|
||||
if(m1.count !== m2.count) {
|
||||
if (m1.count !== m2.count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(var i = 0; i < m1.count; ++i) {
|
||||
for (var i = 0; i < m1.count; ++i) {
|
||||
var e1 = m1.get(i);
|
||||
|
||||
var allDifferent = true;
|
||||
|
||||
// it turns out order of wearables can randomly change so make position-independent comparison here
|
||||
for(var j = 0; j < m2.count; ++j) {
|
||||
for (var j = 0; j < m2.count; ++j) {
|
||||
var e2 = m2.get(j);
|
||||
|
||||
if(comparer(e1, e2)) {
|
||||
if (comparer(e1, e2)) {
|
||||
allDifferent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(allDifferent) {
|
||||
if (allDifferent) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -115,18 +115,20 @@ ListModel {
|
|||
}
|
||||
|
||||
function compareNumericObjects(o1, o2) {
|
||||
if(o1 === undefined && o2 !== undefined)
|
||||
if (o1 === undefined && o2 !== undefined) {
|
||||
return false;
|
||||
if(o1 !== undefined && o2 === undefined)
|
||||
}
|
||||
if (o1 !== undefined && o2 === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for(var prop in o1) {
|
||||
if(o1.hasOwnProperty(prop) && o2.hasOwnProperty(prop)) {
|
||||
for (var prop in o1) {
|
||||
if (o1.hasOwnProperty(prop) && o2.hasOwnProperty(prop)) {
|
||||
var v1 = o1[prop];
|
||||
var v2 = o2[prop];
|
||||
|
||||
|
||||
if(v1 !== v2 && Math.round(v1 * 500) != Math.round(v2 * 500)) {
|
||||
if (v1 !== v2 && Math.round(v1 * 500) != Math.round(v2 * 500)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +138,7 @@ ListModel {
|
|||
}
|
||||
|
||||
function compareObjects(o1, o2, props, arrayProp) {
|
||||
for(var i = 0; i < props.length; ++i) {
|
||||
for (var i = 0; i < props.length; ++i) {
|
||||
var prop = props[i];
|
||||
var propertyName = prop.propertyName;
|
||||
var comparer = prop.comparer;
|
||||
|
@ -144,12 +146,12 @@ ListModel {
|
|||
var o1Value = arrayProp ? o1[arrayProp][propertyName] : o1[propertyName];
|
||||
var o2Value = arrayProp ? o2[arrayProp][propertyName] : o2[propertyName];
|
||||
|
||||
if(comparer) {
|
||||
if(comparer(o1Value, o2Value) === false) {
|
||||
if (comparer) {
|
||||
if (comparer(o1Value, o2Value) === false) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if(JSON.stringify(o1Value) !== JSON.stringify(o2Value)) {
|
||||
if (JSON.stringify(o1Value) !== JSON.stringify(o2Value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -164,42 +166,34 @@ ListModel {
|
|||
{'propertyName' : 'marketplaceID'},
|
||||
{'propertyName' : 'itemName'},
|
||||
{'propertyName' : 'script'},
|
||||
{'propertyName' : 'relayParentJoints'},
|
||||
{'propertyName' : 'localPosition', 'comparer' : compareNumericObjects},
|
||||
{'propertyName' : 'localRotationAngles', 'comparer' : compareNumericObjects},
|
||||
{'propertyName' : 'dimensions', 'comparer' : compareNumericObjects}], 'properties')
|
||||
}
|
||||
|
||||
function compareAttachments(a1, a2) {
|
||||
return compareObjects(a1, a2, [{'propertyName' : 'position', 'comparer' : compareNumericObjects},
|
||||
{'propertyName' : 'orientation'},
|
||||
{'propertyName' : 'parentJointIndex'},
|
||||
{'propertyName' : 'modelurl'}])
|
||||
}
|
||||
|
||||
function findAvatarIndexByValue(avatar) {
|
||||
|
||||
var index = -1;
|
||||
|
||||
// 2DO: find better way of determining selected avatar in bookmarks
|
||||
for(var i = 0; i < allAvatars.count; ++i) {
|
||||
for (var i = 0; i < allAvatars.count; ++i) {
|
||||
var thesame = true;
|
||||
var bookmarkedAvatar = allAvatars.get(i);
|
||||
|
||||
if(bookmarkedAvatar.avatarUrl !== avatar.avatarUrl)
|
||||
continue;
|
||||
|
||||
if(bookmarkedAvatar.avatarScale !== avatar.avatarScale)
|
||||
continue;
|
||||
|
||||
if(!modelsAreEqual(bookmarkedAvatar.attachments, avatar.attachments, compareAttachments)) {
|
||||
if (bookmarkedAvatar.avatarUrl !== avatar.avatarUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!modelsAreEqual(bookmarkedAvatar.wearables, avatar.wearables, compareWearables)) {
|
||||
if (bookmarkedAvatar.avatarScale !== avatar.avatarScale) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(thesame) {
|
||||
if (!modelsAreEqual(bookmarkedAvatar.wearables, avatar.wearables, compareWearables)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (thesame) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
|
@ -209,8 +203,8 @@ ListModel {
|
|||
}
|
||||
|
||||
function findAvatarIndex(avatarName) {
|
||||
for(var i = 0; i < count; ++i) {
|
||||
if(get(i).name === avatarName) {
|
||||
for (var i = 0; i < count; ++i) {
|
||||
if (get(i).name === avatarName) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -219,8 +213,9 @@ ListModel {
|
|||
|
||||
function findAvatar(avatarName) {
|
||||
var avatarIndex = findAvatarIndex(avatarName);
|
||||
if(avatarIndex === -1)
|
||||
if (avatarIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return get(avatarIndex);
|
||||
}
|
||||
|
|
|
@ -125,9 +125,9 @@ Rectangle {
|
|||
size: 15
|
||||
color: 'red'
|
||||
visible: {
|
||||
for(var i = 0; i < avatars.count; ++i) {
|
||||
for (var i = 0; i < avatars.count; ++i) {
|
||||
var avatarName = avatars.get(i).name;
|
||||
if(avatarName === favoriteName.text) {
|
||||
if (avatarName === favoriteName.text) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +165,7 @@ Rectangle {
|
|||
}
|
||||
|
||||
onYesClicked: function() {
|
||||
if(onSaveClicked) {
|
||||
if (onSaveClicked) {
|
||||
onSaveClicked();
|
||||
} else {
|
||||
root.close();
|
||||
|
@ -173,7 +173,7 @@ Rectangle {
|
|||
}
|
||||
|
||||
onNoClicked: function() {
|
||||
if(onCancelClicked) {
|
||||
if (onCancelClicked) {
|
||||
onCancelClicked();
|
||||
} else {
|
||||
root.close();
|
||||
|
|
|
@ -33,7 +33,7 @@ Row {
|
|||
onClicked: {
|
||||
console.debug('whitebutton.clicked', onNoClicked);
|
||||
|
||||
if(onNoClicked) {
|
||||
if (onNoClicked) {
|
||||
onNoClicked();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ Rectangle {
|
|||
wrapMode: Text.WordWrap;
|
||||
|
||||
onLinkActivated: {
|
||||
if(onLinkClicked)
|
||||
if (onLinkClicked)
|
||||
onLinkClicked(link);
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ Rectangle {
|
|||
noText: root.button1text
|
||||
|
||||
onYesClicked: function() {
|
||||
if(onButton2Clicked) {
|
||||
if (onButton2Clicked) {
|
||||
onButton2Clicked();
|
||||
} else {
|
||||
root.close();
|
||||
|
@ -174,7 +174,7 @@ Rectangle {
|
|||
}
|
||||
|
||||
onNoClicked: function() {
|
||||
if(onButton1Clicked) {
|
||||
if (onButton1Clicked) {
|
||||
onButton1Clicked();
|
||||
} else {
|
||||
root.close();
|
||||
|
|
|
@ -18,15 +18,38 @@ MessageBox {
|
|||
popup.button2text = 'CONFIRM';
|
||||
|
||||
popup.onButton2Clicked = function() {
|
||||
if(callback)
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
popup.close();
|
||||
}
|
||||
|
||||
popup.onLinkClicked = function(link) {
|
||||
if(linkCallback)
|
||||
if (linkCallback) {
|
||||
linkCallback(link);
|
||||
}
|
||||
}
|
||||
|
||||
popup.open();
|
||||
popup.inputText.forceActiveFocus();
|
||||
}
|
||||
|
||||
function showSpecifyWearableUrl(callback) {
|
||||
popup.button2text = 'CONFIRM'
|
||||
popup.button1text = 'CANCEL'
|
||||
popup.titleText = 'Specify Wearable URL'
|
||||
popup.bodyText = 'If you want to add a custom wearable, you can specify the URL of the wearable file here.'
|
||||
|
||||
popup.inputText.visible = true;
|
||||
popup.inputText.placeholderText = 'Enter Wearable URL';
|
||||
|
||||
popup.onButton2Clicked = function() {
|
||||
if (callback) {
|
||||
callback(popup.inputText.text);
|
||||
}
|
||||
|
||||
popup.close();
|
||||
}
|
||||
|
||||
popup.open();
|
||||
|
@ -41,22 +64,24 @@ MessageBox {
|
|||
popup.button1text = 'CANCEL'
|
||||
popup.titleText = 'Get Wearables'
|
||||
popup.bodyText = 'Buy wearables from <b><a href="app://marketplace">Marketplace.</a></b>' + '<br/>' +
|
||||
'Use wearables in <b><a href="app://purchases">My Purchases.</a></b>' + '<br/>' + '<br/>' +
|
||||
'Visit “AvatarIsland” to get wearables.'
|
||||
'Wear wearable from <b><a href="app://purchases">My Purchases.</a></b>' + '<br/>' + '<br/>' +
|
||||
'Visit “AvatarIsland” to get wearables'
|
||||
|
||||
popup.imageSource = getWearablesUrl;
|
||||
popup.onButton2Clicked = function() {
|
||||
popup.close();
|
||||
|
||||
if(callback)
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
popup.onLinkClicked = function(link) {
|
||||
popup.close();
|
||||
|
||||
if(linkCallback)
|
||||
if (linkCallback) {
|
||||
linkCallback(link);
|
||||
}
|
||||
}
|
||||
|
||||
popup.open();
|
||||
|
@ -72,8 +97,9 @@ MessageBox {
|
|||
popup.onButton2Clicked = function() {
|
||||
popup.close();
|
||||
|
||||
if(callback)
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
popup.open();
|
||||
}
|
||||
|
@ -87,8 +113,9 @@ MessageBox {
|
|||
popup.onButton2Clicked = function() {
|
||||
popup.close();
|
||||
|
||||
if(callback)
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
popup.open();
|
||||
}
|
||||
|
@ -109,15 +136,17 @@ MessageBox {
|
|||
popup.onButton2Clicked = function() {
|
||||
popup.close();
|
||||
|
||||
if(callback)
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
popup.onLinkClicked = function(link) {
|
||||
popup.close();
|
||||
|
||||
if(linkCallback)
|
||||
if (linkCallback) {
|
||||
linkCallback(link);
|
||||
}
|
||||
}
|
||||
|
||||
popup.open();
|
||||
|
|
|
@ -33,13 +33,13 @@ Rectangle {
|
|||
scaleSlider.value = Math.round(avatarScale * 10);
|
||||
scaleSlider.notify = true;;
|
||||
|
||||
if(settings.dominantHand === 'left') {
|
||||
if (settings.dominantHand === 'left') {
|
||||
leftHandRadioButton.checked = true;
|
||||
} else {
|
||||
rightHandRadioButton.checked = true;
|
||||
}
|
||||
|
||||
if(settings.collisionsEnabled) {
|
||||
if (settings.collisionsEnabled) {
|
||||
collisionsEnabledRadiobutton.checked = true;
|
||||
} else {
|
||||
collisionsDisabledRadioButton.checked = true;
|
||||
|
@ -113,7 +113,7 @@ Rectangle {
|
|||
|
||||
onValueChanged: {
|
||||
console.debug('value changed: ', value);
|
||||
if(notify) {
|
||||
if (notify) {
|
||||
console.debug('notifying.. ');
|
||||
root.scaleChanged(value / 10);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ Item {
|
|||
highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
|
||||
highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y));
|
||||
|
||||
if(maskColor.a > 0.0)
|
||||
if (maskColor.a > 0.0)
|
||||
gl_FragColor = sourceColor;
|
||||
else
|
||||
gl_FragColor = maskColor;
|
||||
|
|
|
@ -20,6 +20,7 @@ Row {
|
|||
|
||||
spacing: spinboxSpace
|
||||
|
||||
property bool enabled: false;
|
||||
property alias xvalue: xspinner.realValue
|
||||
property alias yvalue: yspinner.realValue
|
||||
property alias zvalue: zspinner.realValue
|
||||
|
@ -35,6 +36,7 @@ Row {
|
|||
realFrom: root.realFrom
|
||||
realTo: root.realTo
|
||||
realStepSize: root.realStepSize
|
||||
enabled: root.enabled
|
||||
}
|
||||
|
||||
HifiControlsUit.SpinBox {
|
||||
|
@ -48,6 +50,7 @@ Row {
|
|||
realFrom: root.realFrom
|
||||
realTo: root.realTo
|
||||
realStepSize: root.realStepSize
|
||||
enabled: root.enabled
|
||||
}
|
||||
|
||||
HifiControlsUit.SpinBox {
|
||||
|
@ -61,5 +64,6 @@ Row {
|
|||
realFrom: root.realFrom
|
||||
realTo: root.realTo
|
||||
realStepSize: root.realStepSize
|
||||
enabled: root.enabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,47 +63,6 @@ Item {
|
|||
question: "How can I get HFC?";
|
||||
answer: "High Fidelity commerce is in open beta right now. Want more HFC? \
|
||||
Get it by going to <br><br><b><font color='#0093C5'><a href='#bank'>BankOfHighFidelity.</a></font></b> and meeting with the banker!";
|
||||
}
|
||||
ListElement {
|
||||
isExpanded: false;
|
||||
question: "What are private keys and where are they stored?";
|
||||
answer:
|
||||
"A private key is a secret piece of text that is used to prove ownership, unlock confidential information, and sign transactions. \
|
||||
In High Fidelity, your private key is used to securely access the contents of your Wallet and Purchases. \
|
||||
After wallet setup, a hifikey file is stored on your computer in High Fidelity Interface's AppData directory. \
|
||||
Your hifikey file contains your private key and is protected by your wallet passphrase. \
|
||||
<br><br>It is very important to back up your hifikey file! \
|
||||
<b><font color='#0093C5'><a href='#privateKeyPath'>Tap here to open the folder where your HifiKeys are stored on your main display.</a></font></b>"
|
||||
}
|
||||
ListElement {
|
||||
isExpanded: false;
|
||||
question: "How do I back up my private keys?";
|
||||
answer: "You can back up your hifikey file (which contains your private key and is encrypted using your wallet passphrase) by copying it to a USB flash drive, or to a service like Dropbox or Google Drive. \
|
||||
Restore your hifikey file by replacing the file in Interface's AppData directory with your backup copy. \
|
||||
Others with access to your back up should not be able to spend your HFC without your passphrase. \
|
||||
<b><font color='#0093C5'><a href='#privateKeyPath'>Tap here to open the folder where your HifiKeys are stored on your main display.</a></font></b>";
|
||||
}
|
||||
ListElement {
|
||||
isExpanded: false;
|
||||
question: "What happens if I lose my private keys?";
|
||||
answer: "We cannot stress enough that you should keep a backup! For security reasons, High Fidelity does not keep a copy, and cannot restore it for you. \
|
||||
If you lose your private key, you will no longer have access to the contents of your Wallet or My Purchases. \
|
||||
Here are some things to try:<ul>\
|
||||
<li>If you have backed up your hifikey file before, search your backup location</li>\
|
||||
<li>Search your AppData directory in the last machine you used to set up the Wallet</li>\
|
||||
<li>If you are a developer and have installed multiple builds of High Fidelity, your hifikey file might be in another folder</li>\
|
||||
</ul><br><br>As a last resort, you can set up your Wallet again and generate a new hifikey file. \
|
||||
Unfortunately, this means you will start with 0 HFC and your purchased items will not be transferred over.";
|
||||
}
|
||||
ListElement {
|
||||
isExpanded: false;
|
||||
question: "What if I forget my wallet passphrase?";
|
||||
answer: "Your wallet passphrase is used to encrypt your private keys. Please write it down and store it securely! \
|
||||
<br><br>If you forget your passphrase, you will no longer be able to decrypt the hifikey file that the passphrase protects. \
|
||||
You will also no longer have access to the contents of your Wallet or My Purchases. \
|
||||
For security reasons, High Fidelity does not keep a copy of your passphrase, and can't restore it for you. \
|
||||
<br><br>If you still cannot remember your wallet passphrase, you can set up your Wallet again and generate a new hifikey file. \
|
||||
Unfortunately, this means you will start with 0 HFC and your purchased items will not be transferred over.";
|
||||
}
|
||||
ListElement {
|
||||
isExpanded: false;
|
||||
|
@ -114,11 +73,9 @@ In your Wallet's Send Money tab, choose from your list of connections, or choose
|
|||
ListElement {
|
||||
isExpanded: false;
|
||||
question: "What is a Security Pic?"
|
||||
answer: "Your Security Pic is an encrypted image that you select during Wallet Setup. \
|
||||
It acts as an extra layer of Wallet security. \
|
||||
When you see your Security Pic, you know that your actions and data are securely making use of your private keys.\
|
||||
<br><br>Don't enter your passphrase anywhere that doesn't display your Security Pic! \
|
||||
If you don't see your Security Pic on a page that requests your Wallet passphrase, someone untrustworthy may be trying to access your Wallet.";
|
||||
answer: "Your Security Pic acts as an extra layer of Wallet security. \
|
||||
When you see your Security Pic, you know that your actions and data are securely making use of your account. \
|
||||
<br><br><b><font color='#0093C5'><a href='#securitypic'>Tap here to change your Security Pic.</a></font></b>";
|
||||
}
|
||||
ListElement {
|
||||
isExpanded: false;
|
||||
|
@ -260,6 +217,8 @@ At the moment, there is currently no way to convert HFC to other currencies. Sta
|
|||
}
|
||||
} else if (link === "#support") {
|
||||
Qt.openUrlExternally("mailto:support@highfidelity.com");
|
||||
} else if (link === "#securitypic") {
|
||||
sendSignalToWallet({method: 'walletSecurity_changeSecurityImage'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,76 +88,9 @@ Item {
|
|||
color: hifi.colors.faintGray;
|
||||
}
|
||||
|
||||
Item {
|
||||
id: changePassphraseContainer;
|
||||
anchors.top: securityTextSeparator.bottom;
|
||||
anchors.topMargin: 8;
|
||||
anchors.left: parent.left;
|
||||
anchors.leftMargin: 40;
|
||||
anchors.right: parent.right;
|
||||
anchors.rightMargin: 55;
|
||||
height: 75;
|
||||
|
||||
HiFiGlyphs {
|
||||
id: changePassphraseImage;
|
||||
text: hifi.glyphs.passphrase;
|
||||
// Size
|
||||
size: 80;
|
||||
// Anchors
|
||||
anchors.top: parent.top;
|
||||
anchors.bottom: parent.bottom;
|
||||
anchors.left: parent.left;
|
||||
// Style
|
||||
color: hifi.colors.white;
|
||||
}
|
||||
|
||||
RalewaySemiBold {
|
||||
text: "Passphrase";
|
||||
// Anchors
|
||||
anchors.top: parent.top;
|
||||
anchors.bottom: parent.bottom;
|
||||
anchors.left: changePassphraseImage.right;
|
||||
anchors.leftMargin: 30;
|
||||
width: 50;
|
||||
// Text size
|
||||
size: 18;
|
||||
// Style
|
||||
color: hifi.colors.white;
|
||||
}
|
||||
|
||||
// "Change Passphrase" button
|
||||
HifiControlsUit.Button {
|
||||
id: changePassphraseButton;
|
||||
color: hifi.buttons.blue;
|
||||
colorScheme: hifi.colorSchemes.dark;
|
||||
anchors.right: parent.right;
|
||||
anchors.verticalCenter: parent.verticalCenter;
|
||||
width: 140;
|
||||
height: 40;
|
||||
text: "Change";
|
||||
onClicked: {
|
||||
sendSignalToWallet({method: 'walletSecurity_changePassphrase'});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: changePassphraseSeparator;
|
||||
// Size
|
||||
width: parent.width;
|
||||
height: 1;
|
||||
// Anchors
|
||||
anchors.left: parent.left;
|
||||
anchors.right: parent.right;
|
||||
anchors.top: changePassphraseContainer.bottom;
|
||||
anchors.topMargin: 8;
|
||||
// Style
|
||||
color: hifi.colors.faintGray;
|
||||
}
|
||||
|
||||
Item {
|
||||
id: changeSecurityImageContainer;
|
||||
anchors.top: changePassphraseSeparator.bottom;
|
||||
anchors.top: securityTextSeparator.bottom;
|
||||
anchors.topMargin: 8;
|
||||
anchors.left: parent.left;
|
||||
anchors.leftMargin: 40;
|
||||
|
@ -208,139 +141,77 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: privateKeysSeparator;
|
||||
// Size
|
||||
width: parent.width;
|
||||
height: 1;
|
||||
// Anchors
|
||||
anchors.left: parent.left;
|
||||
anchors.right: parent.right;
|
||||
Item {
|
||||
id: autoLogoutContainer;
|
||||
anchors.top: changeSecurityImageContainer.bottom;
|
||||
anchors.topMargin: 8;
|
||||
// Style
|
||||
color: hifi.colors.faintGray;
|
||||
}
|
||||
|
||||
Item {
|
||||
id: yourPrivateKeysContainer;
|
||||
anchors.top: privateKeysSeparator.bottom;
|
||||
anchors.left: parent.left;
|
||||
anchors.leftMargin: 40;
|
||||
anchors.right: parent.right;
|
||||
anchors.rightMargin: 55;
|
||||
anchors.bottom: parent.bottom;
|
||||
height: 75;
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
Commerce.getKeyFilePathIfExists();
|
||||
}
|
||||
HiFiGlyphs {
|
||||
id: autoLogoutImage;
|
||||
text: hifi.glyphs.walletKey;
|
||||
// Size
|
||||
size: 80;
|
||||
// Anchors
|
||||
anchors.top: parent.top;
|
||||
anchors.topMargin: 20;
|
||||
anchors.left: parent.left;
|
||||
// Style
|
||||
color: hifi.colors.white;
|
||||
}
|
||||
|
||||
HiFiGlyphs {
|
||||
id: yourPrivateKeysImage;
|
||||
text: hifi.glyphs.walletKey;
|
||||
// Size
|
||||
size: 80;
|
||||
HifiControlsUit.CheckBox {
|
||||
id: autoLogoutCheckbox;
|
||||
checked: Settings.getValue("wallet/autoLogout", false);
|
||||
text: "Automatically Log Out when Exiting Interface"
|
||||
// Anchors
|
||||
anchors.top: parent.top;
|
||||
anchors.topMargin: 20;
|
||||
anchors.left: parent.left;
|
||||
// Style
|
||||
anchors.verticalCenter: autoLogoutImage.verticalCenter;
|
||||
anchors.left: autoLogoutImage.right;
|
||||
anchors.leftMargin: 20;
|
||||
anchors.right: autoLogoutHelp.left;
|
||||
anchors.rightMargin: 12;
|
||||
boxSize: 28;
|
||||
labelFontSize: 18;
|
||||
color: hifi.colors.white;
|
||||
onCheckedChanged: {
|
||||
Settings.setValue("wallet/autoLogout", checked);
|
||||
if (checked) {
|
||||
Settings.setValue("wallet/savedUsername", Account.username);
|
||||
} else {
|
||||
Settings.setValue("wallet/savedUsername", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RalewaySemiBold {
|
||||
id: yourPrivateKeysText;
|
||||
text: "Private Keys";
|
||||
size: 18;
|
||||
id: autoLogoutHelp;
|
||||
text: '[?]';
|
||||
// Anchors
|
||||
anchors.top: parent.top;
|
||||
anchors.topMargin: 32;
|
||||
anchors.left: yourPrivateKeysImage.right;
|
||||
anchors.leftMargin: 30;
|
||||
anchors.verticalCenter: autoLogoutImage.verticalCenter;
|
||||
anchors.right: parent.right;
|
||||
width: 30;
|
||||
height: 30;
|
||||
// Style
|
||||
color: hifi.colors.white;
|
||||
}
|
||||
|
||||
// Text below "private keys"
|
||||
RalewayRegular {
|
||||
id: explanitoryText;
|
||||
text: "Your money and purchases are secured with private keys that only you have access to.";
|
||||
// Text size
|
||||
size: 18;
|
||||
// Anchors
|
||||
anchors.top: yourPrivateKeysText.bottom;
|
||||
anchors.topMargin: 10;
|
||||
anchors.left: yourPrivateKeysText.left;
|
||||
anchors.right: yourPrivateKeysText.right;
|
||||
height: paintedHeight;
|
||||
// Style
|
||||
color: hifi.colors.white;
|
||||
wrapMode: Text.WordWrap;
|
||||
// Alignment
|
||||
horizontalAlignment: Text.AlignLeft;
|
||||
verticalAlignment: Text.AlignVCenter;
|
||||
}
|
||||
color: hifi.colors.blueHighlight;
|
||||
|
||||
Rectangle {
|
||||
id: removeHmdContainer;
|
||||
z: 998;
|
||||
visible: false;
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop {
|
||||
position: 0.2;
|
||||
color: hifi.colors.baseGrayHighlight;
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0;
|
||||
color: hifi.colors.baseGrayShadow;
|
||||
}
|
||||
}
|
||||
anchors.fill: backupInstructionsButton;
|
||||
radius: 5;
|
||||
MouseArea {
|
||||
anchors.fill: parent;
|
||||
propagateComposedEvents: false;
|
||||
hoverEnabled: true;
|
||||
}
|
||||
|
||||
RalewayBold {
|
||||
anchors.fill: parent;
|
||||
text: "INSTRUCTIONS OPEN ON DESKTOP";
|
||||
size: 15;
|
||||
color: hifi.colors.white;
|
||||
verticalAlignment: Text.AlignVCenter;
|
||||
horizontalAlignment: Text.AlignHCenter;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: removeHmdContainerTimer;
|
||||
interval: 5000;
|
||||
onTriggered: removeHmdContainer.visible = false
|
||||
onEntered: {
|
||||
parent.color = hifi.colors.blueAccent;
|
||||
}
|
||||
onExited: {
|
||||
parent.color = hifi.colors.blueHighlight;
|
||||
}
|
||||
onClicked: {
|
||||
sendSignalToWallet({method: 'walletSecurity_autoLogoutHelp'});
|
||||
}
|
||||
}
|
||||
|
||||
HifiControlsUit.Button {
|
||||
id: backupInstructionsButton;
|
||||
text: "View Backup Instructions";
|
||||
color: hifi.buttons.blue;
|
||||
colorScheme: hifi.colorSchemes.dark;
|
||||
anchors.left: explanitoryText.left;
|
||||
anchors.right: explanitoryText.right;
|
||||
anchors.top: explanitoryText.bottom;
|
||||
anchors.topMargin: 16;
|
||||
height: 40;
|
||||
|
||||
onClicked: {
|
||||
var keyPath = "file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/'));
|
||||
Qt.openUrlExternally(keyPath + "/backup_instructions.html");
|
||||
Qt.openUrlExternally(keyPath);
|
||||
removeHmdContainer.visible = true;
|
||||
removeHmdContainerTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -382,6 +382,17 @@ Rectangle {
|
|||
} else if (msg.method === 'walletSecurity_changeSecurityImage') {
|
||||
securityImageChange.initModel();
|
||||
root.activeView = "securityImageChange";
|
||||
} else if (msg.method === 'walletSecurity_autoLogoutHelp') {
|
||||
lightboxPopup.titleText = "Automatically Log Out";
|
||||
lightboxPopup.bodyText = "By default, after you log in to High Fidelity, you will stay logged in to your High Fidelity " +
|
||||
"account even after you close and re-open Interface. This means anyone who opens Interface on your computer " +
|
||||
"could make purchases with your Wallet.\n\n" +
|
||||
"If you do not want to stay logged in across Interface sessions, check this box.";
|
||||
lightboxPopup.button1text = "CLOSE";
|
||||
lightboxPopup.button1method = function() {
|
||||
lightboxPopup.visible = false;
|
||||
}
|
||||
lightboxPopup.visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -399,6 +410,9 @@ Rectangle {
|
|||
onSendSignalToWallet: {
|
||||
if (msg.method === 'walletReset' || msg.method === 'passphraseReset') {
|
||||
sendToScript(msg);
|
||||
} else if (msg.method === 'walletSecurity_changeSecurityImage') {
|
||||
securityImageChange.initModel();
|
||||
root.activeView = "securityImageChange";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -803,12 +817,24 @@ Rectangle {
|
|||
}
|
||||
|
||||
function walletResetSetup() {
|
||||
/* Bypass all this and do it automatically
|
||||
root.activeView = "walletSetup";
|
||||
var timestamp = new Date();
|
||||
walletSetup.startingTimestamp = timestamp;
|
||||
walletSetup.setupAttemptID = generateUUID();
|
||||
UserActivityLogger.commerceWalletSetupStarted(timestamp, walletSetup.setupAttemptID, walletSetup.setupFlowVersion, walletSetup.referrer ? walletSetup.referrer : "wallet app",
|
||||
(AddressManager.placename || AddressManager.hostname || '') + (AddressManager.pathname ? AddressManager.pathname.match(/\/[^\/]+/)[0] : ''));
|
||||
*/
|
||||
|
||||
var randomNumber = Math.floor(Math.random() * 34) + 1;
|
||||
var securityImagePath = "images/" + addLeadingZero(randomNumber) + ".jpg";
|
||||
Commerce.getWalletAuthenticatedStatus(); // before writing security image, ensures that salt/account password is set.
|
||||
Commerce.chooseSecurityImage(securityImagePath);
|
||||
Commerce.generateKeyPair();
|
||||
}
|
||||
|
||||
function addLeadingZero(n) {
|
||||
return n < 10 ? '0' + n : '' + n;
|
||||
}
|
||||
|
||||
function followReferrer(msg) {
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
//
|
||||
// AttachmentsDialog.qml
|
||||
//
|
||||
// Created by David Rowe on 9 Mar 2017.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
import Qt.labs.settings 1.0
|
||||
|
||||
import "../../styles-uit"
|
||||
import "../../windows"
|
||||
import "content"
|
||||
|
||||
ScrollingWindow {
|
||||
id: root
|
||||
title: "Attachments"
|
||||
objectName: "AttachmentsDialog"
|
||||
width: 600
|
||||
height: 600
|
||||
resizable: true
|
||||
destroyOnHidden: true
|
||||
minSize: Qt.vector2d(400, 500)
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
// This is for JS/QML communication, which is unused in the AttachmentsDialog,
|
||||
// but not having this here results in spurious warnings about a
|
||||
// missing signal
|
||||
signal sendToScript(var message);
|
||||
|
||||
property var settings: Settings {
|
||||
category: "AttachmentsDialog"
|
||||
property alias x: root.x
|
||||
property alias y: root.y
|
||||
property alias width: root.width
|
||||
property alias height: root.height
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
root.destroy();
|
||||
}
|
||||
|
||||
AttachmentsContent { }
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Controls 1.5
|
||||
import QtQuick.XmlListModel 2.0
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import "../../windows"
|
||||
import "../../js/Utils.js" as Utils
|
||||
import "../models"
|
||||
|
||||
ModalWindow {
|
||||
id: root
|
||||
resizable: true
|
||||
width: 640
|
||||
height: 480
|
||||
|
||||
property var result;
|
||||
|
||||
signal selected(var modelUrl);
|
||||
signal canceled();
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "white"
|
||||
|
||||
Item {
|
||||
anchors { fill: parent; margins: 8 }
|
||||
|
||||
TextField {
|
||||
id: filterEdit
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
style: TextFieldStyle { renderType: Text.QtRendering }
|
||||
placeholderText: "filter"
|
||||
onTextChanged: tableView.model.filter = text
|
||||
}
|
||||
|
||||
TableView {
|
||||
id: tableView
|
||||
anchors { left: parent.left; right: parent.right; top: filterEdit.bottom; topMargin: 8; bottom: buttonRow.top; bottomMargin: 8 }
|
||||
model: S3Model{}
|
||||
onCurrentRowChanged: {
|
||||
if (currentRow == -1) {
|
||||
root.result = null;
|
||||
return;
|
||||
}
|
||||
result = model.baseUrl + "/" + model.get(tableView.currentRow).key;
|
||||
}
|
||||
itemDelegate: Component {
|
||||
Item {
|
||||
clip: true
|
||||
Text {
|
||||
x: 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: tableView.activeFocus && styleData.row === tableView.currentRow ? "yellow" : styleData.textColor
|
||||
elide: styleData.elideMode
|
||||
text: getText()
|
||||
|
||||
function getText() {
|
||||
switch(styleData.column) {
|
||||
case 1:
|
||||
return Utils.formatSize(styleData.value)
|
||||
default:
|
||||
return styleData.value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
TableViewColumn {
|
||||
role: "name"
|
||||
title: "Name"
|
||||
width: 200
|
||||
}
|
||||
TableViewColumn {
|
||||
role: "size"
|
||||
title: "Size"
|
||||
width: 100
|
||||
}
|
||||
TableViewColumn {
|
||||
role: "modified"
|
||||
title: "Last Modified"
|
||||
width: 200
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: tableView.model.status !== XmlListModel.Ready
|
||||
color: "#7fffffff"
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
width: 48; height: 48
|
||||
running: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
anchors { right: parent.right; bottom: parent.bottom }
|
||||
Button { action: acceptAction }
|
||||
Button { action: cancelAction }
|
||||
}
|
||||
|
||||
Action {
|
||||
id: acceptAction
|
||||
text: qsTr("OK")
|
||||
enabled: root.result ? true : false
|
||||
shortcut: "Return"
|
||||
onTriggered: {
|
||||
root.selected(root.result);
|
||||
root.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
id: cancelAction
|
||||
text: qsTr("Cancel")
|
||||
shortcut: "Esc"
|
||||
onTriggered: {
|
||||
root.canceled();
|
||||
root.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
import QtQuick 2.5
|
||||
|
||||
import "."
|
||||
import ".."
|
||||
import "../../tablet"
|
||||
import "../../../styles-uit"
|
||||
import "../../../controls-uit" as HifiControls
|
||||
import "../../../windows"
|
||||
|
||||
Item {
|
||||
height: column.height + 2 * 8
|
||||
|
||||
property var attachment;
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
signal selectAttachment();
|
||||
signal deleteAttachment(var attachment);
|
||||
signal updateAttachment();
|
||||
property bool completed: false;
|
||||
|
||||
function doSelectAttachment(control, focus) {
|
||||
if (focus) {
|
||||
selectAttachment();
|
||||
|
||||
// Refocus control after possibly changing focus to attachment.
|
||||
if (control.setControlFocus !== undefined) {
|
||||
control.setControlFocus();
|
||||
} else {
|
||||
control.focus = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { color: hifi.colors.baseGray; anchors.fill: parent; radius: 4 }
|
||||
|
||||
Component.onCompleted: {
|
||||
jointChooser.model = MyAvatar.jointNames;
|
||||
completed = true;
|
||||
}
|
||||
|
||||
Column {
|
||||
y: 8
|
||||
id: column
|
||||
anchors { left: parent.left; right: parent.right; margins: 20 }
|
||||
spacing: 8
|
||||
|
||||
Item {
|
||||
height: modelChooserButton.height + urlLabel.height + 4
|
||||
anchors { left: parent.left; right: parent.right;}
|
||||
HifiControls.Label { id: urlLabel; color: hifi.colors.lightGrayText; text: "Model URL"; anchors.top: parent.top;}
|
||||
HifiControls.TextField {
|
||||
id: modelUrl;
|
||||
height: jointChooser.height;
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors { bottom: parent.bottom; left: parent.left; rightMargin: 8; right: modelChooserButton.left }
|
||||
text: attachment ? attachment.modelUrl : ""
|
||||
onTextChanged: {
|
||||
if (completed && attachment && attachment.modelUrl !== text) {
|
||||
attachment.modelUrl = text;
|
||||
updateAttachment();
|
||||
}
|
||||
}
|
||||
onFocusChanged: doSelectAttachment(this, focus);
|
||||
}
|
||||
HifiControls.Button {
|
||||
id: modelChooserButton;
|
||||
text: "Choose";
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors { right: parent.right; verticalCenter: modelUrl.verticalCenter }
|
||||
Component {
|
||||
id: modelBrowserBuilder;
|
||||
ModelBrowserDialog {}
|
||||
}
|
||||
Component {
|
||||
id: tabletModelBrowserBuilder;
|
||||
TabletModelBrowserDialog {}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
var browser;
|
||||
if (typeof desktop !== "undefined") {
|
||||
browser = modelBrowserBuilder.createObject(desktop);
|
||||
browser.selected.connect(function(newModelUrl){
|
||||
modelUrl.text = newModelUrl;
|
||||
});
|
||||
} else {
|
||||
browser = tabletModelBrowserBuilder.createObject(tabletRoot);
|
||||
browser.selected.connect(function(newModelUrl){
|
||||
modelUrl.text = newModelUrl;
|
||||
tabletRoot.openModal = null;
|
||||
});
|
||||
browser.canceled.connect(function() {
|
||||
tabletRoot.openModal = null;
|
||||
});
|
||||
|
||||
// Make dialog modal.
|
||||
tabletRoot.openModal = browser;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
z: 1000
|
||||
height: jointChooser.height + jointLabel.height + 4
|
||||
anchors { left: parent.left; right: parent.right; }
|
||||
HifiControls.Label {
|
||||
id: jointLabel;
|
||||
text: "Joint";
|
||||
color: hifi.colors.lightGrayText;
|
||||
anchors.top: parent.top
|
||||
}
|
||||
HifiControls.ComboBox {
|
||||
id: jointChooser;
|
||||
dropdownHeight: (typeof desktop !== "undefined") ? 480 : 206
|
||||
anchors { bottom: parent.bottom; left: parent.left; right: parent.right }
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
currentIndex: attachment ? model.indexOf(attachment.jointName) : -1
|
||||
onCurrentIndexChanged: {
|
||||
if (completed && attachment && currentIndex != -1 && attachment.jointName !== model[currentIndex]) {
|
||||
attachment.jointName = model[currentIndex];
|
||||
updateAttachment();
|
||||
}
|
||||
}
|
||||
onFocusChanged: doSelectAttachment(this, focus);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
height: translation.height + translationLabel.height + 4
|
||||
anchors { left: parent.left; right: parent.right; }
|
||||
HifiControls.Label { id: translationLabel; color: hifi.colors.lightGrayText; text: "Translation"; anchors.top: parent.top; }
|
||||
Translation {
|
||||
id: translation;
|
||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom}
|
||||
vector: attachment ? attachment.translation : {x: 0, y: 0, z: 0};
|
||||
onValueChanged: {
|
||||
if (completed && attachment) {
|
||||
attachment.translation = vector;
|
||||
updateAttachment();
|
||||
}
|
||||
}
|
||||
onControlFocusChanged: doSelectAttachment(this, controlFocus);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
height: rotation.height + rotationLabel.height + 4
|
||||
anchors { left: parent.left; right: parent.right; }
|
||||
HifiControls.Label { id: rotationLabel; color: hifi.colors.lightGrayText; text: "Rotation"; anchors.top: parent.top; }
|
||||
Rotation {
|
||||
id: rotation;
|
||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom; }
|
||||
vector: attachment ? attachment.rotation : {x: 0, y: 0, z: 0};
|
||||
onValueChanged: {
|
||||
if (completed && attachment) {
|
||||
attachment.rotation = vector;
|
||||
updateAttachment();
|
||||
}
|
||||
}
|
||||
onControlFocusChanged: doSelectAttachment(this, controlFocus);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
height: scaleItem.height
|
||||
anchors { left: parent.left; right: parent.right; }
|
||||
|
||||
Item {
|
||||
id: scaleItem
|
||||
height: scaleSpinner.height + scaleLabel.height + 4
|
||||
width: parent.width / 3 - 8
|
||||
anchors { right: parent.right; }
|
||||
HifiControls.Label { id: scaleLabel; color: hifi.colors.lightGrayText; text: "Scale"; anchors.top: parent.top; }
|
||||
HifiControls.SpinBox {
|
||||
id: scaleSpinner;
|
||||
anchors { left: parent.left; right: parent.right; bottom: parent.bottom; }
|
||||
decimals: 2;
|
||||
minimumValue: 0.01
|
||||
maximumValue: 10
|
||||
realStepSize: 0.05;
|
||||
realValue: attachment ? attachment.scale : 1.0
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
onRealValueChanged: {
|
||||
if (completed && attachment && attachment.scale !== realValue) {
|
||||
attachment.scale = realValue;
|
||||
updateAttachment();
|
||||
}
|
||||
}
|
||||
onFocusChanged: doSelectAttachment(this, focus);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: isSoftItem
|
||||
height: scaleSpinner.height
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
}
|
||||
HifiControls.CheckBox {
|
||||
id: soft
|
||||
text: "Is soft"
|
||||
anchors {
|
||||
left: parent.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
checked: attachment ? attachment.soft : false
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
onCheckedChanged: {
|
||||
if (completed && attachment && attachment.soft !== checked) {
|
||||
attachment.soft = checked;
|
||||
updateAttachment();
|
||||
}
|
||||
}
|
||||
onFocusChanged: doSelectAttachment(this, focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
HifiControls.Button {
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
anchors { left: parent.left; right: parent.right; }
|
||||
text: "Delete"
|
||||
onClicked: deleteAttachment(root.attachment);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import "."
|
||||
|
||||
Vector3 {
|
||||
decimals: 1;
|
||||
stepSize: 1;
|
||||
maximumValue: 180
|
||||
minimumValue: -180
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import "."
|
||||
|
||||
Vector3 {
|
||||
decimals: 3;
|
||||
stepSize: 0.01;
|
||||
maximumValue: 10
|
||||
minimumValue: -10
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import QtQuick 2.5
|
||||
|
||||
import "../../../styles-uit"
|
||||
import "../../../controls-uit" as HifiControls
|
||||
import "../../../windows"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitHeight: xspinner.height
|
||||
readonly property real spacing: 8
|
||||
property real spinboxWidth: (width / 3) - spacing
|
||||
property var vector;
|
||||
property real decimals: 0
|
||||
property real stepSize: 1
|
||||
property real maximumValue: 99
|
||||
property real minimumValue: 0
|
||||
property bool controlFocus: false; // True if one of the ordinate controls has focus.
|
||||
property var controlFocusControl: undefined
|
||||
|
||||
signal valueChanged();
|
||||
|
||||
function setControlFocus() {
|
||||
if (controlFocusControl) {
|
||||
controlFocusControl.focus = true;
|
||||
// The controlFocus value is updated via onFocusChanged.
|
||||
}
|
||||
}
|
||||
|
||||
function setFocus(control, focus) {
|
||||
if (focus) {
|
||||
controlFocusControl = control;
|
||||
setControlFocusTrue.start(); // After any subsequent false from previous control.
|
||||
} else {
|
||||
controlFocus = false;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: setControlFocusTrue
|
||||
interval: 50
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
controlFocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
HifiControls.SpinBox {
|
||||
id: xspinner
|
||||
width: root.spinboxWidth
|
||||
anchors { left: parent.left }
|
||||
realValue: root.vector.x
|
||||
labelInside: "X:"
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
colorLabelInside: hifi.colors.redHighlight
|
||||
decimals: root.decimals
|
||||
realStepSize: root.stepSize
|
||||
maximumValue: root.maximumValue
|
||||
minimumValue: root.minimumValue
|
||||
onRealValueChanged: {
|
||||
if (realValue !== vector.x) {
|
||||
vector.x = realValue
|
||||
root.valueChanged();
|
||||
}
|
||||
}
|
||||
onFocusChanged: setFocus(this, focus);
|
||||
}
|
||||
|
||||
HifiControls.SpinBox {
|
||||
id: yspinner
|
||||
width: root.spinboxWidth
|
||||
anchors { horizontalCenter: parent.horizontalCenter }
|
||||
realValue: root.vector.y
|
||||
labelInside: "Y:"
|
||||
colorLabelInside: hifi.colors.greenHighlight
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
decimals: root.decimals
|
||||
realStepSize: root.stepSize
|
||||
maximumValue: root.maximumValue
|
||||
minimumValue: root.minimumValue
|
||||
onRealValueChanged: {
|
||||
if (realValue !== vector.y) {
|
||||
vector.y = realValue
|
||||
root.valueChanged();
|
||||
}
|
||||
}
|
||||
onFocusChanged: setFocus(this, focus);
|
||||
}
|
||||
|
||||
HifiControls.SpinBox {
|
||||
id: zspinner
|
||||
width: root.spinboxWidth
|
||||
anchors { right: parent.right; }
|
||||
realValue: root.vector.z
|
||||
labelInside: "Z:"
|
||||
colorLabelInside: hifi.colors.primaryHighlight
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
decimals: root.decimals
|
||||
realStepSize: root.stepSize
|
||||
maximumValue: root.maximumValue
|
||||
minimumValue: root.minimumValue
|
||||
onRealValueChanged: {
|
||||
if (realValue !== vector.z) {
|
||||
vector.z = realValue
|
||||
root.valueChanged();
|
||||
}
|
||||
}
|
||||
onFocusChanged: setFocus(this, focus);
|
||||
}
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Dialogs 1.2 as OriginalDialogs
|
||||
|
||||
import "../../../styles-uit"
|
||||
import "../../../controls-uit" as HifiControls
|
||||
import "../../../windows"
|
||||
import "../attachments"
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
readonly property var originalAttachments: MyAvatar.getAttachmentsVariant();
|
||||
property var attachments: [];
|
||||
|
||||
function reload(){
|
||||
content.attachments = [];
|
||||
var currentAttachments = MyAvatar.getAttachmentsVariant();
|
||||
listView.model.clear();
|
||||
for (var i = 0; i < currentAttachments.length; ++i) {
|
||||
var attachment = currentAttachments[i];
|
||||
content.attachments.push(attachment);
|
||||
listView.model.append({});
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
id: onAttachmentsChangedConnection
|
||||
target: MyAvatar
|
||||
onAttachmentsChanged: reload()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
reload()
|
||||
}
|
||||
|
||||
function setAttachmentsVariant(attachments) {
|
||||
onAttachmentsChangedConnection.enabled = false;
|
||||
MyAvatar.setAttachmentsVariant(attachments);
|
||||
onAttachmentsChangedConnection.enabled = true;
|
||||
}
|
||||
|
||||
Column {
|
||||
width: pane.width
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: root.height - (keyboardEnabled && keyboardRaised ? 200 : 0)
|
||||
color: hifi.colors.baseGray
|
||||
|
||||
Rectangle {
|
||||
id: attachmentsBackground
|
||||
anchors {
|
||||
left: parent.left; right: parent.right; top: parent.top; bottom: newAttachmentButton.top;
|
||||
margins: hifi.dimensions.contentMargin.x
|
||||
bottomMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
color: hifi.colors.baseGrayShadow
|
||||
radius: 4
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: scrollBar.left
|
||||
bottom: parent.bottom
|
||||
margins: 4
|
||||
}
|
||||
clip: true
|
||||
cacheBuffer: 4000
|
||||
|
||||
model: ListModel {}
|
||||
delegate: Item {
|
||||
id: attachmentDelegate
|
||||
implicitHeight: attachmentView.height + 8;
|
||||
implicitWidth: attachmentView.width
|
||||
|
||||
MouseArea {
|
||||
// User can click on whitespace to select item.
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
onClicked: {
|
||||
listView.currentIndex = index;
|
||||
attachmentsBackground.forceActiveFocus(); // Unfocus from any control.
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
Attachment {
|
||||
id: attachmentView
|
||||
width: listView.width
|
||||
attachment: content.attachments[index]
|
||||
onSelectAttachment: {
|
||||
listView.currentIndex = index;
|
||||
}
|
||||
onDeleteAttachment: {
|
||||
attachments.splice(index, 1);
|
||||
listView.model.remove(index, 1);
|
||||
}
|
||||
onUpdateAttachment: {
|
||||
setAttachmentsVariant(attachments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCountChanged: {
|
||||
setAttachmentsVariant(attachments);
|
||||
}
|
||||
|
||||
/*
|
||||
// DEBUG
|
||||
highlight: Rectangle { color: "#40ffff00" }
|
||||
highlightFollowsCurrentItem: true
|
||||
*/
|
||||
|
||||
onHeightChanged: {
|
||||
// Keyboard has been raised / lowered.
|
||||
positionViewAtIndex(listView.currentIndex, ListView.SnapPosition);
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
if (!yScrollTimer.running) {
|
||||
scrollSlider.y = currentIndex * (scrollBar.height - scrollSlider.height) / (listView.count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
onContentYChanged: {
|
||||
// User may have dragged content up/down.
|
||||
yScrollTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: yScrollTimer
|
||||
interval: 200
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: {
|
||||
var index = (listView.count - 1) * listView.contentY / (listView.contentHeight - scrollBar.height);
|
||||
index = Math.round(index);
|
||||
listView.currentIndex = index;
|
||||
scrollSlider.y = index * (scrollBar.height - scrollSlider.height) / (listView.count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: scrollBar
|
||||
|
||||
property bool scrolling: listView.contentHeight > listView.height
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
topMargin: 4
|
||||
bottomMargin: 4
|
||||
}
|
||||
width: scrolling ? 18 : 0
|
||||
radius: attachmentsBackground.radius
|
||||
color: hifi.colors.baseGrayShadow
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onClicked: {
|
||||
var index = listView.currentIndex;
|
||||
index = index + (mouse.y <= scrollSlider.y ? -1 : 1);
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
if (index > listView.count - 1) {
|
||||
index = listView.count - 1;
|
||||
}
|
||||
listView.currentIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: scrollSlider
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: 3
|
||||
}
|
||||
width: 16
|
||||
height: (listView.height / listView.contentHeight) * listView.height
|
||||
radius: width / 2
|
||||
color: hifi.colors.lightGray
|
||||
|
||||
visible: scrollBar.scrolling;
|
||||
|
||||
onYChanged: {
|
||||
var index = y * (listView.count - 1) / (scrollBar.height - scrollSlider.height);
|
||||
index = Math.round(index);
|
||||
listView.currentIndex = index;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
drag.target: scrollSlider
|
||||
drag.axis: Drag.YAxis
|
||||
drag.minimumY: 0
|
||||
drag.maximumY: scrollBar.height - scrollSlider.height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Button {
|
||||
id: newAttachmentButton
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: buttonRow.top
|
||||
margins: hifi.dimensions.contentMargin.x;
|
||||
topMargin: hifi.dimensions.contentSpacing.y
|
||||
bottomMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
text: "New Attachment"
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
onClicked: {
|
||||
var template = {
|
||||
modelUrl: "",
|
||||
translation: { x: 0, y: 0, z: 0 },
|
||||
rotation: { x: 0, y: 0, z: 0 },
|
||||
scale: 1,
|
||||
jointName: MyAvatar.jointNames[0],
|
||||
soft: false
|
||||
};
|
||||
attachments.push(template);
|
||||
listView.model.append({});
|
||||
setAttachmentsVariant(attachments);
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: buttonRow
|
||||
spacing: 8
|
||||
anchors {
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
margins: hifi.dimensions.contentMargin.x
|
||||
topMargin: hifi.dimensions.contentSpacing.y
|
||||
bottomMargin: hifi.dimensions.contentSpacing.y
|
||||
}
|
||||
HifiControls.Button {
|
||||
action: okAction
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
}
|
||||
HifiControls.Button {
|
||||
action: cancelAction
|
||||
color: hifi.buttons.black
|
||||
colorScheme: hifi.colorSchemes.dark
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
id: cancelAction
|
||||
text: "Cancel"
|
||||
onTriggered: {
|
||||
setAttachmentsVariant(originalAttachments);
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Action {
|
||||
id: okAction
|
||||
text: "OK"
|
||||
onTriggered: {
|
||||
for (var i = 0; i < attachments.length; ++i) {
|
||||
console.log("Attachment " + i + ": " + attachments[i]);
|
||||
}
|
||||
|
||||
setAttachmentsVariant(attachments);
|
||||
closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
//
|
||||
// TabletAttachmentsDialog.qml
|
||||
//
|
||||
// Created by David Rowe on 9 Mar 2017.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
import QtQuick 2.5
|
||||
|
||||
import "../../controls-uit" as HifiControls
|
||||
import "../../styles-uit"
|
||||
import "../dialogs/content"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
objectName: "AttachmentsDialog"
|
||||
|
||||
property string title: "Avatar Attachments"
|
||||
|
||||
property bool keyboardEnabled: false
|
||||
property bool keyboardRaised: false
|
||||
property bool punctuationMode: false
|
||||
|
||||
signal sendToScript(var message);
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
HifiConstants { id: hifi }
|
||||
|
||||
Rectangle {
|
||||
id: pane // Surrogate for ScrollingWindow's pane.
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
Tablet.getTablet("com.highfidelity.interface.tablet.system").gotoHomeScreen();
|
||||
}
|
||||
|
||||
anchors.topMargin: hifi.dimensions.tabletMenuHeader // Space for header.
|
||||
|
||||
HifiControls.TabletHeader {
|
||||
id: header
|
||||
title: root.title
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.top
|
||||
}
|
||||
}
|
||||
|
||||
AttachmentsContent {
|
||||
id: attachments
|
||||
|
||||
anchors {
|
||||
top: header.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: keyboard.top
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// Defocuses any current control so that the keyboard gets hidden.
|
||||
id: defocuser
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: {
|
||||
parent.forceActiveFocus();
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HifiControls.Keyboard {
|
||||
id: keyboard
|
||||
raised: parent.keyboardEnabled && parent.keyboardRaised
|
||||
numeric: parent.punctuationMode
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: activator
|
||||
anchors.fill: parent
|
||||
propagateComposedEvents: true
|
||||
enabled: true
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
keyboardEnabled = HMD.active;
|
||||
}
|
||||
}
|
|
@ -266,19 +266,10 @@ class RenderEventHandler : public QObject {
|
|||
using Parent = QObject;
|
||||
Q_OBJECT
|
||||
public:
|
||||
RenderEventHandler(QOpenGLContext* context) {
|
||||
_renderContext = new OffscreenGLCanvas();
|
||||
_renderContext->setObjectName("RenderContext");
|
||||
_renderContext->create(context);
|
||||
if (!_renderContext->makeCurrent()) {
|
||||
qFatal("Unable to make rendering context current");
|
||||
}
|
||||
_renderContext->doneCurrent();
|
||||
|
||||
RenderEventHandler() {
|
||||
// Transfer to a new thread
|
||||
moveToNewNamedThread(this, "RenderThread", [this](QThread* renderThread) {
|
||||
hifi::qt::addBlockingForbiddenThread("Render", renderThread);
|
||||
_renderContext->moveToThreadWithContext(renderThread);
|
||||
qApp->_lastTimeRendered.start();
|
||||
}, std::bind(&RenderEventHandler::initialize, this), QThread::HighestPriority);
|
||||
}
|
||||
|
@ -288,9 +279,6 @@ private:
|
|||
setObjectName("Render");
|
||||
PROFILE_SET_THREAD_NAME("Render");
|
||||
setCrashAnnotation("render_thread_id", std::to_string((size_t)QThread::currentThreadId()));
|
||||
if (!_renderContext->makeCurrent()) {
|
||||
qFatal("Unable to make rendering context current on render thread");
|
||||
}
|
||||
}
|
||||
|
||||
void render() {
|
||||
|
@ -311,8 +299,6 @@ private:
|
|||
}
|
||||
return Parent::event(event);
|
||||
}
|
||||
|
||||
OffscreenGLCanvas* _renderContext { nullptr };
|
||||
};
|
||||
|
||||
|
||||
|
@ -389,6 +375,7 @@ static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds
|
|||
static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop";
|
||||
static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin";
|
||||
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
|
||||
static const QString AUTO_LOGOUT_SETTING_NAME = "wallet/autoLogout";
|
||||
|
||||
const std::vector<std::pair<QString, Application::AcceptURLMethod>> Application::_acceptedExtensions {
|
||||
{ SVO_EXTENSION, &Application::importSVOFromURL },
|
||||
|
@ -1027,7 +1014,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
// This is done so as not break previous command line scripts
|
||||
if (testScriptPath.left(URL_SCHEME_HTTP.length()) == URL_SCHEME_HTTP ||
|
||||
testScriptPath.left(URL_SCHEME_FTP.length()) == URL_SCHEME_FTP) {
|
||||
|
||||
|
||||
setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath));
|
||||
} else if (QFileInfo(testScriptPath).exists()) {
|
||||
setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath));
|
||||
|
@ -1108,6 +1095,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
// Set File Logger Session UUID
|
||||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||
auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr;
|
||||
if (avatarManager) {
|
||||
workload::SpacePointer space = getEntities()->getWorkloadSpace();
|
||||
avatarManager->setSpace(space);
|
||||
}
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
|
||||
_logger->setSessionID(accountManager->getSessionID());
|
||||
|
@ -1744,6 +1735,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
QTimer* settingsTimer = new QTimer();
|
||||
moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{
|
||||
connect(qApp, &Application::beforeAboutToQuit, [this, settingsTimer]{
|
||||
bool autoLogout = Setting::Handle<bool>(AUTO_LOGOUT_SETTING_NAME, false).get();
|
||||
if (autoLogout) {
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
accountManager->logout();
|
||||
}
|
||||
// Disconnect the signal from the save settings
|
||||
QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings);
|
||||
// Stop the settings timer
|
||||
|
@ -1844,14 +1840,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
|||
}
|
||||
});
|
||||
|
||||
connect(getEntities()->getTree().get(), &EntityTree::deletingEntity, [](const EntityItemID& entityItemID) {
|
||||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||
auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr;
|
||||
if (myAvatar) {
|
||||
myAvatar->clearAvatarEntity(entityItemID);
|
||||
}
|
||||
});
|
||||
|
||||
EntityTree::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
|
||||
// try to find the renderable
|
||||
auto renderable = getEntities()->renderableForEntityId(entityID);
|
||||
|
@ -2558,11 +2546,15 @@ void Application::cleanupBeforeQuit() {
|
|||
|
||||
Application::~Application() {
|
||||
// remove avatars from physics engine
|
||||
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
|
||||
VectorOfMotionStates motionStates;
|
||||
DependencyManager::get<AvatarManager>()->getObjectsToRemoveFromPhysics(motionStates);
|
||||
_physicsEngine->removeObjects(motionStates);
|
||||
DependencyManager::get<AvatarManager>()->deleteAllAvatars();
|
||||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||
avatarManager->clearOtherAvatars();
|
||||
|
||||
PhysicsEngine::Transaction transaction;
|
||||
avatarManager->buildPhysicsTransaction(transaction);
|
||||
_physicsEngine->processTransaction(transaction);
|
||||
avatarManager->handleProcessedPhysicsTransaction(transaction);
|
||||
|
||||
avatarManager->deleteAllAvatars();
|
||||
|
||||
_physicsEngine->setCharacterController(nullptr);
|
||||
|
||||
|
@ -2631,7 +2623,7 @@ Application::~Application() {
|
|||
|
||||
// Can't log to file passed this point, FileLogger about to be deleted
|
||||
qInstallMessageHandler(LogHandler::verboseMessageHandler);
|
||||
|
||||
|
||||
_renderEventHandler->deleteLater();
|
||||
}
|
||||
|
||||
|
@ -2695,26 +2687,14 @@ void Application::initializeGL() {
|
|||
}
|
||||
}
|
||||
|
||||
// Build an offscreen GL context for the main thread.
|
||||
_offscreenContext = new OffscreenGLCanvas();
|
||||
_offscreenContext->setObjectName("MainThreadContext");
|
||||
_offscreenContext->create(_glWidget->qglContext());
|
||||
if (!_offscreenContext->makeCurrent()) {
|
||||
qFatal("Unable to make offscreen context current");
|
||||
}
|
||||
_offscreenContext->doneCurrent();
|
||||
_offscreenContext->setThreadContext();
|
||||
_renderEventHandler = new RenderEventHandler();
|
||||
|
||||
// Build an offscreen GL context for the main thread.
|
||||
_glWidget->makeCurrent();
|
||||
glClearColor(0.2f, 0.2f, 0.2f, 1);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
_glWidget->swapBuffers();
|
||||
|
||||
// Move the GL widget context to the render event handler thread
|
||||
_renderEventHandler = new RenderEventHandler(_glWidget->qglContext());
|
||||
if (!_offscreenContext->makeCurrent()) {
|
||||
qFatal("Unable to make offscreen context current");
|
||||
}
|
||||
|
||||
// Create the GPU backend
|
||||
|
||||
|
@ -2727,9 +2707,6 @@ void Application::initializeGL() {
|
|||
_gpuContext = std::make_shared<gpu::Context>();
|
||||
|
||||
DependencyManager::get<TextureCache>()->setGPUContext(_gpuContext);
|
||||
|
||||
// Restore the default main thread context
|
||||
_offscreenContext->makeCurrent();
|
||||
}
|
||||
|
||||
static const QString SPLASH_SKYBOX{ "{\"ProceduralEntity\":{ \"version\":2, \"shaderUrl\":\"qrc:///shaders/splashSkybox.frag\" } }" };
|
||||
|
@ -2768,8 +2745,6 @@ void Application::initializeDisplayPlugins() {
|
|||
// Submit a default frame to render until the engine starts up
|
||||
updateRenderArgs(0.0f);
|
||||
|
||||
_offscreenContext->makeCurrent();
|
||||
|
||||
#define ENABLE_SPLASH_FRAME 0
|
||||
#if ENABLE_SPLASH_FRAME
|
||||
{
|
||||
|
@ -2811,8 +2786,6 @@ void Application::initializeDisplayPlugins() {
|
|||
}
|
||||
|
||||
void Application::initializeRenderEngine() {
|
||||
_offscreenContext->makeCurrent();
|
||||
|
||||
// FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread.
|
||||
DeadlockWatchdogThread::withPause([&] {
|
||||
// Set up the render engine
|
||||
|
@ -3700,7 +3673,7 @@ bool Application::event(QEvent* event) {
|
|||
|
||||
bool Application::eventFilter(QObject* object, QEvent* event) {
|
||||
|
||||
if (_aboutToQuit) {
|
||||
if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -4562,7 +4535,6 @@ void Application::idle() {
|
|||
if (offscreenUi->size() != fromGlm(uiSize)) {
|
||||
qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize;
|
||||
offscreenUi->resize(fromGlm(uiSize));
|
||||
_offscreenContext->makeCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4620,10 +4592,6 @@ void Application::idle() {
|
|||
bool showWarnings = getLogger()->extraDebugging();
|
||||
PerformanceWarning warn(showWarnings, "idle()");
|
||||
|
||||
if (!_offscreenContext->makeCurrent()) {
|
||||
qFatal("Unable to make main thread context current");
|
||||
}
|
||||
|
||||
{
|
||||
_gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition());
|
||||
_gameWorkload._engine->run();
|
||||
|
@ -4951,7 +4919,6 @@ QVector<EntityItemID> Application::pasteEntities(float x, float y, float z) {
|
|||
}
|
||||
|
||||
void Application::init() {
|
||||
_offscreenContext->makeCurrent();
|
||||
// Make sure Login state is up to date
|
||||
DependencyManager::get<DialogsManager>()->toggleLoginDialog();
|
||||
if (!DISABLE_DEFERRED) {
|
||||
|
@ -5300,6 +5267,7 @@ void Application::resetPhysicsReadyInformation() {
|
|||
_nearbyEntitiesCountAtLastPhysicsCheck = 0;
|
||||
_nearbyEntitiesStabilityCount = 0;
|
||||
_physicsEnabled = false;
|
||||
_octreeProcessor.startEntitySequence();
|
||||
}
|
||||
|
||||
|
||||
|
@ -5534,10 +5502,7 @@ void Application::update(float deltaTime) {
|
|||
// we haven't yet enabled physics. we wait until we think we have all the collision information
|
||||
// for nearby entities before starting bullet up.
|
||||
quint64 now = usecTimestampNow();
|
||||
// Check for flagged EntityData having arrived.
|
||||
auto entityTreeRenderer = getEntities();
|
||||
if (isServerlessMode() ||
|
||||
(entityTreeRenderer && _octreeProcessor.octreeSequenceIsComplete(entityTreeRenderer->getLastOctreeMessageSequence()) )) {
|
||||
if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) {
|
||||
// we've received a new full-scene octree stats packet, or it's been long enough to try again anyway
|
||||
_lastPhysicsCheckTime = now;
|
||||
_fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter;
|
||||
|
@ -5546,7 +5511,7 @@ void Application::update(float deltaTime) {
|
|||
// process octree stats packets are sent in between full sends of a scene (this isn't currently true).
|
||||
// We keep physics disabled until we've received a full scene and everything near the avatar in that
|
||||
// scene is ready to compute its collision shape.
|
||||
if (nearbyEntitiesAreReadyForPhysics() && getMyAvatar()->isReadyForPhysics()) {
|
||||
if (getMyAvatar()->isReadyForPhysics()) {
|
||||
_physicsEnabled = true;
|
||||
getMyAvatar()->updateMotionBehaviorFromMenu();
|
||||
}
|
||||
|
@ -5751,12 +5716,10 @@ void Application::update(float deltaTime) {
|
|||
|
||||
t1 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
avatarManager->getObjectsToRemoveFromPhysics(motionStates);
|
||||
_physicsEngine->removeObjects(motionStates);
|
||||
avatarManager->getObjectsToAddToPhysics(motionStates);
|
||||
_physicsEngine->addObjects(motionStates);
|
||||
avatarManager->getObjectsToChange(motionStates);
|
||||
_physicsEngine->changeObjects(motionStates);
|
||||
PhysicsEngine::Transaction transaction;
|
||||
avatarManager->buildPhysicsTransaction(transaction);
|
||||
_physicsEngine->processTransaction(transaction);
|
||||
avatarManager->handleProcessedPhysicsTransaction(transaction);
|
||||
|
||||
myAvatar->prepareForPhysicsSimulation();
|
||||
_physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) {
|
||||
|
@ -6227,6 +6190,10 @@ bool Application::isHMDMode() const {
|
|||
return getActiveDisplayPlugin()->isHmd();
|
||||
}
|
||||
|
||||
float Application::getNumCollisionObjects() const {
|
||||
return _physicsEngine ? _physicsEngine->getNumCollisionObjects() : 0;
|
||||
}
|
||||
|
||||
float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); }
|
||||
|
||||
QRect Application::getDesirableApplicationGeometry() const {
|
||||
|
@ -6350,7 +6317,6 @@ void Application::clearDomainOctreeDetails() {
|
|||
_octreeServerSceneStats.clear();
|
||||
});
|
||||
|
||||
_octreeProcessor.resetCompletionSequenceNumber();
|
||||
// reset the model renderer
|
||||
getEntities()->clear();
|
||||
|
||||
|
@ -6432,8 +6398,8 @@ void Application::nodeActivated(SharedNodePointer node) {
|
|||
if (_avatarOverrideUrl.isValid()) {
|
||||
getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl);
|
||||
}
|
||||
static const QUrl empty{};
|
||||
if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->cannonicalSkeletonModelURL(empty)) {
|
||||
|
||||
if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) {
|
||||
getMyAvatar()->resetFullAvatarURL();
|
||||
}
|
||||
getMyAvatar()->markIdentityDataChanged();
|
||||
|
@ -8342,7 +8308,7 @@ QOpenGLContext* Application::getPrimaryContext() {
|
|||
}
|
||||
|
||||
bool Application::makeRenderingContextCurrent() {
|
||||
return _offscreenContext->makeCurrent();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Application::isForeground() const {
|
||||
|
|
|
@ -81,7 +81,6 @@
|
|||
|
||||
#include "Sound.h"
|
||||
|
||||
class OffscreenGLCanvas;
|
||||
class GLCanvas;
|
||||
class FaceTracker;
|
||||
class MainWindow;
|
||||
|
@ -208,6 +207,7 @@ public:
|
|||
|
||||
size_t getRenderFrameCount() const { return _renderFrameCount; }
|
||||
float getRenderLoopRate() const { return _renderLoopCounter.rate(); }
|
||||
float getNumCollisionObjects() const;
|
||||
float getTargetRenderFrameRate() const; // frames/second
|
||||
|
||||
float getFieldOfView() { return _fieldOfView.get(); }
|
||||
|
@ -554,7 +554,6 @@ private:
|
|||
|
||||
bool _previousSessionCrashed;
|
||||
|
||||
OffscreenGLCanvas* _offscreenContext { nullptr };
|
||||
DisplayPluginPointer _displayPlugin;
|
||||
QMetaObject::Connection _displayPluginPresentConnection;
|
||||
mutable std::mutex _displayPluginLock;
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include <avatar/AvatarManager.h>
|
||||
#include <EntityItemID.h>
|
||||
#include <EntityTree.h>
|
||||
#include <ModelEntityItem.h>
|
||||
#include <PhysicalEntitySimulation.h>
|
||||
#include <EntityEditPacketSender.h>
|
||||
#include <VariantMapToScriptValue.h>
|
||||
|
@ -35,7 +36,7 @@
|
|||
#include "QVariantGLM.h"
|
||||
|
||||
#include <QtQuick/QQuickWindow>
|
||||
|
||||
#include <memory>
|
||||
|
||||
void addAvatarEntities(const QVariantList& avatarEntities) {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
@ -145,8 +146,8 @@ void AvatarBookmarks::removeBookmark(const QString& bookmarkName) {
|
|||
}
|
||||
|
||||
bool isWearableEntity(const EntityItemPointer& entity) {
|
||||
return entity->isVisible() && entity->getParentJointIndex() != INVALID_JOINT_INDEX &&
|
||||
(entity->getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || entity->getParentID() == DependencyManager::get<AvatarManager>()->getMyAvatar()->getSelfID());
|
||||
return entity->isVisible() && (entity->getParentJointIndex() != INVALID_JOINT_INDEX || (entity->getType() == EntityTypes::Model && (std::static_pointer_cast<ModelEntityItem>(entity))->getRelayParentJoints()))
|
||||
&& (entity->getParentID() == DependencyManager::get<NodeList>()->getSessionUUID() || entity->getParentID() == DependencyManager::get<AvatarManager>()->getMyAvatar()->getSelfID());
|
||||
}
|
||||
|
||||
void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) {
|
||||
|
@ -254,7 +255,6 @@ QVariantMap AvatarBookmarks::getAvatarDataToBookmark() {
|
|||
bookmark.insert(ENTRY_VERSION, AVATAR_BOOKMARK_VERSION);
|
||||
bookmark.insert(ENTRY_AVATAR_URL, avatarUrl);
|
||||
bookmark.insert(ENTRY_AVATAR_SCALE, avatarScale);
|
||||
bookmark.insert(ENTRY_AVATAR_ATTACHMENTS, myAvatar->getAttachmentsVariant());
|
||||
|
||||
QScriptEngine scriptEngine;
|
||||
QVariantList wearableEntities;
|
||||
|
|
|
@ -277,13 +277,6 @@ Menu::Menu() {
|
|||
QString("hifi/tablet/TabletGraphicsPreferences.qml"), "GraphicsPreferencesDialog");
|
||||
});
|
||||
|
||||
// Settings > Attachments...
|
||||
action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::Attachments);
|
||||
connect(action, &QAction::triggered, [] {
|
||||
qApp->showDialog(QString("hifi/dialogs/AttachmentsDialog.qml"),
|
||||
QString("hifi/tablet/TabletAttachmentsDialog.qml"), "AttachmentsDialog");
|
||||
});
|
||||
|
||||
// Settings > Developer Menu
|
||||
addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus()));
|
||||
|
||||
|
@ -458,6 +451,9 @@ Menu::Menu() {
|
|||
});
|
||||
}
|
||||
|
||||
addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ComputeBlendshapes, 0, true,
|
||||
DependencyManager::get<ModelBlender>().data(), SLOT(setComputeBlendshapes(bool)));
|
||||
|
||||
// Developer > Assets >>>
|
||||
// Menu item is not currently needed but code should be kept in case it proves useful again at some stage.
|
||||
//#define WANT_ASSET_MIGRATION
|
||||
|
|
|
@ -36,7 +36,6 @@ namespace MenuOption {
|
|||
const QString AskToResetSettings = "Ask To Reset Settings on Start";
|
||||
const QString AssetMigration = "ATP Asset Migration";
|
||||
const QString AssetServer = "Asset Browser";
|
||||
const QString Attachments = "Attachments...";
|
||||
const QString AudioScope = "Show Scope";
|
||||
const QString AudioScopeFiftyFrames = "Fifty";
|
||||
const QString AudioScopeFiveFrames = "Five";
|
||||
|
@ -222,6 +221,7 @@ namespace MenuOption {
|
|||
const QString NotificationSounds = "play_notification_sounds";
|
||||
const QString NotificationSoundsSnapshot = "play_notification_sounds_snapshot";
|
||||
const QString NotificationSoundsTablet = "play_notification_sounds_tablet";
|
||||
const QString ComputeBlendshapes = "Compute Blendshapes";
|
||||
}
|
||||
|
||||
#endif // hifi_Menu_h
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
#include <string>
|
||||
|
||||
#include <QScriptEngine>
|
||||
#include <QtCore/QJsonDocument>
|
||||
|
||||
#include "AvatarLogging.h"
|
||||
|
||||
|
@ -45,7 +44,6 @@
|
|||
#include "InterfaceLogging.h"
|
||||
#include "Menu.h"
|
||||
#include "MyAvatar.h"
|
||||
#include "OtherAvatar.h"
|
||||
#include "SceneScriptingInterface.h"
|
||||
|
||||
// 50 times per second - target is 45hz, but this helps account for any small deviations
|
||||
|
@ -72,10 +70,6 @@ AvatarManager::AvatarManager(QObject* parent) :
|
|||
qRegisterMetaType<QWeakPointer<Node> >("NodeWeakPointer");
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto& packetReceiver = nodeList->getPacketReceiver();
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket");
|
||||
packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar");
|
||||
packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket");
|
||||
|
||||
// when we hear that the user has ignored an avatar by session UUID
|
||||
// immediately remove that avatar instead of waiting for the absence of packets from avatar mixer
|
||||
|
@ -86,8 +80,25 @@ AvatarManager::AvatarManager(QObject* parent) :
|
|||
});
|
||||
}
|
||||
|
||||
AvatarSharedPointer AvatarManager::addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) {
|
||||
AvatarSharedPointer avatar = AvatarHashMap::addAvatar(sessionUUID, mixerWeakPointer);
|
||||
|
||||
const auto otherAvatar = std::static_pointer_cast<OtherAvatar>(avatar);
|
||||
if (otherAvatar && _space) {
|
||||
std::unique_lock<std::mutex> lock(_spaceLock);
|
||||
auto spaceIndex = _space->allocateID();
|
||||
otherAvatar->setSpaceIndex(spaceIndex);
|
||||
workload::Sphere sphere(otherAvatar->getWorldPosition(), otherAvatar->getBoundingRadius());
|
||||
workload::Transaction transaction;
|
||||
SpatiallyNestablePointer nestable = std::static_pointer_cast<SpatiallyNestable>(otherAvatar);
|
||||
transaction.reset(spaceIndex, sphere, workload::Owner(nestable));
|
||||
_space->enqueueTransaction(transaction);
|
||||
}
|
||||
return avatar;
|
||||
}
|
||||
|
||||
AvatarManager::~AvatarManager() {
|
||||
assert(_motionStates.empty());
|
||||
assert(_avatarsToChangeInPhysics.empty());
|
||||
}
|
||||
|
||||
void AvatarManager::init() {
|
||||
|
@ -109,6 +120,11 @@ void AvatarManager::init() {
|
|||
}
|
||||
}
|
||||
|
||||
void AvatarManager::setSpace(workload::SpacePointer& space ) {
|
||||
assert(!_space);
|
||||
_space = space;
|
||||
}
|
||||
|
||||
void AvatarManager::updateMyAvatar(float deltaTime) {
|
||||
bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings);
|
||||
PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()");
|
||||
|
@ -197,20 +213,20 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
uint64_t updateExpiry = startTime + UPDATE_BUDGET;
|
||||
int numAvatarsUpdated = 0;
|
||||
int numAVatarsNotUpdated = 0;
|
||||
bool physicsEnabled = qApp->isPhysicsEnabled();
|
||||
|
||||
render::Transaction transaction;
|
||||
render::Transaction renderTransaction;
|
||||
workload::Transaction workloadTransaction;
|
||||
while (!sortedAvatars.empty()) {
|
||||
const SortableAvatar& sortData = sortedAvatars.top();
|
||||
const auto avatar = std::static_pointer_cast<Avatar>(sortData.getAvatar());
|
||||
const auto otherAvatar = std::static_pointer_cast<OtherAvatar>(sortData.getAvatar());
|
||||
const auto avatar = std::static_pointer_cast<OtherAvatar>(sortData.getAvatar());
|
||||
|
||||
// TODO: to help us scale to more avatars it would be nice to not have to poll orb state here
|
||||
// if the geometry is loaded then turn off the orb
|
||||
if (avatar->getSkeletonModel()->isLoaded()) {
|
||||
// remove the orb if it is there
|
||||
otherAvatar->removeOrb();
|
||||
avatar->removeOrb();
|
||||
} else {
|
||||
otherAvatar->updateOrbPosition();
|
||||
avatar->updateOrbPosition();
|
||||
}
|
||||
|
||||
bool ignoring = DependencyManager::get<NodeList>()->isPersonalMutingNode(avatar->getID());
|
||||
|
@ -223,18 +239,6 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
if (_shouldRender) {
|
||||
avatar->ensureInScene(avatar, qApp->getMain3DScene());
|
||||
}
|
||||
if (physicsEnabled && !avatar->isInPhysicsSimulation()) {
|
||||
ShapeInfo shapeInfo;
|
||||
avatar->computeShapeInfo(shapeInfo);
|
||||
btCollisionShape* shape = const_cast<btCollisionShape*>(ObjectMotionState::getShapeManager()->getShape(shapeInfo));
|
||||
if (shape) {
|
||||
AvatarMotionState* motionState = new AvatarMotionState(avatar, shape);
|
||||
motionState->setMass(avatar->computeMass());
|
||||
avatar->setPhysicsCallback([=] (uint32_t flags) { motionState->addDirtyFlags(flags); });
|
||||
_motionStates.insert(avatar.get(), motionState);
|
||||
_motionStatesToAddToPhysics.insert(motionState);
|
||||
}
|
||||
}
|
||||
avatar->animateScaleChanges(deltaTime);
|
||||
|
||||
const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY;
|
||||
|
@ -246,15 +250,16 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
numAvatarsUpdated++;
|
||||
}
|
||||
avatar->simulate(deltaTime, inView);
|
||||
avatar->updateRenderItem(transaction);
|
||||
avatar->updateRenderItem(renderTransaction);
|
||||
avatar->updateSpaceProxy(workloadTransaction);
|
||||
avatar->setLastRenderUpdateTime(startTime);
|
||||
} else {
|
||||
// we've spent our full time budget --> bail on the rest of the avatar updates
|
||||
// --> more avatars may freeze until their priority trickles up
|
||||
// --> some scale or fade animations may glitch
|
||||
// --> some scale animations may glitch
|
||||
// --> some avatar velocity measurements may be a little off
|
||||
|
||||
// no time simulate, but we take the time to count how many were tragically missed
|
||||
// no time to simulate, but we take the time to count how many were tragically missed
|
||||
bool inView = sortData.getPriority() > OUT_OF_VIEW_THRESHOLD;
|
||||
if (!inView) {
|
||||
break;
|
||||
|
@ -278,8 +283,16 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
|
|||
}
|
||||
|
||||
if (_shouldRender) {
|
||||
qApp->getMain3DScene()->enqueueTransaction(transaction);
|
||||
qApp->getMain3DScene()->enqueueTransaction(renderTransaction);
|
||||
}
|
||||
|
||||
if (!_spaceProxiesToDelete.empty() && _space) {
|
||||
std::unique_lock<std::mutex> lock(_spaceLock);
|
||||
workloadTransaction.remove(_spaceProxiesToDelete);
|
||||
_spaceProxiesToDelete.clear();
|
||||
}
|
||||
_space->enqueueTransaction(workloadTransaction);
|
||||
|
||||
_numAvatarsUpdated = numAvatarsUpdated;
|
||||
_numAvatarsNotUpdated = numAVatarsNotUpdated;
|
||||
|
||||
|
@ -367,19 +380,64 @@ AvatarSharedPointer AvatarManager::newSharedAvatar() {
|
|||
return AvatarSharedPointer(new OtherAvatar(qApp->thread()), [](OtherAvatar* ptr) { ptr->deleteLater(); });
|
||||
}
|
||||
|
||||
void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
|
||||
AvatarHashMap::handleRemovedAvatar(removedAvatar, removalReason);
|
||||
void AvatarManager::queuePhysicsChange(const OtherAvatarPointer& avatar) {
|
||||
_avatarsToChangeInPhysics.insert(avatar);
|
||||
}
|
||||
|
||||
// remove from physics
|
||||
auto avatar = std::static_pointer_cast<Avatar>(removedAvatar);
|
||||
avatar->setPhysicsCallback(nullptr);
|
||||
AvatarMotionStateMap::iterator itr = _motionStates.find(avatar.get());
|
||||
if (itr != _motionStates.end()) {
|
||||
AvatarMotionState* motionState = *itr;
|
||||
_motionStatesToAddToPhysics.remove(motionState);
|
||||
_motionStatesToRemoveFromPhysics.push_back(motionState);
|
||||
_motionStates.erase(itr);
|
||||
void AvatarManager::buildPhysicsTransaction(PhysicsEngine::Transaction& transaction) {
|
||||
SetOfOtherAvatars failedShapeBuilds;
|
||||
for (auto avatar : _avatarsToChangeInPhysics) {
|
||||
bool isInPhysics = avatar->isInPhysicsSimulation();
|
||||
if (isInPhysics != avatar->shouldBeInPhysicsSimulation()) {
|
||||
if (isInPhysics) {
|
||||
transaction.objectsToRemove.push_back(avatar->_motionState);
|
||||
avatar->_motionState = nullptr;
|
||||
} else {
|
||||
ShapeInfo shapeInfo;
|
||||
avatar->computeShapeInfo(shapeInfo);
|
||||
btCollisionShape* shape = const_cast<btCollisionShape*>(ObjectMotionState::getShapeManager()->getShape(shapeInfo));
|
||||
if (shape) {
|
||||
AvatarMotionState* motionState = new AvatarMotionState(avatar, shape);
|
||||
motionState->setMass(avatar->computeMass());
|
||||
avatar->_motionState = motionState;
|
||||
transaction.objectsToAdd.push_back(motionState);
|
||||
} else {
|
||||
failedShapeBuilds.insert(avatar);
|
||||
}
|
||||
}
|
||||
} else if (isInPhysics) {
|
||||
transaction.objectsToChange.push_back(avatar->_motionState);
|
||||
}
|
||||
}
|
||||
_avatarsToChangeInPhysics.swap(failedShapeBuilds);
|
||||
}
|
||||
|
||||
void AvatarManager::handleProcessedPhysicsTransaction(PhysicsEngine::Transaction& transaction) {
|
||||
// things on objectsToChange correspond to failed changes
|
||||
// so we push them back onto _avatarsToChangeInPhysics
|
||||
for (auto object : transaction.objectsToChange) {
|
||||
AvatarMotionState* motionState = static_cast<AvatarMotionState*>(object);
|
||||
assert(motionState);
|
||||
assert(motionState->_avatar);
|
||||
_avatarsToChangeInPhysics.insert(motionState->_avatar);
|
||||
}
|
||||
// things on objectsToRemove are ready for delete
|
||||
for (auto object : transaction.objectsToRemove) {
|
||||
delete object;
|
||||
}
|
||||
transaction.clear();
|
||||
}
|
||||
|
||||
void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
|
||||
auto avatar = std::static_pointer_cast<OtherAvatar>(removedAvatar);
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_spaceLock);
|
||||
_spaceProxiesToDelete.push_back(avatar->getSpaceIndex());
|
||||
}
|
||||
AvatarHashMap::handleRemovedAvatar(avatar, removalReason);
|
||||
|
||||
avatar->die();
|
||||
queuePhysicsChange(avatar);
|
||||
|
||||
if (removalReason == KillAvatarReason::TheirAvatarEnteredYourBubble) {
|
||||
emit DependencyManager::get<UsersScriptingInterface>()->enteredIgnoreRadius();
|
||||
|
@ -415,48 +473,15 @@ void AvatarManager::clearOtherAvatars() {
|
|||
}
|
||||
|
||||
void AvatarManager::deleteAllAvatars() {
|
||||
assert(_motionStates.empty()); // should have called clearOtherAvatars() before getting here
|
||||
deleteMotionStates();
|
||||
assert(_avatarsToChangeInPhysics.empty());
|
||||
|
||||
QReadLocker locker(&_hashLock);
|
||||
AvatarHash::iterator avatarIterator = _avatarHash.begin();
|
||||
while (avatarIterator != _avatarHash.end()) {
|
||||
auto avatar = std::static_pointer_cast<Avatar>(avatarIterator.value());
|
||||
auto avatar = std::static_pointer_cast<OtherAvatar>(avatarIterator.value());
|
||||
avatarIterator = _avatarHash.erase(avatarIterator);
|
||||
avatar->die();
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarManager::deleteMotionStates() {
|
||||
// delete motionstates that were removed from physics last frame
|
||||
for (auto state : _motionStatesToDelete) {
|
||||
delete state;
|
||||
}
|
||||
_motionStatesToDelete.clear();
|
||||
}
|
||||
|
||||
void AvatarManager::getObjectsToRemoveFromPhysics(VectorOfMotionStates& result) {
|
||||
deleteMotionStates();
|
||||
result = _motionStatesToRemoveFromPhysics;
|
||||
_motionStatesToDelete.swap(_motionStatesToRemoveFromPhysics);
|
||||
}
|
||||
|
||||
void AvatarManager::getObjectsToAddToPhysics(VectorOfMotionStates& result) {
|
||||
result.clear();
|
||||
for (auto motionState : _motionStatesToAddToPhysics) {
|
||||
result.push_back(motionState);
|
||||
}
|
||||
_motionStatesToAddToPhysics.clear();
|
||||
}
|
||||
|
||||
void AvatarManager::getObjectsToChange(VectorOfMotionStates& result) {
|
||||
result.clear();
|
||||
AvatarMotionStateMap::iterator motionStateItr = _motionStates.begin();
|
||||
while (motionStateItr != _motionStates.end()) {
|
||||
if ((*motionStateItr)->getIncomingDirtyFlags() != 0) {
|
||||
result.push_back(*motionStateItr);
|
||||
}
|
||||
++motionStateItr;
|
||||
assert(!avatar->_motionState);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#ifndef hifi_AvatarManager_h
|
||||
#define hifi_AvatarManager_h
|
||||
|
||||
#include <set>
|
||||
|
||||
#include <QtCore/QHash>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QSharedPointer>
|
||||
|
@ -23,9 +25,11 @@
|
|||
#include <shared/RateCounter.h>
|
||||
#include <avatars-renderer/ScriptAvatar.h>
|
||||
#include <AudioInjector.h>
|
||||
#include <workload/Space.h>
|
||||
|
||||
#include "AvatarMotionState.h"
|
||||
#include "MyAvatar.h"
|
||||
#include "OtherAvatar.h"
|
||||
|
||||
/**jsdoc
|
||||
* The AvatarManager API has properties and methods which manage Avatars within the same domain.
|
||||
|
@ -62,6 +66,7 @@ public:
|
|||
virtual ~AvatarManager();
|
||||
|
||||
void init();
|
||||
void setSpace(workload::SpacePointer& space );
|
||||
|
||||
std::shared_ptr<MyAvatar> getMyAvatar() { return _myAvatar; }
|
||||
glm::vec3 getMyAvatarPosition() const { return _myAvatar->getWorldPosition(); }
|
||||
|
@ -92,6 +97,7 @@ public:
|
|||
void getObjectsToRemoveFromPhysics(VectorOfMotionStates& motionStates);
|
||||
void getObjectsToAddToPhysics(VectorOfMotionStates& motionStates);
|
||||
void getObjectsToChange(VectorOfMotionStates& motionStates);
|
||||
|
||||
void handleChangedMotionStates(const VectorOfMotionStates& motionStates);
|
||||
void handleCollisionEvents(const CollisionEvents& collisionEvents);
|
||||
|
||||
|
@ -102,23 +108,21 @@ public:
|
|||
* @returns {number}
|
||||
*/
|
||||
Q_INVOKABLE float getAvatarDataRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
|
||||
|
||||
|
||||
/**jsdoc
|
||||
* @function AvatarManager.getAvatarUpdateRate
|
||||
* @param {Uuid} sessionID
|
||||
* @param {string} [rateName=""]
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
Q_INVOKABLE float getAvatarUpdateRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
|
||||
|
||||
|
||||
/**jsdoc
|
||||
* @function AvatarManager.getAvatarSimulationRate
|
||||
* @param {Uuid} sessionID
|
||||
* @param {string} [rateName=""]
|
||||
* @returns {number}
|
||||
*/
|
||||
|
||||
Q_INVOKABLE float getAvatarSimulationRate(const QUuid& sessionID, const QString& rateName = QString("")) const;
|
||||
|
||||
/**jsdoc
|
||||
|
@ -153,7 +157,7 @@ public:
|
|||
*/
|
||||
// TODO: remove this HACK once we settle on optimal default sort coefficients
|
||||
Q_INVOKABLE float getAvatarSortCoefficient(const QString& name);
|
||||
|
||||
|
||||
/**jsdoc
|
||||
* @function AvatarManager.setAvatarSortCoefficient
|
||||
* @param {string} name
|
||||
|
@ -175,14 +179,20 @@ public:
|
|||
float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); }
|
||||
int getIdentityRequestsSent() const { return _identityRequestsSent; }
|
||||
|
||||
public slots:
|
||||
void queuePhysicsChange(const OtherAvatarPointer& avatar);
|
||||
void buildPhysicsTransaction(PhysicsEngine::Transaction& transaction);
|
||||
void handleProcessedPhysicsTransaction(PhysicsEngine::Transaction& transaction);
|
||||
|
||||
public slots:
|
||||
/**jsdoc
|
||||
* @function AvatarManager.updateAvatarRenderStatus
|
||||
* @param {boolean} shouldRenderAvatars
|
||||
*/
|
||||
void updateAvatarRenderStatus(bool shouldRenderAvatars);
|
||||
|
||||
protected:
|
||||
AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer<Node>& mixerWeakPointer) override;
|
||||
|
||||
private:
|
||||
explicit AvatarManager(QObject* parent = 0);
|
||||
explicit AvatarManager(const AvatarManager& other);
|
||||
|
@ -190,16 +200,12 @@ private:
|
|||
void simulateAvatarFades(float deltaTime);
|
||||
|
||||
AvatarSharedPointer newSharedAvatar() override;
|
||||
void deleteMotionStates();
|
||||
void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override;
|
||||
|
||||
QVector<AvatarSharedPointer> _avatarsToFade;
|
||||
|
||||
using AvatarMotionStateMap = QMap<Avatar*, AvatarMotionState*>;
|
||||
AvatarMotionStateMap _motionStates;
|
||||
VectorOfMotionStates _motionStatesToRemoveFromPhysics;
|
||||
VectorOfMotionStates _motionStatesToDelete;
|
||||
SetOfMotionStates _motionStatesToAddToPhysics;
|
||||
using SetOfOtherAvatars = std::set<OtherAvatarPointer>;
|
||||
SetOfOtherAvatars _avatarsToChangeInPhysics;
|
||||
|
||||
std::shared_ptr<MyAvatar> _myAvatar;
|
||||
quint64 _lastSendAvatarDataTime = 0; // Controls MyAvatar send data rate.
|
||||
|
@ -212,6 +218,10 @@ private:
|
|||
float _avatarSimulationTime { 0.0f };
|
||||
bool _shouldRender { true };
|
||||
mutable int _identityRequestsSent { 0 };
|
||||
|
||||
mutable std::mutex _spaceLock;
|
||||
workload::SpacePointer _space;
|
||||
std::vector<int32_t> _spaceProxiesToDelete;
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarManager_h
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
#include <PhysicsHelpers.h>
|
||||
|
||||
|
||||
AvatarMotionState::AvatarMotionState(AvatarSharedPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
|
||||
AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
|
||||
assert(_avatar);
|
||||
_type = MOTIONSTATE_TYPE_AVATAR;
|
||||
cacheShapeDiameter();
|
||||
|
@ -57,7 +57,7 @@ PhysicsMotionType AvatarMotionState::computePhysicsMotionType() const {
|
|||
// virtual and protected
|
||||
const btCollisionShape* AvatarMotionState::computeNewShape() {
|
||||
ShapeInfo shapeInfo;
|
||||
std::static_pointer_cast<Avatar>(_avatar)->computeShapeInfo(shapeInfo);
|
||||
_avatar->computeShapeInfo(shapeInfo);
|
||||
return getShapeManager()->getShape(shapeInfo);
|
||||
}
|
||||
|
||||
|
@ -151,7 +151,7 @@ glm::vec3 AvatarMotionState::getObjectAngularVelocity() const {
|
|||
|
||||
// virtual
|
||||
glm::vec3 AvatarMotionState::getObjectGravity() const {
|
||||
return std::static_pointer_cast<Avatar>(_avatar)->getAcceleration();
|
||||
return _avatar->getAcceleration();
|
||||
}
|
||||
|
||||
// virtual
|
||||
|
@ -176,7 +176,7 @@ void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& ma
|
|||
|
||||
// virtual
|
||||
float AvatarMotionState::getMass() const {
|
||||
return std::static_pointer_cast<Avatar>(_avatar)->computeMass();
|
||||
return _avatar->computeMass();
|
||||
}
|
||||
|
||||
void AvatarMotionState::cacheShapeDiameter() {
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
|
||||
#include <QSet>
|
||||
|
||||
#include <avatars-renderer/Avatar.h>
|
||||
#include <ObjectMotionState.h>
|
||||
#include <BulletUtil.h>
|
||||
|
||||
#include "OtherAvatar.h"
|
||||
|
||||
class AvatarMotionState : public ObjectMotionState {
|
||||
public:
|
||||
AvatarMotionState(AvatarSharedPointer avatar, const btCollisionShape* shape);
|
||||
AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape);
|
||||
|
||||
virtual void handleEasyChanges(uint32_t& flags) override;
|
||||
virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override;
|
||||
|
@ -85,7 +85,7 @@ protected:
|
|||
virtual bool isReadyToComputeShape() const override { return true; }
|
||||
virtual const btCollisionShape* computeNewShape() override;
|
||||
|
||||
AvatarSharedPointer _avatar;
|
||||
OtherAvatarPointer _avatar;
|
||||
float _diameter { 0.0f };
|
||||
|
||||
uint32_t _dirtyFlags;
|
||||
|
|
|
@ -105,7 +105,7 @@ MyAvatar::MyAvatar(QThread* thread) :
|
|||
_eyeContactTarget(LEFT_EYE),
|
||||
_realWorldFieldOfView("realWorldFieldOfView",
|
||||
DEFAULT_REAL_WORLD_FIELD_OF_VIEW_DEGREES),
|
||||
_useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", false),
|
||||
_useAdvancedMovementControls("advancedMovementForHandControllersIsChecked", true),
|
||||
_smoothOrientationTimer(std::numeric_limits<float>::max()),
|
||||
_smoothOrientationInitial(),
|
||||
_smoothOrientationTarget(),
|
||||
|
@ -121,6 +121,7 @@ MyAvatar::MyAvatar(QThread* thread) :
|
|||
_audioListenerMode(FROM_HEAD),
|
||||
_hmdAtRestDetector(glm::vec3(0), glm::quat())
|
||||
{
|
||||
_clientTraitsHandler = std::unique_ptr<ClientTraitsHandler>(new ClientTraitsHandler(this));
|
||||
|
||||
// give the pointer to our head to inherited _headData variable from AvatarData
|
||||
_headData = new MyHead(this);
|
||||
|
@ -202,6 +203,7 @@ MyAvatar::MyAvatar(QThread* thread) :
|
|||
|
||||
connect(recorder.data(), &Recorder::recordingStateChanged, [=] {
|
||||
if (recorder->isRecording()) {
|
||||
createRecordingIDs();
|
||||
setRecordingBasis();
|
||||
} else {
|
||||
clearRecordingBasis();
|
||||
|
@ -443,7 +445,6 @@ void MyAvatar::reset(bool andRecenter, bool andReload, bool andHead) {
|
|||
}
|
||||
|
||||
void MyAvatar::update(float deltaTime) {
|
||||
|
||||
// update moving average of HMD facing in xz plane.
|
||||
const float HMD_FACING_TIMESCALE = getRotationRecenterFilterLength();
|
||||
|
||||
|
@ -513,6 +514,8 @@ void MyAvatar::update(float deltaTime) {
|
|||
sendIdentityPacket();
|
||||
}
|
||||
|
||||
_clientTraitsHandler->sendChangedTraitsToMixer();
|
||||
|
||||
simulate(deltaTime);
|
||||
|
||||
currentEnergy += energyChargeRate;
|
||||
|
@ -1289,7 +1292,6 @@ void MyAvatar::loadData() {
|
|||
// HACK: manually remove empty 'avatarEntityData' else legacy data may persist in settings file
|
||||
settings.remove("avatarEntityData");
|
||||
}
|
||||
setAvatarEntityDataChanged(true);
|
||||
|
||||
// Flying preferences must be loaded before calling setFlyingEnabled()
|
||||
Setting::Handle<bool> firstRunVal { Settings::firstRun, true };
|
||||
|
@ -1666,7 +1668,10 @@ void MyAvatar::clearJointsData() {
|
|||
void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
|
||||
_skeletonModelChangeCount++;
|
||||
int skeletonModelChangeCount = _skeletonModelChangeCount;
|
||||
|
||||
auto previousSkeletonModelURL = _skeletonModelURL;
|
||||
Avatar::setSkeletonModelURL(skeletonModelURL);
|
||||
|
||||
_skeletonModel->setTagMask(render::hifi::TAG_NONE);
|
||||
_skeletonModel->setGroupCulled(true);
|
||||
_skeletonModel->setVisibleInScene(true, qApp->getMain3DScene());
|
||||
|
@ -1693,9 +1698,9 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
|
|||
}
|
||||
QObject::disconnect(*skeletonConnection);
|
||||
});
|
||||
|
||||
saveAvatarUrl();
|
||||
emit skeletonChanged();
|
||||
emit skeletonModelURLChanged();
|
||||
}
|
||||
|
||||
void MyAvatar::removeAvatarEntities(const std::function<bool(const QUuid& entityID)>& condition) {
|
||||
|
@ -1766,8 +1771,6 @@ void MyAvatar::useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelN
|
|||
setSkeletonModelURL(fullAvatarURL);
|
||||
UserActivityLogger::getInstance().changedModel("skeleton", urlString);
|
||||
}
|
||||
|
||||
markIdentityDataChanged();
|
||||
}
|
||||
|
||||
glm::vec3 MyAvatar::getSkeletonPosition() const {
|
||||
|
@ -2111,7 +2114,10 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
|
|||
attachmentDataToEntityProperties(data, properties);
|
||||
newEntitiesProperties.push_back(properties);
|
||||
}
|
||||
removeAvatarEntities();
|
||||
|
||||
// clear any existing avatar entities
|
||||
setAvatarEntityData(AvatarEntityMap());
|
||||
|
||||
for (auto& properties : newEntitiesProperties) {
|
||||
DependencyManager::get<EntityScriptingInterface>()->addEntity(properties, true);
|
||||
}
|
||||
|
|
|
@ -18,22 +18,22 @@
|
|||
|
||||
#include <QUuid>
|
||||
|
||||
#include <SettingHandle.h>
|
||||
#include <Rig.h>
|
||||
#include <Sound.h>
|
||||
#include <ScriptEngine.h>
|
||||
|
||||
#include <controllers/Pose.h>
|
||||
#include <controllers/Actions.h>
|
||||
#include <AvatarConstants.h>
|
||||
#include <avatars-renderer/Avatar.h>
|
||||
#include <avatars-renderer/ScriptAvatar.h>
|
||||
#include <ClientTraitsHandler.h>
|
||||
#include <controllers/Pose.h>
|
||||
#include <controllers/Actions.h>
|
||||
#include <EntityItem.h>
|
||||
#include <ThreadSafeValueCache.h>
|
||||
#include <Rig.h>
|
||||
#include <ScriptEngine.h>
|
||||
#include <SettingHandle.h>
|
||||
#include <Sound.h>
|
||||
|
||||
#include "AtRestDetector.h"
|
||||
#include "MyCharacterController.h"
|
||||
#include "RingBufferHistory.h"
|
||||
#include <ThreadSafeValueCache.h>
|
||||
#include <EntityItem.h>
|
||||
|
||||
class AvatarActionHold;
|
||||
class ModelItemID;
|
||||
|
@ -1334,7 +1334,6 @@ public slots:
|
|||
*/
|
||||
void setAnimGraphUrl(const QUrl& url); // thread-safe
|
||||
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.getPositionForAudio
|
||||
* @returns {Vec3}
|
||||
|
@ -1347,7 +1346,6 @@ public slots:
|
|||
*/
|
||||
glm::quat getOrientationForAudio();
|
||||
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.setModelScale
|
||||
* @param {number} scale
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
#include "OtherAvatar.h"
|
||||
#include "Application.h"
|
||||
|
||||
#include "AvatarMotionState.h"
|
||||
|
||||
OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) {
|
||||
// give the pointer to our head to inherited _headData variable from AvatarData
|
||||
_headData = new Head(this);
|
||||
|
@ -58,3 +60,38 @@ void OtherAvatar::createOrb() {
|
|||
_otherAvatarOrbMeshPlaceholder->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
void OtherAvatar::setSpaceIndex(int32_t index) {
|
||||
assert(_spaceIndex == -1);
|
||||
_spaceIndex = index;
|
||||
}
|
||||
|
||||
void OtherAvatar::updateSpaceProxy(workload::Transaction& transaction) const {
|
||||
if (_spaceIndex > -1) {
|
||||
float approximateBoundingRadius = glm::length(getTargetScale());
|
||||
workload::Sphere sphere(getWorldPosition(), approximateBoundingRadius);
|
||||
transaction.update(_spaceIndex, sphere);
|
||||
}
|
||||
}
|
||||
|
||||
int OtherAvatar::parseDataFromBuffer(const QByteArray& buffer) {
|
||||
int32_t bytesRead = Avatar::parseDataFromBuffer(buffer);
|
||||
if (_moving && _motionState) {
|
||||
_motionState->addDirtyFlags(Simulation::DIRTY_POSITION);
|
||||
}
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
void OtherAvatar::setWorkloadRegion(uint8_t region) {
|
||||
_workloadRegion = region;
|
||||
}
|
||||
|
||||
bool OtherAvatar::shouldBeInPhysicsSimulation() const {
|
||||
return (_workloadRegion < workload::Region::R3 && !isDead());
|
||||
}
|
||||
|
||||
void OtherAvatar::rebuildCollisionShape() {
|
||||
if (_motionState) {
|
||||
_motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
// Created by Bradley Austin Davis on 2017/04/27
|
||||
// Copyright 2013-2017 High Fidelity, Inc.
|
||||
// Created by amantly 2018.06.26
|
||||
// Copyright 2018 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
|
||||
|
@ -9,10 +9,17 @@
|
|||
#ifndef hifi_OtherAvatar_h
|
||||
#define hifi_OtherAvatar_h
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <avatars-renderer/Avatar.h>
|
||||
#include <workload/Space.h>
|
||||
|
||||
#include "InterfaceLogging.h"
|
||||
#include "ui/overlays/Overlays.h"
|
||||
#include "ui/overlays/Sphere3DOverlay.h"
|
||||
#include "InterfaceLogging.h"
|
||||
|
||||
class AvatarManager;
|
||||
class AvatarMotionState;
|
||||
|
||||
class OtherAvatar : public Avatar {
|
||||
public:
|
||||
|
@ -24,9 +31,28 @@ public:
|
|||
void updateOrbPosition();
|
||||
void removeOrb();
|
||||
|
||||
void setSpaceIndex(int32_t index);
|
||||
int32_t getSpaceIndex() const { return _spaceIndex; }
|
||||
void updateSpaceProxy(workload::Transaction& transaction) const;
|
||||
|
||||
int parseDataFromBuffer(const QByteArray& buffer) override;
|
||||
|
||||
bool isInPhysicsSimulation() const { return _motionState != nullptr; }
|
||||
void rebuildCollisionShape() override;
|
||||
|
||||
void setWorkloadRegion(uint8_t region);
|
||||
bool shouldBeInPhysicsSimulation() const;
|
||||
|
||||
friend AvatarManager;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<Sphere3DOverlay> _otherAvatarOrbMeshPlaceholder { nullptr };
|
||||
OverlayID _otherAvatarOrbMeshPlaceholderID { UNKNOWN_OVERLAY_ID };
|
||||
AvatarMotionState* _motionState { nullptr };
|
||||
int32_t _spaceIndex { -1 };
|
||||
uint8_t _workloadRegion { workload::Region::INVALID };
|
||||
};
|
||||
|
||||
using OtherAvatarPointer = std::shared_ptr<OtherAvatar>;
|
||||
|
||||
#endif // hifi_OtherAvatar_h
|
||||
|
|
|
@ -31,7 +31,9 @@
|
|||
QJsonObject Ledger::apiResponse(const QString& label, QNetworkReply* reply) {
|
||||
QByteArray response = reply->readAll();
|
||||
QJsonObject data = QJsonDocument::fromJson(response).object();
|
||||
#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy.
|
||||
qInfo(commerce) << label << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact);
|
||||
#endif
|
||||
return data;
|
||||
}
|
||||
// Non-200 responses are not json:
|
||||
|
@ -69,7 +71,9 @@ void Ledger::send(const QString& endpoint, const QString& success, const QString
|
|||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
const QString URL = "/api/v1/commerce/";
|
||||
JSONCallbackParameters callbackParams(this, success, fail);
|
||||
#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy.
|
||||
qCInfo(commerce) << "Sending" << endpoint << QJsonDocument(request).toJson(QJsonDocument::Compact);
|
||||
#endif
|
||||
accountManager->sendRequest(URL + endpoint,
|
||||
authType,
|
||||
method,
|
||||
|
@ -117,7 +121,7 @@ void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, cons
|
|||
signedSend("transaction", transactionString, hfc_key, "buy", "buySuccess", "buyFailure", controlled_failure);
|
||||
}
|
||||
|
||||
bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key) {
|
||||
bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key, const QByteArray& locker) {
|
||||
auto accountManager = DependencyManager::get<AccountManager>();
|
||||
if (!accountManager->isLoggedIn()) {
|
||||
qCWarning(commerce) << "Cannot set receiveAt when not logged in.";
|
||||
|
@ -125,11 +129,25 @@ bool Ledger::receiveAt(const QString& hfc_key, const QString& signing_key) {
|
|||
emit receiveAtResult(result);
|
||||
return false; // We know right away that we will fail, so tell the caller.
|
||||
}
|
||||
|
||||
signedSend("public_key", hfc_key.toUtf8(), signing_key, "receive_at", "receiveAtSuccess", "receiveAtFailure");
|
||||
QJsonObject transaction;
|
||||
transaction["public_key"] = hfc_key;
|
||||
transaction["locker"] = QString::fromUtf8(locker);
|
||||
QJsonDocument transactionDoc{ transaction };
|
||||
auto transactionString = transactionDoc.toJson(QJsonDocument::Compact);
|
||||
signedSend("text", transactionString, signing_key, "receive_at", "receiveAtSuccess", "receiveAtFailure");
|
||||
return true; // Note that there may still be an asynchronous signal of failure that callers might be interested in.
|
||||
}
|
||||
|
||||
bool Ledger::receiveAt() {
|
||||
auto wallet = DependencyManager::get<Wallet>();
|
||||
auto keys = wallet->listPublicKeys();
|
||||
if (keys.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
auto key = keys.first();
|
||||
return receiveAt(key, key, wallet->getWallet());
|
||||
}
|
||||
|
||||
void Ledger::balance(const QStringList& keys) {
|
||||
keysQuery("balance", "balanceSuccess", "balanceFailure");
|
||||
}
|
||||
|
@ -283,24 +301,30 @@ void Ledger::accountSuccess(QNetworkReply* reply) {
|
|||
auto iv = QByteArray::fromBase64(data["iv"].toString().toUtf8());
|
||||
auto ckey = QByteArray::fromBase64(data["ckey"].toString().toUtf8());
|
||||
QString remotePublicKey = data["public_key"].toString();
|
||||
const QByteArray locker = data["locker"].toString().toUtf8();
|
||||
bool isOverride = wallet->wasSoftReset();
|
||||
|
||||
wallet->setSalt(salt);
|
||||
wallet->setIv(iv);
|
||||
wallet->setCKey(ckey);
|
||||
if (!locker.isEmpty()) {
|
||||
wallet->setWallet(locker);
|
||||
wallet->setPassphrase("ACCOUNT"); // We only locker wallets that have been converted to account-based auth.
|
||||
}
|
||||
|
||||
QString keyStatus = "ok";
|
||||
QStringList localPublicKeys = wallet->listPublicKeys();
|
||||
if (remotePublicKey.isEmpty() || isOverride) {
|
||||
if (!localPublicKeys.isEmpty()) {
|
||||
QString key = localPublicKeys.first();
|
||||
receiveAt(key, key);
|
||||
if (!localPublicKeys.isEmpty()) { // Let the metaverse know about a local wallet.
|
||||
receiveAt();
|
||||
}
|
||||
} else {
|
||||
if (localPublicKeys.isEmpty()) {
|
||||
keyStatus = "preexisting";
|
||||
} else if (localPublicKeys.first() != remotePublicKey) {
|
||||
keyStatus = "conflicting";
|
||||
} else if (locker.isEmpty()) { // Matches metaverse data, but we haven't lockered it yet.
|
||||
receiveAt();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,8 @@ class Ledger : public QObject, public Dependency {
|
|||
|
||||
public:
|
||||
void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false);
|
||||
bool receiveAt(const QString& hfc_key, const QString& signing_key);
|
||||
bool receiveAt(const QString& hfc_key, const QString& signing_key, const QByteArray& locker);
|
||||
bool receiveAt();
|
||||
void balance(const QStringList& keys);
|
||||
void inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage);
|
||||
void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage);
|
||||
|
|
|
@ -131,7 +131,7 @@ bool Wallet::writeBackupInstructions() {
|
|||
QFile outputFile(outputFilename);
|
||||
bool retval = false;
|
||||
|
||||
if (getKeyFilePath() == "")
|
||||
if (getKeyFilePath().isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -190,6 +190,30 @@ bool writeKeys(const char* filename, EC_KEY* keys) {
|
|||
return retval;
|
||||
}
|
||||
|
||||
bool Wallet::setWallet(const QByteArray& wallet) {
|
||||
QFile file(keyFilePath());
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
qCCritical(commerce) << "Unable to open wallet for write in" << keyFilePath();
|
||||
return false;
|
||||
}
|
||||
if (file.write(wallet) != wallet.count()) {
|
||||
qCCritical(commerce) << "Unable to write wallet in" << keyFilePath();
|
||||
return false;
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
QByteArray Wallet::getWallet() {
|
||||
QFile file(keyFilePath());
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qCInfo(commerce) << "No existing wallet in" << keyFilePath();
|
||||
return QByteArray();
|
||||
}
|
||||
QByteArray wallet = file.readAll();
|
||||
file.close();
|
||||
return wallet;
|
||||
}
|
||||
|
||||
QPair<QByteArray*, QByteArray*> generateECKeypair() {
|
||||
|
||||
EC_KEY* keyPair = EC_KEY_new_by_curve_name(NID_secp256k1);
|
||||
|
@ -334,7 +358,7 @@ Wallet::Wallet() {
|
|||
uint status;
|
||||
QString keyStatus = result.contains("data") ? result["data"].toObject()["keyStatus"].toString() : "";
|
||||
|
||||
if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) {
|
||||
if (wallet->getKeyFilePath().isEmpty() || !wallet->getSecurityImage()) {
|
||||
if (keyStatus == "preexisting") {
|
||||
status = (uint) WalletStatus::WALLET_STATUS_PREEXISTING;
|
||||
} else{
|
||||
|
@ -524,15 +548,23 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() {
|
|||
|
||||
// FIXME: initialize OpenSSL elsewhere soon
|
||||
initialize();
|
||||
qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: checking" << (!_passphrase || !_passphrase->isEmpty());
|
||||
|
||||
// this should always be false if we don't have a passphrase
|
||||
// cached yet
|
||||
if (!_passphrase || _passphrase->isEmpty()) {
|
||||
return false;
|
||||
if (!getKeyFilePath().isEmpty()) { // If file exists, then it is an old school file that has not been lockered. Must get user's passphrase.
|
||||
qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: No passphrase, but there is an existing wallet.";
|
||||
return false;
|
||||
} else {
|
||||
qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: New setup.";
|
||||
setPassphrase("ACCOUNT"); // Going forward, consider this an account-based client.
|
||||
}
|
||||
}
|
||||
if (_publicKeys.count() > 0) {
|
||||
// we _must_ be authenticated if the publicKeys are there
|
||||
DependencyManager::get<WalletScriptingInterface>()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY);
|
||||
qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: wallet was ready";
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -545,10 +577,15 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() {
|
|||
|
||||
// be sure to add the public key so we don't do this over and over
|
||||
_publicKeys.push_back(publicKey.toBase64());
|
||||
|
||||
if (*_passphrase != "ACCOUNT") {
|
||||
changePassphrase("ACCOUNT"); // Rewrites with salt and constant, and will be lockered that way.
|
||||
}
|
||||
qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: wallet now ready";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(commerce) << "walletIsAuthenticatedWithPassphrase: wallet not ready";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -559,6 +596,7 @@ bool Wallet::generateKeyPair() {
|
|||
qCInfo(commerce) << "Generating keypair.";
|
||||
auto keyPair = generateECKeypair();
|
||||
if (!keyPair.first) {
|
||||
qCWarning(commerce) << "Empty keypair";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -576,7 +614,7 @@ bool Wallet::generateKeyPair() {
|
|||
// 2. It is maximally private, and we can step back from that later if desired.
|
||||
// 3. It maximally exercises all the machinery, so we are most likely to surface issues now.
|
||||
auto ledger = DependencyManager::get<Ledger>();
|
||||
return ledger->receiveAt(key, key);
|
||||
return ledger->receiveAt(key, key, getWallet());
|
||||
}
|
||||
|
||||
QStringList Wallet::listPublicKeys() {
|
||||
|
@ -666,11 +704,13 @@ void Wallet::chooseSecurityImage(const QString& filename) {
|
|||
// there _is_ a keyfile, we need to update it (similar to changing the
|
||||
// passphrase, we need to do so into a temp file and move it).
|
||||
if (!QFile(keyFilePath()).exists()) {
|
||||
qCDebug(commerce) << "initial security pic set for empty wallet";
|
||||
emit securityImageResult(true);
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = writeWallet();
|
||||
qCDebug(commerce) << "updated security pic" << success;
|
||||
emit securityImageResult(success);
|
||||
}
|
||||
|
||||
|
@ -715,6 +755,11 @@ QString Wallet::getKeyFilePath() {
|
|||
|
||||
bool Wallet::writeWallet(const QString& newPassphrase) {
|
||||
EC_KEY* keys = readKeys(keyFilePath().toStdString().c_str());
|
||||
auto ledger = DependencyManager::get<Ledger>();
|
||||
// Remove any existing locker, because it will be out of date.
|
||||
if (!_publicKeys.isEmpty() && !ledger->receiveAt(_publicKeys.first(), _publicKeys.first(), QByteArray())) {
|
||||
return false; // FIXME: receiveAt could fail asynchronously.
|
||||
}
|
||||
if (keys) {
|
||||
// we read successfully, so now write to a new temp file
|
||||
QString tempFileName = QString("%1.%2").arg(keyFilePath(), QString("temp"));
|
||||
|
@ -722,6 +767,7 @@ bool Wallet::writeWallet(const QString& newPassphrase) {
|
|||
if (!newPassphrase.isEmpty()) {
|
||||
setPassphrase(newPassphrase);
|
||||
}
|
||||
|
||||
if (writeKeys(tempFileName.toStdString().c_str(), keys)) {
|
||||
if (writeSecurityImage(_securityImage, tempFileName)) {
|
||||
// ok, now move the temp file to the correct spot
|
||||
|
@ -729,6 +775,11 @@ bool Wallet::writeWallet(const QString& newPassphrase) {
|
|||
QFile(tempFileName).rename(QString(keyFilePath()));
|
||||
qCDebug(commerce) << "wallet written successfully";
|
||||
emit keyFilePathIfExistsResult(getKeyFilePath());
|
||||
if (!walletIsAuthenticatedWithPassphrase() || !ledger->receiveAt()) {
|
||||
// FIXME: Should we fail the whole operation?
|
||||
// Tricky, because we'll need the the key and file from the TEMP location...
|
||||
qCWarning(commerce) << "Failed to update locker";
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
qCDebug(commerce) << "couldn't write security image to temp wallet";
|
||||
|
|
|
@ -73,6 +73,7 @@ private slots:
|
|||
void handleChallengeOwnershipPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
|
||||
|
||||
private:
|
||||
friend class Ledger;
|
||||
QStringList _publicKeys{};
|
||||
QPixmap* _securityImage { nullptr };
|
||||
QByteArray _salt;
|
||||
|
@ -87,6 +88,9 @@ private:
|
|||
bool readSecurityImage(const QString& inputFilePath, unsigned char** outputBufferPtr, int* outputBufferLen);
|
||||
bool writeBackupInstructions();
|
||||
|
||||
bool setWallet(const QByteArray& wallet);
|
||||
QByteArray getWallet();
|
||||
|
||||
void account();
|
||||
};
|
||||
|
||||
|
|
|
@ -16,8 +16,11 @@
|
|||
#include "Application.h"
|
||||
#include "Menu.h"
|
||||
#include "SceneScriptingInterface.h"
|
||||
#include "SafeLanding.h"
|
||||
|
||||
OctreePacketProcessor::OctreePacketProcessor() {
|
||||
OctreePacketProcessor::OctreePacketProcessor():
|
||||
_safeLanding(new SafeLanding())
|
||||
{
|
||||
setObjectName("Octree Packet Processor");
|
||||
|
||||
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
|
||||
|
@ -26,6 +29,8 @@ OctreePacketProcessor::OctreePacketProcessor() {
|
|||
packetReceiver.registerDirectListenerForTypes(octreePackets, this, "handleOctreePacket");
|
||||
}
|
||||
|
||||
OctreePacketProcessor::~OctreePacketProcessor() { }
|
||||
|
||||
void OctreePacketProcessor::handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
|
||||
queueReceivedPacket(message, senderNode);
|
||||
}
|
||||
|
@ -107,6 +112,7 @@ void OctreePacketProcessor::processPacket(QSharedPointer<ReceivedMessage> messag
|
|||
auto renderer = qApp->getEntities();
|
||||
if (renderer) {
|
||||
renderer->processDatagram(*message, sendingNode);
|
||||
_safeLanding->noteReceivedsequenceNumber(renderer->getLastOctreeMessageSequence());
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
@ -115,8 +121,7 @@ void OctreePacketProcessor::processPacket(QSharedPointer<ReceivedMessage> messag
|
|||
// Read sequence #
|
||||
OCTREE_PACKET_SEQUENCE completionNumber;
|
||||
message->readPrimitive(&completionNumber);
|
||||
|
||||
_completionSequenceNumber = completionNumber;
|
||||
_safeLanding->setCompletionSequenceNumbers(0, completionNumber);
|
||||
} break;
|
||||
|
||||
default: {
|
||||
|
@ -125,22 +130,10 @@ void OctreePacketProcessor::processPacket(QSharedPointer<ReceivedMessage> messag
|
|||
}
|
||||
}
|
||||
|
||||
void OctreePacketProcessor::resetCompletionSequenceNumber() {
|
||||
_completionSequenceNumber = INVALID_SEQUENCE;
|
||||
void OctreePacketProcessor::startEntitySequence() {
|
||||
_safeLanding->startEntitySequence(qApp->getEntities());
|
||||
}
|
||||
|
||||
namespace {
|
||||
template<typename T> bool lessThanWraparound(int a, int b) {
|
||||
constexpr int MAX_T_VALUE = std::numeric_limits<T>::max();
|
||||
if (b <= a) {
|
||||
b += MAX_T_VALUE;
|
||||
}
|
||||
return (b - a) < (MAX_T_VALUE / 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool OctreePacketProcessor::octreeSequenceIsComplete(int sequenceNumber) const {
|
||||
// If we've received the flagged seq # and the current one is >= it.
|
||||
return _completionSequenceNumber != INVALID_SEQUENCE &&
|
||||
!lessThanWraparound<OCTREE_PACKET_SEQUENCE>(sequenceNumber, _completionSequenceNumber);
|
||||
bool OctreePacketProcessor::isLoadSequenceComplete() const {
|
||||
return _safeLanding->isLoadSequenceComplete();
|
||||
}
|
||||
|
|
|
@ -15,21 +15,22 @@
|
|||
#include <ReceivedPacketProcessor.h>
|
||||
#include <ReceivedMessage.h>
|
||||
|
||||
class SafeLanding;
|
||||
|
||||
/// Handles processing of incoming voxel packets for the interface application. As with other ReceivedPacketProcessor classes
|
||||
/// the user is responsible for reading inbound packets and adding them to the processing queue by calling queueReceivedPacket()
|
||||
class OctreePacketProcessor : public ReceivedPacketProcessor {
|
||||
Q_OBJECT
|
||||
public:
|
||||
OctreePacketProcessor();
|
||||
~OctreePacketProcessor();
|
||||
|
||||
bool octreeSequenceIsComplete(int sequenceNumber) const;
|
||||
void startEntitySequence();
|
||||
bool isLoadSequenceComplete() const;
|
||||
|
||||
signals:
|
||||
void packetVersionMismatch();
|
||||
|
||||
public slots:
|
||||
void resetCompletionSequenceNumber();
|
||||
|
||||
protected:
|
||||
virtual void processPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) override;
|
||||
|
||||
|
@ -37,8 +38,6 @@ private slots:
|
|||
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
|
||||
|
||||
private:
|
||||
static constexpr int INVALID_SEQUENCE = -1;
|
||||
std::atomic<int> _completionSequenceNumber { INVALID_SEQUENCE };
|
||||
|
||||
std::unique_ptr<SafeLanding> _safeLanding;
|
||||
};
|
||||
#endif // hifi_OctreePacketProcessor_h
|
||||
|
|
172
interface/src/octree/SafeLanding.cpp
Normal file
172
interface/src/octree/SafeLanding.cpp
Normal file
|
@ -0,0 +1,172 @@
|
|||
//
|
||||
// SafeLanding.cpp
|
||||
// interface/src/octree
|
||||
//
|
||||
// Created by Simon Walton.
|
||||
// Copyright 2018 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 "SafeLanding.h"
|
||||
#include "EntityTreeRenderer.h"
|
||||
#include "ModelEntityItem.h"
|
||||
#include "InterfaceLogging.h"
|
||||
|
||||
const int SafeLanding::SEQUENCE_MODULO = std::numeric_limits<OCTREE_PACKET_SEQUENCE>::max() + 1;
|
||||
|
||||
namespace {
|
||||
template<typename T> bool lessThanWraparound(int a, int b) {
|
||||
constexpr int MAX_T_VALUE = std::numeric_limits<T>::max();
|
||||
if (b <= a) {
|
||||
b += MAX_T_VALUE;
|
||||
}
|
||||
return (b - a) < (MAX_T_VALUE / 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool SafeLanding::SequenceLessThan::operator()(const int& a, const int& b) const {
|
||||
return lessThanWraparound<OCTREE_PACKET_SEQUENCE>(a, b);
|
||||
}
|
||||
|
||||
void SafeLanding::startEntitySequence(QSharedPointer<EntityTreeRenderer> entityTreeRenderer) {
|
||||
auto entityTree = entityTreeRenderer->getTree();
|
||||
|
||||
if (entityTree) {
|
||||
Locker lock(_lock);
|
||||
_entityTree = entityTree;
|
||||
_trackedEntities.clear();
|
||||
_trackingEntities = true;
|
||||
connect(std::const_pointer_cast<EntityTree>(_entityTree).get(),
|
||||
&EntityTree::addingEntity, this, &SafeLanding::addTrackedEntity);
|
||||
connect(std::const_pointer_cast<EntityTree>(_entityTree).get(),
|
||||
&EntityTree::deletingEntity, this, &SafeLanding::deleteTrackedEntity);
|
||||
|
||||
_sequenceNumbers.clear();
|
||||
_initialStart = INVALID_SEQUENCE;
|
||||
_initialEnd = INVALID_SEQUENCE;
|
||||
EntityTreeRenderer::setEntityLoadingPriorityFunction(&ElevatedPriority);
|
||||
}
|
||||
}
|
||||
|
||||
void SafeLanding::stopEntitySequence() {
|
||||
Locker lock(_lock);
|
||||
_trackingEntities = false;
|
||||
_initialStart = INVALID_SEQUENCE;
|
||||
_initialEnd = INVALID_SEQUENCE;
|
||||
_trackedEntities.clear();
|
||||
_sequenceNumbers.clear();
|
||||
}
|
||||
|
||||
void SafeLanding::addTrackedEntity(const EntityItemID& entityID) {
|
||||
if (_trackingEntities) {
|
||||
Locker lock(_lock);
|
||||
EntityItemPointer entity = _entityTree->findEntityByID(entityID);
|
||||
|
||||
if (entity && !entity->getCollisionless()) {
|
||||
const auto& entityType = entity->getType();
|
||||
if (entityType == EntityTypes::Model) {
|
||||
ModelEntityItem * modelEntity = std::dynamic_pointer_cast<ModelEntityItem>(entity).get();
|
||||
static const std::set<ShapeType> downloadedCollisionTypes
|
||||
{ SHAPE_TYPE_COMPOUND, SHAPE_TYPE_SIMPLE_COMPOUND, SHAPE_TYPE_STATIC_MESH, SHAPE_TYPE_SIMPLE_HULL };
|
||||
bool hasAABox;
|
||||
entity->getAABox(hasAABox);
|
||||
if (hasAABox && downloadedCollisionTypes.count(modelEntity->getShapeType()) != 0) {
|
||||
// Only track entities with downloaded collision bodies.
|
||||
_trackedEntities.emplace(entityID, entity);
|
||||
qCDebug(interfaceapp) << "Safe Landing: Tracking entity " << entity->getItemName();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SafeLanding::deleteTrackedEntity(const EntityItemID& entityID) {
|
||||
Locker lock(_lock);
|
||||
_trackedEntities.erase(entityID);
|
||||
}
|
||||
|
||||
void SafeLanding::setCompletionSequenceNumbers(int first, int last) {
|
||||
Locker lock(_lock);
|
||||
if (_initialStart == INVALID_SEQUENCE) {
|
||||
_initialStart = first;
|
||||
_initialEnd = last;
|
||||
}
|
||||
}
|
||||
|
||||
void SafeLanding::noteReceivedsequenceNumber(int sequenceNumber) {
|
||||
if (_trackingEntities) {
|
||||
Locker lock(_lock);
|
||||
_sequenceNumbers.insert(sequenceNumber);
|
||||
}
|
||||
}
|
||||
|
||||
bool SafeLanding::isLoadSequenceComplete() {
|
||||
if (isEntityPhysicsComplete() && isSequenceNumbersComplete()) {
|
||||
Locker lock(_lock);
|
||||
_trackedEntities.clear();
|
||||
_initialStart = INVALID_SEQUENCE;
|
||||
_initialEnd = INVALID_SEQUENCE;
|
||||
_entityTree = nullptr;
|
||||
EntityTreeRenderer::setEntityLoadingPriorityFunction(StandardPriority);
|
||||
qCDebug(interfaceapp) << "Safe Landing: load sequence complete";
|
||||
}
|
||||
|
||||
return !_trackingEntities;
|
||||
}
|
||||
|
||||
bool SafeLanding::isSequenceNumbersComplete() {
|
||||
if (_initialStart != INVALID_SEQUENCE) {
|
||||
Locker lock(_lock);
|
||||
int sequenceSize = _initialStart <= _initialEnd ? _initialEnd - _initialStart:
|
||||
_initialEnd + SEQUENCE_MODULO - _initialStart;
|
||||
auto startIter = _sequenceNumbers.find(_initialStart);
|
||||
auto endIter = _sequenceNumbers.find(_initialEnd - 1);
|
||||
if (sequenceSize == 0 ||
|
||||
(startIter != _sequenceNumbers.end()
|
||||
&& endIter != _sequenceNumbers.end()
|
||||
&& distance(startIter, endIter) == sequenceSize - 1) ) {
|
||||
_trackingEntities = false; // Don't track anything else that comes in.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SafeLanding::isEntityPhysicsComplete() {
|
||||
Locker lock(_lock);
|
||||
for (auto entityMapIter = _trackedEntities.begin(); entityMapIter != _trackedEntities.end(); ++entityMapIter) {
|
||||
auto entity = entityMapIter->second;
|
||||
if (!entity->shouldBePhysical() || entity->isReadyToComputeShape()) {
|
||||
entityMapIter = _trackedEntities.erase(entityMapIter);
|
||||
if (entityMapIter == _trackedEntities.end()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return _trackedEntities.empty();
|
||||
}
|
||||
|
||||
float SafeLanding::ElevatedPriority(const EntityItem& entityItem) {
|
||||
return entityItem.getCollisionless() ? 0.0f : 10.0f;
|
||||
}
|
||||
|
||||
void SafeLanding::debugDumpSequenceIDs() const {
|
||||
int p = -1;
|
||||
qCDebug(interfaceapp) << "Sequence set size:" << _sequenceNumbers.size();
|
||||
for (auto s: _sequenceNumbers) {
|
||||
if (p == -1) {
|
||||
p = s;
|
||||
qCDebug(interfaceapp) << "First:" << s;
|
||||
} else {
|
||||
if (s != p + 1) {
|
||||
qCDebug(interfaceapp) << "Gap from" << p << "to" << s << "(exclusive)";
|
||||
p = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (p != -1) {
|
||||
qCDebug(interfaceapp) << "Last:" << p;
|
||||
}
|
||||
}
|
65
interface/src/octree/SafeLanding.h
Normal file
65
interface/src/octree/SafeLanding.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// SafeLanding.h
|
||||
// interface/src/octree
|
||||
//
|
||||
// Created by Simon Walton.
|
||||
// Copyright 2018 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
|
||||
//
|
||||
|
||||
// Controller for logic to wait for local collision bodies before enabling physics.
|
||||
|
||||
#ifndef hifi_SafeLanding_h
|
||||
#define hifi_SafeLanding_h
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QSharedPointer>
|
||||
|
||||
#include "EntityItem.h"
|
||||
|
||||
class EntityTreeRenderer;
|
||||
class EntityItemID;
|
||||
|
||||
class SafeLanding : public QObject {
|
||||
public:
|
||||
void startEntitySequence(QSharedPointer<EntityTreeRenderer> entityTreeRenderer);
|
||||
void stopEntitySequence();
|
||||
void setCompletionSequenceNumbers(int first, int last); // 'last' exclusive.
|
||||
void noteReceivedsequenceNumber(int sequenceNumber);
|
||||
bool isLoadSequenceComplete();
|
||||
|
||||
private slots:
|
||||
void addTrackedEntity(const EntityItemID& entityID);
|
||||
void deleteTrackedEntity(const EntityItemID& entityID);
|
||||
|
||||
private:
|
||||
bool isSequenceNumbersComplete();
|
||||
void debugDumpSequenceIDs() const;
|
||||
bool isEntityPhysicsComplete();
|
||||
|
||||
std::mutex _lock;
|
||||
using Locker = std::lock_guard<std::mutex>;
|
||||
bool _trackingEntities { false };
|
||||
EntityTreePointer _entityTree;
|
||||
using EntityMap = std::map<EntityItemID, EntityItemPointer>;
|
||||
EntityMap _trackedEntities;
|
||||
|
||||
static constexpr int INVALID_SEQUENCE = -1;
|
||||
int _initialStart { INVALID_SEQUENCE };
|
||||
int _initialEnd { INVALID_SEQUENCE };
|
||||
|
||||
struct SequenceLessThan {
|
||||
bool operator()(const int& a, const int& b) const;
|
||||
};
|
||||
|
||||
std::set<int, SequenceLessThan> _sequenceNumbers;
|
||||
|
||||
static float ElevatedPriority(const EntityItem& entityItem);
|
||||
static float StandardPriority(const EntityItem&) { return 0.0f; }
|
||||
|
||||
static const int SEQUENCE_MODULO;
|
||||
};
|
||||
|
||||
#endif // hifi_SafeLanding_h
|
|
@ -12,9 +12,33 @@
|
|||
|
||||
#include <glm/gtx/transform.hpp>
|
||||
|
||||
#include "PhysicsCollisionGroups.h"
|
||||
#include "ScriptEngineLogging.h"
|
||||
#include "UUIDHasher.h"
|
||||
|
||||
PickResultPointer CollisionPickResult::compareAndProcessNewResult(const PickResultPointer& newRes) {
|
||||
const std::shared_ptr<CollisionPickResult> newCollisionResult = std::static_pointer_cast<CollisionPickResult>(newRes);
|
||||
|
||||
if (entityIntersections.size()) {
|
||||
entityIntersections.insert(entityIntersections.cend(), newCollisionResult->entityIntersections.begin(), newCollisionResult->entityIntersections.end());
|
||||
} else {
|
||||
entityIntersections = newCollisionResult->entityIntersections;
|
||||
}
|
||||
|
||||
if (avatarIntersections.size()) {
|
||||
avatarIntersections.insert(avatarIntersections.cend(), newCollisionResult->avatarIntersections.begin(), newCollisionResult->avatarIntersections.end());
|
||||
} else {
|
||||
avatarIntersections = newCollisionResult->avatarIntersections;
|
||||
}
|
||||
|
||||
intersects = entityIntersections.size() || avatarIntersections.size();
|
||||
if (newCollisionResult->loadState == LOAD_STATE_NOT_LOADED || loadState == LOAD_STATE_UNKNOWN) {
|
||||
loadState = (LoadState)newCollisionResult->loadState;
|
||||
}
|
||||
|
||||
return std::make_shared<CollisionPickResult>(*this);
|
||||
}
|
||||
|
||||
void buildObjectIntersectionsMap(IntersectionType intersectionType, const std::vector<ContactTestResult>& objectIntersections, std::unordered_map<QUuid, QVariantMap>& intersections, std::unordered_map<QUuid, QVariantList>& collisionPointPairs) {
|
||||
for (auto& objectIntersection : objectIntersections) {
|
||||
auto at = intersections.find(objectIntersection.foundID);
|
||||
|
@ -308,20 +332,27 @@ CollisionRegion CollisionPick::getMathematicalPick() const {
|
|||
return _mathPick;
|
||||
}
|
||||
|
||||
const std::vector<ContactTestResult> CollisionPick::filterIntersections(const std::vector<ContactTestResult>& intersections) const {
|
||||
std::vector<ContactTestResult> filteredIntersections;
|
||||
|
||||
void CollisionPick::filterIntersections(std::vector<ContactTestResult>& intersections) const {
|
||||
const QVector<QUuid>& ignoreItems = getIgnoreItems();
|
||||
const QVector<QUuid>& includeItems = getIncludeItems();
|
||||
bool isWhitelist = includeItems.size();
|
||||
for (const auto& intersection : intersections) {
|
||||
bool isWhitelist = !includeItems.empty();
|
||||
|
||||
if (!isWhitelist && ignoreItems.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<ContactTestResult> filteredIntersections;
|
||||
|
||||
int n = (int)intersections.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
auto& intersection = intersections[i];
|
||||
const QUuid& id = intersection.foundID;
|
||||
if (!ignoreItems.contains(id) && (!isWhitelist || includeItems.contains(id))) {
|
||||
filteredIntersections.push_back(intersection);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredIntersections;
|
||||
intersections = filteredIntersections;
|
||||
}
|
||||
|
||||
PickResultPointer CollisionPick::getEntityIntersection(const CollisionRegion& pick) {
|
||||
|
@ -330,7 +361,8 @@ PickResultPointer CollisionPick::getEntityIntersection(const CollisionRegion& pi
|
|||
return std::make_shared<CollisionPickResult>(pick.toVariantMap(), CollisionPickResult::LOAD_STATE_NOT_LOADED, std::vector<ContactTestResult>(), std::vector<ContactTestResult>());
|
||||
}
|
||||
|
||||
const auto& entityIntersections = filterIntersections(_physicsEngine->getCollidingInRegion(MOTIONSTATE_TYPE_ENTITY, *pick.shapeInfo, pick.transform));
|
||||
auto entityIntersections = _physicsEngine->contactTest(USER_COLLISION_MASK_ENTITIES, *pick.shapeInfo, pick.transform);
|
||||
filterIntersections(entityIntersections);
|
||||
return std::make_shared<CollisionPickResult>(pick, CollisionPickResult::LOAD_STATE_LOADED, entityIntersections, std::vector<ContactTestResult>());
|
||||
}
|
||||
|
||||
|
@ -343,8 +375,9 @@ PickResultPointer CollisionPick::getAvatarIntersection(const CollisionRegion& pi
|
|||
// Cannot compute result
|
||||
return std::make_shared<CollisionPickResult>(pick.toVariantMap(), CollisionPickResult::LOAD_STATE_NOT_LOADED, std::vector<ContactTestResult>(), std::vector<ContactTestResult>());
|
||||
}
|
||||
|
||||
const auto& avatarIntersections = filterIntersections(_physicsEngine->getCollidingInRegion(MOTIONSTATE_TYPE_AVATAR, *pick.shapeInfo, pick.transform));
|
||||
|
||||
auto avatarIntersections = _physicsEngine->contactTest(USER_COLLISION_MASK_AVATARS, *pick.shapeInfo, pick.transform);
|
||||
filterIntersections(avatarIntersections);
|
||||
return std::make_shared<CollisionPickResult>(pick, CollisionPickResult::LOAD_STATE_LOADED, std::vector<ContactTestResult>(), avatarIntersections);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,23 +49,7 @@ public:
|
|||
bool doesIntersect() const override { return intersects; }
|
||||
bool checkOrFilterAgainstMaxDistance(float maxDistance) override { return true; }
|
||||
|
||||
PickResultPointer compareAndProcessNewResult(const PickResultPointer& newRes) override {
|
||||
const std::shared_ptr<CollisionPickResult> newCollisionResult = std::static_pointer_cast<CollisionPickResult>(newRes);
|
||||
|
||||
for (ContactTestResult& entityIntersection : newCollisionResult->entityIntersections) {
|
||||
entityIntersections.push_back(entityIntersection);
|
||||
}
|
||||
for (ContactTestResult& avatarIntersection : newCollisionResult->avatarIntersections) {
|
||||
avatarIntersections.push_back(avatarIntersection);
|
||||
}
|
||||
|
||||
intersects = entityIntersections.size() || avatarIntersections.size();
|
||||
if (newCollisionResult->loadState == LOAD_STATE_NOT_LOADED || loadState == LOAD_STATE_UNKNOWN) {
|
||||
loadState = (LoadState)newCollisionResult->loadState;
|
||||
}
|
||||
|
||||
return std::make_shared<CollisionPickResult>(*this);
|
||||
}
|
||||
PickResultPointer compareAndProcessNewResult(const PickResultPointer& newRes) override;
|
||||
};
|
||||
|
||||
class CollisionPick : public Pick<CollisionRegion> {
|
||||
|
@ -92,7 +76,7 @@ protected:
|
|||
// Returns true if pick.shapeInfo is valid. Otherwise, attempts to get the shapeInfo ready for use.
|
||||
bool isShapeInfoReady();
|
||||
void computeShapeInfo(CollisionRegion& pick, ShapeInfo& shapeInfo, QSharedPointer<GeometryResource> resource);
|
||||
const std::vector<ContactTestResult> filterIntersections(const std::vector<ContactTestResult>& intersections) const;
|
||||
void filterIntersections(std::vector<ContactTestResult>& intersections) const;
|
||||
|
||||
CollisionRegion _mathPick;
|
||||
PhysicsEnginePointer _physicsEngine;
|
||||
|
|
|
@ -272,7 +272,7 @@ void setupPreferences() {
|
|||
auto getter = [myAvatar]()->bool { return myAvatar->useAdvancedMovementControls(); };
|
||||
auto setter = [myAvatar](bool value) { myAvatar->setUseAdvancedMovementControls(value); };
|
||||
preferences->addPreference(new CheckPreference(VR_MOVEMENT,
|
||||
QStringLiteral("Advanced movement for hand controllers"),
|
||||
QStringLiteral("Advanced movement in VR (Teleport movement when unchecked)"),
|
||||
getter, setter));
|
||||
}
|
||||
{
|
||||
|
|
|
@ -121,6 +121,7 @@ void Stats::updateStats(bool force) {
|
|||
auto avatarManager = DependencyManager::get<AvatarManager>();
|
||||
// we need to take one avatar out so we don't include ourselves
|
||||
STAT_UPDATE(avatarCount, avatarManager->size() - 1);
|
||||
STAT_UPDATE(physicsObjectCount, qApp->getNumCollisionObjects());
|
||||
STAT_UPDATE(updatedAvatarCount, avatarManager->getNumAvatarsUpdated());
|
||||
STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated());
|
||||
STAT_UPDATE(serverCount, (int)nodeList->size());
|
||||
|
|
|
@ -49,6 +49,7 @@ private: \
|
|||
* @property {number} presentdroprate - <em>Read-only.</em>
|
||||
* @property {number} gameLoopRate - <em>Read-only.</em>
|
||||
* @property {number} avatarCount - <em>Read-only.</em>
|
||||
* @property {number} physicsObjectCount - <em>Read-only.</em>
|
||||
* @property {number} updatedAvatarCount - <em>Read-only.</em>
|
||||
* @property {number} notUpdatedAvatarCount - <em>Read-only.</em>
|
||||
* @property {number} packetInCount - <em>Read-only.</em>
|
||||
|
@ -195,6 +196,7 @@ class Stats : public QQuickItem {
|
|||
STATS_PROPERTY(float, presentdroprate, 0)
|
||||
STATS_PROPERTY(int, gameLoopRate, 0)
|
||||
STATS_PROPERTY(int, avatarCount, 0)
|
||||
STATS_PROPERTY(int, physicsObjectCount, 0)
|
||||
STATS_PROPERTY(int, updatedAvatarCount, 0)
|
||||
STATS_PROPERTY(int, notUpdatedAvatarCount, 0)
|
||||
STATS_PROPERTY(int, packetInCount, 0)
|
||||
|
@ -406,6 +408,13 @@ signals:
|
|||
*/
|
||||
void gameLoopRateChanged();
|
||||
|
||||
/**jsdoc
|
||||
* Trigered when
|
||||
* @function Stats.numPhysicsBodiesChanged
|
||||
* @returns {Signal}
|
||||
*/
|
||||
void physicsObjectCountChanged();
|
||||
|
||||
/**jsdoc
|
||||
* Triggered when the value of the <code>avatarCount</code> property changes.
|
||||
* @function Stats.avatarCountChanged
|
||||
|
|
|
@ -97,7 +97,8 @@ namespace render {
|
|||
}
|
||||
}
|
||||
|
||||
GameWorkloadRenderItem::GameWorkloadRenderItem() : _key(render::ItemKey::Builder::opaqueShape().withoutShadowCaster().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1)) {
|
||||
|
||||
GameWorkloadRenderItem::GameWorkloadRenderItem() : _key(render::ItemKey::Builder::opaqueShape().withShadowCaster().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1)) {
|
||||
}
|
||||
|
||||
render::ItemKey GameWorkloadRenderItem::getKey() const {
|
||||
|
|
|
@ -10,9 +10,13 @@
|
|||
|
||||
#include "PhysicsBoundary.h"
|
||||
|
||||
#include <EntityItem.h>
|
||||
#include <PhysicalEntitySimulation.h>
|
||||
#include <PhysicsLogging.h>
|
||||
#include <workload/Space.h>
|
||||
|
||||
#include "avatar/AvatarManager.h"
|
||||
#include "avatar/OtherAvatar.h"
|
||||
#include "workload/GameWorkload.h"
|
||||
|
||||
void PhysicsBoundary::run(const workload::WorkloadContextPointer& context, const Inputs& inputs) {
|
||||
|
@ -21,13 +25,27 @@ void PhysicsBoundary::run(const workload::WorkloadContextPointer& context, const
|
|||
return;
|
||||
}
|
||||
GameWorkloadContext* gameContext = static_cast<GameWorkloadContext*>(context.get());
|
||||
PhysicalEntitySimulationPointer simulation = gameContext->_simulation;
|
||||
const auto& regionChanges = inputs.get0();
|
||||
for (uint32_t i = 0; i < (uint32_t)regionChanges.size(); ++i) {
|
||||
const workload::Space::Change& change = regionChanges[i];
|
||||
auto entity = space->getOwner(change.proxyId).get<EntityItemPointer>();
|
||||
if (entity) {
|
||||
simulation->changeEntity(entity);
|
||||
auto nestable = space->getOwner(change.proxyId).get<SpatiallyNestablePointer>();
|
||||
if (nestable) {
|
||||
switch (nestable->getNestableType()) {
|
||||
case NestableType::Entity: {
|
||||
gameContext->_simulation->changeEntity(std::static_pointer_cast<EntityItem>(nestable));
|
||||
}
|
||||
break;
|
||||
case NestableType::Avatar: {
|
||||
auto avatar = std::static_pointer_cast<OtherAvatar>(nestable);
|
||||
avatar->setWorkloadRegion(change.region);
|
||||
if (avatar->isInPhysicsSimulation() != avatar->shouldBeInPhysicsSimulation()) {
|
||||
DependencyManager::get<AvatarManager>()->queuePhysicsChange(avatar);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,25 +7,22 @@
|
|||
// 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_PhysicsGatekeeper_h
|
||||
#define hifi_PhysicsGatekeeper_h
|
||||
#ifndef hifi_PhysicsBoundary_h
|
||||
#define hifi_PhysicsBoundary_h
|
||||
|
||||
#include <EntityItem.h>
|
||||
#include <workload/Engine.h>
|
||||
#include <workload/RegionTracker.h>
|
||||
|
||||
#include "PhysicalEntitySimulation.h"
|
||||
|
||||
class PhysicsBoundary {
|
||||
public:
|
||||
using Config = workload::Job::Config;
|
||||
using Inputs = workload::RegionTracker::Outputs;
|
||||
using Outputs = bool;
|
||||
using JobModel = workload::Job::ModelI<PhysicsBoundary, Inputs, Config>; // this doesn't work
|
||||
using JobModel = workload::Job::ModelI<PhysicsBoundary, Inputs, Config>;
|
||||
|
||||
PhysicsBoundary() {}
|
||||
void configure(const Config& config) { }
|
||||
void run(const workload::WorkloadContextPointer& context, const Inputs& inputs);
|
||||
};
|
||||
|
||||
#endif // hifi_PhysicsGatekeeper_h
|
||||
#endif // hifi_PhysicsBoundary_h
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
set(TARGET_NAME avatars-renderer)
|
||||
AUTOSCRIBE_SHADER_LIB(gpu graphics render render-utils)
|
||||
setup_hifi_library(Network Script)
|
||||
link_hifi_libraries(shared gpu graphics animation model-networking script-engine render render-utils image trackers entities-renderer)
|
||||
include_hifi_library_headers(avatars)
|
||||
|
|
|
@ -224,14 +224,24 @@ void Avatar::setAvatarEntityDataChanged(bool value) {
|
|||
|
||||
void Avatar::updateAvatarEntities() {
|
||||
PerformanceTimer perfTimer("attachments");
|
||||
|
||||
// AVATAR ENTITY UPDATE FLOW
|
||||
// - if queueEditEntityMessage sees clientOnly flag it does _myAvatar->updateAvatarEntity()
|
||||
// - updateAvatarEntity saves the bytes and sets _avatarEntityDataLocallyEdited
|
||||
// - MyAvatar::update notices _avatarEntityDataLocallyEdited and calls sendIdentityPacket
|
||||
// - sendIdentityPacket sends the entity bytes to the server which relays them to other interfaces
|
||||
// - AvatarHashMap::processAvatarIdentityPacket on other interfaces call avatar->setAvatarEntityData()
|
||||
// - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true
|
||||
// - updateAvatarEntity saves the bytes and flags the trait instance for the entity as updated
|
||||
// - ClientTraitsHandler::sendChangedTraitsToMixer sends the entity bytes to the mixer which relays them to other interfaces
|
||||
// - AvatarHashMap::processBulkAvatarTraits on other interfaces calls avatar->processTraitInstace
|
||||
// - AvatarData::processTraitInstance calls updateAvatarEntity, which sets _avatarEntityDataChanged = true
|
||||
// - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are...
|
||||
|
||||
// AVATAR ENTITY DELETE FLOW
|
||||
// - EntityScriptingInterface::deleteEntity calls _myAvatar->clearAvatarEntity() for deleted avatar entities
|
||||
// - clearAvatarEntity removes the avatar entity and flags the trait instance for the entity as deleted
|
||||
// - ClientTraitsHandler::sendChangedTraitsToMixer sends a deletion to the mixer which relays to other interfaces
|
||||
// - AvatarHashMap::processBulkAvatarTraits on other interfaces calls avatar->processDeletedTraitInstace
|
||||
// - AvatarData::processDeletedTraitInstance calls clearAvatarEntity
|
||||
// - AvatarData::clearAvatarEntity sets _avatarEntityDataChanged = true and adds the ID to the detached list
|
||||
// - Avatar::simulate notices _avatarEntityDataChanged and here we are...
|
||||
|
||||
if (!_avatarEntityDataChanged) {
|
||||
return;
|
||||
}
|
||||
|
@ -345,27 +355,30 @@ void Avatar::updateAvatarEntities() {
|
|||
stateItr.value().success = success;
|
||||
}
|
||||
|
||||
AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs();
|
||||
if (!recentlyDettachedAvatarEntities.empty()) {
|
||||
AvatarEntityIDs recentlyDetachedAvatarEntities = getAndClearRecentlyDetachedIDs();
|
||||
if (!recentlyDetachedAvatarEntities.empty()) {
|
||||
// only lock this thread when absolutely necessary
|
||||
AvatarEntityMap avatarEntityData;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityData = _avatarEntityData;
|
||||
});
|
||||
foreach (auto entityID, recentlyDettachedAvatarEntities) {
|
||||
foreach (auto entityID, recentlyDetachedAvatarEntities) {
|
||||
if (!avatarEntityData.contains(entityID)) {
|
||||
entityTree->deleteEntity(entityID, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// remove stale data hashes
|
||||
foreach (auto entityID, recentlyDettachedAvatarEntities) {
|
||||
foreach (auto entityID, recentlyDetachedAvatarEntities) {
|
||||
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
||||
if (stateItr != _avatarEntityDataHashes.end()) {
|
||||
_avatarEntityDataHashes.erase(stateItr);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (avatarEntities.size() != _avatarEntityForRecording.size()) {
|
||||
createRecordingIDs();
|
||||
}
|
||||
});
|
||||
|
||||
setAvatarEntityDataChanged(false);
|
||||
|
@ -1470,9 +1483,6 @@ int Avatar::parseDataFromBuffer(const QByteArray& buffer) {
|
|||
|
||||
const float MOVE_DISTANCE_THRESHOLD = 0.001f;
|
||||
_moving = glm::distance(oldPosition, getWorldPosition()) > MOVE_DISTANCE_THRESHOLD;
|
||||
if (_moving) {
|
||||
addPhysicsFlags(Simulation::DIRTY_POSITION);
|
||||
}
|
||||
if (_moving || _hasNewJointData) {
|
||||
locationChanged();
|
||||
}
|
||||
|
@ -1614,20 +1624,6 @@ float Avatar::computeMass() {
|
|||
return _density * TWO_PI * radius * radius * (glm::length(end - start) + 2.0f * radius / 3.0f);
|
||||
}
|
||||
|
||||
void Avatar::rebuildCollisionShape() {
|
||||
addPhysicsFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS);
|
||||
}
|
||||
|
||||
void Avatar::setPhysicsCallback(AvatarPhysicsCallback cb) {
|
||||
_physicsCallback = cb;
|
||||
}
|
||||
|
||||
void Avatar::addPhysicsFlags(uint32_t flags) {
|
||||
if (_physicsCallback) {
|
||||
_physicsCallback(flags);
|
||||
}
|
||||
}
|
||||
|
||||
// thread-safe
|
||||
glm::vec3 Avatar::getLeftPalmPosition() const {
|
||||
return _leftPalmPositionCache.get();
|
||||
|
|
|
@ -50,8 +50,6 @@ enum ScreenTintLayer {
|
|||
|
||||
class Texture;
|
||||
|
||||
using AvatarPhysicsCallback = std::function<void(uint32_t)>;
|
||||
|
||||
class Avatar : public AvatarData, public scriptable::ModelProvider {
|
||||
Q_OBJECT
|
||||
|
||||
|
@ -244,7 +242,7 @@ public:
|
|||
// (otherwise floating point error will cause problems at large positions).
|
||||
void applyPositionDelta(const glm::vec3& delta);
|
||||
|
||||
virtual void rebuildCollisionShape();
|
||||
virtual void rebuildCollisionShape() = 0;
|
||||
|
||||
virtual void computeShapeInfo(ShapeInfo& shapeInfo);
|
||||
void getCapsule(glm::vec3& start, glm::vec3& end, float& radius);
|
||||
|
@ -332,10 +330,6 @@ public:
|
|||
render::ItemID getRenderItemID() { return _renderItemID; }
|
||||
bool isMoving() const { return _moving; }
|
||||
|
||||
void setPhysicsCallback(AvatarPhysicsCallback cb);
|
||||
void addPhysicsFlags(uint32_t flags);
|
||||
bool isInPhysicsSimulation() const { return _physicsCallback != nullptr; }
|
||||
|
||||
void fadeIn(render::ScenePointer scene);
|
||||
void fadeOut(render::ScenePointer scene, KillAvatarReason reason);
|
||||
bool isFading() const { return _isFading; }
|
||||
|
@ -530,8 +524,6 @@ protected:
|
|||
|
||||
int _voiceSphereID;
|
||||
|
||||
AvatarPhysicsCallback _physicsCallback { nullptr };
|
||||
|
||||
float _displayNameTargetAlpha { 1.0f };
|
||||
float _displayNameAlpha { 1.0f };
|
||||
|
||||
|
|
158
libraries/avatars/src/AssociatedTraitValues.h
Normal file
158
libraries/avatars/src/AssociatedTraitValues.h
Normal file
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// AssociatedTraitValues.h
|
||||
// libraries/avatars/src
|
||||
//
|
||||
// Created by Stephen Birarda on 8/8/18.
|
||||
// Copyright 2018 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_AssociatedTraitValues_h
|
||||
#define hifi_AssociatedTraitValues_h
|
||||
|
||||
#include "AvatarTraits.h"
|
||||
|
||||
namespace AvatarTraits {
|
||||
template<typename T, T defaultValue>
|
||||
class AssociatedTraitValues {
|
||||
public:
|
||||
AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {}
|
||||
|
||||
void insert(TraitType type, T value) { _simpleTypes[type] = value; }
|
||||
void erase(TraitType type) { _simpleTypes[type] = defaultValue; }
|
||||
|
||||
T& getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID);
|
||||
void instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value);
|
||||
|
||||
struct InstanceIDValuePair {
|
||||
TraitInstanceID id;
|
||||
T value;
|
||||
|
||||
InstanceIDValuePair(TraitInstanceID id, T value) : id(id), value(value) {};
|
||||
};
|
||||
|
||||
using InstanceIDValuePairs = std::vector<InstanceIDValuePair>;
|
||||
|
||||
InstanceIDValuePairs& getInstanceIDValuePairs(TraitType traitType);
|
||||
|
||||
void instanceErase(TraitType traitType, TraitInstanceID instanceID);
|
||||
void eraseAllInstances(TraitType traitType);
|
||||
|
||||
// will return defaultValue for instanced traits
|
||||
T operator[](TraitType traitType) const { return _simpleTypes[traitType]; }
|
||||
T& operator[](TraitType traitType) { return _simpleTypes[traitType]; }
|
||||
|
||||
void reset() {
|
||||
std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue);
|
||||
_instancedTypes.clear();
|
||||
}
|
||||
|
||||
typename std::vector<T>::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); }
|
||||
typename std::vector<T>::const_iterator simpleCEnd() const { return _simpleTypes.cend(); }
|
||||
|
||||
typename std::vector<T>::iterator simpleBegin() { return _simpleTypes.begin(); }
|
||||
typename std::vector<T>::iterator simpleEnd() { return _simpleTypes.end(); }
|
||||
|
||||
struct TraitWithInstances {
|
||||
TraitType traitType;
|
||||
InstanceIDValuePairs instances;
|
||||
|
||||
TraitWithInstances(TraitType traitType) : traitType(traitType) {};
|
||||
TraitWithInstances(TraitType traitType, TraitInstanceID instanceID, T value) :
|
||||
traitType(traitType), instances({{ instanceID, value }}) {};
|
||||
};
|
||||
|
||||
typename std::vector<TraitWithInstances>::const_iterator instancedCBegin() const { return _instancedTypes.cbegin(); }
|
||||
typename std::vector<TraitWithInstances>::const_iterator instancedCEnd() const { return _instancedTypes.cend(); }
|
||||
|
||||
typename std::vector<TraitWithInstances>::iterator instancedBegin() { return _instancedTypes.begin(); }
|
||||
typename std::vector<TraitWithInstances>::iterator instancedEnd() { return _instancedTypes.end(); }
|
||||
|
||||
private:
|
||||
std::vector<T> _simpleTypes;
|
||||
|
||||
typename std::vector<TraitWithInstances>::iterator instancesForTrait(TraitType traitType) {
|
||||
return std::find_if(_instancedTypes.begin(), _instancedTypes.end(),
|
||||
[traitType](TraitWithInstances& traitWithInstances){
|
||||
return traitWithInstances.traitType == traitType;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<TraitWithInstances> _instancedTypes;
|
||||
};
|
||||
|
||||
template <typename T, T defaultValue>
|
||||
inline typename AssociatedTraitValues<T, defaultValue>::InstanceIDValuePairs&
|
||||
AssociatedTraitValues<T, defaultValue>::getInstanceIDValuePairs(TraitType traitType) {
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
return it->instances;
|
||||
} else {
|
||||
_instancedTypes.emplace_back(traitType);
|
||||
return _instancedTypes.back().instances;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, T defaultValue>
|
||||
inline T& AssociatedTraitValues<T, defaultValue>::getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID) {
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
auto& instancesVector = it->instances;
|
||||
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
|
||||
[instanceID](InstanceIDValuePair& idValuePair){
|
||||
return idValuePair.id == instanceID;
|
||||
});
|
||||
if (instanceIt != instancesVector.end()) {
|
||||
return instanceIt->value;
|
||||
} else {
|
||||
instancesVector.emplace_back(instanceID, defaultValue);
|
||||
return instancesVector.back().value;
|
||||
}
|
||||
} else {
|
||||
_instancedTypes.emplace_back(traitType, instanceID, defaultValue);
|
||||
return _instancedTypes.back().instances.back().value;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, T defaultValue>
|
||||
inline void AssociatedTraitValues<T, defaultValue>::instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value) {
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
auto& instancesVector = it->instances;
|
||||
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
|
||||
[instanceID](InstanceIDValuePair& idValuePair){
|
||||
return idValuePair.id == instanceID;
|
||||
});
|
||||
if (instanceIt != instancesVector.end()) {
|
||||
instanceIt->value = value;
|
||||
} else {
|
||||
instancesVector.emplace_back(instanceID, value);
|
||||
}
|
||||
} else {
|
||||
_instancedTypes.emplace_back(traitType, instanceID, value);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T, T defaultValue>
|
||||
inline void AssociatedTraitValues<T, defaultValue>::instanceErase(TraitType traitType, TraitInstanceID instanceID) {
|
||||
auto it = instancesForTrait(traitType);
|
||||
|
||||
if (it != _instancedTypes.end()) {
|
||||
auto& instancesVector = it->instances;
|
||||
instancesVector.erase(std::remove_if(instancesVector.begin(),
|
||||
instancesVector.end(),
|
||||
[&instanceID](InstanceIDValuePair& idValuePair){
|
||||
return idValuePair.id == instanceID;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
using TraitVersions = AssociatedTraitValues<TraitVersion, DEFAULT_TRAIT_VERSION>;
|
||||
};
|
||||
|
||||
#endif // hifi_AssociatedTraitValues_h
|
|
@ -42,6 +42,8 @@
|
|||
#include <BitVectorHelpers.h>
|
||||
|
||||
#include "AvatarLogging.h"
|
||||
#include "AvatarTraits.h"
|
||||
#include "ClientTraitsHandler.h"
|
||||
|
||||
//#define WANT_DEBUG
|
||||
|
||||
|
@ -1749,14 +1751,8 @@ glm::quat AvatarData::getOrientationOutbound() const {
|
|||
return (getLocalOrientation());
|
||||
}
|
||||
|
||||
static const QUrl emptyURL("");
|
||||
QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const {
|
||||
// We don't put file urls on the wire, but instead convert to empty.
|
||||
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
|
||||
}
|
||||
|
||||
void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
|
||||
bool& displayNameChanged, bool& skeletonModelUrlChanged) {
|
||||
bool& displayNameChanged) {
|
||||
|
||||
QDataStream packetStream(identityData);
|
||||
|
||||
|
@ -1777,28 +1773,17 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
|
|||
if (incomingSequenceNumber > _identitySequenceNumber) {
|
||||
Identity identity;
|
||||
|
||||
packetStream >> identity.skeletonModelURL
|
||||
packetStream
|
||||
>> identity.attachmentData
|
||||
>> identity.displayName
|
||||
>> identity.sessionDisplayName
|
||||
>> identity.isReplicated
|
||||
>> identity.avatarEntityData
|
||||
>> identity.lookAtSnappingEnabled
|
||||
;
|
||||
|
||||
// set the store identity sequence number to match the incoming identity
|
||||
_identitySequenceNumber = incomingSequenceNumber;
|
||||
|
||||
if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) {
|
||||
setSkeletonModelURL(identity.skeletonModelURL);
|
||||
identityChanged = true;
|
||||
skeletonModelUrlChanged = true;
|
||||
if (_firstSkeletonCheck) {
|
||||
displayNameChanged = true;
|
||||
}
|
||||
_firstSkeletonCheck = false;
|
||||
}
|
||||
|
||||
if (identity.displayName != _displayName) {
|
||||
_displayName = identity.displayName;
|
||||
identityChanged = true;
|
||||
|
@ -1816,16 +1801,6 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
|
|||
identityChanged = true;
|
||||
}
|
||||
|
||||
bool avatarEntityDataChanged = false;
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
avatarEntityDataChanged = (identity.avatarEntityData != _avatarEntityData);
|
||||
});
|
||||
|
||||
if (avatarEntityDataChanged) {
|
||||
setAvatarEntityData(identity.avatarEntityData);
|
||||
identityChanged = true;
|
||||
}
|
||||
|
||||
if (identity.lookAtSnappingEnabled != _lookAtSnappingEnabled) {
|
||||
setProperty("lookAtSnappingEnabled", identity.lookAtSnappingEnabled);
|
||||
identityChanged = true;
|
||||
|
@ -1834,7 +1809,6 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
|
|||
#ifdef WANT_DEBUG
|
||||
qCDebug(avatars) << __FUNCTION__
|
||||
<< "identity.uuid:" << identity.uuid
|
||||
<< "identity.skeletonModelURL:" << identity.skeletonModelURL
|
||||
<< "identity.displayName:" << identity.displayName
|
||||
<< "identity.sessionDisplayName:" << identity.sessionDisplayName;
|
||||
} else {
|
||||
|
@ -1846,26 +1820,105 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide
|
|||
}
|
||||
}
|
||||
|
||||
QUrl AvatarData::getWireSafeSkeletonModelURL() const {
|
||||
if (_skeletonModelURL.scheme() != "file" && _skeletonModelURL.scheme() != "qrc") {
|
||||
return _skeletonModelURL;
|
||||
} else {
|
||||
return QUrl();
|
||||
}
|
||||
}
|
||||
|
||||
qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination,
|
||||
AvatarTraits::TraitVersion traitVersion) {
|
||||
qint64 bytesWritten = 0;
|
||||
bytesWritten += destination.writePrimitive(traitType);
|
||||
|
||||
if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
|
||||
bytesWritten += destination.writePrimitive(traitVersion);
|
||||
}
|
||||
|
||||
if (traitType == AvatarTraits::SkeletonModelURL) {
|
||||
QByteArray encodedSkeletonURL = getWireSafeSkeletonModelURL().toEncoded();
|
||||
|
||||
AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size();
|
||||
bytesWritten += destination.writePrimitive(encodedURLSize);
|
||||
|
||||
bytesWritten += destination.write(encodedSkeletonURL);
|
||||
}
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID,
|
||||
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
|
||||
qint64 bytesWritten = 0;
|
||||
|
||||
bytesWritten += destination.writePrimitive(traitType);
|
||||
|
||||
if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
|
||||
bytesWritten += destination.writePrimitive(traitVersion);
|
||||
}
|
||||
|
||||
bytesWritten += destination.write(traitInstanceID.toRfc4122());
|
||||
|
||||
if (traitType == AvatarTraits::AvatarEntity) {
|
||||
// grab a read lock on the avatar entities and check for entity data for the given ID
|
||||
QByteArray entityBinaryData;
|
||||
|
||||
_avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
|
||||
if (_avatarEntityData.contains(traitInstanceID)) {
|
||||
entityBinaryData = _avatarEntityData[traitInstanceID];
|
||||
}
|
||||
});
|
||||
|
||||
if (!entityBinaryData.isNull()) {
|
||||
AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size();
|
||||
|
||||
bytesWritten += destination.writePrimitive(entityBinarySize);
|
||||
bytesWritten += destination.write(entityBinaryData);
|
||||
} else {
|
||||
bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) {
|
||||
if (traitType == AvatarTraits::SkeletonModelURL) {
|
||||
// get the URL from the binary data
|
||||
auto skeletonModelURL = QUrl::fromEncoded(traitBinaryData);
|
||||
setSkeletonModelURL(skeletonModelURL);
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
|
||||
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) {
|
||||
if (traitType == AvatarTraits::AvatarEntity) {
|
||||
updateAvatarEntity(instanceID, traitBinaryData);
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID) {
|
||||
if (traitType == AvatarTraits::AvatarEntity) {
|
||||
clearAvatarEntity(instanceID);
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray AvatarData::identityByteArray(bool setIsReplicated) const {
|
||||
QByteArray identityData;
|
||||
QDataStream identityStream(&identityData, QIODevice::Append);
|
||||
const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); // depends on _skeletonModelURL
|
||||
|
||||
// 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
|
||||
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
identityStream << getSessionUUID()
|
||||
<< (udt::SequenceNumber::Type) _identitySequenceNumber
|
||||
<< urlToSend
|
||||
<< _attachmentData
|
||||
<< _displayName
|
||||
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
|
||||
<< (_isReplicated || setIsReplicated)
|
||||
<< _avatarEntityData
|
||||
<< _lookAtSnappingEnabled
|
||||
;
|
||||
});
|
||||
identityStream << getSessionUUID()
|
||||
<< (udt::SequenceNumber::Type) _identitySequenceNumber
|
||||
<< _attachmentData
|
||||
<< _displayName
|
||||
<< getSessionDisplayNameForTransport() // depends on _sessionDisplayName
|
||||
<< (_isReplicated || setIsReplicated)
|
||||
<< _lookAtSnappingEnabled;
|
||||
|
||||
return identityData;
|
||||
}
|
||||
|
@ -1879,11 +1932,17 @@ void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) {
|
|||
if (expanded == _skeletonModelURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
_skeletonModelURL = expanded;
|
||||
qCDebug(avatars) << "Changing skeleton model for avatar" << getSessionUUID() << "to" << _skeletonModelURL.toString();
|
||||
|
||||
updateJointMappings();
|
||||
markIdentityDataChanged();
|
||||
|
||||
if (_clientTraitsHandler) {
|
||||
_clientTraitsHandler->markTraitUpdated(AvatarTraits::SkeletonModelURL);
|
||||
}
|
||||
|
||||
emit skeletonModelURLChanged();
|
||||
}
|
||||
|
||||
void AvatarData::setDisplayName(const QString& displayName) {
|
||||
|
@ -1978,6 +2037,13 @@ void AvatarData::setJointMappingsFromNetworkReply() {
|
|||
|
||||
QNetworkReply* networkReply = static_cast<QNetworkReply*>(sender());
|
||||
|
||||
// before we process this update, make sure that the skeleton model URL hasn't changed
|
||||
// since we made the FST request
|
||||
if (networkReply->url() != _skeletonModelURL) {
|
||||
qCDebug(avatars) << "Refusing to set joint mappings for FST URL that does not match the current URL";
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
QWriteLocker writeLock(&_jointDataLock);
|
||||
QByteArray line;
|
||||
|
@ -2076,7 +2142,6 @@ void AvatarData::sendIdentityPacket() {
|
|||
nodeList->sendPacketList(std::move(packetList), *node);
|
||||
});
|
||||
|
||||
_avatarEntityDataLocallyEdited = false;
|
||||
_identityDataChanged = false;
|
||||
}
|
||||
|
||||
|
@ -2243,6 +2308,15 @@ void AvatarData::setRecordingBasis(std::shared_ptr<Transform> recordingBasis) {
|
|||
_recordingBasis = recordingBasis;
|
||||
}
|
||||
|
||||
void AvatarData::createRecordingIDs() {
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
_avatarEntityForRecording.clear();
|
||||
for (int i = 0; i < _avatarEntityData.size(); i++) {
|
||||
_avatarEntityForRecording.insert(QUuid::createUuid());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void AvatarData::clearRecordingBasis() {
|
||||
_recordingBasis.reset();
|
||||
}
|
||||
|
@ -2303,21 +2377,15 @@ QJsonObject AvatarData::toJson() const {
|
|||
if (!getDisplayName().isEmpty()) {
|
||||
root[JSON_AVATAR_DISPLAY_NAME] = getDisplayName();
|
||||
}
|
||||
if (!getAttachmentData().isEmpty()) {
|
||||
QJsonArray attachmentsJson;
|
||||
for (auto attachment : getAttachmentData()) {
|
||||
attachmentsJson.push_back(attachment.toJson());
|
||||
}
|
||||
root[JSON_AVATAR_ATTACHMENTS] = attachmentsJson;
|
||||
}
|
||||
|
||||
_avatarEntitiesLock.withReadLock([&] {
|
||||
if (!_avatarEntityData.empty()) {
|
||||
QJsonArray avatarEntityJson;
|
||||
int entityCount = 0;
|
||||
for (auto entityID : _avatarEntityData.keys()) {
|
||||
QVariantMap entityData;
|
||||
entityData.insert("id", entityID);
|
||||
entityData.insert("properties", _avatarEntityData.value(entityID));
|
||||
QUuid newId = _avatarEntityForRecording.size() == _avatarEntityData.size() ? _avatarEntityForRecording.values()[entityCount++] : entityID;
|
||||
entityData.insert("id", newId);
|
||||
entityData.insert("properties", _avatarEntityData.value(entityID).toBase64());
|
||||
avatarEntityJson.push_back(QVariant(entityData).toJsonObject());
|
||||
}
|
||||
root[JSON_AVATAR_ENTITIES] = avatarEntityJson;
|
||||
|
@ -2439,12 +2507,17 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) {
|
|||
setAttachmentData(attachments);
|
||||
}
|
||||
|
||||
// if (json.contains(JSON_AVATAR_ENTITIES) && json[JSON_AVATAR_ENTITIES].isArray()) {
|
||||
// QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHMENTS].toArray();
|
||||
// for (auto attachmentJson : attachmentsJson) {
|
||||
// // TODO -- something
|
||||
// }
|
||||
// }
|
||||
if (json.contains(JSON_AVATAR_ENTITIES) && json[JSON_AVATAR_ENTITIES].isArray()) {
|
||||
QJsonArray attachmentsJson = json[JSON_AVATAR_ENTITIES].toArray();
|
||||
for (auto attachmentJson : attachmentsJson) {
|
||||
if (attachmentJson.isObject()) {
|
||||
QVariantMap entityData = attachmentJson.toObject().toVariantMap();
|
||||
QUuid entityID = entityData.value("id").toUuid();
|
||||
QByteArray properties = QByteArray::fromBase64(entityData.value("properties").toByteArray());
|
||||
updateAvatarEntity(entityID, properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.contains(JSON_AVATAR_JOINT_ARRAY)) {
|
||||
if (version == (int)JsonAvatarFrameVersion::JointRotationsInRelativeFrame) {
|
||||
|
@ -2631,23 +2704,36 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent
|
|||
if (itr == _avatarEntityData.end()) {
|
||||
if (_avatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
|
||||
_avatarEntityData.insert(entityID, entityData);
|
||||
_avatarEntityDataLocallyEdited = true;
|
||||
markIdentityDataChanged();
|
||||
}
|
||||
} else {
|
||||
itr.value() = entityData;
|
||||
_avatarEntityDataLocallyEdited = true;
|
||||
markIdentityDataChanged();
|
||||
}
|
||||
});
|
||||
|
||||
_avatarEntityDataChanged = true;
|
||||
|
||||
if (_clientTraitsHandler) {
|
||||
// we have a client traits handler, so we need to mark this instanced trait as changed
|
||||
// so that changes will be sent next frame
|
||||
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarData::clearAvatarEntity(const QUuid& entityID) {
|
||||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_avatarEntityData.remove(entityID);
|
||||
_avatarEntityDataLocallyEdited = true;
|
||||
markIdentityDataChanged();
|
||||
void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
|
||||
|
||||
bool removedEntity = false;
|
||||
|
||||
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
|
||||
removedEntity = _avatarEntityData.remove(entityID);
|
||||
});
|
||||
|
||||
insertDetachedEntityID(entityID);
|
||||
|
||||
if (removedEntity && _clientTraitsHandler) {
|
||||
// we have a client traits handler, so we need to mark this removed instance trait as deleted
|
||||
// so that changes are sent next frame
|
||||
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, entityID);
|
||||
}
|
||||
}
|
||||
|
||||
AvatarEntityMap AvatarData::getAvatarEntityData() const {
|
||||
|
@ -2662,6 +2748,8 @@ void AvatarData::insertDetachedEntityID(const QUuid entityID) {
|
|||
_avatarEntitiesLock.withWriteLock([&] {
|
||||
_avatarEntityDetached.insert(entityID);
|
||||
});
|
||||
|
||||
_avatarEntityDataChanged = true;
|
||||
}
|
||||
|
||||
void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
||||
|
@ -2681,6 +2769,20 @@ void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
|||
foreach (auto entityID, previousAvatarEntityIDs) {
|
||||
if (!_avatarEntityData.contains(entityID)) {
|
||||
_avatarEntityDetached.insert(entityID);
|
||||
|
||||
if (_clientTraitsHandler) {
|
||||
// we have a client traits handler, so we flag this removed entity as deleted
|
||||
// so that changes are sent next frame
|
||||
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, entityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_clientTraitsHandler) {
|
||||
// if we have a client traits handler, flag any updated or created entities
|
||||
// so that we send changes for them next frame
|
||||
foreach (auto entityID, _avatarEntityData) {
|
||||
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
#include <udt/SequenceNumber.h>
|
||||
|
||||
#include "AABox.h"
|
||||
#include "AvatarTraits.h"
|
||||
#include "HeadData.h"
|
||||
#include "PathUtils.h"
|
||||
|
||||
|
@ -371,6 +372,8 @@ public:
|
|||
bool operator<(const AvatarPriority& other) const { return priority < other.priority; }
|
||||
};
|
||||
|
||||
class ClientTraitsHandler;
|
||||
|
||||
class AvatarData : public QObject, public SpatiallyNestable {
|
||||
Q_OBJECT
|
||||
|
||||
|
@ -425,7 +428,6 @@ public:
|
|||
virtual ~AvatarData();
|
||||
|
||||
static const QUrl& defaultFullAvatarModelUrl();
|
||||
QUrl cannonicalSkeletonModelURL(const QUrl& empty) const;
|
||||
|
||||
virtual bool isMyAvatar() const { return false; }
|
||||
|
||||
|
@ -924,11 +926,12 @@ public:
|
|||
* @param {string} entityData
|
||||
*/
|
||||
Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData);
|
||||
|
||||
/**jsdoc
|
||||
* @function MyAvatar.clearAvatarEntity
|
||||
* @param {Uuid} entityID
|
||||
*/
|
||||
Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID);
|
||||
Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);
|
||||
|
||||
|
||||
/**jsdoc
|
||||
|
@ -944,23 +947,32 @@ public:
|
|||
const HeadData* getHeadData() const { return _headData; }
|
||||
|
||||
struct Identity {
|
||||
QUrl skeletonModelURL;
|
||||
QVector<AttachmentData> attachmentData;
|
||||
QString displayName;
|
||||
QString sessionDisplayName;
|
||||
bool isReplicated;
|
||||
AvatarEntityMap avatarEntityData;
|
||||
bool lookAtSnappingEnabled;
|
||||
};
|
||||
|
||||
// identityChanged returns true if identity has changed, false otherwise.
|
||||
// identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange.
|
||||
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged,
|
||||
bool& displayNameChanged, bool& skeletonModelUrlChanged);
|
||||
void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged);
|
||||
|
||||
qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination,
|
||||
AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION);
|
||||
qint64 packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID,
|
||||
ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION);
|
||||
|
||||
void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData);
|
||||
void processTraitInstance(AvatarTraits::TraitType traitType,
|
||||
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData);
|
||||
void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID);
|
||||
|
||||
QByteArray identityByteArray(bool setIsReplicated = false) const;
|
||||
|
||||
QUrl getWireSafeSkeletonModelURL() const;
|
||||
const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; }
|
||||
|
||||
const QString& getDisplayName() const { return _displayName; }
|
||||
const QString& getSessionDisplayName() const { return _sessionDisplayName; }
|
||||
bool getLookAtSnappingEnabled() const { return _lookAtSnappingEnabled; }
|
||||
|
@ -1077,6 +1089,7 @@ public:
|
|||
void clearRecordingBasis();
|
||||
TransformPointer getRecordingBasis() const;
|
||||
void setRecordingBasis(TransformPointer recordingBasis = TransformPointer());
|
||||
void createRecordingIDs();
|
||||
QJsonObject toJson() const;
|
||||
void fromJson(const QJsonObject& json, bool useFrameSkeleton = true);
|
||||
|
||||
|
@ -1327,7 +1340,6 @@ protected:
|
|||
mutable HeadData* _headData { nullptr };
|
||||
|
||||
QUrl _skeletonModelURL;
|
||||
bool _firstSkeletonCheck { true };
|
||||
QUrl _skeletonFBXURL;
|
||||
QVector<AttachmentData> _attachmentData;
|
||||
QVector<AttachmentData> _oldAttachmentData;
|
||||
|
@ -1410,8 +1422,8 @@ protected:
|
|||
|
||||
mutable ReadWriteLockable _avatarEntitiesLock;
|
||||
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar
|
||||
AvatarEntityIDs _avatarEntityForRecording; // create new entities id for avatar recording
|
||||
AvatarEntityMap _avatarEntityData;
|
||||
bool _avatarEntityDataLocallyEdited { false };
|
||||
bool _avatarEntityDataChanged { false };
|
||||
|
||||
// used to transform any sensor into world space, including the _hmdSensorMat, or hand controllers.
|
||||
|
@ -1434,6 +1446,9 @@ protected:
|
|||
bool _hasProcessedFirstIdentity { false };
|
||||
float _density;
|
||||
|
||||
// null unless MyAvatar or ScriptableAvatar sending traits data to mixer
|
||||
std::unique_ptr<ClientTraitsHandler> _clientTraitsHandler;
|
||||
|
||||
template <typename T, typename F>
|
||||
T readLockWithNamedJointIndex(const QString& name, const T& defaultValue, F f) const {
|
||||
int index = getFauxJointIndex(name);
|
||||
|
|
|
@ -19,10 +19,17 @@
|
|||
#include <SharedUtil.h>
|
||||
|
||||
#include "AvatarLogging.h"
|
||||
#include "AvatarTraits.h"
|
||||
|
||||
AvatarHashMap::AvatarHashMap() {
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
|
||||
auto& packetReceiver = nodeList->getPacketReceiver();
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket");
|
||||
packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar");
|
||||
packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket");
|
||||
packetReceiver.registerListener(PacketType::BulkAvatarTraits, this, "processBulkAvatarTraits");
|
||||
|
||||
connect(nodeList.data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged);
|
||||
}
|
||||
|
||||
|
@ -182,9 +189,72 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
|
|||
auto avatar = newOrExistingAvatar(identityUUID, sendingNode, isNewAvatar);
|
||||
bool identityChanged = false;
|
||||
bool displayNameChanged = false;
|
||||
bool skeletonModelUrlChanged = false;
|
||||
// In this case, the "sendingNode" is the Avatar Mixer.
|
||||
avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
|
||||
avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
while (message->getBytesLeftToRead()) {
|
||||
// read the avatar ID to figure out which avatar this is for
|
||||
auto avatarID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
// grab the avatar so we can ask it to process trait data
|
||||
bool isNewAvatar;
|
||||
auto avatar = newOrExistingAvatar(avatarID, sendingNode, isNewAvatar);
|
||||
|
||||
// read the first trait type for this avatar
|
||||
AvatarTraits::TraitType traitType;
|
||||
message->readPrimitive(&traitType);
|
||||
|
||||
// grab the last trait versions for this avatar
|
||||
auto& lastProcessedVersions = _processedTraitVersions[avatarID];
|
||||
|
||||
while (traitType != AvatarTraits::NullTrait) {
|
||||
AvatarTraits::TraitVersion packetTraitVersion;
|
||||
message->readPrimitive(&packetTraitVersion);
|
||||
|
||||
AvatarTraits::TraitWireSize traitBinarySize;
|
||||
bool skipBinaryTrait = false;
|
||||
|
||||
|
||||
if (AvatarTraits::isSimpleTrait(traitType)) {
|
||||
message->readPrimitive(&traitBinarySize);
|
||||
|
||||
// check if this trait version is newer than what we already have for this avatar
|
||||
if (packetTraitVersion > lastProcessedVersions[traitType]) {
|
||||
avatar->processTrait(traitType, message->read(traitBinarySize));
|
||||
lastProcessedVersions[traitType] = packetTraitVersion;
|
||||
} else {
|
||||
skipBinaryTrait = true;
|
||||
}
|
||||
} else {
|
||||
AvatarTraits::TraitInstanceID traitInstanceID =
|
||||
QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||
|
||||
message->readPrimitive(&traitBinarySize);
|
||||
|
||||
auto& processedInstanceVersion = lastProcessedVersions.getInstanceValueRef(traitType, traitInstanceID);
|
||||
if (packetTraitVersion > processedInstanceVersion) {
|
||||
if (traitBinarySize == AvatarTraits::DELETED_TRAIT_SIZE) {
|
||||
avatar->processDeletedTraitInstance(traitType, traitInstanceID);
|
||||
} else {
|
||||
avatar->processTraitInstance(traitType, traitInstanceID, message->read(traitBinarySize));
|
||||
}
|
||||
processedInstanceVersion = packetTraitVersion;
|
||||
} else {
|
||||
skipBinaryTrait = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (skipBinaryTrait) {
|
||||
// we didn't read this trait because it was older or because we didn't have an avatar to process it for
|
||||
message->seek(message->getPosition() + traitBinarySize);
|
||||
}
|
||||
|
||||
// read the next trait type, which is null if there are no more traits for this avatar
|
||||
message->readPrimitive(&traitType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,6 +279,9 @@ void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason remo
|
|||
}
|
||||
|
||||
void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) {
|
||||
// remove any information about processed traits for this avatar
|
||||
_processedTraitVersions.erase(removedAvatar->getID());
|
||||
|
||||
qCDebug(avatars) << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID())
|
||||
<< "from AvatarHashMap" << removalReason;
|
||||
emit avatarRemovedEvent(removedAvatar->getSessionUUID());
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#include "ScriptAvatarData.h"
|
||||
|
||||
#include "AvatarData.h"
|
||||
#include "AssociatedTraitValues.h"
|
||||
|
||||
/**jsdoc
|
||||
* <strong>Note:</strong> An <code>AvatarList</code> API is also provided for Interface and client entity scripts: it is a
|
||||
|
@ -133,6 +134,8 @@ protected slots:
|
|||
*/
|
||||
void processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
||||
|
||||
void processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
||||
|
||||
/**jsdoc
|
||||
* @function AvatarList.processKillAvatar
|
||||
* @param {} message
|
||||
|
@ -152,7 +155,7 @@ protected:
|
|||
virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason);
|
||||
|
||||
virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason);
|
||||
|
||||
|
||||
AvatarHash _avatarHash;
|
||||
struct PendingAvatar {
|
||||
std::chrono::steady_clock::time_point creationTime;
|
||||
|
@ -163,6 +166,7 @@ protected:
|
|||
AvatarPendingHash _pendingAvatars;
|
||||
mutable QReadWriteLock _hashLock;
|
||||
|
||||
std::unordered_map<QUuid, AvatarTraits::TraitVersions> _processedTraitVersions;
|
||||
private:
|
||||
QUuid _lastOwnerSessionUUID;
|
||||
};
|
||||
|
|
61
libraries/avatars/src/AvatarTraits.h
Normal file
61
libraries/avatars/src/AvatarTraits.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// AvatarTraits.h
|
||||
// libraries/avatars/src
|
||||
//
|
||||
// Created by Stephen Birarda on 7/30/18.
|
||||
// Copyright 2018 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_AvatarTraits_h
|
||||
#define hifi_AvatarTraits_h
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <QtCore/QUuid>
|
||||
|
||||
namespace AvatarTraits {
|
||||
enum TraitType : int8_t {
|
||||
NullTrait = -1,
|
||||
SkeletonModelURL,
|
||||
FirstInstancedTrait,
|
||||
AvatarEntity = FirstInstancedTrait,
|
||||
TotalTraitTypes
|
||||
};
|
||||
|
||||
using TraitInstanceID = QUuid;
|
||||
|
||||
inline bool isSimpleTrait(TraitType traitType) {
|
||||
return traitType > NullTrait && traitType < FirstInstancedTrait;
|
||||
}
|
||||
|
||||
using TraitVersion = int32_t;
|
||||
const TraitVersion DEFAULT_TRAIT_VERSION = 0;
|
||||
const TraitVersion NULL_TRAIT_VERSION = -1;
|
||||
|
||||
using TraitWireSize = int16_t;
|
||||
const TraitWireSize DELETED_TRAIT_SIZE = -1;
|
||||
|
||||
inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
|
||||
TraitVersion traitVersion = NULL_TRAIT_VERSION) {
|
||||
qint64 bytesWritten = 0;
|
||||
|
||||
bytesWritten += destination.writePrimitive(traitType);
|
||||
|
||||
if (traitVersion > DEFAULT_TRAIT_VERSION) {
|
||||
bytesWritten += destination.writePrimitive(traitVersion);
|
||||
}
|
||||
|
||||
bytesWritten += destination.write(instanceID.toRfc4122());
|
||||
|
||||
bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE);
|
||||
|
||||
return bytesWritten;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // hifi_AvatarTraits_h
|
143
libraries/avatars/src/ClientTraitsHandler.cpp
Normal file
143
libraries/avatars/src/ClientTraitsHandler.cpp
Normal file
|
@ -0,0 +1,143 @@
|
|||
//
|
||||
// ClientTraitsHandler.cpp
|
||||
// libraries/avatars/src
|
||||
//
|
||||
// Created by Stephen Birarda on 7/30/18.
|
||||
// Copyright 2018 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 "ClientTraitsHandler.h"
|
||||
|
||||
#include <QtCore/QObject>
|
||||
|
||||
#include <NodeList.h>
|
||||
#include <NLPacketList.h>
|
||||
|
||||
#include "AvatarData.h"
|
||||
|
||||
ClientTraitsHandler::ClientTraitsHandler(AvatarData* owningAvatar) :
|
||||
_owningAvatar(owningAvatar)
|
||||
{
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
QObject::connect(nodeList.data(), &NodeList::nodeAdded, [this](SharedNodePointer addedNode){
|
||||
if (addedNode->getType() == NodeType::AvatarMixer) {
|
||||
resetForNewMixer();
|
||||
}
|
||||
});
|
||||
|
||||
nodeList->getPacketReceiver().registerListener(PacketType::SetAvatarTraits, this, "processTraitOverride");
|
||||
}
|
||||
|
||||
void ClientTraitsHandler::resetForNewMixer() {
|
||||
// re-set the current version to 0
|
||||
_currentTraitVersion = AvatarTraits::DEFAULT_TRAIT_VERSION;
|
||||
|
||||
// mark that all traits should be sent next time
|
||||
_shouldPerformInitialSend = true;
|
||||
}
|
||||
|
||||
void ClientTraitsHandler::sendChangedTraitsToMixer() {
|
||||
if (hasChangedTraits() || _shouldPerformInitialSend) {
|
||||
// we have at least one changed trait to send
|
||||
|
||||
auto nodeList = DependencyManager::get<NodeList>();
|
||||
auto avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
|
||||
if (!avatarMixer || !avatarMixer->getActiveSocket()) {
|
||||
// we don't have an avatar mixer with an active socket, we can't send changed traits at this time
|
||||
return;
|
||||
}
|
||||
|
||||
// we have a mixer to send to, setup our set traits packet
|
||||
auto traitsPacketList = NLPacketList::create(PacketType::SetAvatarTraits, QByteArray(), true, true);
|
||||
|
||||
// bump and write the current trait version to an extended header
|
||||
// the trait version is the same for all traits in this packet list
|
||||
traitsPacketList->writePrimitive(++_currentTraitVersion);
|
||||
|
||||
// take a copy of the set of changed traits and clear the stored set
|
||||
auto traitStatusesCopy { _traitStatuses };
|
||||
_traitStatuses.reset();
|
||||
_hasChangedTraits = false;
|
||||
|
||||
auto simpleIt = traitStatusesCopy.simpleCBegin();
|
||||
while (simpleIt != traitStatusesCopy.simpleCEnd()) {
|
||||
// because the vector contains all trait types (for access using trait type as index)
|
||||
// we double check that it is a simple iterator here
|
||||
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt));
|
||||
|
||||
if (_shouldPerformInitialSend || *simpleIt == Updated) {
|
||||
if (traitType == AvatarTraits::SkeletonModelURL) {
|
||||
_owningAvatar->packTrait(traitType, *traitsPacketList);
|
||||
|
||||
// keep track of our skeleton version in case we get an override back
|
||||
_currentSkeletonVersion = _currentTraitVersion;
|
||||
}
|
||||
}
|
||||
|
||||
++simpleIt;
|
||||
}
|
||||
|
||||
auto instancedIt = traitStatusesCopy.instancedCBegin();
|
||||
while (instancedIt != traitStatusesCopy.instancedCEnd()) {
|
||||
for (auto& instanceIDValuePair : instancedIt->instances) {
|
||||
if ((_shouldPerformInitialSend && instanceIDValuePair.value != Deleted)
|
||||
|| instanceIDValuePair.value == Updated) {
|
||||
// this is a changed trait we need to send or we haven't send out trait information yet
|
||||
// ask the owning avatar to pack it
|
||||
_owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList);
|
||||
} else if (!_shouldPerformInitialSend && instanceIDValuePair.value == Deleted) {
|
||||
// pack delete for this trait instance
|
||||
AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id,
|
||||
*traitsPacketList);
|
||||
}
|
||||
}
|
||||
|
||||
++instancedIt;
|
||||
}
|
||||
|
||||
nodeList->sendPacketList(std::move(traitsPacketList), *avatarMixer);
|
||||
|
||||
// if this was an initial send of all traits, consider it completed
|
||||
_shouldPerformInitialSend = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||
if (sendingNode->getType() == NodeType::AvatarMixer) {
|
||||
while (message->getBytesLeftToRead()) {
|
||||
AvatarTraits::TraitType traitType;
|
||||
message->readPrimitive(&traitType);
|
||||
|
||||
AvatarTraits::TraitVersion traitVersion;
|
||||
message->readPrimitive(&traitVersion);
|
||||
|
||||
AvatarTraits::TraitWireSize traitBinarySize;
|
||||
message->readPrimitive(&traitBinarySize);
|
||||
|
||||
// only accept an override if this is for a trait type we override
|
||||
// and the version matches what we last sent for skeleton
|
||||
if (traitType == AvatarTraits::SkeletonModelURL
|
||||
&& traitVersion == _currentSkeletonVersion
|
||||
&& _traitStatuses[AvatarTraits::SkeletonModelURL] != Updated) {
|
||||
|
||||
// override the skeleton URL but do not mark the trait as having changed
|
||||
// so that we don't unecessarily send a new trait packet to the mixer with the overriden URL
|
||||
auto encodedSkeletonURL = QUrl::fromEncoded(message->readWithoutCopy(traitBinarySize));
|
||||
|
||||
auto hasChangesBefore = _hasChangedTraits;
|
||||
|
||||
_owningAvatar->setSkeletonModelURL(encodedSkeletonURL);
|
||||
|
||||
// setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to
|
||||
// avoid unnecessarily sending the overriden skeleton model URL back to the mixer
|
||||
_traitStatuses.erase(AvatarTraits::SkeletonModelURL);
|
||||
_hasChangedTraits = hasChangesBefore;
|
||||
} else {
|
||||
message->seek(message->getPosition() + traitBinarySize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
libraries/avatars/src/ClientTraitsHandler.h
Normal file
61
libraries/avatars/src/ClientTraitsHandler.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// ClientTraitsHandler.h
|
||||
// libraries/avatars/src
|
||||
//
|
||||
// Created by Stephen Birarda on 7/30/18.
|
||||
// Copyright 2018 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_ClientTraitsHandler_h
|
||||
#define hifi_ClientTraitsHandler_h
|
||||
|
||||
#include <ReceivedMessage.h>
|
||||
|
||||
#include "AssociatedTraitValues.h"
|
||||
#include "Node.h"
|
||||
|
||||
class AvatarData;
|
||||
|
||||
class ClientTraitsHandler : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ClientTraitsHandler(AvatarData* owningAvatar);
|
||||
|
||||
void sendChangedTraitsToMixer();
|
||||
|
||||
bool hasChangedTraits() { return _hasChangedTraits; }
|
||||
|
||||
void markTraitUpdated(AvatarTraits::TraitType updatedTrait)
|
||||
{ _traitStatuses[updatedTrait] = Updated; _hasChangedTraits = true; }
|
||||
void markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID)
|
||||
{ _traitStatuses.instanceInsert(traitType, updatedInstanceID, Updated); _hasChangedTraits = true; }
|
||||
void markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID)
|
||||
{ _traitStatuses.instanceInsert(traitType, deleteInstanceID, Deleted); _hasChangedTraits = true; }
|
||||
|
||||
void resetForNewMixer();
|
||||
|
||||
public slots:
|
||||
void processTraitOverride(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
||||
|
||||
private:
|
||||
enum ClientTraitStatus {
|
||||
Unchanged,
|
||||
Updated,
|
||||
Deleted
|
||||
};
|
||||
|
||||
AvatarData* _owningAvatar;
|
||||
|
||||
AvatarTraits::AssociatedTraitValues<ClientTraitStatus, Unchanged> _traitStatuses;
|
||||
AvatarTraits::TraitVersion _currentTraitVersion { AvatarTraits::DEFAULT_TRAIT_VERSION };
|
||||
|
||||
AvatarTraits::TraitVersion _currentSkeletonVersion { AvatarTraits::NULL_TRAIT_VERSION };
|
||||
|
||||
bool _shouldPerformInitialSend { false };
|
||||
bool _hasChangedTraits { false };
|
||||
};
|
||||
|
||||
#endif // hifi_ClientTraitsHandler_h
|
|
@ -269,6 +269,26 @@ QVector<AttachmentData> ScriptAvatarData::getAttachmentData() const {
|
|||
// END
|
||||
//
|
||||
|
||||
#if PR_BUILD || DEV_BUILD
|
||||
//
|
||||
// ENTITY PROPERTIES
|
||||
// START
|
||||
//
|
||||
AvatarEntityMap ScriptAvatarData::getAvatarEntities() const {
|
||||
AvatarEntityMap scriptEntityData;
|
||||
|
||||
if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
|
||||
return sharedAvatarData->getAvatarEntityData();
|
||||
}
|
||||
|
||||
return scriptEntityData;
|
||||
}
|
||||
//
|
||||
// ENTITY PROPERTIES
|
||||
// END
|
||||
//
|
||||
#endif
|
||||
|
||||
|
||||
//
|
||||
// AUDIO PROPERTIES
|
||||
|
|
|
@ -116,6 +116,10 @@ public:
|
|||
Q_INVOKABLE QStringList getJointNames() const;
|
||||
Q_INVOKABLE QVector<AttachmentData> getAttachmentData() const;
|
||||
|
||||
#if DEV_BUILD || PR_BUILD
|
||||
Q_INVOKABLE AvatarEntityMap getAvatarEntities() const;
|
||||
#endif
|
||||
|
||||
//
|
||||
// AUDIO PROPERTIES
|
||||
//
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
set(TARGET_NAME display-plugins)
|
||||
AUTOSCRIBE_SHADER_LIB(gpu display-plugins)
|
||||
setup_hifi_library(Gui)
|
||||
link_hifi_libraries(shared shaders plugins ui-plugins gl ui render-utils ${PLATFORM_GL_BACKEND})
|
||||
include_hifi_library_headers(gpu)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
VERTEX gpu::vertex::DrawUnitQuadTexcoord
|
|
@ -0,0 +1 @@
|
|||
VERTEX gpu::vertex::DrawUnitQuadTexcoord
|
|
@ -295,7 +295,8 @@ void EntityTreeRenderer::addPendingEntities(const render::ScenePointer& scene, r
|
|||
auto spaceIndex = _space->allocateID();
|
||||
workload::Sphere sphere(entity->getWorldPosition(), entity->getBoundingRadius());
|
||||
workload::Transaction transaction;
|
||||
transaction.reset(spaceIndex, sphere, workload::Owner(entity));
|
||||
SpatiallyNestablePointer nestable = std::static_pointer_cast<SpatiallyNestable>(entity);
|
||||
transaction.reset(spaceIndex, sphere, workload::Owner(nestable));
|
||||
_space->enqueueTransaction(transaction);
|
||||
entity->setSpaceIndex(spaceIndex);
|
||||
connect(entity.get(), &EntityItem::spaceUpdate, this, &EntityTreeRenderer::handleSpaceUpdate, Qt::QueuedConnection);
|
||||
|
@ -642,6 +643,14 @@ bool EntityTreeRenderer::applyLayeredZones() {
|
|||
}
|
||||
|
||||
void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode) {
|
||||
OCTREE_PACKET_FLAGS flags;
|
||||
message.readPrimitive(&flags);
|
||||
|
||||
OCTREE_PACKET_SEQUENCE sequence;
|
||||
message.readPrimitive(&sequence);
|
||||
|
||||
_lastOctreeMessageSequence = sequence;
|
||||
message.seek(0);
|
||||
std::static_pointer_cast<EntityTree>(_tree)->processEraseMessage(message, sourceNode);
|
||||
}
|
||||
|
||||
|
|
|
@ -149,11 +149,6 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
|
|||
|
||||
void EntityEditPacketSender::queueEraseEntityMessage(const EntityItemID& entityItemID) {
|
||||
|
||||
// in case this was a clientOnly entity:
|
||||
if(_myAvatar) {
|
||||
_myAvatar->clearAvatarEntity(entityItemID);
|
||||
}
|
||||
|
||||
QByteArray bufferOut(NLPacket::maxPayloadSize(PacketType::EntityErase), 0);
|
||||
|
||||
if (EntityItemProperties::encodeEraseEntityMessage(entityItemID, bufferOut)) {
|
||||
|
|
|
@ -27,7 +27,6 @@ public:
|
|||
|
||||
void setMyAvatar(AvatarData* myAvatar) { _myAvatar = myAvatar; }
|
||||
AvatarData* getMyAvatar() { return _myAvatar; }
|
||||
void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); }
|
||||
|
||||
/// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines
|
||||
/// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in
|
||||
|
|
|
@ -574,7 +574,7 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
|
|||
_activityTracking.deletedEntityCount++;
|
||||
|
||||
EntityItemID entityID(id);
|
||||
bool shouldDelete = true;
|
||||
bool shouldSendDeleteToServer = true;
|
||||
|
||||
// If we have a local entity tree set, then also update it.
|
||||
if (_entityTree) {
|
||||
|
@ -591,16 +591,21 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
|
|||
auto avatarHashMap = DependencyManager::get<AvatarHashMap>();
|
||||
AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID);
|
||||
myAvatar->insertDetachedEntityID(id);
|
||||
shouldDelete = false;
|
||||
shouldSendDeleteToServer = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity->getLocked()) {
|
||||
shouldDelete = false;
|
||||
shouldSendDeleteToServer = false;
|
||||
} else {
|
||||
// only delete local entities, server entities will round trip through the server filters
|
||||
if (entity->getClientOnly() || _entityTree->isServerlessMode()) {
|
||||
shouldSendDeleteToServer = false;
|
||||
_entityTree->deleteEntity(entityID);
|
||||
|
||||
if (entity->getClientOnly() && getEntityPacketSender()->getMyAvatar()) {
|
||||
getEntityPacketSender()->getMyAvatar()->clearAvatarEntity(entityID, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -608,7 +613,7 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
|
|||
}
|
||||
|
||||
// if at this point, we know the id, and we should still delete the entity, send the update to the entity server
|
||||
if (shouldDelete) {
|
||||
if (shouldSendDeleteToServer) {
|
||||
getEntityPacketSender()->queueEraseEntityMessage(entityID);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue