Merge branch 'master' into 20852

This commit is contained in:
David Rowe 2016-03-17 10:35:26 +13:00
commit df870d6e82
74 changed files with 3149 additions and 857 deletions

View file

@ -12,19 +12,21 @@
#include "AssetServer.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QString>
#include <QtCore/QCoreApplication>
#include <QtCore/QCryptographicHash>
#include <QtCore/QDateTime>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QJsonDocument>
#include <QtCore/QString>
#include <ServerPathUtils.h>
#include "NetworkLogging.h"
#include "NodeType.h"
#include "SendAssetTask.h"
#include "UploadAssetTask.h"
#include <ServerPathUtils.h>
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
@ -42,6 +44,7 @@ AssetServer::AssetServer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet");
packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo");
packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload");
packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation");
}
void AssetServer::run() {
@ -56,6 +59,8 @@ void AssetServer::run() {
ThreadedAssignment::commonInit(ASSET_SERVER_LOGGING_TARGET_NAME, NodeType::AssetServer);
}
static const QString ASSET_FILES_SUBDIR = "files";
void AssetServer::completeSetup() {
auto nodeList = DependencyManager::get<NodeList>();
@ -96,85 +101,208 @@ void AssetServer::completeSetup() {
qDebug() << "Creating resources directory";
_resourcesDirectory.mkpath(".");
_filesDirectory = _resourcesDirectory;
bool noExistingAssets = !_resourcesDirectory.exists() || _resourcesDirectory.entryList(QDir::Files).size() == 0;
if (noExistingAssets) {
qDebug() << "Asset resources directory empty, searching for existing asset resources to migrate";
QString oldDataDirectory = QCoreApplication::applicationDirPath();
const QString OLD_RESOURCES_PATH = "assets";
auto oldResourcesDirectory = QDir(oldDataDirectory).filePath("resources/" + OLD_RESOURCES_PATH);
if (QDir(oldResourcesDirectory).exists()) {
qDebug() << "Existing assets found in " << oldResourcesDirectory << ", copying to " << _resourcesDirectory;
QDir resourcesParentDirectory = _resourcesDirectory.filePath("..");
if (!resourcesParentDirectory.exists()) {
qDebug() << "Creating data directory " << resourcesParentDirectory.absolutePath();
resourcesParentDirectory.mkpath(".");
}
auto files = QDir(oldResourcesDirectory).entryList(QDir::Files);
for (auto& file : files) {
auto from = oldResourcesDirectory + QDir::separator() + file;
auto to = _resourcesDirectory.absoluteFilePath(file);
qDebug() << "\tCopying from " << from << " to " << to;
QFile::copy(from, to);
}
}
if (!_resourcesDirectory.mkpath(ASSET_FILES_SUBDIR) || !_filesDirectory.cd(ASSET_FILES_SUBDIR)) {
qCritical() << "Unable to create file directory for asset-server files. Stopping assignment.";
setFinished(true);
return;
}
qDebug() << "Serving files from: " << _resourcesDirectory.path();
// Scan for new files
qDebug() << "Looking for new files in asset directory";
auto files = _resourcesDirectory.entryInfoList(QDir::Files);
QRegExp filenameRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}(\\..+)?$" };
for (const auto& fileInfo : files) {
auto filename = fileInfo.fileName();
if (!filenameRegex.exactMatch(filename)) {
qDebug() << "Found file: " << filename;
if (!fileInfo.isReadable()) {
qDebug() << "\tCan't open file for reading: " << filename;
continue;
}
// load whatever mappings we currently have from the local file
loadMappingsFromFile();
// Read file
QFile file { fileInfo.absoluteFilePath() };
file.open(QFile::ReadOnly);
QByteArray data = file.readAll();
qInfo() << "Serving files from: " << _filesDirectory.path();
auto hash = hashData(data);
auto hexHash = hash.toHex();
// Check the asset directory to output some information about what we have
auto files = _filesDirectory.entryList(QDir::Files);
qDebug() << "\tMoving " << filename << " to " << hexHash;
file.rename(_resourcesDirectory.absoluteFilePath(hexHash) + "." + fileInfo.suffix());
}
}
QRegExp hashFileRegex { ASSET_HASH_REGEX_STRING };
auto hashedFiles = files.filter(hashFileRegex);
qInfo() << "There are" << hashedFiles.size() << "asset files in the asset directory.";
performMappingMigration();
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
}
void AssetServer::performMappingMigration() {
QRegExp hashFileRegex { "^[a-f0-9]{" + QString::number(SHA256_HASH_HEX_LENGTH) + "}(\\.[\\w]+)+$" };
auto files = _resourcesDirectory.entryInfoList(QDir::Files);
for (const auto& fileInfo : files) {
if (hashFileRegex.exactMatch(fileInfo.fileName())) {
// we have a pre-mapping file that we should migrate to the new mapping system
qDebug() << "Migrating pre-mapping file" << fileInfo.fileName();
// rename the file to the same name with no extension
QFile oldFile { fileInfo.absoluteFilePath() };
auto oldAbsolutePath = fileInfo.absoluteFilePath();
auto oldFilename = fileInfo.fileName();
auto hash = oldFilename.left(SHA256_HASH_HEX_LENGTH);
auto fullExtension = oldFilename.mid(oldFilename.indexOf('.'));
qDebug() << "\tMoving" << oldAbsolutePath << "to" << oldAbsolutePath.replace(fullExtension, "");
bool renamed = oldFile.copy(_filesDirectory.filePath(hash));
if (!renamed) {
qWarning() << "\tCould not migrate pre-mapping file" << fileInfo.fileName();
} else {
qDebug() << "\tRenamed pre-mapping file" << fileInfo.fileName();
// add a new mapping with the old extension and a truncated version of the hash
const int TRUNCATED_HASH_NUM_CHAR = 16;
auto fakeFileName = "/" + hash.left(TRUNCATED_HASH_NUM_CHAR) + fullExtension;
qDebug() << "\tAdding a migration mapping from" << fakeFileName << "to" << hash;
auto it = _fileMappings.find(fakeFileName);
if (it == _fileMappings.end()) {
_fileMappings[fakeFileName] = hash;
if (writeMappingsToFile()) {
// mapping added and persisted, we can remove the migrated file
oldFile.remove();
qDebug() << "\tMigration completed for" << oldFilename;
}
} else {
qDebug() << "\tCould not add migration mapping for" << hash << "since a mapping for" << fakeFileName
<< "already exists.";
}
}
}
}
}
void AssetServer::handleAssetMappingOperation(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
MessageID messageID;
message->readPrimitive(&messageID);
AssetMappingOperationType operationType;
message->readPrimitive(&operationType);
auto replyPacket = NLPacketList::create(PacketType::AssetMappingOperationReply, QByteArray(), true, true);
replyPacket->writePrimitive(messageID);
switch (operationType) {
case AssetMappingOperationType::Get: {
handleGetMappingOperation(*message, senderNode, *replyPacket);
break;
}
case AssetMappingOperationType::GetAll: {
handleGetAllMappingOperation(*message, senderNode, *replyPacket);
break;
}
case AssetMappingOperationType::Set: {
handleSetMappingOperation(*message, senderNode, *replyPacket);
break;
}
case AssetMappingOperationType::Delete: {
handleDeleteMappingsOperation(*message, senderNode, *replyPacket);
break;
}
case AssetMappingOperationType::Rename: {
handleRenameMappingOperation(*message, senderNode, *replyPacket);
break;
}
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(replyPacket), *senderNode);
}
void AssetServer::handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
QString assetPath = message.readString();
auto it = _fileMappings.find(assetPath);
if (it != _fileMappings.end()) {
auto assetHash = it->toString();
replyPacket.writePrimitive(AssetServerError::NoError);
replyPacket.write(QByteArray::fromHex(assetHash.toUtf8()));
} else {
replyPacket.writePrimitive(AssetServerError::AssetNotFound);
}
}
void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
replyPacket.writePrimitive(AssetServerError::NoError);
auto count = _fileMappings.size();
replyPacket.writePrimitive(count);
for (auto it = _fileMappings.cbegin(); it != _fileMappings.cend(); ++ it) {
replyPacket.writeString(it.key());
replyPacket.write(QByteArray::fromHex(it.value().toString().toUtf8()));
}
}
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
QString assetPath = message.readString();
auto assetHash = message.read(SHA256_HASH_LENGTH).toHex();
if (setMapping(assetPath, assetHash)) {
replyPacket.writePrimitive(AssetServerError::NoError);
} else {
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
}
} else {
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
}
}
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
int numberOfDeletedMappings { 0 };
message.readPrimitive(&numberOfDeletedMappings);
QStringList mappingsToDelete;
for (int i = 0; i < numberOfDeletedMappings; ++i) {
mappingsToDelete << message.readString();
}
if (deleteMappings(mappingsToDelete)) {
replyPacket.writePrimitive(AssetServerError::NoError);
} else {
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
}
} else {
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
}
}
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
QString oldPath = message.readString();
QString newPath = message.readString();
if (renameMapping(oldPath, newPath)) {
replyPacket.writePrimitive(AssetServerError::NoError);
} else {
replyPacket.writePrimitive(AssetServerError::MappingOperationFailed);
}
} else {
replyPacket.writePrimitive(AssetServerError::PermissionDenied);
}
}
void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
QByteArray assetHash;
MessageID messageID;
uint8_t extensionLength;
if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID) + sizeof(extensionLength))) {
if (message->getSize() < qint64(SHA256_HASH_LENGTH + sizeof(messageID))) {
qDebug() << "ERROR bad file request";
return;
}
message->readPrimitive(&messageID);
assetHash = message->readWithoutCopy(SHA256_HASH_LENGTH);
message->readPrimitive(&extensionLength);
QByteArray extension = message->read(extensionLength);
auto replyPacket = NLPacket::create(PacketType::AssetGetInfoReply);
@ -183,8 +311,8 @@ void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, Sh
replyPacket->writePrimitive(messageID);
replyPacket->write(assetHash);
QString fileName = QString(hexHash) + "." + extension;
QFileInfo fileInfo { _resourcesDirectory.filePath(fileName) };
QString fileName = QString(hexHash);
QFileInfo fileInfo { _filesDirectory.filePath(fileName) };
if (fileInfo.exists() && fileInfo.isReadable()) {
qDebug() << "Opening file: " << fileInfo.filePath();
@ -201,39 +329,39 @@ void AssetServer::handleAssetGetInfo(QSharedPointer<ReceivedMessage> message, Sh
void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + sizeof(DataOffset) + sizeof(DataOffset));
auto minSize = qint64(sizeof(MessageID) + SHA256_HASH_LENGTH + sizeof(DataOffset) + sizeof(DataOffset));
if (message->getSize() < minSize) {
qDebug() << "ERROR bad file request";
return;
}
// Queue task
auto task = new SendAssetTask(message, senderNode, _resourcesDirectory);
auto task = new SendAssetTask(message, senderNode, _filesDirectory);
_taskPool.start(task);
}
void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
if (senderNode->getCanRez()) {
qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
auto task = new UploadAssetTask(message, senderNode, _resourcesDirectory);
auto task = new UploadAssetTask(message, senderNode, _filesDirectory);
_taskPool.start(task);
} else {
// this is a node the domain told us is not allowed to rez entities
// for now this also means it isn't allowed to add assets
// so return a packet with error that indicates that
auto permissionErrorPacket = NLPacket::create(PacketType::AssetUploadReply, sizeof(MessageID) + sizeof(AssetServerError));
MessageID messageID;
message->readPrimitive(&messageID);
// write the message ID and a permission denied error
permissionErrorPacket->writePrimitive(messageID);
permissionErrorPacket->writePrimitive(AssetServerError::PermissionDenied);
// send off the packet
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacket(std::move(permissionErrorPacket), *senderNode);
@ -242,19 +370,19 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
void AssetServer::sendStatsPacket() {
QJsonObject serverStats;
auto stats = DependencyManager::get<NodeList>()->sampleStatsForAllConnections();
for (const auto& stat : stats) {
QJsonObject nodeStats;
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stat.second.endTime);
QDateTime date = QDateTime::fromMSecsSinceEpoch(endTimeMs.count());
static const float USEC_PER_SEC = 1000000.0f;
static const float MEGABITS_PER_BYTE = 8.0f / 1000000.0f; // Bytes => Mbits
float elapsed = (float)(stat.second.endTime - stat.second.startTime).count() / USEC_PER_SEC; // sec
float megabitsPerSecPerByte = MEGABITS_PER_BYTE / elapsed; // Bytes => Mb/s
QJsonObject connectionStats;
connectionStats["1. Last Heard"] = date.toString();
connectionStats["2. Est. Max (P/s)"] = stat.second.estimatedBandwith;
@ -264,10 +392,10 @@ void AssetServer::sendStatsPacket() {
connectionStats["6. Up (Mb/s)"] = stat.second.sentBytes * megabitsPerSecPerByte;
connectionStats["7. Down (Mb/s)"] = stat.second.receivedBytes * megabitsPerSecPerByte;
nodeStats["Connection Stats"] = connectionStats;
using Events = udt::ConnectionStats::Stats::Event;
const auto& events = stat.second.events;
QJsonObject upstreamStats;
upstreamStats["1. Sent (P/s)"] = stat.second.sendRate;
upstreamStats["2. Sent Packets"] = stat.second.sentPackets;
@ -279,7 +407,7 @@ void AssetServer::sendStatsPacket() {
upstreamStats["8. Sent ACK2"] = events[Events::SentACK2];
upstreamStats["9. Retransmitted"] = events[Events::Retransmission];
nodeStats["Upstream Stats"] = upstreamStats;
QJsonObject downstreamStats;
downstreamStats["1. Recvd (P/s)"] = stat.second.receiveRate;
downstreamStats["2. Recvd Packets"] = stat.second.receivedPackets;
@ -290,7 +418,7 @@ void AssetServer::sendStatsPacket() {
downstreamStats["7. Recvd ACK2"] = events[Events::ReceivedACK2];
downstreamStats["8. Duplicates"] = events[Events::Duplicate];
nodeStats["Downstream Stats"] = downstreamStats;
QString uuid;
auto nodelist = DependencyManager::get<NodeList>();
if (stat.first == nodelist->getDomainHandler().getSockAddr()) {
@ -301,10 +429,274 @@ void AssetServer::sendStatsPacket() {
uuid = uuidStringWithoutCurlyBraces(node ? node->getUUID() : QUuid());
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
}
serverStats[uuid] = nodeStats;
}
// send off the stats packets
ThreadedAssignment::addPacketStatsAndSendStatsPacket(serverStats);
}
static const QString MAP_FILE_NAME = "map.json";
void AssetServer::loadMappingsFromFile() {
auto mapFilePath = _resourcesDirectory.absoluteFilePath(MAP_FILE_NAME);
QFile mapFile { mapFilePath };
if (mapFile.exists()) {
if (mapFile.open(QIODevice::ReadOnly)) {
QJsonParseError error;
auto jsonDocument = QJsonDocument::fromJson(mapFile.readAll(), &error);
if (error.error == QJsonParseError::NoError) {
_fileMappings = jsonDocument.object().toVariantHash();
// remove any mappings that don't match the expected format
auto it = _fileMappings.begin();
while (it != _fileMappings.end()) {
bool shouldDrop = false;
if (!isValidPath(it.key())) {
qWarning() << "Will not keep mapping for" << it.key() << "since it is not a valid path.";
shouldDrop = true;
}
if (!isValidHash(it.value().toString())) {
qWarning() << "Will not keep mapping for" << it.key() << "since it does not have a valid hash.";
shouldDrop = true;
}
if (shouldDrop) {
it = _fileMappings.erase(it);
} else {
++it;
}
}
qInfo() << "Loaded" << _fileMappings.count() << "mappings from map file at" << mapFilePath;
return;
}
}
qCritical() << "Failed to read mapping file at" << mapFilePath << "- assignment will not continue.";
setFinished(true);
} else {
qInfo() << "No existing mappings loaded from file since no file was found at" << mapFilePath;
}
}
bool AssetServer::writeMappingsToFile() {
auto mapFilePath = _resourcesDirectory.absoluteFilePath(MAP_FILE_NAME);
QFile mapFile { mapFilePath };
if (mapFile.open(QIODevice::WriteOnly)) {
auto jsonObject = QJsonObject::fromVariantHash(_fileMappings);
QJsonDocument jsonDocument { jsonObject };
if (mapFile.write(jsonDocument.toJson()) != -1) {
qDebug() << "Wrote JSON mappings to file at" << mapFilePath;
return true;
} else {
qWarning() << "Failed to write JSON mappings to file at" << mapFilePath;
}
} else {
qWarning() << "Failed to open map file at" << mapFilePath;
}
return false;
}
bool AssetServer::setMapping(AssetPath path, AssetHash hash) {
path = path.trimmed();
if (!isValidPath(path)) {
qWarning() << "Cannot set a mapping for invalid path:" << path << "=>" << hash;
return false;
}
if (!isValidHash(hash)) {
qWarning() << "Cannot set a mapping for invalid hash" << path << "=>" << hash;
return false;
}
// remember what the old mapping was in case persistence fails
auto oldMapping = _fileMappings.value(path).toString();
// update the in memory QHash
_fileMappings[path] = hash;
// attempt to write to file
if (writeMappingsToFile()) {
// persistence succeeded, we are good to go
qDebug() << "Set mapping:" << path << "=>" << hash;
return true;
} else {
// failed to persist this mapping to file - put back the old one in our in-memory representation
if (oldMapping.isEmpty()) {
_fileMappings.remove(path);
} else {
_fileMappings[path] = oldMapping;
}
qWarning() << "Failed to persist mapping:" << path << "=>" << hash;
return false;
}
}
bool pathIsFolder(const AssetPath& path) {
return path.endsWith('/');
}
bool AssetServer::deleteMappings(AssetPathList& paths) {
// take a copy of the current mappings in case persistence of these deletes fails
auto oldMappings = _fileMappings;
// enumerate the paths to delete and remove them all
for (auto& path : paths) {
path = path.trimmed();
// figure out if this path will delete a file or folder
if (pathIsFolder(path)) {
// enumerate the in memory file mappings and remove anything that matches
auto it = _fileMappings.begin();
auto sizeBefore = _fileMappings.size();
while (it != _fileMappings.end()) {
if (it.key().startsWith(path)) {
it = _fileMappings.erase(it);
} else {
++it;
}
}
auto sizeNow = _fileMappings.size();
if (sizeBefore != sizeNow) {
qDebug() << "Deleted" << sizeBefore - sizeNow << "mappings in folder: " << path;
} else {
qDebug() << "Did not find any mappings to delete in folder:" << path;
}
} else {
auto oldMapping = _fileMappings.take(path);
if (!oldMapping.isNull()) {
qDebug() << "Deleted a mapping:" << path << "=>" << oldMapping.toString();
} else {
qDebug() << "Unable to delete a mapping that was not found:" << path;
}
}
}
// deleted the old mappings, attempt to persist to file
if (writeMappingsToFile()) {
// persistence succeeded we are good to go
return true;
} else {
qWarning() << "Failed to persist deleted mappings, rolling back";
// we didn't delete the previous mapping, put it back in our in-memory representation
_fileMappings = oldMappings;
return false;
}
}
bool AssetServer::renameMapping(AssetPath oldPath, AssetPath newPath) {
oldPath = oldPath.trimmed();
newPath = newPath.trimmed();
if (!isValidPath(oldPath) || !isValidPath(newPath)) {
qWarning() << "Cannot perform rename with invalid paths - both should have leading forward slashes:"
<< oldPath << "=>" << newPath;
return false;
}
// figure out if this rename is for a file or folder
if (pathIsFolder(oldPath)) {
if (!pathIsFolder(newPath)) {
// we were asked to rename a path to a folder to a path that isn't a folder, this is a fail
qWarning() << "Cannot rename mapping from folder path" << oldPath << "to file path" << newPath;
return false;
}
// take a copy of the old mappings
auto oldMappings = _fileMappings;
// iterate the current mappings and adjust any that matches the renamed folder
auto it = oldMappings.begin();
while (it != oldMappings.end()) {
if (it.key().startsWith(oldPath)) {
auto newKey = it.key();
newKey.replace(0, oldPath.size(), newPath);
// remove the old version from the in memory file mappings
_fileMappings.remove(it.key());
_fileMappings.insert(newKey, it.value());
}
++it;
}
if (writeMappingsToFile()) {
// persisted the changed mappings, return success
qDebug() << "Renamed folder mapping:" << oldPath << "=>" << newPath;
return true;
} else {
// couldn't persist the renamed paths, rollback and return failure
_fileMappings = oldMappings;
qWarning() << "Failed to persist renamed folder mapping:" << oldPath << "=>" << newPath;
return false;
}
} else {
if (pathIsFolder(newPath)) {
// we were asked to rename a path to a file to a path that is a folder, this is a fail
qWarning() << "Cannot rename mapping from file path" << oldPath << "to folder path" << newPath;
return false;
}
// take the old hash to remove the old mapping
auto oldSourceMapping = _fileMappings.take(oldPath).toString();
// in case we're overwriting, keep the current destination mapping for potential rollback
auto oldDestinationMapping = _fileMappings.value(newPath);
if (!oldSourceMapping.isEmpty()) {
_fileMappings[newPath] = oldSourceMapping;
if (writeMappingsToFile()) {
// persisted the renamed mapping, return success
qDebug() << "Renamed mapping:" << oldPath << "=>" << newPath;
return true;
} else {
// we couldn't persist the renamed mapping, rollback and return failure
_fileMappings[oldPath] = oldSourceMapping;
if (!oldDestinationMapping.isNull()) {
// put back the overwritten mapping for the destination path
_fileMappings[newPath] = oldDestinationMapping.toString();
} else {
// clear the new mapping
_fileMappings.remove(newPath);
}
qDebug() << "Failed to persist renamed mapping:" << oldPath << "=>" << newPath;
return false;
}
} else {
// failed to find a mapping that was to be renamed, return failure
return false;
}
}
}

View file

@ -12,10 +12,10 @@
#ifndef hifi_AssetServer_h
#define hifi_AssetServer_h
#include <QDir>
#include <QtCore/QDir>
#include <QtCore/QThreadPool>
#include <ThreadedAssignment.h>
#include <QThreadPool>
#include "AssetUtils.h"
#include "ReceivedMessage.h"
@ -34,17 +34,39 @@ private slots:
void handleAssetGetInfo(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
void handleAssetGet(QSharedPointer<ReceivedMessage> packet, SharedNodePointer senderNode);
void handleAssetUpload(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer senderNode);
void handleAssetMappingOperation(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void sendStatsPacket();
private:
static void writeError(NLPacketList* packetList, AssetServerError error);
using Mappings = QVariantHash;
void handleGetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleGetAllMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
void handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket);
// Mapping file operations must be called from main assignment thread only
void loadMappingsFromFile();
bool writeMappingsToFile();
/// Set the mapping for path to hash
bool setMapping(AssetPath path, AssetHash hash);
/// Delete mapping `path`. Returns `true` if deletion of mappings succeeds, else `false`.
bool deleteMappings(AssetPathList& paths);
/// Rename mapping from `oldPath` to `newPath`. Returns true if successful
bool renameMapping(AssetPath oldPath, AssetPath newPath);
void performMappingMigration();
Mappings _fileMappings;
QDir _resourcesDirectory;
QDir _filesDirectory;
QThreadPool _taskPool;
};
inline void writeError(NLPacketList* packetList, AssetServerError error) {
packetList->writePrimitive(error);
}
#endif

View file

@ -33,13 +33,10 @@ SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const Shar
void SendAssetTask::run() {
MessageID messageID;
uint8_t extensionLength;
DataOffset start, end;
_message->readPrimitive(&messageID);
QByteArray assetHash = _message->read(SHA256_HASH_LENGTH);
_message->readPrimitive(&extensionLength);
QByteArray extension = _message->read(extensionLength);
// `start` and `end` indicate the range of data to retrieve for the asset identified by `assetHash`.
// `start` is inclusive, `end` is exclusive. Requesting `start` = 1, `end` = 10 will retrieve 9 bytes of data,
@ -59,15 +56,15 @@ void SendAssetTask::run() {
replyPacketList->writePrimitive(messageID);
if (end <= start) {
writeError(replyPacketList.get(), AssetServerError::InvalidByteRange);
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
} else {
QString filePath = _resourcesDir.filePath(QString(hexHash) + "." + QString(extension));
QString filePath = _resourcesDir.filePath(QString(hexHash));
QFile file { filePath };
if (file.open(QIODevice::ReadOnly)) {
if (file.size() < end) {
writeError(replyPacketList.get(), AssetServerError::InvalidByteRange);
replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
qCDebug(networking) << "Bad byte range: " << hexHash << " " << start << ":" << end;
} else {
auto size = end - start;
@ -80,7 +77,7 @@ void SendAssetTask::run() {
file.close();
} else {
qCDebug(networking) << "Asset not found: " << filePath << "(" << hexHash << ")";
writeError(replyPacketList.get(), AssetServerError::AssetNotFound);
replyPacketList->writePrimitive(AssetServerError::AssetNotFound);
}
}

View file

@ -37,15 +37,10 @@ void UploadAssetTask::run() {
MessageID messageID;
buffer.read(reinterpret_cast<char*>(&messageID), sizeof(messageID));
uint8_t extensionLength;
buffer.read(reinterpret_cast<char*>(&extensionLength), sizeof(extensionLength));
QByteArray extension = buffer.read(extensionLength);
uint64_t fileSize;
buffer.read(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes and extension" << extension << "from"
qDebug() << "UploadAssetTask reading a file of " << fileSize << "bytes from"
<< uuidStringWithoutCurlyBraces(_senderNode->getUUID());
auto replyPacket = NLPacket::create(PacketType::AssetUploadReply);
@ -62,17 +57,47 @@ void UploadAssetTask::run() {
qDebug() << "Hash for uploaded file from" << uuidStringWithoutCurlyBraces(_senderNode->getUUID())
<< "is: (" << hexHash << ") ";
QFile file { _resourcesDir.filePath(QString(hexHash)) + "." + QString(extension) };
QFile file { _resourcesDir.filePath(QString(hexHash)) };
bool existingCorrectFile = false;
if (file.exists()) {
qDebug() << "[WARNING] This file already exists: " << hexHash;
} else {
file.open(QIODevice::WriteOnly);
file.write(fileData);
file.close();
// check if the local file has the correct contents, otherwise we overwrite
if (file.open(QIODevice::ReadOnly) && hashData(file.readAll()) == hash) {
qDebug() << "Not overwriting existing verified file: " << hexHash;
existingCorrectFile = true;
replyPacket->writePrimitive(AssetServerError::NoError);
replyPacket->write(hash);
} else {
qDebug() << "Overwriting an existing file whose contents did not match the expected hash: " << hexHash;
file.close();
}
}
replyPacket->writePrimitive(AssetServerError::NoError);
replyPacket->write(hash);
if (!existingCorrectFile) {
if (file.open(QIODevice::WriteOnly) && file.write(fileData) == qint64(fileSize)) {
qDebug() << "Wrote file" << hexHash << "to disk. Upload complete";
file.close();
replyPacket->writePrimitive(AssetServerError::NoError);
replyPacket->write(hash);
} else {
qWarning() << "Failed to upload or write to file" << hexHash << " - upload failed.";
// upload has failed - remove the file and return an error
auto removed = file.remove();
if (!removed) {
qWarning() << "Removal of failed upload file" << hexHash << "failed.";
}
replyPacket->writePrimitive(AssetServerError::FileOperationFailed);
}
}
}
auto nodeList = DependencyManager::get<NodeList>();

View file

@ -0,0 +1,22 @@
//
// BG.qml
// examples/utilities/tools/render
//
// Created by Zach Pomerantz on 2/8/2016
// Copyright 2016 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 QtQuick.Controls 1.4
Item {
Timer {
running: true; repeat: true
onTriggered: time.text = Render.getConfig("DrawBackgroundDeferred").gpuTime
}
Text { id: time; font.pointSize: 20 }
}

View file

@ -0,0 +1,21 @@
//
// debugBG.js
// examples/utilities/tools/render
//
// Zach Pomerantz, created on 1/27/2016.
// Copyright 2016 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
//
// Set up the qml ui
var qml = Script.resolvePath('BG.qml');
var window = new OverlayWindow({
title: 'Background Timer',
source: qml,
width: 300
});
window.setPosition(25, 50);
window.closed.connect(function() { Script.stop(); });

View file

@ -0,0 +1,520 @@
//
// AssetServer.qml
//
// Created by Clement on 3/1/16
// Copyright 2016 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Dialogs 1.2 as OriginalDialogs
import Qt.labs.settings 1.0
import "styles-uit"
import "controls-uit" as HifiControls
import "windows-uit"
import "dialogs"
Window {
id: root
objectName: "AssetServer"
title: "My Asset Server"
resizable: true
destroyOnInvisible: true
x: 40; y: 40
implicitWidth: 384; implicitHeight: 640
minSize: Qt.vector2d(200, 300)
property int colorScheme: hifi.colorSchemes.dark
HifiConstants { id: hifi }
property var scripts: ScriptDiscoveryService;
property var assetProxyModel: Assets.proxyModel;
property var assetMappingsModel: Assets.mappingModel;
property var currentDirectory;
Settings {
category: "Overlay.AssetServer"
property alias x: root.x
property alias y: root.y
property alias directory: root.currentDirectory
}
Component.onCompleted: {
ApplicationInterface.uploadRequest.connect(uploadClicked);
assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError);
reload();
}
function doDeleteFile(path) {
console.log("Deleting " + path);
Assets.deleteMappings(path, function(err) {
if (err) {
console.log("Asset browser - error deleting path: ", path, err);
box = errorMessageBox("There was an error deleting:\n" + path + "\n" + err);
box.selected.connect(reload);
} else {
console.log("Asset browser - finished deleting path: ", path);
reload();
}
});
}
function doRenameFile(oldPath, newPath) {
if (newPath[0] != "/") {
newPath = "/" + newPath;
}
if (oldPath[oldPath.length - 1] == '/' && newPath[newPath.length - 1] != '/') {
// this is a folder rename but the user neglected to add a trailing slash when providing a new path
newPath = newPath + "/";
}
if (Assets.isKnownFolder(newPath)) {
box = errorMessageBox("Cannot overwrite existing directory.");
box.selected.connect(reload);
}
console.log("Asset browser - renaming " + oldPath + " to " + newPath);
Assets.renameMapping(oldPath, newPath, function(err) {
if (err) {
console.log("Asset browser - error renaming: ", oldPath, "=>", newPath, " - error ", err);
box = errorMessageBox("There was an error renaming:\n" + oldPath + " to " + newPath + "\n" + err);
box.selected.connect(reload);
} else {
console.log("Asset browser - finished rename: ", oldPath, "=>", newPath);
}
reload();
});
}
function fileExists(path) {
return Assets.isKnownMapping(path);
}
function askForOverwrite(path, callback) {
var object = desktop.messageBox({
icon: hifi.icons.question,
buttons: OriginalDialogs.StandardButton.Yes | OriginalDialogs.StandardButton.No,
defaultButton: OriginalDialogs.StandardButton.No,
title: "Overwrite File",
text: path + "\n" + "This file already exists. Do you want to overwrite it?"
});
object.selected.connect(function(button) {
if (button === OriginalDialogs.StandardButton.Yes) {
callback();
}
});
}
function canAddToWorld(path) {
var supportedExtensions = [/\.fbx\b/i, /\.obj\b/i];
return supportedExtensions.reduce(function(total, current) {
return total | new RegExp(current).test(path);
}, false);
}
function reload() {
Assets.mappingModel.refresh();
treeView.selection.clear();
}
function handleGetMappingsError(errorString) {
errorMessageBox(
"There was a problem retreiving the list of assets from your Asset Server.\n"
+ errorString
);
}
function addToWorld() {
var url = assetProxyModel.data(treeView.selection.currentIndex, 0x103);
if (!url || !canAddToWorld(url)) {
return;
}
var name = assetProxyModel.data(treeView.selection.currentIndex);
console.log("Asset browser - adding asset " + url + " (" + name + ") to world.");
var addPosition = Vec3.sum(MyAvatar.position, Vec3.multiply(2, Quat.getFront(MyAvatar.orientation)));
Entities.addModelEntity(name, url, addPosition);
}
function copyURLToClipboard(index) {
if (!index) {
index = treeView.selection.currentIndex;
}
var url = assetProxyModel.data(treeView.selection.currentIndex, 0x103);
if (!url) {
return;
}
Window.copyToClipboard(url);
}
function renameEl(index, data) {
if (!index) {
return false;
}
var path = assetProxyModel.data(index, 0x100);
if (!path) {
return false;
}
var destinationPath = path.split('/');
destinationPath[destinationPath.length - (path[path.length - 1] === '/' ? 2 : 1)] = data;
destinationPath = destinationPath.join('/').trim();
if (path === destinationPath) {
return;
}
if (!fileExists(destinationPath)) {
doRenameFile(path, destinationPath);
}
}
function renameFile(index) {
if (!index) {
index = treeView.selection.currentIndex;
}
var path = assetProxyModel.data(index, 0x100);
if (!path) {
return;
}
var object = desktop.inputDialog({
label: "Enter new path:",
current: path,
placeholderText: "Enter path here"
});
object.selected.connect(function(destinationPath) {
destinationPath = destinationPath.trim();
if (path == destinationPath) {
return;
}
if (fileExists(destinationPath)) {
askForOverwrite(destinationPath, function() {
doRenameFile(path, destinationPath);
});
} else {
doRenameFile(path, destinationPath);
}
});
}
function deleteFile(index) {
if (!index) {
index = treeView.selection.currentIndex;
}
var path = assetProxyModel.data(index, 0x100);
if (!path) {
return;
}
var isFolder = assetProxyModel.data(treeView.selection.currentIndex, 0x101);
var typeString = isFolder ? 'folder' : 'file';
var object = desktop.messageBox({
icon: hifi.icons.question,
buttons: OriginalDialogs.StandardButton.Yes + OriginalDialogs.StandardButton.No,
defaultButton: OriginalDialogs.StandardButton.Yes,
title: "Delete",
text: "You are about to delete the following " + typeString + ":\n" + path + "\nDo you want to continue?"
});
object.selected.connect(function(button) {
if (button === OriginalDialogs.StandardButton.Yes) {
doDeleteFile(path);
}
});
}
property var uploadOpen: false;
Timer {
id: timer
}
function uploadClicked(fileUrl) {
if (uploadOpen) {
return;
}
uploadOpen = true;
function doUpload(url, dropping) {
var fileUrl = fileDialogHelper.urlToPath(url);
var path = assetProxyModel.data(treeView.selection.currentIndex, 0x100);
var directory = path ? path.slice(0, path.lastIndexOf('/') + 1) : "/";
var filename = fileUrl.slice(fileUrl.lastIndexOf('/') + 1);
Assets.uploadFile(fileUrl, directory + filename,
function() {
// Upload started
uploadSpinner.visible = true;
uploadButton.enabled = false;
uploadProgressLabel.text = "In progress...";
},
function(err, path) {
print(err, path);
if (!err) {
uploadProgressLabel.text = "Upload Complete";
timer.interval = 1000;
timer.repeat = false;
timer.triggered.connect(function() {
uploadSpinner.visible = false;
uploadButton.enabled = true;
uploadOpen = false;
});
timer.start();
console.log("Asset Browser - finished uploading: ", fileUrl);
reload();
} else {
if (err > 0) {
console.log("Asset Browser - error uploading: ", fileUrl, " - error ", err);
var box = errorMessageBox("There was an error uploading:\n" + fileUrl + "\n" + Assets.getErrorString(err));
box.selected.connect(reload);
}
uploadSpinner.visible = false;
uploadButton.enabled = true;
uploadOpen = false;
}
}, dropping);
}
if (fileUrl) {
doUpload(fileUrl, true);
} else {
var browser = desktop.fileDialog({
selectDirectory: false,
dir: currentDirectory
});
browser.canceled.connect(function() {
uploadOpen = false;
});
browser.selectedFile.connect(function(url) {
currentDirectory = browser.dir;
doUpload(url, false);
});
}
}
function errorMessageBox(message) {
return desktop.messageBox({
icon: hifi.icons.warning,
defaultButton: OriginalDialogs.StandardButton.Ok,
title: "Error",
text: message
});
}
Item {
width: pane.contentWidth
height: pane.height
HifiControls.ContentSection {
id: assetDirectory
name: "Asset Directory"
spacing: hifi.dimensions.contentSpacing.y
isFirst: true
Row {
id: buttonRow
anchors.left: parent.left
anchors.right: parent.right
spacing: hifi.dimensions.contentSpacing.x
HifiControls.GlyphButton {
glyph: hifi.glyphs.reload
color: hifi.buttons.white
colorScheme: root.colorScheme
height: 26
width: 26
onClicked: root.reload()
}
HifiControls.Button {
text: "ADD TO WORLD"
color: hifi.buttons.white
colorScheme: root.colorScheme
height: 26
width: 120
enabled: canAddToWorld(assetProxyModel.data(treeView.selection.currentIndex, 0x100))
onClicked: root.addToWorld()
}
HifiControls.Button {
text: "RENAME"
color: hifi.buttons.white
colorScheme: root.colorScheme
height: 26
width: 80
onClicked: root.renameFile()
enabled: treeView.selection.hasSelection
}
HifiControls.Button {
id: deleteButton
text: "DELETE"
color: hifi.buttons.red
colorScheme: root.colorScheme
height: 26
width: 80
onClicked: root.deleteFile()
enabled: treeView.selection.hasSelection
}
}
Menu {
id: contextMenu
title: "Edit"
property var url: ""
property var currentIndex: null
MenuItem {
text: "Copy URL"
onTriggered: {
copyURLToClipboard(contextMenu.currentIndex);
}
}
MenuItem {
text: "Rename"
onTriggered: {
renameFile(contextMenu.currentIndex);
}
}
MenuItem {
text: "Delete"
onTriggered: {
deleteFile(contextMenu.currentIndex);
}
}
}
}
HifiControls.Tree {
id: treeView
anchors.top: assetDirectory.bottom
anchors.bottom: uploadSection.top
anchors.margins: 12
anchors.bottomMargin: 0
anchors.left: parent.left
anchors.right: parent.right
treeModel: assetProxyModel
canEdit: true
colorScheme: root.colorScheme
modifyEl: renameEl
MouseArea {
propagateComposedEvents: true
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
var index = treeView.indexAt(mouse.x, mouse.y);
treeView.selection.setCurrentIndex(index, 0x0002);
contextMenu.currentIndex = index;
contextMenu.popup();
}
}
}
HifiControls.ContentSection {
id: uploadSection
name: "Upload A File"
spacing: hifi.dimensions.contentSpacing.y
anchors.bottom: parent.bottom
height: 130
Item {
height: parent.height
width: parent.width
HifiControls.Button {
id: uploadButton
anchors.right: parent.right
text: "Choose File"
color: hifi.buttons.blue
colorScheme: root.colorScheme
height: 30
width: 155
onClicked: uploadClickedTimer.running = true
// For some reason trigginer an API that enters
// an internal event loop directly from the button clicked
// trigger below causes the appliction to behave oddly.
// Most likely because the button onClicked handling is never
// completed until the function returns.
// FIXME find a better way of handling the input dialogs that
// doesn't trigger this.
Timer {
id: uploadClickedTimer
interval: 5
repeat: false
running: false
onTriggered: uploadClicked();
}
}
Item {
id: uploadSpinner
visible: false
anchors.top: parent.top
anchors.left: parent.left
width: 40
height: 32
Image {
id: image
width: 24
height: 24
source: "../images/Loading-Outer-Ring.png"
RotationAnimation on rotation {
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
Image {
width: 24
height: 24
source: "../images/Loading-Inner-H.png"
}
HifiControls.Label {
id: uploadProgressLabel
anchors.left: image.right
anchors.leftMargin: 10
anchors.verticalCenter: image.verticalCenter
text: "In progress..."
colorScheme: root.colorScheme
}
}
}
}
}
}

View file

@ -15,8 +15,9 @@ import QtQuick.Controls.Styles 1.4
import "../styles-uit"
Original.Button {
id: button
property int color: 0
property int colorScheme: hifi.colorSchemes.light
width: 120
height: 28
@ -24,27 +25,43 @@ Original.Button {
background: Rectangle {
radius: hifi.buttons.radius
gradient: Gradient {
GradientStop {
position: 0.2
color: enabled
? (!pressed && button.color != hifi.buttons.black || (!hovered || pressed) && button.color == hifi.buttons.black
? hifi.buttons.colorStart[button.color] : hifi.buttons.colorFinish[button.color])
: hifi.buttons.colorStart[hifi.buttons.white]
color: {
if (!control.enabled) {
hifi.buttons.disabledColorStart[control.colorScheme]
} else if (control.pressed) {
hifi.buttons.pressedColor[control.color]
} else if (control.hovered) {
hifi.buttons.hoveredColor[control.color]
} else {
hifi.buttons.colorStart[control.color]
}
}
}
GradientStop {
position: 1.0
color: enabled
? ((!hovered || pressed) && button.color != hifi.buttons.black || !pressed && button.color == hifi.buttons.black
? hifi.buttons.colorFinish[button.color] : hifi.buttons.colorStart[button.color])
: hifi.buttons.colorFinish[hifi.buttons.white]
color: {
if (!control.enabled) {
hifi.buttons.disabledColorFinish[control.colorScheme]
} else if (control.pressed) {
hifi.buttons.pressedColor[control.color]
} else if (control.hovered) {
hifi.buttons.hoveredColor[control.color]
} else {
hifi.buttons.colorFinish[control.color]
}
}
}
}
}
label: RalewayBold {
font.capitalization: Font.AllUppercase
color: enabled ? hifi.buttons.textColor[button.color] : hifi.colors.lightGrayText
color: enabled ? hifi.buttons.textColor[control.color]
: hifi.buttons.disabledTextColor[control.colorScheme]
size: hifi.fontSizes.buttonLabel
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter

View file

@ -14,7 +14,7 @@ import QtGraphicalEffects 1.0
import "../styles-uit"
Column {
property string name: "Static Section"
property string name: "Content Section"
property bool isFirst: false
property bool isCollapsible: false // Set at creation.
property bool isCollapsed: false

View file

@ -0,0 +1,71 @@
//
// GlyphButton.qml
//
// Created by Clement on 3/7/16
// Copyright 2016 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
//
import QtQuick 2.5
import QtQuick.Controls 1.4 as Original
import QtQuick.Controls.Styles 1.4
import "../styles-uit"
Original.Button {
property int color: 0
property int colorScheme: hifi.colorShemes.light
property string glyph: ""
width: 120
height: 28
style: ButtonStyle {
background: Rectangle {
radius: hifi.buttons.radius
gradient: Gradient {
GradientStop {
position: 0.2
color: {
if (!control.enabled) {
hifi.buttons.disabledColorStart[control.colorScheme]
} else if (control.pressed) {
hifi.buttons.pressedColor[control.color]
} else if (control.hovered) {
hifi.buttons.hoveredColor[control.color]
} else {
hifi.buttons.colorStart[control.color]
}
}
}
GradientStop {
position: 1.0
color: {
if (!control.enabled) {
hifi.buttons.disabledColorFinish[control.colorScheme]
} else if (control.pressed) {
hifi.buttons.pressedColor[control.color]
} else if (control.hovered) {
hifi.buttons.hoveredColor[control.color]
} else {
hifi.buttons.colorFinish[control.color]
}
}
}
}
}
label: HiFiGlyphs {
color: enabled ? hifi.buttons.textColor[control.color]
: hifi.buttons.disabledTextColor[control.colorScheme]
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
text: control.glyph
}
}
}

View file

@ -38,7 +38,7 @@ TextField {
: (textField.focus ? hifi.colors.white : hifi.colors.lightGrayText)
background: Rectangle {
color: isLightColorScheme
? (textField.focus ? hifi.colors.white : hifi.colors.lightGray)
? (textField.focus ? hifi.colors.white : hifi.colors.textFieldLightBackground)
: (textField.focus ? hifi.colors.black : hifi.colors.baseGrayShadow)
border.color: hifi.colors.primaryHighlight
border.width: textField.focus ? 1 : 0

View file

@ -8,6 +8,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQml.Models 2.2
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@ -18,10 +19,16 @@ TreeView {
id: treeView
property var treeModel: ListModel { }
property var canEdit: false
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property var modifyEl: function(index, data) { return false; }
model: treeModel
selection: ItemSelectionModel {
model: treeModel
}
TableViewColumn {
role: "display";
@ -120,7 +127,9 @@ TreeView {
: (styleData.alternate ? hifi.colors.tableRowDarkEven : hifi.colors.tableRowDarkOdd)
}
itemDelegate: FiraSansSemiBold {
itemDelegate: Loader {
id: itemDelegateLoader
anchors {
left: parent ? parent.left : undefined
leftMargin: (2 + styleData.depth) * hifi.dimensions.tablePadding
@ -129,11 +138,83 @@ TreeView {
verticalCenter: parent ? parent.verticalCenter : undefined
}
text: styleData.value
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
function getComponent() {
if (treeView.canEdit && styleData.selected) {
return textFieldComponent;
} else {
return labelComponent;
}
}
sourceComponent: getComponent()
Component {
id: labelComponent
FiraSansSemiBold {
text: styleData.value
size: hifi.fontSizes.tableText
color: colorScheme == hifi.colorSchemes.light
? (styleData.selected ? hifi.colors.black : hifi.colors.baseGrayHighlight)
: (styleData.selected ? hifi.colors.black : hifi.colors.lightGrayText)
}
}
Component {
id: textFieldComponent
TextField {
id: textField
readOnly: !activeFocus
text: styleData.value
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
font.family: firaSansSemiBold.name
font.pixelSize: hifi.fontSizes.textFieldInput
height: hifi.dimensions.tableRowHeight
style: TextFieldStyle {
textColor: readOnly
? hifi.colors.black
: (treeView.isLightColorScheme ? hifi.colors.black : hifi.colors.white)
background: Rectangle {
visible: !readOnly
color: treeView.isLightColorScheme ? hifi.colors.white : hifi.colors.black
border.color: hifi.colors.primaryHighlight
border.width: 1
}
selectedTextColor: hifi.colors.black
selectionColor: hifi.colors.primaryHighlight
padding.left: readOnly ? 0 : hifi.dimensions.textPadding
padding.right: readOnly ? 0 : hifi.dimensions.textPadding
}
validator: RegExpValidator {
regExp: /[^/]+/
}
Keys.onPressed: {
if (event.key == Qt.Key_Escape) {
text = styleData.value;
unfocusHelper.forceActiveFocus();
event.accepted = true;
}
}
onAccepted: {
if (acceptableInput && styleData.selected) {
if (!modifyEl(selection.currentIndex, text)) {
text = styleData.value;
}
unfocusHelper.forceActiveFocus();
}
}
}
}
}
Item {
id: unfocusHelper
visible: false
}
onDoubleClicked: isExpanded(index) ? collapse(index) : expand(index)

View file

@ -72,7 +72,7 @@ ModalWindow {
readonly property int maxHeight: 720
function resize() {
var targetWidth = mainTextContainer.width
var targetWidth = mainTextContainer.contentWidth
var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y
+ (informativeTextContainer.text != "" ? informativeTextContainer.contentHeight + 3 * hifi.dimensions.contentSpacing.y : 0)
+ buttons.height
@ -96,6 +96,7 @@ ModalWindow {
}
lineHeight: 2
lineHeightMode: Text.ProportionalHeight
horizontalAlignment: Text.AlignHCenter
}
RalewaySemiBold {

View file

@ -34,8 +34,7 @@ ModalWindow {
property var items;
property string label
property var result;
// FIXME not current honored
property var current;
property alias current: textResult.text
// For text boxes
property alias placeholderText: textResult.placeholderText

View file

@ -10,6 +10,7 @@ QtObject {
readonly property string animDebugDrawPosition: "Debug Draw Position";
readonly property string antialiasing: "Antialiasing";
readonly property string assetMigration: "ATP Asset Migration";
readonly property string assetServer = "Asset Server";
readonly property string atmosphere: "Atmosphere";
readonly property string attachments: "Attachments...";
readonly property string audioNetworkStats: "Audio Network Stats";
@ -154,7 +155,6 @@ QtObject {
readonly property string toolWindow: "Tool Window";
readonly property string transmitterDrive: "Transmitter Drive";
readonly property string turnWithHead: "Turn using Head";
readonly property string uploadAsset: "Upload File to Asset Server";
readonly property string useAudioForMouth: "Use Audio for Mouth";
readonly property string useCamera: "Use Camera";
readonly property string velocityFilter: "Velocity Filter";

View file

@ -35,7 +35,7 @@ Item {
glyph = hifi.glyphs.alert;
break;
case hifi.icons.critical:
glyph = hifi.glyphs.critical;
glyph = hifi.glyphs.error;
break;
case hifi.icons.placemark:
glyph = hifi.glyphs.placemark;
@ -113,6 +113,7 @@ Item {
readonly property color dropDownLightFinish: "#afafaf"
readonly property color dropDownDarkStart: "#7d7d7d"
readonly property color dropDownDarkFinish: "#6b6a6b"
readonly property color textFieldLightBackground: "#d4d4d4"
}
Item {
@ -164,32 +165,6 @@ Item {
readonly property real disclosureButton: dimensions.largeScreen ? 20 : 15
}
Item {
id: glyphs
readonly property string alert: "+"
readonly property string backward: "E"
readonly property string caratDn: "5"
readonly property string caratR: "3"
readonly property string caratUp: "6"
readonly property string close: "w"
readonly property string closeInverted: "x"
readonly property string closeSmall: "C"
readonly property string critical: "="
readonly property string disclosureButtonCollapse: "M"
readonly property string disclosureButtonExpand: "L"
readonly property string disclosureCollapse: "Z"
readonly property string disclosureExpand: "B"
readonly property string forward: "D"
readonly property string info: "["
readonly property string noIcon: ""
readonly property string pin: "y"
readonly property string pinInverted: "z"
readonly property string placemark: "U"
readonly property string question: "]"
readonly property string reloadSmall: "a"
readonly property string resizeHandle: "A"
}
Item {
id: icons
// Values per OffscreenUi::Icon
@ -208,8 +183,13 @@ Item {
readonly property int red: 2
readonly property int black: 3
readonly property var textColor: [ colors.darkGray, colors.white, colors.white, colors.white ]
readonly property var colorStart: [ "#ffffff", "#00b4ef", "#d42043", "#343434" ]
readonly property var colorFinish: [ "#afafaf", "#1080b8", "#94132e", "#000000" ]
readonly property var colorStart: [ colors.white, colors.primaryHighlight, "#d42043", "#343434" ]
readonly property var colorFinish: [ colors.lightGrayText, colors.blueAccent, "#94132e", colors.black ]
readonly property var hoveredColor: [ colorStart[white], colorStart[blue], colorStart[red], colorFinish[black] ]
readonly property var pressedColor: [ colorFinish[white], colorFinish[blue], colorFinish[red], colorStart[black] ]
readonly property var disabledColorStart: [ colorStart[white], colors.baseGrayHighlight]
readonly property var disabledColorFinish: [ colorFinish[white], colors.baseGrayShadow]
readonly property var disabledTextColor: [ colors.lightGrayText, colors.baseGrayShadow]
readonly property int radius: 5
}
@ -217,4 +197,103 @@ Item {
id: effects
readonly property int fadeInDuration: 300
}
Item {
id: glyphs
readonly property string noIcon: ""
readonly property string hmd: "b"
readonly property string screen: "c"
readonly property string keyboard: "d"
readonly property string handControllers: "e"
readonly property string headphonesMic: "f"
readonly property string gamepad: "g"
readonly property string headphones: "h"
readonly property string mic: "i"
readonly property string upload: "j"
readonly property string script: "k"
readonly property string text: "l"
readonly property string cube: "m"
readonly property string sphere: "n"
readonly property string zone: "o"
readonly property string light: "p"
readonly property string web: "q"
readonly property string web2: "r"
readonly property string edit: "s"
readonly property string market: "t"
readonly property string directory: "u"
readonly property string menu: "v"
readonly property string close: "w"
readonly property string closeInverted: "x"
readonly property string pin: "y"
readonly property string pinInverted: "z"
readonly property string resizeHandle: "A"
readonly property string disclosureExpand: "B"
readonly property string reloadSmall: "a"
readonly property string closeSmall: "C"
readonly property string forward: "D"
readonly property string backward: "E"
readonly property string reload: "F"
readonly property string unmuted: "G"
readonly property string muted: "H"
readonly property string minimize: "I"
readonly property string maximize: "J"
readonly property string maximizeInverted: "K"
readonly property string disclosureButtonExpand: "L"
readonly property string disclosureButtonCollapse: "M"
readonly property string scriptStop: "N"
readonly property string scriptReload: "O"
readonly property string scriptRun: "P"
readonly property string scriptNew: "Q"
readonly property string hifiForum: "2"
readonly property string hifiLogoSmall: "S"
readonly property string avatar1: "T"
readonly property string placemark: "U"
readonly property string box: "V"
readonly property string community: "0"
readonly property string grabHandle: "X"
readonly property string search: "Y"
readonly property string disclosureCollapse: "Z"
readonly property string scriptUpload: "R"
readonly property string code: "W"
readonly property string avatar: "<"
readonly property string arrowsH: ":"
readonly property string arrowsV: ";"
readonly property string arrows: "`"
readonly property string compress: "!"
readonly property string expand: "\""
readonly property string placemark1: "#"
readonly property string circle: "$"
readonly property string handPointer: "9"
readonly property string plusSquareO: "%"
readonly property string sliders: "&"
readonly property string square: "'"
readonly property string alignCenter: "8"
readonly property string alignJustify: ")"
readonly property string alignLeft: "*"
readonly property string alignRight: "^"
readonly property string bars: "7"
readonly property string circleSlash: ","
readonly property string sync: "()"
readonly property string key: "-"
readonly property string link: "."
readonly property string location: "/"
readonly property string caratR: "3"
readonly property string caratL: "4"
readonly property string caratDn: "5"
readonly property string caratUp: "6"
readonly property string folderLg: ">"
readonly property string folderSm: "?"
readonly property string levelUp: "1"
readonly property string info: "["
readonly property string question: "]"
readonly property string alert: "+"
readonly property string home: "_"
readonly property string error: "="
readonly property string settings: "@"
readonly property string trash: "{"
readonly property string objectGroup: "&#xe000;"
readonly property string cm: "}"
readonly property string msvg79: "~"
readonly property string deg: "\\"
readonly property string px: "|"
}
}

View file

@ -141,6 +141,7 @@
#include "ModelPackager.h"
#include "PluginContainerProxy.h"
#include "scripting/AccountScriptingInterface.h"
#include "scripting/AssetMappingsScriptingInterface.h"
#include "scripting/AudioDeviceScriptingInterface.h"
#include "scripting/ClipboardScriptingInterface.h"
#include "scripting/DesktopScriptingInterface.h"
@ -158,7 +159,6 @@
#include "Stars.h"
#include "ui/AddressBarDialog.h"
#include "ui/AvatarInputs.h"
#include "ui/AssetUploadDialogFactory.h"
#include "ui/DialogsManager.h"
#include "ui/LoginDialog.h"
#include "ui/overlays/Cube3DOverlay.h"
@ -1295,6 +1295,7 @@ void Application::initializeUi() {
rootContext->setContextProperty("Quat", new Quat());
rootContext->setContextProperty("Vec3", new Vec3());
rootContext->setContextProperty("Uuid", new ScriptUUID());
rootContext->setContextProperty("Assets", new AssetMappingsScriptingInterface());
rootContext->setContextProperty("AvatarList", DependencyManager::get<AvatarManager>().data());
@ -4018,9 +4019,6 @@ void Application::nodeAdded(SharedNodePointer node) {
if (node->getType() == NodeType::AvatarMixer) {
// new avatar mixer, send off our identity packet right away
getMyAvatar()->sendIdentityPacket();
} else if (node->getType() == NodeType::AssetServer) {
// the addition of an asset-server always re-enables the upload to asset server menu option
Menu::getInstance()->getActionForOption(MenuOption::UploadAsset)->setEnabled(true);
}
}
@ -4070,10 +4068,6 @@ void Application::nodeKilled(SharedNodePointer node) {
} else if (node->getType() == NodeType::AvatarMixer) {
// our avatar mixer has gone away - clear the hash of avatars
DependencyManager::get<AvatarManager>()->clearOtherAvatars();
} else if (node->getType() == NodeType::AssetServer
&& !DependencyManager::get<NodeList>()->soloNodeOfType(NodeType::AssetServer)) {
// this was our last asset server - disable the menu option to upload an asset
Menu::getInstance()->getActionForOption(MenuOption::UploadAsset)->setEnabled(false);
}
}
void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) {
@ -4299,7 +4293,10 @@ bool Application::acceptURL(const QString& urlString, bool defaultUpload) {
}
}
return defaultUpload && askToUploadAsset(urlString);
if (defaultUpload) {
toggleAssetServerWidget(urlString);
}
return defaultUpload;
}
void Application::setSessionUUID(const QUuid& sessionUUID) {
@ -4327,8 +4324,8 @@ bool Application::askToSetAvatarUrl(const QString& url) {
case FSTReader::HEAD_AND_BODY_MODEL:
ok = QMessageBox::Ok == OffscreenUi::question("Set Avatar",
"Would you like to use '" + modelName + "' for your avatar?",
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok);
"Would you like to use '" + modelName + "' for your avatar?",
QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok);
break;
default:
@ -4361,79 +4358,6 @@ bool Application::askToLoadScript(const QString& scriptFilenameOrURL) {
return true;
}
bool Application::askToUploadAsset(const QString& filename) {
if (!DependencyManager::get<NodeList>()->getThisNodeCanRez()) {
OffscreenUi::warning(_window, "Failed Upload",
QString("You don't have upload rights on that domain.\n\n"));
return false;
}
QUrl url { filename };
if (auto upload = DependencyManager::get<AssetClient>()->createUpload(url.toLocalFile())) {
QMessageBox messageBox;
messageBox.setWindowTitle("Asset upload");
messageBox.setText("You are about to upload the following file to the asset server:\n" +
url.toDisplayString());
messageBox.setInformativeText("Do you want to continue?");
messageBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
messageBox.setDefaultButton(QMessageBox::Ok);
// Option to drop model in world for models
if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive) || filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) {
auto checkBox = new QCheckBox(&messageBox);
checkBox->setText("Add to scene");
messageBox.setCheckBox(checkBox);
}
if (messageBox.exec() != QMessageBox::Ok) {
upload->deleteLater();
return false;
}
// connect to the finished signal so we know when the AssetUpload is done
if (messageBox.checkBox() && (messageBox.checkBox()->checkState() == Qt::Checked)) {
// Custom behavior for models
QObject::connect(upload, &AssetUpload::finished, this, &Application::modelUploadFinished);
} else {
QObject::connect(upload, &AssetUpload::finished,
&AssetUploadDialogFactory::getInstance(),
&AssetUploadDialogFactory::handleUploadFinished);
}
// start the upload now
upload->start();
return true;
}
// display a message box with the error
OffscreenUi::warning(_window, "Failed Upload", QString("Failed to upload %1.\n\n").arg(filename));
return false;
}
void Application::modelUploadFinished(AssetUpload* upload, const QString& hash) {
auto filename = QFileInfo(upload->getFilename()).fileName();
if ((upload->getError() == AssetUpload::NoError) &&
(FBX_EXTENSION.endsWith(upload->getExtension(), Qt::CaseInsensitive) ||
OBJ_EXTENSION.endsWith(upload->getExtension(), Qt::CaseInsensitive))) {
auto entities = DependencyManager::get<EntityScriptingInterface>();
EntityItemProperties properties;
properties.setType(EntityTypes::Model);
properties.setModelURL(QString("%1:%2.%3").arg(URL_SCHEME_ATP).arg(hash).arg(upload->getExtension()));
properties.setPosition(_myCamera.getPosition() + _myCamera.getOrientation() * Vectors::FRONT * 2.0f);
properties.setName(QUrl(upload->getFilename()).fileName());
entities->addEntity(properties);
upload->deleteLater();
} else {
AssetUploadDialogFactory::getInstance().handleUploadFinished(upload, hash);
}
}
bool Application::askToWearAvatarAttachmentUrl(const QString& url) {
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
@ -4533,6 +4457,22 @@ void Application::toggleRunningScriptsWidget() {
//}
}
void Application::toggleAssetServerWidget(QString filePath) {
if (!DependencyManager::get<NodeList>()->getThisNodeCanRez()) {
return;
}
static const QUrl url { "AssetServer.qml" };
auto startUpload = [=](QQmlContext* context, QObject* newObject){
if (!filePath.isEmpty()) {
emit uploadRequest(filePath);
}
};
DependencyManager::get<OffscreenUi>()->show(url, "AssetServer", startUpload);
startUpload(nullptr, nullptr);
}
void Application::packageModel() {
ModelPackager::package();
}

View file

@ -231,6 +231,8 @@ signals:
void beforeAboutToQuit();
void activeDisplayPluginChanged();
void uploadRequest(QString path);
public slots:
QVector<EntityItemID> pasteEntities(float x, float y, float z);
bool exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs);
@ -242,6 +244,7 @@ public slots:
Q_INVOKABLE void loadScriptURLDialog();
void toggleLogDialog();
void toggleRunningScriptsWidget();
void toggleAssetServerWidget(QString filePath = "");
void handleLocalServerConnection();
void readArgumentsFromLocalSocket();
@ -303,8 +306,6 @@ private slots:
bool acceptSnapshot(const QString& urlString);
bool askToSetAvatarUrl(const QString& url);
bool askToLoadScript(const QString& scriptFilenameOrURL);
bool askToUploadAsset(const QString& asset);
void modelUploadFinished(AssetUpload* upload, const QString& hash);
bool askToWearAvatarAttachmentUrl(const QString& url);
void displayAvatarAttachmentWarning(const QString& message) const;

View file

@ -35,7 +35,6 @@
#include "MainWindow.h"
#include "render/DrawStatus.h"
#include "scripting/MenuScriptingInterface.h"
#include "ui/AssetUploadDialogFactory.h"
#include "ui/DialogsManager.h"
#include "ui/StandAloneJSConsole.h"
#include "InterfaceLogging.h"
@ -92,7 +91,7 @@ Menu::Menu() {
redoAction->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Z);
addActionToQMenuAndActionHash(editMenu, redoAction);
// Edit > Running Sccripts
// Edit > Running Scripts
addActionToQMenuAndActionHash(editMenu, MenuOption::RunningScripts, Qt::CTRL | Qt::Key_J,
qApp, SLOT(toggleRunningScriptsWidget()));
@ -128,10 +127,20 @@ Menu::Menu() {
SLOT(toggleConsole()),
QAction::NoRole, UNSPECIFIED_POSITION, "Advanced");
editMenu->addSeparator();
// Edit > My Asset Server
auto assetServerAction = addActionToQMenuAndActionHash(editMenu, MenuOption::AssetServer,
Qt::CTRL | Qt::SHIFT | Qt::Key_A,
qApp, SLOT(toggleAssetServerWidget()));
auto nodeList = DependencyManager::get<NodeList>();
QObject::connect(nodeList.data(), &NodeList::canRezChanged, assetServerAction, &QAction::setEnabled);
assetServerAction->setEnabled(nodeList->getThisNodeCanRez());
// Edit > Reload All Content [advanced]
addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()),
QAction::NoRole, UNSPECIFIED_POSITION, "Advanced");
// Edit > Package Model... [advanced]
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
@ -354,17 +363,6 @@ Menu::Menu() {
// Developer > Assets >>>
MenuWrapper* assetDeveloperMenu = developerMenu->addMenu("Assets");
auto& assetDialogFactory = AssetUploadDialogFactory::getInstance();
assetDialogFactory.setDialogParent(this);
QAction* assetUpload = addActionToQMenuAndActionHash(assetDeveloperMenu,
MenuOption::UploadAsset,
0,
&assetDialogFactory,
SLOT(showDialog()));
// disable the asset upload action by default - it gets enabled only if asset server becomes present
assetUpload->setEnabled(false);
auto& atpMigrator = ATPAssetMigrator::getInstance();
atpMigrator.setDialogParent(this);

View file

@ -34,6 +34,7 @@ namespace MenuOption {
const QString AnimDebugDrawDefaultPose = "Debug Draw Default Pose";
const QString AnimDebugDrawPosition= "Debug Draw Position";
const QString AssetMigration = "ATP Asset Migration";
const QString AssetServer = "My Asset Server";
const QString Attachments = "Attachments...";
const QString AudioNetworkStats = "Audio Network Stats";
const QString AudioNoiseReduction = "Audio Noise Reduction";
@ -168,7 +169,6 @@ namespace MenuOption {
const QString ToolWindow = "Tool Window";
const QString TransmitterDrive = "Transmitter Drive";
const QString TurnWithHead = "Turn using Head";
const QString UploadAsset = "Upload File to Asset Server";
const QString UseAudioForMouth = "Use Audio for Mouth";
const QString UseCamera = "Use Camera";
const QString UseAnimPreAndPostRotations = "Use Anim Pre and Post Rotations";

View file

@ -24,9 +24,9 @@
#include <AssetClient.h>
#include <AssetUpload.h>
#include <ResourceManager.h>
#include <MappingRequest.h>
#include "OffscreenUi.h"
#include "../ui/AssetUploadDialogFactory.h"
Q_DECLARE_LOGGING_CATEGORY(asset_migrator);
Q_LOGGING_CATEGORY(asset_migrator, "hf.asset_migrator");
@ -38,18 +38,18 @@ ATPAssetMigrator& ATPAssetMigrator::getInstance() {
static const QString ENTITIES_OBJECT_KEY = "Entities";
static const QString MODEL_URL_KEY = "modelURL";
static const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL";
static const QString MESSAGE_BOX_TITLE = "ATP Asset Migration";
void ATPAssetMigrator::loadEntityServerFile() {
auto filename = QFileDialog::getOpenFileName(_dialogParent, "Select an entity-server content file to migrate",
QString(), QString("Entity-Server Content (*.gz)"));
auto filename = OffscreenUi::getOpenFileName(_dialogParent, tr("Select an entity-server content file to migrate"), QString(), tr("Entity-Server Content (*.gz)"));
if (!filename.isEmpty()) {
qCDebug(asset_migrator) << "Selected filename for ATP asset migration: " << filename;
static const QString MIGRATION_CONFIRMATION_TEXT {
"The ATP Asset Migration process will scan the selected entity-server file, upload discovered resources to the"\
" current asset-server and then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\
"The ATP Asset Migration process will scan the selected entity-server file,\nupload discovered resources to the"\
" current asset-server\nand then save a new entity-server file with the ATP URLs.\n\nAre you ready to"\
" continue?\n\nMake sure you are connected to the right domain."
};
@ -77,57 +77,75 @@ void ATPAssetMigrator::loadEntityServerFile() {
for (auto jsonValue : _entitiesArray) {
QJsonObject entityObject = jsonValue.toObject();
QString modelURLString = entityObject.value(MODEL_URL_KEY).toString();
if (!modelURLString.isEmpty()) {
QUrl modelURL = QUrl(modelURLString);
if (!_ignoredUrls.contains(modelURL)
&& (modelURL.scheme() == URL_SCHEME_HTTP || modelURL.scheme() == URL_SCHEME_HTTPS
|| modelURL.scheme() == URL_SCHEME_FILE || modelURL.scheme() == URL_SCHEME_FTP)) {
if (_pendingReplacements.contains(modelURL)) {
// we already have a request out for this asset, just store the QJsonValueRef
// so we can do the hash replacement when the request comes back
_pendingReplacements.insert(modelURL, jsonValue);
} else if (_uploadedAssets.contains(modelURL)) {
// we already have a hash for this asset
// so just do the replacement immediately
entityObject[MODEL_URL_KEY] = _uploadedAssets.value(modelURL).toString();
jsonValue = entityObject;
} else if (wantsToMigrateResource(modelURL)) {
auto request = ResourceManager::createResourceRequest(this, modelURL);
if (request) {
qCDebug(asset_migrator) << "Requesting" << modelURL << "for ATP asset migration";
// add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL
// to an ATP one once ready
_pendingReplacements.insert(modelURL, jsonValue);
connect(request, &ResourceRequest::finished, this, [=]() {
if (request->getResult() == ResourceRequest::Success) {
migrateResource(request);
QString compoundURLString = entityObject.value(COMPOUND_SHAPE_URL_KEY).toString();
for (int i = 0; i < 2; ++i) {
bool isModelURL = (i == 0);
quint8 replacementType = i;
auto migrationURLString = (isModelURL) ? modelURLString : compoundURLString;
if (!migrationURLString.isEmpty()) {
QUrl migrationURL = QUrl(migrationURLString);
if (!_ignoredUrls.contains(migrationURL)
&& (migrationURL.scheme() == URL_SCHEME_HTTP || migrationURL.scheme() == URL_SCHEME_HTTPS
|| migrationURL.scheme() == URL_SCHEME_FILE || migrationURL.scheme() == URL_SCHEME_FTP)) {
if (_pendingReplacements.contains(migrationURL)) {
// we already have a request out for this asset, just store the QJsonValueRef
// so we can do the hash replacement when the request comes back
_pendingReplacements.insert(migrationURL, { jsonValue, replacementType });
} else if (_uploadedAssets.contains(migrationURL)) {
// we already have a hash for this asset
// so just do the replacement immediately
if (isModelURL) {
entityObject[MODEL_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
} else {
OffscreenUi::warning(_dialogParent, "Error",
QString("Could not retrieve asset at %1").arg(modelURL.toString()));
entityObject[COMPOUND_SHAPE_URL_KEY] = _uploadedAssets.value(migrationURL).toString();
}
request->deleteLater();
});
request->send();
} else {
OffscreenUi::warning(_dialogParent, "Error",
QString("Could not create request for asset at %1").arg(modelURL.toString()));
jsonValue = entityObject;
} else if (wantsToMigrateResource(migrationURL)) {
auto request = ResourceManager::createResourceRequest(this, migrationURL);
if (request) {
qCDebug(asset_migrator) << "Requesting" << migrationURL << "for ATP asset migration";
// add this combination of QUrl and QJsonValueRef to our multi hash so we can change the URL
// to an ATP one once ready
_pendingReplacements.insert(migrationURL, { jsonValue, (isModelURL ? 0 : 1)});
connect(request, &ResourceRequest::finished, this, [=]() {
if (request->getResult() == ResourceRequest::Success) {
migrateResource(request);
} else {
++_errorCount;
_pendingReplacements.remove(migrationURL);
qWarning() << "Could not retrieve asset at" << migrationURL.toString();
checkIfFinished();
}
request->deleteLater();
});
request->send();
} else {
++_errorCount;
qWarning() << "Count not create request for asset at" << migrationURL.toString();
}
} else {
_ignoredUrls.insert(migrationURL);
}
}
} else {
_ignoredUrls.insert(modelURL);
}
}
}
}
_doneReading = true;
checkIfFinished();
} else {
OffscreenUi::warning(_dialogParent, "Error",
@ -140,76 +158,109 @@ void ATPAssetMigrator::migrateResource(ResourceRequest* request) {
// use an asset client to upload the asset
auto assetClient = DependencyManager::get<AssetClient>();
QFileInfo assetInfo { request->getUrl().fileName() };
auto upload = assetClient->createUpload(request->getData(), assetInfo.completeSuffix());
if (upload) {
// add this URL to our hash of AssetUpload to original URL
_originalURLs.insert(upload, request->getUrl());
qCDebug(asset_migrator) << "Starting upload of asset from" << request->getUrl();
// connect to the finished signal so we know when the AssetUpload is done
QObject::connect(upload, &AssetUpload::finished, this, &ATPAssetMigrator::assetUploadFinished);
// start the upload now
upload->start();
} else {
// show a QMessageBox to say that there is no local asset server
QString messageBoxText = QString("Could not upload \n\n%1\n\nbecause you are currently not connected" \
" to a local asset-server.").arg(assetInfo.fileName());
QMessageBox::information(_dialogParent, "Failed to Upload", messageBoxText);
}
auto upload = assetClient->createUpload(request->getData());
// add this URL to our hash of AssetUpload to original URL
_originalURLs.insert(upload, request->getUrl());
qCDebug(asset_migrator) << "Starting upload of asset from" << request->getUrl();
// connect to the finished signal so we know when the AssetUpload is done
QObject::connect(upload, &AssetUpload::finished, this, &ATPAssetMigrator::assetUploadFinished);
// start the upload now
upload->start();
}
void ATPAssetMigrator::assetUploadFinished(AssetUpload *upload, const QString& hash) {
// remove this migrationURL from the key for the AssetUpload pointer
auto migrationURL = _originalURLs.take(upload);
if (upload->getError() == AssetUpload::NoError) {
const auto& modelURL = _originalURLs[upload];
// successfully uploaded asset - make any required replacements found in the pending replacements
auto values = _pendingReplacements.values(modelURL);
QString atpURL = getATPUrl(hash, upload->getExtension()).toString();
for (auto value : values) {
// replace the modelURL in this QJsonValueRef with the hash
QJsonObject valueObject = value.toObject();
valueObject[MODEL_URL_KEY] = atpURL;
value = valueObject;
}
// add this URL to our list of uploaded assets
_uploadedAssets.insert(modelURL, atpURL);
// pull the replaced models from _pendingReplacements
_pendingReplacements.remove(modelURL);
// are we out of pending replacements? if so it is time to save the entity-server file
if (_doneReading && _pendingReplacements.empty()) {
saveEntityServerFile();
// reset after the attempted save, success or fail
reset();
}
// use the path of the migrationURL to add a mapping in the Asset Server
auto assetClient = DependencyManager::get<AssetClient>();
auto setMappingRequest = assetClient->createSetMappingRequest(migrationURL.path(), hash);
connect(setMappingRequest, &SetMappingRequest::finished, this, &ATPAssetMigrator::setMappingFinished);
// add this migrationURL with the key for the SetMappingRequest pointer
_originalURLs[setMappingRequest] = migrationURL;
setMappingRequest->start();
} else {
AssetUploadDialogFactory::showErrorDialog(upload, _dialogParent);
// this is a fail for this modelURL, remove it from pending replacements
_pendingReplacements.remove(migrationURL);
++_errorCount;
qWarning() << "Failed to upload" << migrationURL << "- error was" << upload->getErrorString();
}
checkIfFinished();
upload->deleteLater();
}
void ATPAssetMigrator::setMappingFinished(SetMappingRequest* request) {
// take the migrationURL for this SetMappingRequest
auto migrationURL = _originalURLs.take(request);
if (request->getError() == MappingRequest::NoError) {
// successfully uploaded asset - make any required replacements found in the pending replacements
auto values = _pendingReplacements.values(migrationURL);
QString atpURL = QString("atp:%1").arg(request->getPath());
for (auto value : values) {
// replace the modelURL in this QJsonValueRef with the hash
QJsonObject valueObject = value.first.toObject();
if (value.second == 0) {
valueObject[MODEL_URL_KEY] = atpURL;
} else {
valueObject[COMPOUND_SHAPE_URL_KEY] = atpURL;
}
value.first = valueObject;
}
// add this URL to our list of uploaded assets
_uploadedAssets.insert(migrationURL, atpURL);
// pull the replaced urls from _pendingReplacements
_pendingReplacements.remove(migrationURL);
} else {
// this is a fail for this migrationURL, remove it from pending replacements
_pendingReplacements.remove(migrationURL);
++_errorCount;
qWarning() << "Error setting mapping for" << migrationURL << "- error was " << request->getErrorString();
}
checkIfFinished();
request->deleteLater();
}
void ATPAssetMigrator::checkIfFinished() {
// are we out of pending replacements? if so it is time to save the entity-server file
if (_doneReading && _pendingReplacements.empty()) {
saveEntityServerFile();
// reset after the attempted save, success or fail
reset();
}
}
bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) {
static bool hasAskedForCompleteMigration { false };
static bool wantsCompleteMigration { false };
if (!hasAskedForCompleteMigration) {
// this is the first resource migration - ask the user if they just want to migrate everything
static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n\n"\
"Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n\n"\
static const QString COMPLETE_MIGRATION_TEXT { "Do you want to migrate all assets found in this entity-server file?\n"\
"Select \"Yes\" to upload all discovered assets to the current asset-server immediately.\n"\
"Select \"No\" to be prompted for each discovered asset."
};
@ -236,7 +287,7 @@ bool ATPAssetMigrator::wantsToMigrateResource(const QUrl& url) {
void ATPAssetMigrator::saveEntityServerFile() {
// show a dialog to ask the user where they want to save the file
QString saveName = QFileDialog::getSaveFileName(_dialogParent, "Save Migrated Entities File");
QString saveName = OffscreenUi::getSaveFileName(_dialogParent, "Save Migrated Entities File");
QFile saveFile { saveName };
@ -251,9 +302,16 @@ void ATPAssetMigrator::saveEntityServerFile() {
saveFile.write(jsonDataForFile);
saveFile.close();
QMessageBox::information(_dialogParent, "Success",
QString("Your new entities file has been saved at %1").arg(saveName));
QString infoMessage = QString("Your new entities file has been saved at\n%1.").arg(saveName);
if (_errorCount > 0) {
infoMessage += QString("\nThere were %1 models that could not be migrated.\n").arg(_errorCount);
infoMessage += "Check the warnings in your log for details.\n";
infoMessage += "You can re-attempt migration on those models\nby restarting this process with the newly saved file.";
}
OffscreenUi::information(_dialogParent, "Success", infoMessage);
} else {
OffscreenUi::warning(_dialogParent, "Error", "Could not gzip JSON data for new entities file.");
}
@ -271,4 +329,5 @@ void ATPAssetMigrator::reset() {
_uploadedAssets.clear();
_originalURLs.clear();
_ignoredUrls.clear();
_errorCount = 0;
}

View file

@ -21,6 +21,7 @@
class AssetUpload;
class ResourceRequest;
class SetMappingRequest;
class ATPAssetMigrator : public QObject {
Q_OBJECT
@ -32,8 +33,11 @@ public slots:
void loadEntityServerFile();
private slots:
void assetUploadFinished(AssetUpload* upload, const QString& hash);
void setMappingFinished(SetMappingRequest* request);
private:
void migrateResource(ResourceRequest* request);
void checkIfFinished();
void saveEntityServerFile();
void reset();
@ -44,11 +48,14 @@ private:
QJsonArray _entitiesArray;
bool _doneReading { false };
QMultiHash<QUrl, QJsonValueRef> _pendingReplacements;
using JSONTypePair = std::pair<QJsonValueRef, quint8>;
QMultiHash<QUrl, JSONTypePair> _pendingReplacements;
QHash<QUrl, QUrl> _uploadedAssets;
QHash<AssetUpload*, QUrl> _originalURLs;
QHash<QObject*, QUrl> _originalURLs;
QSet<QUrl> _ignoredUrls;
int _errorCount { 0 };
};

View file

@ -39,7 +39,7 @@ int main(int argc, const char* argv[]) {
static const DWORD BUG_SPLAT_FLAGS = MDSF_PREVENTHIJACKING | MDSF_USEGUARDMEMORY;
static const char* BUG_SPLAT_DATABASE = "interface_alpha";
static const char* BUG_SPLAT_APPLICATION_NAME = "Interface";
MiniDmpSender mpSender { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, BuildInfo::VERSION.toLatin1().constData(),
MiniDmpSender mpSender { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, qPrintable(BuildInfo::VERSION),
nullptr, BUG_SPLAT_FLAGS };
#endif
@ -143,6 +143,14 @@ int main(int argc, const char* argv[]) {
QObject::connect(&server, &QLocalServer::newConnection, &app, &Application::handleLocalServerConnection);
#ifdef HAS_BUGSPLAT
AccountManager& accountManager = AccountManager::getInstance();
mpSender.setDefaultUserName(qPrintable(accountManager.getAccountInfo().getUsername()));
QObject::connect(&accountManager, &AccountManager::usernameChanged, &app, [&mpSender](const QString& newUsername) {
mpSender.setDefaultUserName(qPrintable(newUsername));
});
#endif
QTranslator translator;
translator.load("i18n/interface_en");
app.installTranslator(&translator);

View file

@ -0,0 +1,284 @@
//
// AssetMappingsScriptingInterface.cpp
// libraries/script-engine/src
//
// Created by Ryan Huffman on 2016-03-09.
// Copyright 2016 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 "AssetMappingsScriptingInterface.h"
#include <QtScript/QScriptEngine>
#include <QtCore/QFile>
#include <AssetRequest.h>
#include <AssetUpload.h>
#include <MappingRequest.h>
#include <NetworkLogging.h>
#include <OffscreenUi.h>
AssetMappingsScriptingInterface::AssetMappingsScriptingInterface() {
_proxyModel.setSourceModel(&_assetMappingModel);
_proxyModel.setSortRole(Qt::DisplayRole);
_proxyModel.setDynamicSortFilter(true);
_proxyModel.sort(0);
}
void AssetMappingsScriptingInterface::setMapping(QString path, QString hash, QJSValue callback) {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createSetMappingRequest(path, hash);
connect(request, &SetMappingRequest::finished, this, [this, callback](SetMappingRequest* request) mutable {
if (callback.isCallable()) {
QJSValueList args { request->getErrorString(), request->getPath() };
callback.call(args);
}
request->deleteLater();
});
request->start();
}
void AssetMappingsScriptingInterface::getMapping(QString path, QJSValue callback) {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetMappingRequest(path);
connect(request, &GetMappingRequest::finished, this, [this, callback](GetMappingRequest* request) mutable {
if (callback.isCallable()) {
QJSValueList args { request->getErrorString() };
callback.call(args);
}
request->deleteLater();
});
request->start();
}
void AssetMappingsScriptingInterface::uploadFile(QString path, QString mapping, QJSValue startedCallback, QJSValue completedCallback, bool dropEvent) {
static const QString helpText =
"Upload your asset to a specific folder by entering the full path. Specifying\n"
"a new folder name will automatically create that folder for you.";
static const QString dropHelpText =
"This file will be added to your Asset Server.\n"
"Use the field below to place your file in a specific folder or to rename it.\n"
"Specifying a new folder name will automatically create that folder for you.";
auto offscreenUi = DependencyManager::get<OffscreenUi>();
auto result = offscreenUi->inputDialog(OffscreenUi::ICON_INFORMATION, "Specify Asset Path",
dropEvent ? dropHelpText : helpText, mapping);
if (!result.isValid()) {
completedCallback.call({ -1 });
return;
}
mapping = result.toString();
mapping = mapping.trimmed();
if (mapping[0] != '/') {
mapping = "/" + mapping;
}
// Check for override
if (isKnownMapping(mapping)) {
auto message = mapping + "\n" + "This file already exists. Do you want to overwrite it?";
auto button = offscreenUi->messageBox(OffscreenUi::ICON_QUESTION, "Overwrite File", message,
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (button == QMessageBox::No) {
completedCallback.call({ -1 });
return;
}
}
startedCallback.call();
auto upload = DependencyManager::get<AssetClient>()->createUpload(path);
QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable {
if (upload->getError() != AssetUpload::NoError) {
if (completedCallback.isCallable()) {
QJSValueList args { upload->getErrorString() };
completedCallback.call(args);
}
} else {
setMapping(mapping, hash, completedCallback);
}
upload->deleteLater();
});
upload->start();
}
void AssetMappingsScriptingInterface::deleteMappings(QStringList paths, QJSValue callback) {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createDeleteMappingsRequest(paths);
connect(request, &DeleteMappingsRequest::finished, this, [this, callback](DeleteMappingsRequest* request) mutable {
if (callback.isCallable()) {
QJSValueList args { request->getErrorString() };
callback.call(args);
}
request->deleteLater();
});
request->start();
}
void AssetMappingsScriptingInterface::getAllMappings(QJSValue callback) {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetAllMappingsRequest();
connect(request, &GetAllMappingsRequest::finished, this, [this, callback](GetAllMappingsRequest* request) mutable {
auto mappings = request->getMappings();
auto map = callback.engine()->newObject();
for (auto& kv : mappings ) {
map.setProperty(kv.first, kv.second);
}
if (callback.isCallable()) {
QJSValueList args { request->getErrorString(), map };
callback.call(args);
}
request->deleteLater();
});
request->start();
}
void AssetMappingsScriptingInterface::renameMapping(QString oldPath, QString newPath, QJSValue callback) {
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createRenameMappingRequest(oldPath, newPath);
connect(request, &RenameMappingRequest::finished, this, [this, callback](RenameMappingRequest* request) mutable {
if (callback.isCallable()) {
QJSValueList args { request->getErrorString() };
callback.call(args);
}
request->deleteLater();
});
request->start();
}
bool AssetMappingModel::isKnownFolder(QString path) const {
if (!path.endsWith("/")) {
return false;
}
auto existingPaths = _pathToItemMap.keys();
for (auto& entry : existingPaths) {
if (entry.startsWith(path)) {
return true;
}
}
return false;
}
static int assetMappingModelMetatypeId = qRegisterMetaType<AssetMappingModel*>("AssetMappingModel*");
void AssetMappingModel::refresh() {
qDebug() << "Refreshing asset mapping model";
auto assetClient = DependencyManager::get<AssetClient>();
auto request = assetClient->createGetAllMappingsRequest();
connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) mutable {
if (request->getError() == MappingRequest::NoError) {
auto mappings = request->getMappings();
auto existingPaths = _pathToItemMap.keys();
for (auto& mapping : mappings) {
auto& path = mapping.first;
auto parts = path.split("/");
auto length = parts.length();
existingPaths.removeOne(mapping.first);
QString fullPath = "/";
QStandardItem* lastItem = nullptr;
// start index at 1 to avoid empty string from leading slash
for (int i = 1; i < length; ++i) {
fullPath += (i == 1 ? "" : "/") + parts[i];
auto it = _pathToItemMap.find(fullPath);
if (it == _pathToItemMap.end()) {
auto item = new QStandardItem(parts[i]);
bool isFolder = i < length - 1;
item->setData(isFolder ? fullPath + "/" : fullPath, Qt::UserRole);
item->setData(isFolder, Qt::UserRole + 1);
item->setData(parts[i], Qt::UserRole + 2);
item->setData("atp:" + fullPath, Qt::UserRole + 3);
item->setData(fullPath, Qt::UserRole + 4);
if (lastItem) {
lastItem->setChild(lastItem->rowCount(), 0, item);
} else {
appendRow(item);
}
lastItem = item;
_pathToItemMap[fullPath] = lastItem;
} else {
lastItem = it.value();
}
}
Q_ASSERT(fullPath == path);
}
// Remove folders from list
auto it = existingPaths.begin();
while (it != existingPaths.end()) {
Q_ASSERT(_pathToItemMap.contains(*it));
auto item = _pathToItemMap[*it];
if (item && item->data(Qt::UserRole + 1).toBool()) {
it = existingPaths.erase(it);
} else {
++it;
}
}
for (auto& path : existingPaths) {
Q_ASSERT(_pathToItemMap.contains(path));
auto item = _pathToItemMap[path];
while (item) {
// During each iteration, delete item
QStandardItem* nextItem = nullptr;
auto fullPath = item->data(Qt::UserRole + 4).toString();
auto parent = item->parent();
if (parent) {
parent->removeRow(item->row());
if (parent->rowCount() > 0) {
// The parent still contains children, set the nextItem to null so we stop processing
nextItem = nullptr;
} else {
nextItem = parent;
}
} else {
auto removed = removeRow(item->row());
Q_ASSERT(removed);
}
Q_ASSERT(_pathToItemMap.contains(fullPath));
_pathToItemMap.remove(fullPath);
item = nextItem;
}
}
} else {
emit errorGettingMappings(request->getErrorString());
}
});
request->start();
}

View file

@ -0,0 +1,70 @@
//
// AssetMappingsScriptingInterface.h
// libraries/script-engine/src
//
// Created by Ryan Huffman on 2016-03-09.
// Copyright 2016 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
//
#pragma once
#ifndef hifi_AssetMappingsScriptingInterface_h
#define hifi_AssetMappingsScriptingInterface_h
#include <QtCore/QObject>
#include <QtScript/QScriptValue>
#include <AssetClient.h>
#include <QSortFilterProxyModel>
class AssetMappingModel : public QStandardItemModel {
Q_OBJECT
public:
Q_INVOKABLE void refresh();
bool isKnownMapping(QString path) const { return _pathToItemMap.contains(path); }
bool isKnownFolder(QString path) const;
signals:
void errorGettingMappings(QString errorString);
private:
QHash<QString, QStandardItem*> _pathToItemMap;
};
Q_DECLARE_METATYPE(AssetMappingModel*);
class AssetMappingsScriptingInterface : public QObject {
Q_OBJECT
Q_PROPERTY(AssetMappingModel* mappingModel READ getAssetMappingModel CONSTANT)
Q_PROPERTY(QAbstractProxyModel* proxyModel READ getProxyModel CONSTANT)
public:
AssetMappingsScriptingInterface();
Q_INVOKABLE AssetMappingModel* getAssetMappingModel() { return &_assetMappingModel; }
Q_INVOKABLE QAbstractProxyModel* getProxyModel() { return &_proxyModel; }
Q_INVOKABLE bool isKnownMapping(QString path) const { return _assetMappingModel.isKnownMapping(path); }
Q_INVOKABLE bool isKnownFolder(QString path) const { return _assetMappingModel.isKnownFolder(path); }
Q_INVOKABLE void setMapping(QString path, QString hash, QJSValue callback = QJSValue());
Q_INVOKABLE void getMapping(QString path, QJSValue callback = QJSValue());
Q_INVOKABLE void uploadFile(QString path, QString mapping, QJSValue startedCallback = QJSValue(), QJSValue completedCallback = QJSValue(), bool dropEvent = false);
Q_INVOKABLE void deleteMappings(QStringList paths, QJSValue callback);
Q_INVOKABLE void deleteMapping(QString path, QJSValue callback) { deleteMappings(QStringList(path), callback = QJSValue()); }
Q_INVOKABLE void getAllMappings(QJSValue callback = QJSValue());
Q_INVOKABLE void renameMapping(QString oldPath, QString newPath, QJSValue callback = QJSValue());
protected:
QSet<AssetRequest*> _pendingRequests;
AssetMappingModel _assetMappingModel;
QSortFilterProxyModel _proxyModel;
};
#endif // hifi_AssetMappingsScriptingInterface_h

View file

@ -9,6 +9,7 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QClipboard>
#include <QtCore/QDir>
#include <QMessageBox>
#include <QScriptValue>
@ -139,3 +140,8 @@ int WindowScriptingInterface::getX() {
int WindowScriptingInterface::getY() {
return qApp->getWindow()->y();
}
void WindowScriptingInterface::copyToClipboard(const QString& text) {
qDebug() << "Copying";
QApplication::clipboard()->setText(text);
}

View file

@ -40,6 +40,7 @@ public slots:
QScriptValue prompt(const QString& message = "", const QString& defaultText = "");
QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = "");
void copyToClipboard(const QString& text);
signals:
void domainChanged(const QString& domainHostname);

View file

@ -1,151 +0,0 @@
//
// AssetUploadDialogFactory.cpp
// interface/src/ui
//
// Created by Stephen Birarda on 2015-08-26.
// Copyright 2015 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 "AssetUploadDialogFactory.h"
#include <QtCore/QDebug>
#include <QtWidgets/QDialogButtonBox>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QLabel>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QVBoxLayout>
#include <AssetClient.h>
#include <AssetUpload.h>
#include <AssetUtils.h>
#include <NodeList.h>
#include <OffscreenUi.h>
#include <ResourceManager.h>
AssetUploadDialogFactory& AssetUploadDialogFactory::getInstance() {
static AssetUploadDialogFactory staticInstance;
return staticInstance;
}
void AssetUploadDialogFactory::showDialog() {
auto nodeList = DependencyManager::get<NodeList>();
if (nodeList->getThisNodeCanRez()) {
auto filename = QFileDialog::getOpenFileName(_dialogParent, "Select a file to upload");
if (!filename.isEmpty()) {
qDebug() << "Selected filename for upload to asset-server: " << filename;
auto assetClient = DependencyManager::get<AssetClient>();
auto upload = assetClient->createUpload(filename);
if (upload) {
// connect to the finished signal so we know when the AssetUpload is done
QObject::connect(upload, &AssetUpload::finished, this, &AssetUploadDialogFactory::handleUploadFinished);
// start the upload now
upload->start();
} else {
// show a QMessageBox to say that there is no local asset server
QString messageBoxText = QString("Could not upload \n\n%1\n\nbecause you are currently not connected" \
" to a local asset-server.").arg(QFileInfo(filename).fileName());
QMessageBox::information(_dialogParent, "Failed to Upload", messageBoxText);
}
}
} else {
// we don't have permission to upload to asset server in this domain - show the permission denied error
showErrorDialog(nullptr, _dialogParent, AssetUpload::PERMISSION_DENIED_ERROR);
}
}
void AssetUploadDialogFactory::handleUploadFinished(AssetUpload* upload, const QString& hash) {
if (upload->getError() == AssetUpload::NoError) {
// show message box for successful upload, with copiable text for ATP hash
QDialog* hashCopyDialog = new QDialog(_dialogParent);
// delete the dialog on close
hashCopyDialog->setAttribute(Qt::WA_DeleteOnClose);
// set the window title
hashCopyDialog->setWindowTitle(tr("Successful Asset Upload"));
// setup a layout for the contents of the dialog
QVBoxLayout* boxLayout = new QVBoxLayout;
// set the label text (this shows above the text box)
QLabel* lineEditLabel = new QLabel;
lineEditLabel->setText(QString("ATP URL for %1").arg(QFileInfo(upload->getFilename()).fileName()));
// setup the line edit to hold the copiable text
QLineEdit* lineEdit = new QLineEdit;
QString atpURL = QString("%1:%2.%3").arg(URL_SCHEME_ATP).arg(hash).arg(upload->getExtension());
// set the ATP URL as the text value so it's copiable
lineEdit->insert(atpURL);
// figure out what size this line edit should be using font metrics
QFontMetrics textMetrics { lineEdit->font() };
// set the fixed width on the line edit
// pad it by 10 to cover the border and some extra space on the right side (for clicking)
static const int LINE_EDIT_RIGHT_PADDING { 10 };
lineEdit->setFixedWidth(textMetrics.width(atpURL) + LINE_EDIT_RIGHT_PADDING );
// left align the ATP URL line edit
lineEdit->home(true);
// add the label and line edit to the dialog
boxLayout->addWidget(lineEditLabel);
boxLayout->addWidget(lineEdit);
// setup an OK button to close the dialog
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
connect(buttonBox, &QDialogButtonBox::accepted, hashCopyDialog, &QDialog::close);
boxLayout->addWidget(buttonBox);
// set the new layout on the dialog
hashCopyDialog->setLayout(boxLayout);
// show the new dialog
hashCopyDialog->show();
} else {
// display a message box with the error
showErrorDialog(upload, _dialogParent);
}
upload->deleteLater();
}
void AssetUploadDialogFactory::showErrorDialog(AssetUpload* upload, QWidget* dialogParent, const QString& overrideMessage) {
QString filename;
if (upload) {
filename = QFileInfo { upload->getFilename() }.fileName();
}
QString errorMessage = overrideMessage;
if (errorMessage.isEmpty() && upload) {
errorMessage = upload->getErrorString();
}
QString dialogMessage;
if (upload) {
dialogMessage += QString("Failed to upload %1.\n\n").arg(filename);
}
dialogMessage += errorMessage;
OffscreenUi::warning(dialogParent, "Failed Upload", dialogMessage);
}

View file

@ -1,44 +0,0 @@
//
// AssetUploadDialogFactory.h
// interface/src/ui
//
// Created by Stephen Birarda on 2015-08-26.
// Copyright 2015 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
//
#pragma once
#ifndef hifi_AssetUploadDialogFactory_h
#define hifi_AssetUploadDialogFactory_h
#include <QtCore/QObject>
class AssetUpload;
class AssetUploadDialogFactory : public QObject {
Q_OBJECT
public:
AssetUploadDialogFactory(const AssetUploadDialogFactory& other) = delete;
AssetUploadDialogFactory& operator=(const AssetUploadDialogFactory& rhs) = delete;
static AssetUploadDialogFactory& getInstance();
static void showErrorDialog(AssetUpload* upload, QWidget* dialogParent, const QString& overrideMessage = QString());
void setDialogParent(QWidget* dialogParent) { _dialogParent = dialogParent; }
public slots:
void showDialog();
void handleUploadFinished(AssetUpload* upload, const QString& hash);
private:
AssetUploadDialogFactory() = default;
QWidget* _dialogParent { nullptr };
};
#endif // hifi_AssetUploadDialogFactory_h

View file

@ -48,6 +48,13 @@ RenderableModelEntityItem::~RenderableModelEntityItem() {
void RenderableModelEntityItem::setModelURL(const QString& url) {
auto& currentURL = getParsedModelURL();
if (_model && (currentURL != url)) {
// The machinery for updateModelBounds will give existing models the opportunity to fix their translation/rotation/scale/registration.
// The first two are straightforward, but the latter two have guards to make sure they don't happen after they've already been set.
// Here we reset those guards. This doesn't cause the entity values to change -- it just allows the model to match once it comes in.
_model->setScaleToFit(false, getDimensions());
_model->setSnapModelToRegistrationPoint(false, getRegistrationPoint());
}
ModelEntityItem::setModelURL(url);
if (currentURL != getParsedModelURL() || !_model) {

View file

@ -125,7 +125,8 @@ void main(void) {
vec4 quadPos = radius * UNIT_QUAD[twoTriID];
vec4 anchorPoint;
<$transformModelToEyePos(cam, obj, inPosition, anchorPoint)$>
vec4 _inPosition = vec4(inPosition, 1.0);
<$transformModelToEyePos(cam, obj, _inPosition, anchorPoint)$>
vec4 eyePos = anchorPoint + quadPos;
<$transformEyeToClipPos(cam, eyePos, gl_Position)$>

View file

@ -182,6 +182,15 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
return id;
}
QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position) {
EntityItemProperties properties;
properties.setType(EntityTypes::Model);
properties.setName(name);
properties.setModelURL(modelUrl);
properties.setPosition(position);
return addEntity(properties);
}
EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) {
EntityPropertyFlags noSpecificProperties;
return getEntityProperties(identity, noSpecificProperties);

View file

@ -84,6 +84,9 @@ public slots:
/// adds a model with the specific properties
Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties);
/// temporary method until addEntity can be used from QJSEngine
Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position);
/// gets the current model properties for a specific model
/// this function will not find return results in script engine contexts which don't have access to models
Q_INVOKABLE EntityItemProperties getEntityProperties(QUuid entityID);

View file

@ -74,9 +74,14 @@ void Context::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, cons
_backend->downloadFramebuffer(srcFramebuffer, region, destImage);
}
const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived() const {
const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived(const Transform& xformView) const {
_projectionInverse = glm::inverse(_projection);
_viewInverse = glm::inverse(_view);
// Get the viewEyeToWorld matrix form the transformView as passed to the gpu::Batch
// this is the "_viewInverse" fed to the shader
// Genetrate the "_view" matrix as well from the xform
xformView.getMatrix(_viewInverse);
_view = glm::inverse(_viewInverse);
Mat4 viewUntranslated = _view;
viewUntranslated[3] = Vec4(0.0f, 0.0f, 0.0f, 1.0f);
@ -84,16 +89,16 @@ const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived() con
return *this;
}
Backend::TransformCamera Backend::TransformCamera::getEyeCamera(int eye, const StereoState& _stereo) const {
Backend::TransformCamera Backend::TransformCamera::getEyeCamera(int eye, const StereoState& _stereo, const Transform& xformView) const {
TransformCamera result = *this;
Transform offsetTransform = xformView;
if (!_stereo._skybox) {
result._view = _stereo._eyeViews[eye] * result._view;
offsetTransform.postTranslate(-Vec3(_stereo._eyeViews[eye][3]));
} else {
glm::mat4 skyboxView = _stereo._eyeViews[eye];
skyboxView[3] = vec4(0, 0, 0, 1);
result._view = skyboxView * result._view;
// FIXME: If "skybox" the ipd is set to 0 for now, let s try to propose a better solution for this in the future
}
result._projection = _stereo._eyeProjections[eye];
result.recomputeDerived();
result.recomputeDerived(offsetTransform);
return result;
}

View file

@ -79,15 +79,15 @@ public:
// UBO class... layout MUST match the layout in TransformCamera.slh
class TransformCamera {
public:
Mat4 _view;
mutable Mat4 _view;
mutable Mat4 _viewInverse;
mutable Mat4 _projectionViewUntranslated;
Mat4 _projection;
mutable Mat4 _projectionInverse;
Vec4 _viewport; // Public value is int but float in the shader to stay in floats for all the transform computations.
const Backend::TransformCamera& recomputeDerived() const;
TransformCamera getEyeCamera(int eye, const StereoState& stereo) const;
const Backend::TransformCamera& recomputeDerived(const Transform& xformView) const;
TransformCamera getEyeCamera(int eye, const StereoState& stereo, const Transform& xformView) const;
};

View file

@ -105,6 +105,7 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo
}
if (_invalidView) {
// This is when the _view matrix gets assigned
_view.getInverseMatrix(_camera._view);
}
@ -113,11 +114,11 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo
if (stereo._enable) {
_cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset));
for (int i = 0; i < 2; ++i) {
_cameras.push_back(_camera.getEyeCamera(i, stereo));
_cameras.push_back(_camera.getEyeCamera(i, stereo, _view));
}
} else {
_cameraOffsets.push_back(TransformStageState::Pair(commandIndex, offset));
_cameras.push_back(_camera.recomputeDerived());
_cameras.push_back(_camera.recomputeDerived(_view));
}
}

View file

@ -76,24 +76,39 @@ TransformObject getTransformObject() {
<$viewport$> = <$cameraTransform$>._viewport;
<@endfunc@>
<@func transformModelToClipPos(cameraTransform, objectTransform, modelPos, clipPos)@>
<!// Equivalent to the following but hoppefully a tad more accurate
//return camera._projection * camera._view * object._model * pos; !>
{ // transformModelToClipPos
vec4 _eyepos = (<$objectTransform$>._model * <$modelPos$>) + vec4(-<$modelPos$>.w * <$cameraTransform$>._viewInverse[3].xyz, 0.0);
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * _eyepos;
<@func transformModelToEyeWorldAlignedPos(cameraTransform, objectTransform, modelPos, eyeWorldAlignedPos)@>
<!// Bring the model pos in the world aligned space centered on the eye axis !>
{ // _transformModelToEyeWorldAlignedPos
highp mat4 _mv = <$objectTransform$>._model;
_mv[3].xyz -= <$cameraTransform$>._viewInverse[3].xyz;
highp vec4 _eyeWApos = (_mv * <$modelPos$>);
<$eyeWorldAlignedPos$> = _eyeWApos;
}
<@endfunc@>
<@func $transformModelToEyeAndClipPos(cameraTransform, objectTransform, modelPos, eyePos, clipPos)@>
<!// Equivalent to the following but hoppefully a tad more accurate
//return camera._projection * camera._view * object._model * pos; !>
<@func transformModelToClipPos(cameraTransform, objectTransform, modelPos, clipPos)@>
{ // transformModelToClipPos
vec4 _worldpos = (<$objectTransform$>._model * <$modelPos$>);
<$eyePos$> = (<$cameraTransform$>._view * _worldpos);
vec4 _eyepos =(<$objectTransform$>._model * <$modelPos$>) + vec4(-<$modelPos$>.w * <$cameraTransform$>._viewInverse[3].xyz, 0.0);
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * _eyepos;
// <$eyePos$> = (<$cameraTransform$>._projectionInverse * <$clipPos$>);
vec4 eyeWAPos;
<$transformModelToEyeWorldAlignedPos($cameraTransform$, $objectTransform$, $modelPos$, eyeWAPos)$>
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * eyeWAPos;
}
<@endfunc@>
<@func transformModelToEyeAndClipPos(cameraTransform, objectTransform, modelPos, eyePos, clipPos)@>
{ // transformModelToEyeAndClipPos
vec4 eyeWAPos;
<$transformModelToEyeWorldAlignedPos($cameraTransform$, $objectTransform$, $modelPos$, eyeWAPos)$>
<$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * eyeWAPos;
<$eyePos$> = vec4((<$cameraTransform$>._view * vec4(eyeWAPos.xyz, 0.0)).xyz, 1.0);
}
<@endfunc@>
<@func transformModelToEyePos(cameraTransform, objectTransform, modelPos, eyePos)@>
{ // transformModelToEyePos
vec4 eyeWAPos;
<$transformModelToEyeWorldAlignedPos($cameraTransform$, $objectTransform$, $modelPos$, eyeWAPos)$>
<$eyePos$> = vec4((<$cameraTransform$>._view * vec4(eyeWAPos.xyz, 0.0)).xyz, 1.0);
}
<@endfunc@>
@ -103,6 +118,7 @@ TransformObject getTransformObject() {
}
<@endfunc@>
<@func transformModelToEyeDir(cameraTransform, objectTransform, modelDir, eyeDir)@>
{ // transformModelToEyeDir
vec3 mr0 = vec3(<$objectTransform$>._modelInverse[0].x, <$objectTransform$>._modelInverse[1].x, <$objectTransform$>._modelInverse[2].x);
@ -129,15 +145,6 @@ TransformObject getTransformObject() {
}
<@endfunc@>
<@func $transformModelToEyePos(cameraTransform, objectTransform, modelPos, eyePos)@>
<!// Equivalent to the following but hoppefully a tad more accurate
//return camera._view * object._model * pos; !>
{ // transformModelToEyePos
vec4 _worldpos = (<$objectTransform$>._model * vec4(<$modelPos$>.xyz, 1.0));
<$eyePos$> = (<$cameraTransform$>._view * _worldpos);
}
<@endfunc@>
<@func transformEyeToClipPos(cameraTransform, eyePos, clipPos)@>
{ // transformEyeToClipPos
<$clipPos$> = <$cameraTransform$>._projection * vec4(<$eyePos$>.xyz, 1.0);

View file

@ -135,6 +135,8 @@ bool NetworkGeometry::isLoadedWithTextures() const {
}
if (!_isLoadedWithTextures) {
_hasTransparentTextures = true;
for (auto&& material : _materials) {
if ((material->albedoTexture && !material->albedoTexture->isLoaded()) ||
(material->normalTexture && !material->normalTexture->isLoaded()) ||
@ -145,7 +147,16 @@ bool NetworkGeometry::isLoadedWithTextures() const {
(material->lightmapTexture && !material->lightmapTexture->isLoaded())) {
return false;
}
if (material->albedoTexture && material->albedoTexture->getGPUTexture()) {
// Reset the materialKey transparentTexture key only, as it is albedoTexture-dependent
const auto& usage = material->albedoTexture->getGPUTexture()->getUsage();
bool isTransparentTexture = usage.isAlpha() && !usage.isAlphaMask();
material->_material->setTransparentTexture(isTransparentTexture);
// FIXME: Materials with *some* transparent textures seem to give all *other* textures alphas of 0.
_hasTransparentTextures = isTransparentTexture && _hasTransparentTextures;
}
}
_isLoadedWithTextures = true;
}
return true;

View file

@ -75,6 +75,10 @@ public:
// true when the requested geometry and its textures are loaded.
bool isLoadedWithTextures() const;
// true if the albedo texture has a non-masking alpha channel.
// This can only be known after isLoadedWithTextures().
bool hasTransparentTextures() const { return _hasTransparentTextures; }
// WARNING: only valid when isLoaded returns true.
const FBXGeometry& getFBXGeometry() const { return *_geometry; }
const std::vector<std::unique_ptr<NetworkMesh>>& getMeshes() const { return _meshes; }
@ -151,6 +155,7 @@ protected:
// cache for isLoadedWithTextures()
mutable bool _isLoadedWithTextures = false;
mutable bool _hasTransparentTextures = false;
};
/// Reads geometry in a worker thread.

View file

@ -80,6 +80,10 @@ void Material::setMetallic(float metallic) {
_schemaBuffer.edit<Schema>()._metallic = metallic;
}
void Material::setTransparentTexture(bool isTransparent) {
_key.setTransparentTexture(isTransparent);
_schemaBuffer.edit<Schema>()._key = (uint32)_key._flags.to_ulong();
}
void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textureMap) {
if (textureMap) {
@ -92,6 +96,3 @@ void Material::setTextureMap(MapChannel channel, const TextureMapPointer& textur
_textureMaps.erase(channel);
}
}

View file

@ -32,6 +32,7 @@ public:
METALLIC_VAL_BIT,
GLOSSY_VAL_BIT,
TRANSPARENT_VAL_BIT,
TRANSPARENT_TEX_VAL_BIT,
EMISSIVE_MAP_BIT,
ALBEDO_MAP_BIT,
@ -101,6 +102,9 @@ public:
void setAlbedo(bool value) { _flags.set(ALBEDO_VAL_BIT, value); }
bool isAlbedo() const { return _flags[ALBEDO_VAL_BIT]; }
void setTransparentTexture(bool value) { _flags.set(TRANSPARENT_TEX_VAL_BIT, value); }
bool isTransparentTexture() const { return _flags[TRANSPARENT_TEX_VAL_BIT]; }
void setAlbedoMap(bool value) { _flags.set(ALBEDO_MAP_BIT, value); }
bool isAlbedoMap() const { return _flags[ALBEDO_MAP_BIT]; }
@ -164,6 +168,9 @@ public:
Builder& withoutAlbedo() { _value.reset(MaterialKey::ALBEDO_VAL_BIT); _mask.set(MaterialKey::ALBEDO_VAL_BIT); return (*this); }
Builder& withAlbedo() { _value.set(MaterialKey::ALBEDO_VAL_BIT); _mask.set(MaterialKey::ALBEDO_VAL_BIT); return (*this); }
Builder& withoutTransparentTexture() { _value.reset(MaterialKey::TRANSPARENT_TEX_VAL_BIT); _mask.set(MaterialKey::TRANSPARENT_TEX_VAL_BIT); return (*this); }
Builder& withTransparentTexture() { _value.set(MaterialKey::TRANSPARENT_TEX_VAL_BIT); _mask.set(MaterialKey::TRANSPARENT_TEX_VAL_BIT); return (*this); }
Builder& withoutAlbedoMap() { _value.reset(MaterialKey::ALBEDO_MAP_BIT); _mask.set(MaterialKey::ALBEDO_MAP_BIT); return (*this); }
Builder& withAlbedoMap() { _value.set(MaterialKey::ALBEDO_MAP_BIT); _mask.set(MaterialKey::ALBEDO_MAP_BIT); return (*this); }
@ -248,6 +255,8 @@ public:
void setRoughness(float roughness);
float getRoughness() const { return _schemaBuffer.get<Schema>()._roughness; }
void setTransparentTexture(bool isTransparent);
// Schema to access the attribute values of the material
class Schema {
public:
@ -260,8 +269,7 @@ public:
glm::vec3 _fresnel{ 0.03f }; // Fresnel value for a default non metallic
float _metallic{ 0.0f }; // Not Metallic
glm::vec3 _spare0{ 0.0f };
glm::vec3 _spare{ 0.0f };
uint32_t _key{ 0 }; // a copy of the materialKey

View file

@ -15,7 +15,7 @@ struct Material {
vec4 _emissiveOpacity;
vec4 _albedoRoughness;
vec4 _fresnelMetallic;
vec4 _spare;
vec4 _spareKey;
};
uniform materialBuffer {
@ -37,25 +37,23 @@ float getMaterialMetallic(Material m) { return m._fresnelMetallic.a; }
float getMaterialShininess(Material m) { return 1.0 - getMaterialRoughness(m); }
int getMaterialKey(Material m) { return floatBitsToInt(m._spare.w); }
int getMaterialKey(Material m) { return floatBitsToInt(m._spareKey.w); }
const int EMISSIVE_VAL_BIT = 0x00000001;
const int ALBEDO_VAL_BIT = 0x00000002;
const int METALLIC_VAL_BIT = 0x00000004;
const int GLOSSY_VAL_BIT = 0x00000008;
const int TRANSPARENT_VAL_BIT = 0x00000010;
const int TRANSPARENT_TEX_VAL_BIT = 0x00000020;
const int EMISSIVE_MAP_BIT = 0x00000040;
const int ALBEDO_MAP_BIT = 0x00000080;
const int METALLIC_MAP_BIT = 0x00000100;
const int ROUGHNESS_MAP_BIT = 0x00000200;
const int TRANSPARENT_MAP_BIT = 0x00000400;
const int NORMAL_MAP_BIT = 0x00000800;
const int OCCLUSION_MAP_BIT = 0x00001000;
const int EMISSIVE_MAP_BIT = 0x00000020;
const int ALBEDO_MAP_BIT = 0x00000040;
const int METALLIC_MAP_BIT = 0x00000080;
const int ROUGHNESS_MAP_BIT = 0x00000100;
const int TRANSPARENT_MAP_BIT = 0x00000200;
const int NORMAL_MAP_BIT = 0x00000400;
const int OCCLUSION_MAP_BIT = 0x00000800;
const int LIGHTMAP_MAP_BIT = 0x00001000;
const int LIGHTMAP_MAP_BIT = 0x00002000;
<@endif@>

View file

@ -22,6 +22,7 @@
#include "AssetRequest.h"
#include "AssetUpload.h"
#include "AssetUtils.h"
#include "MappingRequest.h"
#include "NetworkAccessManager.h"
#include "NetworkLogging.h"
#include "NodeList.h"
@ -30,7 +31,6 @@
MessageID AssetClient::_currentID = 0;
AssetClient::AssetClient() {
setCustomDeleter([](Dependency* dependency){
@ -39,6 +39,8 @@ AssetClient::AssetClient() {
auto nodeList = DependencyManager::get<NodeList>();
auto& packetReceiver = nodeList->getPacketReceiver();
packetReceiver.registerListener(PacketType::AssetMappingOperationReply, this, "handleAssetMappingOperationReply");
packetReceiver.registerListener(PacketType::AssetGetInfoReply, this, "handleAssetGetInfoReply");
packetReceiver.registerListener(PacketType::AssetGetReply, this, "handleAssetGetReply", true);
packetReceiver.registerListener(PacketType::AssetUploadReply, this, "handleAssetUploadReply");
@ -97,6 +99,33 @@ void AssetClient::clearCache() {
}
}
void AssetClient::handleAssetMappingOperationReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
MessageID messageID;
message->readPrimitive(&messageID);
AssetServerError error;
message->readPrimitive(&error);
// Check if we have any pending requests for this node
auto messageMapIt = _pendingMappingRequests.find(senderNode);
if (messageMapIt != _pendingMappingRequests.end()) {
// Found the node, get the MessageID -> Callback map
auto& messageCallbackMap = messageMapIt->second;
// Check if we have this pending request
auto requestIt = messageCallbackMap.find(messageID);
if (requestIt != messageCallbackMap.end()) {
auto callback = requestIt->second;
callback(true, error, message);
messageCallbackMap.erase(requestIt);
}
// Although the messageCallbackMap may now be empty, we won't delete the node until we have disconnected from
// it to avoid constantly creating/deleting the map on subsequent requests.
}
}
bool haveAssetServer() {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
@ -110,52 +139,68 @@ bool haveAssetServer() {
return true;
}
AssetRequest* AssetClient::createRequest(const QString& hash, const QString& extension) {
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
qCWarning(asset_client) << "Invalid hash size";
return nullptr;
}
if (haveAssetServer()) {
auto request = new AssetRequest(hash, extension);
// Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case)
request->moveToThread(thread());
return request;
} else {
return nullptr;
}
GetMappingRequest* AssetClient::createGetMappingRequest(const AssetPath& path) {
return new GetMappingRequest(path);
}
GetAllMappingsRequest* AssetClient::createGetAllMappingsRequest() {
auto request = new GetAllMappingsRequest();
request->moveToThread(thread());
return request;
}
DeleteMappingsRequest* AssetClient::createDeleteMappingsRequest(const AssetPathList& paths) {
auto request = new DeleteMappingsRequest(paths);
request->moveToThread(thread());
return request;
}
SetMappingRequest* AssetClient::createSetMappingRequest(const AssetPath& path, const AssetHash& hash) {
auto request = new SetMappingRequest(path, hash);
request->moveToThread(thread());
return request;
}
RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) {
auto request = new RenameMappingRequest(oldPath, newPath);
request->moveToThread(thread());
return request;
}
AssetRequest* AssetClient::createRequest(const AssetHash& hash) {
auto request = new AssetRequest(hash);
// Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case)
request->moveToThread(thread());
return request;
}
AssetUpload* AssetClient::createUpload(const QString& filename) {
if (haveAssetServer()) {
auto upload = new AssetUpload(filename);
upload->moveToThread(thread());
return upload;
} else {
return nullptr;
}
auto upload = new AssetUpload(filename);
upload->moveToThread(thread());
return upload;
}
AssetUpload* AssetClient::createUpload(const QByteArray& data, const QString& extension) {
if (haveAssetServer()) {
auto upload = new AssetUpload(data, extension);
upload->moveToThread(thread());
return upload;
} else {
return nullptr;
}
AssetUpload* AssetClient::createUpload(const QByteArray& data) {
auto upload = new AssetUpload(data);
upload->moveToThread(thread());
return upload;
}
bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOffset start, DataOffset end,
bool AssetClient::getAsset(const QString& hash, DataOffset start, DataOffset end,
ReceivedAssetCallback callback, ProgressCallback progressCallback) {
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
qCWarning(asset_client) << "Invalid hash size";
@ -169,8 +214,7 @@ bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOf
auto messageID = ++_currentID;
auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + extension.length()
+ sizeof(start) + sizeof(end);
auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(start) + sizeof(end);
auto packet = NLPacket::create(PacketType::AssetGet, payloadSize, true);
qCDebug(asset_client) << "Requesting data from" << start << "to" << end << "of" << hash << "from asset-server.";
@ -179,9 +223,6 @@ bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOf
packet->write(QByteArray::fromHex(hash.toLatin1()));
packet->writePrimitive(uint8_t(extension.length()));
packet->write(extension.toLatin1());
packet->writePrimitive(start);
packet->writePrimitive(end);
@ -190,34 +231,36 @@ bool AssetClient::getAsset(const QString& hash, const QString& extension, DataOf
_pendingRequests[assetServer][messageID] = { callback, progressCallback };
return true;
} else {
callback(false, AssetServerError::NoError, QByteArray());
return false;
}
return false;
}
bool AssetClient::getAssetInfo(const QString& hash, const QString& extension, GetInfoCallback callback) {
bool AssetClient::getAssetInfo(const QString& hash, GetInfoCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
auto messageID = ++_currentID;
auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH + sizeof(uint8_t) + extension.length();
auto payloadSize = sizeof(messageID) + SHA256_HASH_LENGTH;
auto packet = NLPacket::create(PacketType::AssetGetInfo, payloadSize, true);
packet->writePrimitive(messageID);
packet->write(QByteArray::fromHex(hash.toLatin1()));
packet->writePrimitive(uint8_t(extension.length()));
packet->write(extension.toLatin1());
nodeList->sendPacket(std::move(packet), *assetServer);
_pendingInfoRequests[assetServer][messageID] = callback;
return true;
} else {
callback(false, AssetServerError::NoError, { "", 0 });
return false;
}
return false;
}
void AssetClient::handleAssetGetInfoReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
@ -305,7 +348,137 @@ void AssetClient::handleAssetGetReply(QSharedPointer<ReceivedMessage> message, S
}
}
bool AssetClient::uploadAsset(const QByteArray& data, const QString& extension, UploadResultCallback callback) {
bool AssetClient::getAssetMapping(const AssetPath& path, MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
auto messageID = ++_currentID;
packetList->writePrimitive(messageID);
packetList->writePrimitive(AssetMappingOperationType::Get);
packetList->writeString(path);
nodeList->sendPacketList(std::move(packetList), *assetServer);
_pendingMappingRequests[assetServer][messageID] = callback;
return true;
} else {
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
return false;
}
}
bool AssetClient::getAllAssetMappings(MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
auto messageID = ++_currentID;
packetList->writePrimitive(messageID);
packetList->writePrimitive(AssetMappingOperationType::GetAll);
nodeList->sendPacketList(std::move(packetList), *assetServer);
_pendingMappingRequests[assetServer][messageID] = callback;
return true;
} else {
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
return false;
}
}
bool AssetClient::deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
auto messageID = ++_currentID;
packetList->writePrimitive(messageID);
packetList->writePrimitive(AssetMappingOperationType::Delete);
packetList->writePrimitive(int(paths.size()));
for (auto& path: paths) {
packetList->writeString(path);
}
nodeList->sendPacketList(std::move(packetList), *assetServer);
_pendingMappingRequests[assetServer][messageID] = callback;
return true;
} else {
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
return false;
}
}
bool AssetClient::setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
auto messageID = ++_currentID;
packetList->writePrimitive(messageID);
packetList->writePrimitive(AssetMappingOperationType::Set);
packetList->writeString(path);
packetList->write(QByteArray::fromHex(hash.toUtf8()));
nodeList->sendPacketList(std::move(packetList), *assetServer);
_pendingMappingRequests[assetServer][messageID] = callback;
return true;
} else {
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
return false;
}
}
bool AssetClient::renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
if (assetServer) {
auto packetList = NLPacketList::create(PacketType::AssetMappingOperation, QByteArray(), true, true);
auto messageID = ++_currentID;
packetList->writePrimitive(messageID);
packetList->writePrimitive(AssetMappingOperationType::Rename);
packetList->writeString(oldPath);
packetList->writeString(newPath);
nodeList->sendPacketList(std::move(packetList), *assetServer);
_pendingMappingRequests[assetServer][messageID] = callback;
return true;
} else {
callback(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
return false;
}
}
bool AssetClient::uploadAsset(const QByteArray& data, UploadResultCallback callback) {
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer assetServer = nodeList->soloNodeOfType(NodeType::AssetServer);
@ -315,9 +488,6 @@ bool AssetClient::uploadAsset(const QByteArray& data, const QString& extension,
auto messageID = ++_currentID;
packetList->writePrimitive(messageID);
packetList->writePrimitive(static_cast<uint8_t>(extension.length()));
packetList->write(extension.toLatin1().constData(), extension.length());
uint64_t size = data.length();
packetList->writePrimitive(size);
packetList->write(data.constData(), size);
@ -327,8 +497,10 @@ bool AssetClient::uploadAsset(const QByteArray& data, const QString& extension,
_pendingUploads[assetServer][messageID] = callback;
return true;
} else {
callback(false, AssetServerError::NoError, QString());
return false;
}
return false;
}
void AssetClient::handleAssetUploadReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
@ -404,71 +576,14 @@ void AssetClient::handleNodeKilled(SharedNodePointer node) {
messageMapIt->second.clear();
}
}
}
void AssetScriptingInterface::uploadData(QString data, QString extension, QScriptValue callback) {
QByteArray dataByteArray = data.toUtf8();
auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray, extension);
if (!upload) {
qCWarning(asset_client) << "Error uploading file to asset server";
return;
}
QObject::connect(upload, &AssetUpload::finished, this, [this, callback, extension](AssetUpload* upload, const QString& hash) mutable {
if (callback.isFunction()) {
QString url = "atp://" + hash + "." + extension;
QScriptValueList args { url };
callback.call(_engine->currentContext()->thisObject(), args);
}
});
upload->start();
}
AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) :
_engine(engine)
{
}
void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {
const QString ATP_SCHEME { "atp://" };
if (!urlString.startsWith(ATP_SCHEME)) {
return;
}
// Make request to atp
auto path = urlString.right(urlString.length() - ATP_SCHEME.length());
auto parts = path.split(".", QString::SkipEmptyParts);
auto hash = parts.length() > 0 ? parts[0] : "";
auto extension = parts.length() > 1 ? parts[1] : "";
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
return;
}
auto assetClient = DependencyManager::get<AssetClient>();
auto assetRequest = assetClient->createRequest(hash, extension);
if (!assetRequest) {
return;
}
_pendingRequests << assetRequest;
connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable {
Q_ASSERT(request->getState() == AssetRequest::Finished);
if (request->getError() == AssetRequest::Error::NoError) {
if (callback.isFunction()) {
QString data = QString::fromUtf8(request->getData());
QScriptValueList args { data };
callback.call(_engine->currentContext()->thisObject(), args);
{
auto messageMapIt = _pendingMappingRequests.find(node);
if (messageMapIt != _pendingMappingRequests.end()) {
for (const auto& value : messageMapIt->second) {
value.second(false, AssetServerError::NoError, QSharedPointer<ReceivedMessage>());
}
messageMapIt->second.clear();
}
request->deleteLater();
_pendingRequests.remove(request);
});
assetRequest->start();
}
}
}

View file

@ -9,12 +9,14 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_AssetClient_h
#define hifi_AssetClient_h
#include <QStandardItemModel>
#include <QtQml/QJSEngine>
#include <QString>
#include <QScriptValue>
#include <map>
#include <DependencyManager.h>
@ -25,6 +27,11 @@
#include "ReceivedMessage.h"
#include "ResourceCache.h"
class GetMappingRequest;
class SetMappingRequest;
class GetAllMappingsRequest;
class DeleteMappingsRequest;
class RenameMappingRequest;
class AssetRequest;
class AssetUpload;
@ -33,20 +40,25 @@ struct AssetInfo {
int64_t size;
};
using MappingOperationCallback = std::function<void(bool responseReceived, AssetServerError serverError, QSharedPointer<ReceivedMessage> message)>;
using ReceivedAssetCallback = std::function<void(bool responseReceived, AssetServerError serverError, const QByteArray& data)>;
using GetInfoCallback = std::function<void(bool responseReceived, AssetServerError serverError, AssetInfo info)>;
using UploadResultCallback = std::function<void(bool responseReceived, AssetServerError serverError, const QString& hash)>;
using ProgressCallback = std::function<void(qint64 totalReceived, qint64 total)>;
class AssetClient : public QObject, public Dependency {
Q_OBJECT
public:
AssetClient();
Q_INVOKABLE AssetRequest* createRequest(const QString& hash, const QString& extension);
Q_INVOKABLE GetMappingRequest* createGetMappingRequest(const AssetPath& path);
Q_INVOKABLE GetAllMappingsRequest* createGetAllMappingsRequest();
Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths);
Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash);
Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath);
Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash);
Q_INVOKABLE AssetUpload* createUpload(const QString& filename);
Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data, const QString& extension);
Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data);
public slots:
void init();
@ -55,6 +67,7 @@ public slots:
void clearCache();
private slots:
void handleAssetMappingOperationReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleAssetGetInfoReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleAssetGetReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleAssetUploadReply(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
@ -62,10 +75,16 @@ private slots:
void handleNodeKilled(SharedNodePointer node);
private:
bool getAssetInfo(const QString& hash, const QString& extension, GetInfoCallback callback);
bool getAsset(const QString& hash, const QString& extension, DataOffset start, DataOffset end,
bool getAssetMapping(const AssetHash& hash, MappingOperationCallback callback);
bool getAllAssetMappings(MappingOperationCallback callback);
bool setAssetMapping(const QString& path, const AssetHash& hash, MappingOperationCallback callback);
bool deleteAssetMappings(const AssetPathList& paths, MappingOperationCallback callback);
bool renameAssetMapping(const AssetPath& oldPath, const AssetPath& newPath, MappingOperationCallback callback);
bool getAssetInfo(const QString& hash, GetInfoCallback callback);
bool getAsset(const QString& hash, DataOffset start, DataOffset end,
ReceivedAssetCallback callback, ProgressCallback progressCallback);
bool uploadAsset(const QByteArray& data, const QString& extension, UploadResultCallback callback);
bool uploadAsset(const QByteArray& data, UploadResultCallback callback);
struct GetAssetCallbacks {
ReceivedAssetCallback completeCallback;
@ -73,26 +92,18 @@ private:
};
static MessageID _currentID;
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, MappingOperationCallback>> _pendingMappingRequests;
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, GetAssetCallbacks>> _pendingRequests;
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, GetInfoCallback>> _pendingInfoRequests;
std::unordered_map<SharedNodePointer, std::unordered_map<MessageID, UploadResultCallback>> _pendingUploads;
friend class AssetRequest;
friend class AssetUpload;
friend class GetMappingRequest;
friend class GetAllMappingsRequest;
friend class SetMappingRequest;
friend class DeleteMappingsRequest;
friend class RenameMappingRequest;
};
class AssetScriptingInterface : public QObject {
Q_OBJECT
public:
AssetScriptingInterface(QScriptEngine* engine);
Q_INVOKABLE void uploadData(QString data, QString extension, QScriptValue callback);
Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete);
protected:
QSet<AssetRequest*> _pendingRequests;
QScriptEngine* _engine;
};
#endif

View file

@ -20,10 +20,8 @@
#include "NodeList.h"
#include "ResourceCache.h"
AssetRequest::AssetRequest(const QString& hash, const QString& extension) :
QObject(),
_hash(hash),
_extension(extension)
AssetRequest::AssetRequest(const QString& hash) :
_hash(hash)
{
}
@ -37,6 +35,15 @@ void AssetRequest::start() {
qCWarning(asset_client) << "AssetRequest already started.";
return;
}
// in case we haven't parsed a valid hash, return an error now
if (!isValidHash(_hash)) {
_error = InvalidHash;
_state = Finished;
emit finished(this);
return;
}
// Try to load from cache
_data = loadFromCache(getUrl());
@ -53,9 +60,9 @@ void AssetRequest::start() {
_state = WaitingForInfo;
auto assetClient = DependencyManager::get<AssetClient>();
assetClient->getAssetInfo(_hash, _extension, [this](bool responseReceived, AssetServerError serverError, AssetInfo info) {
assetClient->getAssetInfo(_hash, [this](bool responseReceived, AssetServerError serverError, AssetInfo info) {
_info = info;
if (!responseReceived) {
_error = NetworkError;
} else if (serverError != AssetServerError::NoError) {
@ -85,7 +92,7 @@ void AssetRequest::start() {
int start = 0, end = _info.size;
auto assetClient = DependencyManager::get<AssetClient>();
assetClient->getAsset(_hash, _extension, start, end, [this, start, end](bool responseReceived, AssetServerError serverError,
assetClient->getAsset(_hash, start, end, [this, start, end](bool responseReceived, AssetServerError serverError,
const QByteArray& data) {
if (!responseReceived) {
_error = NetworkError;

View file

@ -34,19 +34,20 @@ public:
NoError,
NotFound,
InvalidByteRange,
InvalidHash,
HashVerificationFailed,
NetworkError,
UnknownError
};
AssetRequest(const QString& hash, const QString& extension);
AssetRequest(const QString& hash);
Q_INVOKABLE void start();
const QByteArray& getData() const { return _data; }
const State& getState() const { return _state; }
const Error& getError() const { return _error; }
QUrl getUrl() const { return ::getATPUrl(_hash, _extension); }
QUrl getUrl() const { return ::getATPUrl(_hash); }
signals:
void finished(AssetRequest* thisRequest);
@ -58,7 +59,6 @@ private:
AssetInfo _info;
uint64_t _totalReceived { 0 };
QString _hash;
QString _extension;
QByteArray _data;
int _numPendingRequests { 0 };
};

View file

@ -13,37 +13,99 @@
#include "AssetClient.h"
#include "AssetUtils.h"
#include "MappingRequest.h"
AssetResourceRequest::~AssetResourceRequest() {
if (_assetMappingRequest) {
_assetMappingRequest->deleteLater();
}
if (_assetRequest) {
_assetRequest->deleteLater();
}
}
bool AssetResourceRequest::urlIsAssetHash() const {
static const QString ATP_HASH_REGEX_STRING { "^atp:([A-Fa-f0-9]{64})(\\.[\\w]+)?$" };
QRegExp hashRegex { ATP_HASH_REGEX_STRING };
return hashRegex.exactMatch(_url.toString());
}
void AssetResourceRequest::doSend() {
// Make request to atp
auto assetClient = DependencyManager::get<AssetClient>();
auto parts = _url.path().split(".", QString::SkipEmptyParts);
auto hash = parts.length() > 0 ? parts[0] : "";
auto extension = parts.length() > 1 ? parts[1] : "";
if (hash.length() != SHA256_HASH_HEX_LENGTH) {
_result = InvalidURL;
_state = Finished;
// We'll either have a hash or an ATP path to a file (that maps to a hash)
emit finished();
return;
if (urlIsAssetHash()) {
// We've detected that this is a hash - simply use AssetClient to request that asset
auto parts = _url.path().split(".", QString::SkipEmptyParts);
auto hash = parts.length() > 0 ? parts[0] : "";
requestHash(hash);
} else {
// This is an ATP path, we'll need to figure out what the mapping is.
// This may incur a roundtrip to the asset-server, or it may return immediately from the cache in AssetClient.
auto path = _url.path();
requestMappingForPath(path);
}
}
_assetRequest = assetClient->createRequest(hash, extension);
void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
auto assetClient = DependencyManager::get<AssetClient>();
_assetMappingRequest = assetClient->createGetMappingRequest(path);
if (!_assetRequest) {
_result = ServerUnavailable;
_state = Finished;
// make sure we'll hear about the result of the get mapping request
connect(_assetMappingRequest, &GetMappingRequest::finished, this, [this, path](GetMappingRequest* request){
Q_ASSERT(_state == InProgress);
Q_ASSERT(request == _assetMappingRequest);
emit finished();
return;
}
switch (request->getError()) {
case MappingRequest::NoError:
// we have no error, we should have a resulting hash - use that to send of a request for that asset
qDebug() << "Got mapping for:" << path << "=>" << request->getHash();
requestHash(request->getHash());
break;
default: {
switch (request->getError()) {
case MappingRequest::NotFound:
// no result for the mapping request, set error to not found
_result = NotFound;
break;
case MappingRequest::NetworkError:
// didn't hear back from the server, mark it unavailable
_result = ServerUnavailable;
break;
default:
_result = Error;
break;
}
// since we've failed we know we are finished
_state = Finished;
emit finished();
break;
}
}
_assetMappingRequest->deleteLater();
_assetMappingRequest = nullptr;
});
_assetMappingRequest->start();
}
void AssetResourceRequest::requestHash(const AssetHash& hash) {
// Make request to atp
auto assetClient = DependencyManager::get<AssetClient>();
_assetRequest = assetClient->createRequest(hash);
connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::progress);
connect(_assetRequest, &AssetRequest::finished, this, [this](AssetRequest* req) {
@ -56,6 +118,9 @@ void AssetResourceRequest::doSend() {
_data = req->getData();
_result = Success;
break;
case AssetRequest::InvalidHash:
_result = InvalidURL;
break;
case AssetRequest::Error::NotFound:
_result = NotFound;
break;

View file

@ -30,6 +30,12 @@ private slots:
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
private:
bool urlIsAssetHash() const;
void requestMappingForPath(const AssetPath& path);
void requestHash(const AssetHash& hash);
GetMappingRequest* _assetMappingRequest { nullptr };
AssetRequest* _assetRequest { nullptr };
};

View file

@ -19,9 +19,8 @@
const QString AssetUpload::PERMISSION_DENIED_ERROR = "You do not have permission to upload content to this asset-server.";
AssetUpload::AssetUpload(const QByteArray& data, const QString& extension) :
_data(data),
_extension(extension)
AssetUpload::AssetUpload(const QByteArray& data) :
_data(data)
{
}
@ -35,6 +34,8 @@ AssetUpload::AssetUpload(const QString& filename) :
QString AssetUpload::getErrorString() const {
// figure out the right error message for error
switch (_error) {
case AssetUpload::NoError:
return QString();
case AssetUpload::PermissionDenied:
return PERMISSION_DENIED_ERROR;
case AssetUpload::TooLarge:
@ -42,10 +43,11 @@ QString AssetUpload::getErrorString() const {
case AssetUpload::FileOpenError:
return "The file could not be opened. Please check your permissions and try again.";
case AssetUpload::NetworkError:
return "The file could not be opened. Please check your network connectivity.";
return "There was a problem reaching your Asset Server. Please check your network connectivity.";
case AssetUpload::ServerFileError:
return "The Asset Server failed to store the asset. Please try again.";
default:
// not handled, do not show a message box
return QString();
return QString("Unknown error with code %1").arg(_error);
}
}
@ -59,12 +61,7 @@ void AssetUpload::start() {
// try to open the file at the given filename
QFile file { _filename };
if (file.open(QIODevice::ReadOnly)) {
// file opened, read the data and grab the extension
_extension = QFileInfo(_filename).suffix();
_extension = _extension.toLower();
if (file.open(QIODevice::ReadOnly)) {
_data = file.readAll();
} else {
// we couldn't open the file - set the error result
@ -72,6 +69,8 @@ void AssetUpload::start() {
// emit that we are done
emit finished(this, QString());
return;
}
}
@ -82,7 +81,7 @@ void AssetUpload::start() {
qCDebug(asset_client) << "Attempting to upload" << _filename << "to asset-server.";
}
assetClient->uploadAsset(_data, _extension, [this](bool responseReceived, AssetServerError error, const QString& hash){
assetClient->uploadAsset(_data, [this](bool responseReceived, AssetServerError error, const QString& hash){
if (!responseReceived) {
_error = NetworkError;
} else {
@ -96,6 +95,9 @@ void AssetUpload::start() {
case AssetServerError::PermissionDenied:
_error = PermissionDenied;
break;
case AssetServerError::FileOperationFailed:
_error = ServerFileError;
break;
default:
_error = FileOpenError;
break;
@ -103,7 +105,7 @@ void AssetUpload::start() {
}
if (_error == NoError && hash == hashData(_data).toHex()) {
saveToCache(getATPUrl(hash, _extension), _data);
saveToCache(getATPUrl(hash), _data);
}
emit finished(this, hash);

View file

@ -32,18 +32,18 @@ public:
Timeout,
TooLarge,
PermissionDenied,
FileOpenError
FileOpenError,
ServerFileError
};
static const QString PERMISSION_DENIED_ERROR;
AssetUpload(const QString& filename);
AssetUpload(const QByteArray& data, const QString& extension);
AssetUpload(const QByteArray& data);
Q_INVOKABLE void start();
const QString& getFilename() const { return _filename; }
const QString& getExtension() const { return _extension; }
const Error& getError() const { return _error; }
QString getErrorString() const;
@ -54,7 +54,6 @@ signals:
private:
QString _filename;
QByteArray _data;
QString _extension;
Error _error;
};

View file

@ -19,12 +19,8 @@
#include "ResourceManager.h"
QUrl getATPUrl(const QString& hash, const QString& extension) {
if (!extension.isEmpty()) {
return QUrl(QString("%1:%2.%3").arg(URL_SCHEME_ATP, hash, extension));
} else {
return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP, hash));
}
QUrl getATPUrl(const QString& hash) {
return QUrl(QString("%1:%2").arg(URL_SCHEME_ATP, hash));
}
QByteArray hashData(const QByteArray& data) {
@ -67,3 +63,13 @@ bool saveToCache(const QUrl& url, const QByteArray& file) {
}
return false;
}
bool isValidPath(const AssetPath& path) {
QRegExp pathRegex { ASSET_PATH_REGEX_STRING };
return pathRegex.exactMatch(path);
}
bool isValidHash(const AssetHash& hash) {
QRegExp hashRegex { ASSET_HASH_REGEX_STRING };
return hashRegex.exactMatch(hash);
}

View file

@ -14,29 +14,52 @@
#include <cstdint>
#include <map>
#include <QtCore/QByteArray>
#include <QtCore/QUrl>
using MessageID = uint32_t;
using DataOffset = int64_t;
using AssetPath = QString;
using AssetHash = QString;
using AssetMapping = std::map<AssetPath, AssetHash>;
using AssetPathList = QStringList;
const size_t SHA256_HASH_LENGTH = 32;
const size_t SHA256_HASH_HEX_LENGTH = 64;
const uint64_t MAX_UPLOAD_SIZE = 1000 * 1000 * 1000; // 1GB
const QString ASSET_PATH_REGEX_STRING = "^\\/(?!\\/)(?:[^\\/]|\\/(?!\\/))*$";
const QString ASSET_HASH_REGEX_STRING = QString("^[a-fA-F0-9]{%1}$").arg(SHA256_HASH_HEX_LENGTH);
enum AssetServerError : uint8_t {
NoError = 0,
AssetNotFound,
InvalidByteRange,
AssetTooLarge,
PermissionDenied
PermissionDenied,
MappingOperationFailed,
FileOperationFailed
};
QUrl getATPUrl(const QString& hash, const QString& extension = QString());
enum AssetMappingOperationType : uint8_t {
Get = 0,
GetAll,
Set,
Delete,
Rename
};
QUrl getATPUrl(const QString& hash);
QByteArray hashData(const QByteArray& data);
QByteArray loadFromCache(const QUrl& url);
bool saveToCache(const QUrl& url, const QByteArray& file);
bool isValidPath(const AssetPath& path);
bool isValidHash(const QString& hashString);
#endif

View file

@ -0,0 +1,240 @@
//
// MappingRequest.cpp
// libraries/networking/src
//
// Created by Stephen Birarda on 2016-03-08.
// Copyright 2016 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 "MappingRequest.h"
#include <QtCore/QThread>
#include <DependencyManager.h>
#include "AssetClient.h"
void MappingRequest::start() {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "start", Qt::AutoConnection);
return;
}
doStart();
};
QString MappingRequest::getErrorString() const {
switch (_error) {
case MappingRequest::NoError:
return QString();
case MappingRequest::NotFound:
return "Asset not found";
case MappingRequest::NetworkError:
return "Unable to communicate with Asset Server";
case MappingRequest::PermissionDenied:
return "Permission denied";
case MappingRequest::InvalidPath:
return "Path is invalid";
case MappingRequest::InvalidHash:
return "Hash is invalid";
case MappingRequest::UnknownError:
return "Asset Server internal error";
default:
return QString("Unknown error with code %1").arg(_error);
}
}
GetMappingRequest::GetMappingRequest(const AssetPath& path) : _path(path.trimmed()) {
};
void GetMappingRequest::doStart() {
// short circuit the request if the path is invalid
if (!isValidPath(_path)) {
_error = MappingRequest::InvalidPath;
emit finished(this);
return;
}
auto assetClient = DependencyManager::get<AssetClient>();
assetClient->getAssetMapping(_path, [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
if (!responseReceived) {
_error = NetworkError;
} else {
switch (error) {
case AssetServerError::NoError:
_error = NoError;
break;
case AssetServerError::AssetNotFound:
_error = NotFound;
break;
default:
_error = UnknownError;
break;
}
}
if (!_error) {
_hash = message->read(SHA256_HASH_LENGTH).toHex();
}
emit finished(this);
});
};
GetAllMappingsRequest::GetAllMappingsRequest() {
};
void GetAllMappingsRequest::doStart() {
auto assetClient = DependencyManager::get<AssetClient>();
assetClient->getAllAssetMappings([this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
if (!responseReceived) {
_error = NetworkError;
} else {
switch (error) {
case AssetServerError::NoError:
_error = NoError;
break;
default:
_error = UnknownError;
break;
}
}
if (!_error) {
int numberOfMappings;
message->readPrimitive(&numberOfMappings);
for (auto i = 0; i < numberOfMappings; ++i) {
auto path = message->readString();
auto hash = message->read(SHA256_HASH_LENGTH).toHex();
_mappings[path] = hash;
}
}
emit finished(this);
});
};
SetMappingRequest::SetMappingRequest(const AssetPath& path, const AssetHash& hash) :
_path(path.trimmed()),
_hash(hash)
{
};
void SetMappingRequest::doStart() {
// short circuit the request if the hash or path are invalid
auto validPath = isValidPath(_path);
auto validHash = isValidHash(_hash);
if (!validPath || !validHash) {
_error = !validPath ? MappingRequest::InvalidPath : MappingRequest::InvalidHash;
emit finished(this);
return;
}
auto assetClient = DependencyManager::get<AssetClient>();
assetClient->setAssetMapping(_path, _hash, [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
if (!responseReceived) {
_error = NetworkError;
} else {
switch (error) {
case AssetServerError::NoError:
_error = NoError;
break;
case AssetServerError::PermissionDenied:
_error = PermissionDenied;
break;
default:
_error = UnknownError;
break;
}
}
emit finished(this);
});
};
DeleteMappingsRequest::DeleteMappingsRequest(const AssetPathList& paths) : _paths(paths) {
for (auto& path : _paths) {
path = path.trimmed();
}
};
void DeleteMappingsRequest::doStart() {
// short circuit the request if any of the paths are invalid
for (auto& path : _paths) {
if (!isValidPath(path)) {
_error = MappingRequest::InvalidPath;
emit finished(this);
return;
}
}
auto assetClient = DependencyManager::get<AssetClient>();
assetClient->deleteAssetMappings(_paths, [this, assetClient](bool responseReceived, AssetServerError error, QSharedPointer<ReceivedMessage> message) {
if (!responseReceived) {
_error = NetworkError;
} else {
switch (error) {
case AssetServerError::NoError:
_error = NoError;
break;
case AssetServerError::PermissionDenied:
_error = PermissionDenied;
break;
default:
_error = UnknownError;
break;
}
}
emit finished(this);
});
};
RenameMappingRequest::RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath) :
_oldPath(oldPath.trimmed()),
_newPath(newPath.trimmed())
{
}
void RenameMappingRequest::doStart() {
// short circuit the request if either of the paths are invalid
if (!isValidPath(_oldPath) || !isValidPath(_newPath)) {
_error = InvalidPath;
emit finished(this);
return;
}
auto assetClient = DependencyManager::get<AssetClient>();
assetClient->renameAssetMapping(_oldPath, _newPath, [this, assetClient](bool responseReceived,
AssetServerError error,
QSharedPointer<ReceivedMessage> message) {
if (!responseReceived) {
_error = NetworkError;
} else {
switch (error) {
case AssetServerError::NoError:
_error = NoError;
break;
case AssetServerError::PermissionDenied:
_error = PermissionDenied;
break;
default:
_error = UnknownError;
break;
}
}
emit finished(this);
});
}

View file

@ -0,0 +1,127 @@
//
// MappingRequest.h
// libraries/networking/src
//
// Created by Stephen Birarda on 2016-03-08.
// Copyright 2016 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
//
#pragma once
#ifndef hifi_MappingRequest_h
#define hifi_MappingRequest_h
#include <QtCore/QObject>
#include "AssetUtils.h"
class MappingRequest : public QObject {
Q_OBJECT
public:
enum Error {
NoError,
NotFound,
NetworkError,
PermissionDenied,
InvalidPath,
InvalidHash,
UnknownError
};
Q_INVOKABLE void start();
Error getError() const { return _error; }
Q_INVOKABLE QString getErrorString() const;
protected:
Error _error { NoError };
private:
virtual void doStart() = 0;
};
class GetMappingRequest : public MappingRequest {
Q_OBJECT
public:
GetMappingRequest(const AssetPath& path);
AssetHash getHash() const { return _hash; }
signals:
void finished(GetMappingRequest* thisRequest);
private:
virtual void doStart() override;
AssetPath _path;
AssetHash _hash;
};
class SetMappingRequest : public MappingRequest {
Q_OBJECT
public:
SetMappingRequest(const AssetPath& path, const AssetHash& hash);
AssetPath getPath() const { return _path; }
AssetHash getHash() const { return _hash; }
signals:
void finished(SetMappingRequest* thisRequest);
private:
virtual void doStart() override;
AssetPath _path;
AssetHash _hash;
};
class DeleteMappingsRequest : public MappingRequest {
Q_OBJECT
public:
DeleteMappingsRequest(const AssetPathList& path);
signals:
void finished(DeleteMappingsRequest* thisRequest);
private:
virtual void doStart() override;
AssetPathList _paths;
};
class RenameMappingRequest : public MappingRequest {
Q_OBJECT
public:
RenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath);
signals:
void finished(RenameMappingRequest* thisRequest);
private:
virtual void doStart() override;
AssetPath _oldPath;
AssetPath _newPath;
};
class GetAllMappingsRequest : public MappingRequest {
Q_OBJECT
public:
GetAllMappingsRequest();
AssetMapping getMappings() const { return _mappings; }
signals:
void finished(GetAllMappingsRequest* thisRequest);
private:
virtual void doStart() override;
std::map<AssetPath, AssetHash> _mappings;
};
#endif // hifi_MappingRequest_h

View file

@ -107,6 +107,15 @@ QByteArray ReceivedMessage::readAll() {
return read(getBytesLeftToRead());
}
QString ReceivedMessage::readString() {
uint32_t size;
readPrimitive(&size);
//Q_ASSERT(size <= _size - _position);
auto string = QString::fromUtf8(_data.constData() + _position, size);
_position += size;
return string;
}
QByteArray ReceivedMessage::readWithoutCopy(qint64 size) {
QByteArray data { QByteArray::fromRawData(_data.constData() + _position, size) };
_position += size;

View file

@ -63,6 +63,8 @@ public:
QByteArray read(qint64 size);
QByteArray readAll();
QString readString();
QByteArray readHead(qint64 size);
// This will return a QByteArray referencing the underlying data _without_ refcounting that data.
@ -86,7 +88,6 @@ private:
QByteArray _data;
QByteArray _headData;
std::atomic<qint64> _size { true };
std::atomic<qint64> _position { 0 };
std::atomic<qint64> _numPackets { 0 };

View file

@ -150,6 +150,20 @@ QByteArray BasePacket::readWithoutCopy(qint64 maxSize) {
return data;
}
qint64 BasePacket::writeString(const QString& string) {
QByteArray data = string.toUtf8();
writePrimitive(static_cast<uint32_t>(data.length()));
return writeData(data.constData(), data.length());
}
QString BasePacket::readString() {
uint32_t size;
readPrimitive(&size);
auto string = QString::fromUtf8(getPayload() + pos(), size);
seek(pos() + size);
return string;
}
bool BasePacket::reset() {
if (isWritable()) {
_payloadSize = 0;

View file

@ -77,7 +77,10 @@ public:
using QIODevice::read; // Bring QIODevice::read methods to scope, otherwise they are hidden by folling method
QByteArray read(qint64 maxSize);
QByteArray readWithoutCopy(qint64 maxSize); // this can only be used if packet will stay in scope
qint64 writeString(const QString& string);
QString readString();
template<typename T> qint64 peekPrimitive(T* data);
template<typename T> qint64 readPrimitive(T* data);
template<typename T> qint64 writePrimitive(const T& data);

View file

@ -53,6 +53,11 @@ PacketVersion versionForPacketType(PacketType packetType) {
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SoftAttachmentSupport);
case PacketType::ICEServerHeartbeat:
return 18; // ICE Server Heartbeat signing
case PacketType::AssetGetInfo:
case PacketType::AssetGet:
case PacketType::AssetUpload:
// Removal of extension from Asset requests
return 18;
default:
return 17;
}

View file

@ -91,7 +91,9 @@ public:
MessagesData,
MessagesSubscribe,
MessagesUnsubscribe,
ICEServerHeartbeatDenied
ICEServerHeartbeatDenied,
AssetMappingOperation,
AssetMappingOperationReply
};
};

View file

@ -158,6 +158,12 @@ void PacketList::preparePackets(MessageNumber messageNumber) {
const qint64 PACKET_LIST_WRITE_ERROR = -1;
qint64 PacketList::writeString(const QString& string) {
QByteArray data = string.toUtf8();
writePrimitive(static_cast<uint32_t>(data.length()));
return writeData(data.constData(), data.length());
}
qint64 PacketList::writeData(const char* data, qint64 maxSize) {
auto sizeRemaining = maxSize;

View file

@ -59,6 +59,8 @@ public:
template<typename T> qint64 readPrimitive(T* data);
template<typename T> qint64 writePrimitive(const T& data);
qint64 writeString(const QString& string);
protected:
PacketList(PacketType packetType, QByteArray extendedHeader = QByteArray(), bool isReliable = false, bool isOrdered = false);

View file

@ -29,6 +29,7 @@
#include "Socket.h"
using namespace udt;
using namespace std::chrono;
template <typename Mutex1, typename Mutex2>
class DoubleLock {
@ -280,11 +281,11 @@ void SendQueue::run() {
// Once we're here we've either received the handshake ACK or it's going to be time to re-send a handshake.
// Either way let's continue processing - no packets will be sent if no handshake ACK has been received.
}
// Keep an HRC to know when the next packet should have been
auto nextPacketTimestamp = p_high_resolution_clock::now();
while (_state == State::Running) {
// Record how long the loop takes to execute
const auto loopStartTimestamp = p_high_resolution_clock::now();
bool sentAPacket = maybeResendPacket();
// if we didn't find a packet to re-send AND we think we can fit a new packet on the wire
@ -303,10 +304,13 @@ void SendQueue::run() {
if (_state != State::Running || isInactive(sentAPacket)) {
return;
}
// push the next packet timestamp forwards by the current packet send period
nextPacketTimestamp += std::chrono::microseconds(_packetSendPeriod);
// sleep as long as we need until next packet send, if we can
const auto loopEndTimestamp = p_high_resolution_clock::now();
const auto timeToSleep = (loopStartTimestamp + std::chrono::microseconds(_packetSendPeriod)) - loopEndTimestamp;
const auto timeToSleep = duration_cast<microseconds>(nextPacketTimestamp - p_high_resolution_clock::now());
std::this_thread::sleep_for(timeToSleep);
}
}

View file

@ -264,7 +264,9 @@ void DeferredLightingEffect::render(const render::RenderContextPointer& renderCo
deferredTransforms[i].projection = projMats[i];
auto sideViewMat = monoViewMat * glm::inverse(eyeViews[i]);
viewTransforms[i].evalFromRawMatrix(sideViewMat);
// viewTransforms[i].evalFromRawMatrix(sideViewMat);
viewTransforms[i] = monoViewTransform;
viewTransforms[i].postTranslate(-glm::vec3((eyeViews[i][3])));// evalFromRawMatrix(sideViewMat);
deferredTransforms[i].viewInverse = sideViewMat;
deferredTransforms[i].stereoSide = (i == 0 ? -1.0f : 1.0f);

View file

@ -81,7 +81,7 @@ ItemKey MeshPartPayload::getKey() const {
if (_drawMaterial) {
auto matKey = _drawMaterial->getKey();
if (matKey.isTransparent() || matKey.isTransparentMap()) {
if (matKey.isTransparent() || matKey.isTransparentTexture() || matKey.isTransparentMap()) {
builder.withTransparent();
}
}
@ -100,7 +100,7 @@ ShapeKey MeshPartPayload::getShapeKey() const {
}
ShapeKey::Builder builder;
if (drawMaterialKey.isTransparent() || drawMaterialKey.isTransparentMap()) {
if (drawMaterialKey.isTransparent() || drawMaterialKey.isTransparentTexture() || drawMaterialKey.isTransparentMap()) {
builder.withTranslucent();
}
if (drawMaterialKey.isNormalMap()) {
@ -365,7 +365,7 @@ ItemKey ModelMeshPartPayload::getKey() const {
if (_drawMaterial) {
auto matKey = _drawMaterial->getKey();
if (matKey.isTransparent() || matKey.isTransparentMap()) {
if (matKey.isTransparent() || matKey.isTransparentTexture() || matKey.isTransparentMap()) {
builder.withTransparent();
}
}
@ -412,7 +412,8 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const {
drawMaterialKey = _drawMaterial->getKey();
}
bool isTranslucent = drawMaterialKey.isTransparent() || drawMaterialKey.isTransparentMap();
bool isTranslucent =
drawMaterialKey.isTransparent() || drawMaterialKey.isTransparentTexture() || drawMaterialKey.isTransparentMap();
bool hasTangents = drawMaterialKey.isNormalMap() && !mesh.tangents.isEmpty();
bool hasSpecular = drawMaterialKey.isMetallicMap();
bool hasLightmap = drawMaterialKey.isLightmapMap();

View file

@ -74,6 +74,21 @@ Model::~Model() {
AbstractViewStateInterface* Model::_viewState = NULL;
bool Model::needsFixupInScene() {
if (readyToAddToScene()) {
// Once textures are loaded, fixup if they are now transparent
if (!_needsReload && _needsUpdateTransparentTextures && _geometry->isLoadedWithTextures()) {
_needsUpdateTransparentTextures = false;
if (_hasTransparentTextures != _geometry->hasTransparentTextures()) {
return true;
}
}
if (!_readyWhenAdded) {
return true;
}
}
return false;
}
void Model::setTranslation(const glm::vec3& translation) {
_translation = translation;
@ -728,6 +743,8 @@ void Model::setURL(const QUrl& url) {
}
_needsReload = true;
_needsUpdateTransparentTextures = true;
_hasTransparentTextures = false;
_meshGroupsKnown = false;
invalidCalculatedMeshBoxes();
deleteGeometry();

View file

@ -70,7 +70,7 @@ public:
// new Scene/Engine rendering support
void setVisibleInScene(bool newValue, std::shared_ptr<render::Scene> scene);
bool needsFixupInScene() { return !_readyWhenAdded && readyToAddToScene(); }
bool needsFixupInScene();
bool readyToAddToScene(RenderArgs* renderArgs = nullptr) {
return !_needsReload && isRenderable() && isActive() && isLoaded();
}
@ -365,10 +365,12 @@ protected:
QSet<std::shared_ptr<MeshPartPayload>> _renderItemsSet;
QMap<render::ItemID, render::PayloadPointer> _renderItems;
bool _readyWhenAdded = false;
bool _needsReload = true;
bool _needsUpdateClusterMatrices = true;
bool _showCollisionHull = false;
bool _readyWhenAdded { false };
bool _needsReload { true };
bool _needsUpdateClusterMatrices { true };
bool _needsUpdateTransparentTextures { true };
bool _hasTransparentTextures { false };
bool _showCollisionHull { false };
friend class ModelMeshPartPayload;
RigPointer _rig;

View file

@ -291,6 +291,7 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const
RenderArgs* args = renderContext->args;
doInBatch(args->_context, [&](gpu::Batch& batch) {
args->_batch = &batch;
_gpuTimer.begin(batch);
auto lightingFBO = DependencyManager::get<FramebufferCache>()->getLightingFramebuffer();
@ -310,8 +311,11 @@ void DrawBackgroundDeferred::run(const SceneContextPointer& sceneContext, const
batch.setViewTransform(viewMat);
renderItems(sceneContext, renderContext, inItems);
_gpuTimer.end(batch);
});
args->_batch = nullptr;
std::static_pointer_cast<Config>(renderContext->jobConfig)->gpuTime = _gpuTimer.getAverage();
}
void Blit::run(const SceneContextPointer& sceneContext, const RenderContextPointer& renderContext) {

View file

@ -82,11 +82,27 @@ protected:
static gpu::PipelinePointer _opaquePipeline; //lazy evaluation hence mutable
};
class DrawBackgroundDeferredConfig : public render::Job::Config {
Q_OBJECT
Q_PROPERTY(double gpuTime READ getGpuTime)
public:
double getGpuTime() { return gpuTime; }
protected:
friend class DrawBackgroundDeferred;
double gpuTime;
};
class DrawBackgroundDeferred {
public:
using Config = DrawBackgroundDeferredConfig;
using JobModel = render::Job::ModelI<DrawBackgroundDeferred, render::ItemBounds, Config>;
void configure(const Config& config) {}
void run(const render::SceneContextPointer& sceneContext, const render::RenderContextPointer& renderContext, const render::ItemBounds& inItems);
using JobModel = render::Job::ModelI<DrawBackgroundDeferred, render::ItemBounds>;
protected:
gpu::RangeTimer _gpuTimer;
};
class DrawOverlay3DConfig : public render::Job::Config {

View file

@ -20,7 +20,8 @@
<@include gpu/Transform.slh@>
<$declareStandardCameraTransform()$>
uniform sampler2D albedoMap;
<@include MaterialTextures.slh@>
<$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, _SCRIBE_NULL, EMISSIVE, OCCLUSION)$>
in vec2 _texCoord0;
in vec4 _position;
@ -31,16 +32,27 @@ in float _alpha;
out vec4 _fragColor;
void main(void) {
vec4 albedo = texture(albedoMap, _texCoord0);
Material mat = getMaterial();
int matKey = getMaterialKey(mat);
<$fetchMaterialTextures(matKey, _texCoord0, albedoTex, roughnessTex, _SCRIBE_NULL, _SCRIBE_NULL, emissiveTex, occlusionTex)$>
vec3 albedo = getMaterialAlbedo(mat);
<$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>;
albedo *= _color;
float roughness = getMaterialRoughness(mat);
<$evalMaterialRoughness(roughnessTex, roughness, matKey, roughness)$>;
float metallic = getMaterialMetallic(mat);
vec3 emissive = getMaterialEmissive(mat);
<$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>;
vec3 fragPosition = _position.xyz;
vec3 fragNormal = normalize(_normal);
vec3 fragAlbedo = getMaterialAlbedo(mat) * albedo.rgb * _color;
float fragMetallic = getMaterialMetallic(mat);
vec3 fragEmissive = getMaterialEmissive(mat);
float fragRoughness = getMaterialRoughness(mat);
float fragOpacity = getMaterialOpacity(mat) * albedo.a * _alpha;
float fragOpacity = getMaterialOpacity(mat) * albedoTex.a * _alpha;
TransformCamera cam = getTransformCamera();
@ -50,9 +62,9 @@ void main(void) {
1.0,
fragPosition,
fragNormal,
fragAlbedo,
fragMetallic,
fragEmissive,
fragRoughness),
albedo,
metallic,
emissive,
roughness),
fragOpacity);
}

View file

@ -0,0 +1,74 @@
//
// AssetScriptingInterface.cpp
// libraries/script-engine/src
//
// Created by Stephen Birarda on 2016-03-08.
// Copyright 2016 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 "AssetScriptingInterface.h"
#include <QtScript/QScriptEngine>
#include <AssetRequest.h>
#include <AssetUpload.h>
#include <MappingRequest.h>
#include <NetworkLogging.h>
AssetScriptingInterface::AssetScriptingInterface(QScriptEngine* engine) :
_engine(engine)
{
}
void AssetScriptingInterface::uploadData(QString data, QScriptValue callback) {
QByteArray dataByteArray = data.toUtf8();
auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray);
QObject::connect(upload, &AssetUpload::finished, this, [this, callback](AssetUpload* upload, const QString& hash) mutable {
if (callback.isFunction()) {
QString url = "atp:" + hash;
QScriptValueList args { url };
callback.call(_engine->currentContext()->thisObject(), args);
}
});
upload->start();
}
void AssetScriptingInterface::downloadData(QString urlString, QScriptValue callback) {
const QString ATP_SCHEME { "atp:" };
if (!urlString.startsWith(ATP_SCHEME)) {
return;
}
// Make request to atp
auto path = urlString.right(urlString.length() - ATP_SCHEME.length());
auto parts = path.split(".", QString::SkipEmptyParts);
auto hash = parts.length() > 0 ? parts[0] : "";
auto assetClient = DependencyManager::get<AssetClient>();
auto assetRequest = assetClient->createRequest(hash);
_pendingRequests << assetRequest;
connect(assetRequest, &AssetRequest::finished, this, [this, callback](AssetRequest* request) mutable {
Q_ASSERT(request->getState() == AssetRequest::Finished);
if (request->getError() == AssetRequest::Error::NoError) {
if (callback.isFunction()) {
QString data = QString::fromUtf8(request->getData());
QScriptValueList args { data };
callback.call(_engine->currentContext()->thisObject(), args);
}
}
request->deleteLater();
_pendingRequests.remove(request);
});
assetRequest->start();
}

View file

@ -0,0 +1,35 @@
//
// AssetScriptingInterface.h
// libraries/script-engine/src
//
// Created by Stephen Birarda on 2016-03-08.
// Copyright 2016 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
//
#pragma once
#ifndef hifi_AssetScriptingInterface_h
#define hifi_AssetScriptingInterface_h
#include <QtCore/QObject>
#include <QtScript/QScriptValue>
#include <AssetClient.h>
class AssetScriptingInterface : public QObject {
Q_OBJECT
public:
AssetScriptingInterface(QScriptEngine* engine);
Q_INVOKABLE void uploadData(QString data, QScriptValue callback);
Q_INVOKABLE void downloadData(QString url, QScriptValue downloadComplete);
protected:
QSet<AssetRequest*> _pendingRequests;
QScriptEngine* _engine;
};
#endif // hifi_AssetScriptingInterface_h

View file

@ -198,13 +198,13 @@ bool RecordingScriptingInterface::saveRecordingToAsset(QScriptValue getClipAtpUr
return false;
}
if (auto upload = DependencyManager::get<AssetClient>()->createUpload(recording::Clip::toBuffer(_lastClip), HFR_EXTENSION)) {
if (auto upload = DependencyManager::get<AssetClient>()->createUpload(recording::Clip::toBuffer(_lastClip))) {
QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable {
QString clip_atp_url = "";
if (upload->getError() == AssetUpload::NoError) {
clip_atp_url = QString("%1:%2.%3").arg(URL_SCHEME_ATP, hash, upload->getExtension());
clip_atp_url = QString("%1:%2").arg(URL_SCHEME_ATP, hash);
upload->deleteLater();
} else {
qCWarning(scriptengine) << "Error during the Asset upload.";

View file

@ -23,7 +23,6 @@
#include <AnimationCache.h>
#include <AnimVariant.h>
#include <AssetClient.h>
#include <AvatarData.h>
#include <AvatarHashMap.h>
#include <LimitedNodeList.h>
@ -32,6 +31,7 @@
#include "MouseEvent.h"
#include "ArrayBufferClass.h"
#include "AssetScriptingInterface.h"
#include "AudioScriptingInterface.h"
#include "Quat.h"
#include "Mat4.h"